Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

poc for aepc #1

Merged
merged 10 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea
9 changes: 9 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Development

## Building aepc

The standard GoLang toolchain is used, with the addition of protobuf for
compiling the resource definition.

1. `protoc ./schema/resourcedefinition.proto --go_opt paths=source_relative --go_out=.`
2. `go build main.go`
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,30 @@
# aepc
# aepc

Generates AEP-compliant RPCs from proto messages. See the main README.md for usage.

## Purpose

aepc is designed to primarily work off of a resource model: rather than having individual RPCs / methods on a resource, the user declares *resources* that live under a *service*. The common operations against a resource (Create, Read, Update, List, and Delete) are generatable based on the desired control plane standard, such as the AEP standard or custom resource definitions for the Kubernetes Resource Model.

## Design

aepc works off of an internal "hub" representation of a resource, while each of the consumers and producers is a "spoke", using the resource information for generation of service, clients, or documentation:

```mermaid
flowchart LR
hub("unified service and resource hub")
protoResources("proto messages")
proto("protobuf")
crd("Custom Resource Definitions (K8S)")
http("HTTP REST APIs")
protoResources --> hub
hub --> proto
hub --> http
hub --> crd
```

## User Guide

```
go run main.go -i ./examples/bookstore.yaml -o ./examples/bookstore.yaml.output.proto
```
140 changes: 140 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Copyright 2023 Yusuke Fredrick Tsutsumi
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd

import (
"fmt"
"io"
"log"
"os"
"path/filepath"

"github.com/ghodss/yaml"

"github.com/aep-dev/aepc/loader"
"github.com/aep-dev/aepc/parser"
"github.com/aep-dev/aepc/schema"
"github.com/aep-dev/aepc/validator"
"github.com/aep-dev/aepc/writer/proto"
"github.com/spf13/cobra"
"google.golang.org/protobuf/encoding/protojson"
)

func NewCommand() *cobra.Command {
var inputFile string
var outputFile string

c := &cobra.Command{
Use: "aepc",
Short: "aepc compiles resource representations to full proto rpcs",
Long: "aepc compiles resource representations to full proto rpcs",
Run: func(cmd *cobra.Command, args []string) {
// TODO: error handling
s := &schema.Service{}
input, err := readFile(inputFile)
fmt.Printf("input: %s\n", string(input))
if err != nil {
log.Fatalf("unable to read file: %v", err)
}
ext := filepath.Ext(inputFile)
err = unmarshal(ext, input, s)
if err != nil {
log.Fatal(err)
}
errors := validator.ValidateService(s)
if len(errors) > 0 {
log.Fatalf("error validating service: %v", errors)
}
ps, err := parser.NewParsedService(s)
if err != nil {
log.Fatal(err)
}
proto, _ := proto.WriteServiceToProto(ps)

err = writeFile(outputFile, proto)
if err != nil {
log.Fatal(err)
}
fmt.Printf("output file: %s\n", outputFile)
fmt.Printf("output proto: %s\n", proto)
},
}
c.Flags().StringVarP(&inputFile, "input", "i", "", "input files with resource")
c.Flags().StringVarP(&outputFile, "output", "o", "", "output file to use")
return c
}

func unmarshal(ext string, b []byte, s *schema.Service) error {
switch ext {
case ".proto":
if err := loader.ReadServiceFromProto(b, s); err != nil {
return fmt.Errorf("unable to decode proto %q: %w", string(b), err)
}
case ".yaml":
asJson, err := yaml.YAMLToJSON(b)
if err != nil {
return fmt.Errorf("unable to decode yaml to JSON %q: %w", string(b), err)
}
if err := protojson.Unmarshal(asJson, s); err != nil {
log.Fatal(fmt.Errorf("unable to decode proto %q: %w", string(b), err))
}
case ".json":
if err := protojson.Unmarshal(b, s); err != nil {
return fmt.Errorf("unable to decode json %q: %w", string(b), err)
}
default:
return fmt.Errorf("extension %v is unsupported", ext)
}
return nil
}

func readFile(fileName string) ([]byte, error) {
var value []byte
f, err := os.OpenFile(fileName, os.O_RDONLY, 0644)
if err != nil {
return nil, err
}
bytesRead := 1
for bytesRead > 0 {
readBytes := make([]byte, 10000)
bytesRead, err = f.Read(readBytes)
if bytesRead > 0 {
value = append(value, readBytes[:bytesRead]...)
}
if err != io.EOF && err != nil {
return nil, err
}
}
err = f.Close()
if err != nil {
return nil, err
}
return value, nil
}

func writeFile(fileName string, value []byte) error {
f, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return err
}
_, err = f.Write(value)
if err != nil {
return err
}
err = f.Close()
if err != nil {
return err
}
return nil
}
20 changes: 20 additions & 0 deletions examples/bookstore.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// NOTE: since we are missing some proto annotations that
// must be added, the proto representation is outdated. See bookstore.yaml
syntax = "proto3";
package tutorial;
option go_package = "proto";

// bookstore.examples.com
service Bookstore {
}

message Book {

// represents the isbn.
string isbn = 3;
message Properties {}
message Status {}

Properties properties = 1;
Status status = 2;
}
29 changes: 29 additions & 0 deletions examples/bookstore.proto.output.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
syntax = "proto3";

import "google/api/annotations.proto";

option go_package = "/newbookstore";

message book {
toumorokoshi marked this conversation as resolved.
Show resolved Hide resolved
string path = 1;
}

message CreatebookRequest {
toumorokoshi marked this conversation as resolved.
Show resolved Hide resolved
string id = 1;

book resource = 2;
}

message ReadbookRequest {
string path = 1;
}

service NewBookStore {
rpc Createbook ( CreatebookRequest ) returns ( book ) {
option (google.api.http) = { post: "/book" };
}

rpc Readbook ( ReadbookRequest ) returns ( book ) {
option (google.api.http) = { get: "/{path=book/*}" };
}
}
6 changes: 6 additions & 0 deletions examples/bookstore.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
name: bookstore.example.com
resources:
- kind: Book
parents:
- "bookstore.example.com/Publisher"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: allow a shorthand for intra-service resources (e.g. "Publisher")

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a DESIGN.md to include thoughts on each of these topics.

- kind: Publisher
69 changes: 69 additions & 0 deletions examples/bookstore.yaml.output.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
syntax = "proto3";

import "google/api/annotations.proto";

import "google/protobuf/empty.proto";

option go_package = "/bookstore";

message Book {
string path = 1;
}

message CreateBookRequest {
string id = 1;

Book resource = 2;
}

message ReadBookRequest {
string path = 1;
}

message DeleteBookRequest {
string path = 1;
}

message Publisher {
string path = 1;
}

message CreatePublisherRequest {
string id = 1;

Publisher resource = 2;
}

message ReadPublisherRequest {
string path = 1;
}

message DeletePublisherRequest {
string path = 1;
}

service Bookstore {
rpc CreateBook ( CreateBookRequest ) returns ( Book ) {
option (google.api.http) = { post: "/book" };
toumorokoshi marked this conversation as resolved.
Show resolved Hide resolved
}

rpc ReadBook ( ReadBookRequest ) returns ( Book ) {
option (google.api.http) = { get: "/{path=publisher/*/book/*}" };
}

rpc DeleteBook ( DeleteBookRequest ) returns ( google.protobuf.Empty ) {
option (google.api.http) = { delete: "/{path=publisher/*/book/*}" };
}

rpc CreatePublisher ( CreatePublisherRequest ) returns ( Publisher ) {
option (google.api.http) = { post: "/publisher" };
}

rpc ReadPublisher ( ReadPublisherRequest ) returns ( Publisher ) {
option (google.api.http) = { get: "/{path=publisher/*}" };
}

rpc DeletePublisher ( DeletePublisherRequest ) returns ( google.protobuf.Empty ) {
option (google.api.http) = { delete: "/{path=publisher/*}" };
}
}
30 changes: 30 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
module github.com/aep-dev/aepc

go 1.20

require (
github.com/jhump/protoreflect v1.15.2
github.com/spf13/cobra v1.7.0
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d
google.golang.org/protobuf v1.31.0
)

require (
github.com/bufbuild/protocompile v0.6.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/net v0.14.0 // indirect
golang.org/x/sync v0.3.0 // indirect
google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

require (
github.com/ghodss/yaml v1.0.0
golang.org/x/sys v0.12.0 // indirect
golang.org/x/text v0.12.0
)
Loading