diff --git a/.gcloudignore b/.gcloudignore new file mode 100644 index 000000000..199e6d9f2 --- /dev/null +++ b/.gcloudignore @@ -0,0 +1,25 @@ +# This file specifies files that are *not* uploaded to Google Cloud Platform +# using gcloud. It follows the same syntax as .gitignore, with the addition of +# "#!include" directives (which insert the entries of the given .gitignore-style +# file at that point). +# +# For more information, run: +# $ gcloud topic gcloudignore +# +.gcloudignore +# If you would like to upload your .git directory, .gitignore file or files +# from your .gitignore file, remove the corresponding line +# below: +.git +.gitignore + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +# Test binary, build with `go test -c` +*.test +# Output of the go coverage tool, specifically when used with LiteIDE +*.out \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..cb0407060 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +# Use the official Golang image to create a build artifact. +# This is based on Debian and sets the GOPATH to /go. +# https://hub.docker.com/_/golang +FROM golang:1.13 as builder + +# Create and change to the app directory. +WORKDIR /app + +# Retrieve application dependencies. +# This allows the container build to reuse cached dependencies. +COPY go.* ./ +RUN go mod download + +# Copy local code to the container image. +COPY . ./ + +# Build the binary. +RUN CGO_ENABLED=0 GOOS=linux go build -mod=readonly -v -o flamed ./cmd/flamed + +# Use the official Alpine image for a lean production container. +# https://hub.docker.com/_/alpine +# https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds +FROM alpine:3 +RUN apk add --no-cache ca-certificates + +# Copy the binary to the production image from the builder stage. +COPY --from=builder /app/flamed /flamed + +# Run the web service on container startup. +CMD ["/flamed"] diff --git a/Makefile b/Makefile index b7d771dc1..5010398e7 100644 --- a/Makefile +++ b/Makefile @@ -8,3 +8,9 @@ test: clean: rm -rf cmd/cli gapic rpc third_party/api-common-protos envoy/proto.pb + +build: + gcloud builds submit --tag gcr.io/${FLAME_PROJECT_IDENTIFIER}/flame + +run: + gcloud run deploy --image gcr.io/${FLAME_PROJECT_IDENTIFIER}/flame --platform managed diff --git a/README.md b/README.md index cbe866d12..00e881aa7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,35 @@ # FLAME Reference Implementation +This directory contains a reference implementation of the FLAME +(Full Lifecycle API Management) API. + +The reference implementation is written in Go. +It stores data using the [Google Cloud Datastore API]() +and can be deployed in a container using [Google Cloud Run](). + +It is implemented as a [gRPC]() service and follows the +Google API Design Guidelines that are published at [aip.dev](https://aip.dev). + +The gRPC service supports gRPC JSON transcoding, which allows a JSON REST API +to be automatically published using a proxy. Configuration files for Envoy are +included. + +The gRPC service is configured to support GAPIC generated API clients +and a Go GAPIC library is generated as part of the build process. + +The build process also creates a command-line interface that is +automatically generated from the API description. + +## Cloud Run + +make build + +make run + +export AUDIENCES=`gcloud beta run services describe flame --platform managed --format="value(status.address.url)"` + +`gcloud auth activate-service-account flame-client@your-project-identifier.iam.gserviceaccount.com --key-file ~/Downloads/your-project-identifier-e48bd9f1c60a.json` + +export CLI_FLAME_TOKEN=`gcloud auth print-identity-token flame-client@your-project-identifier.iam.gserviceaccount.com --audiences="$AUDIENCES"` + +export CLI_FLAME_ADDRESS=flame-ozfrf5bp4a-uw.a.run.app:443 diff --git a/apps/disco-flame/main.go b/apps/disco-flame/main.go index 9fc8b9f1e..11ed6f9e4 100644 --- a/apps/disco-flame/main.go +++ b/apps/disco-flame/main.go @@ -28,6 +28,7 @@ import ( "github.com/golang/protobuf/proto" "github.com/googleapis/gnostic/conversions" discovery "github.com/googleapis/gnostic/discovery" + "golang.org/x/oauth2" "google.golang.org/api/option" "google.golang.org/grpc" ) @@ -201,6 +202,7 @@ func handleExportArgumentsForBytes(arguments map[string]interface{}, bytes []byt initFlame() api := document ctx := context.TODO() + // does the API exist? if not, create it { request := &rpcpb.GetProductRequest{} @@ -371,19 +373,36 @@ func checkSchema(schemaName string, schema *discovery.Schema, depth int) { var FlameClient *gapic.FlameClient func initFlame() error { + var err error var opts []option.ClientOption - address := "localhost:9999" + address := os.Getenv("CLI_FLAME_ADDRESS") if address != "" { opts = append(opts, option.WithEndpoint(address)) } - conn, err := grpc.Dial(address, grpc.WithInsecure()) - if err != nil { - return err + + insecure := false + if insecure { + if address == "" { + return fmt.Errorf("Missing address to use with insecure connection") + } + + conn, err := grpc.Dial(address, grpc.WithInsecure()) + if err != nil { + return err + } + opts = append(opts, option.WithGRPCConn(conn)) } - opts = append(opts, option.WithGRPCConn(conn)) + if token := os.Getenv("CLI_FLAME_TOKEN"); token != "" { + opts = append(opts, option.WithTokenSource(oauth2.StaticTokenSource( + &oauth2.Token{ + AccessToken: token, + TokenType: "Bearer", + }))) + } ctx := context.TODO() FlameClient, err = gapic.NewFlameClient(ctx, opts...) + return err } diff --git a/client/client.go b/client/client.go index aa017b416..748d1dec7 100644 --- a/client/client.go +++ b/client/client.go @@ -16,25 +16,33 @@ package main import ( "context" - "flag" + "crypto/tls" + "crypto/x509" "fmt" "log" + "os" "time" rpc "apigov.dev/flame/rpc" "google.golang.org/grpc" -) - -var ( - serverAddr = flag.String("server_addr", "127.0.0.1:9999", "The server address in the format of host:port") + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/metadata" ) func main() { - flag.Parse() + systemRoots, err := x509.SystemCertPool() + if err != nil { + log.Fatal("failed to load system root CA cert pool") + } + creds := credentials.NewTLS(&tls.Config{ + RootCAs: systemRoots, + }) var opts []grpc.DialOption - opts = append(opts, grpc.WithInsecure()) - conn, err := grpc.Dial(*serverAddr, opts...) + opts = append(opts, grpc.WithTransportCredentials(creds)) + + address := os.Getenv("CLI_FLAME_ADDRESS") + conn, err := grpc.Dial(address, opts...) if err != nil { log.Fatalf("fail to dial: %v", err) } @@ -42,13 +50,18 @@ func main() { client := rpc.NewFlameClient(conn) ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) + token := os.Getenv("CLI_FLAME_TOKEN") + ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+token) req := &rpc.ListProductsRequest{} + req.Parent = "projects/google" res, err := client.ListProducts(ctx, req) if res != nil { fmt.Println("The names of your products:") for _, product := range res.Products { fmt.Println(product.Name) } + } else { + log.Printf("Error %+v", err) } } diff --git a/cmd/flamed/main.go b/cmd/flamed/main.go index c52a3c332..f97d3bbe5 100644 --- a/cmd/flamed/main.go +++ b/cmd/flamed/main.go @@ -14,12 +14,16 @@ package main -import "apigov.dev/flame/server" +import ( + "os" -const ( - port = ":9999" + "apigov.dev/flame/server" ) func main() { - server.RunServer(port) + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + server.RunServer(":" + port) }