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 all 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/*}" };
}
}
28 changes: 28 additions & 0 deletions examples/bookstore.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: "bookstore.example.com"
resources:
- kind: "Book"
properties:
isbn:
type: STRING
number: 1
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.

methods:
create: {}
read: {}
update: {}
delete: {}
list: {}
- kind: "Publisher"
methods:
read: {}
list: {}
- kind: "Author"
properties:
name:
type: STRING
number: 1
parents:
- "Publisher"
methods:
read: {}
101 changes: 101 additions & 0 deletions examples/bookstore.yaml.output.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
syntax = "proto3";

import "google/api/annotations.proto";

import "google/protobuf/empty.proto";

option go_package = "/bookstore";

message Author {
string path = 10000;

string name = 1;
}

message ReadAuthorRequest {
string path = 1;
}

message Book {
string path = 10000;

string isbn = 1;
}

message CreateBookRequest {
string id = 1;

Book resource = 2;
}

message ReadBookRequest {
string path = 1;
}

message UpdateBookRequest {
string path = 1;

UpdateBookRequest resource = 2;
}

message DeleteBookRequest {
string path = 1;
}

message ListBookRequest {
string path = 1;
}

message ListBookResponse {
repeated Book resources = 1;
}

message Publisher {
string path = 10000;
}

message ReadPublisherRequest {
string path = 1;
}

message ListPublisherRequest {
string path = 1;
}

message ListPublisherResponse {
repeated Publisher resources = 1;
}

service Bookstore {
rpc ReadAuthor ( ReadAuthorRequest ) returns ( Author ) {
option (google.api.http) = { get: "/{path=publisher/*/author/*}" };
}

rpc CreateBook ( CreateBookRequest ) returns ( Book ) {
option (google.api.http) = { post: "/{parent=publisher/*}/book" };
}

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

rpc UpdateBook ( UpdateBookRequest ) returns ( Book ) {
option (google.api.http) = { get: "/{resource.name=publisher/*/book/*}" };
}

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

rpc ListBook ( ListBookRequest ) returns ( ListBookResponse ) {
option (google.api.http) = { get: "/{parent=publisher/*}/book" };
}

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

rpc ListPublisher ( ListPublisherRequest ) returns ( ListPublisherResponse ) {
option (google.api.http) = { get: "/publisher" };
}
}
Loading