From bfccd155a9eb9b659faf4918a19de267bf6da123 Mon Sep 17 00:00:00 2001 From: Tim Burks Date: Thu, 12 Oct 2023 21:53:07 -0700 Subject: [PATCH] Add zero experimental tool --- cmd/zero/DOCUMENTATION.md | 113 +++++ cmd/zero/README.md | 25 ++ cmd/zero/cmd/apikeys/cmd.go | 34 ++ cmd/zero/cmd/apikeys/create.go | 73 ++++ cmd/zero/cmd/apikeys/delete.go | 62 +++ cmd/zero/cmd/apikeys/get.go | 62 +++ cmd/zero/cmd/apikeys/getKeyString.go | 62 +++ cmd/zero/cmd/apikeys/list.go | 79 ++++ cmd/zero/cmd/apikeys/operations/cmd.go | 27 ++ cmd/zero/cmd/apikeys/operations/get.go | 62 +++ cmd/zero/cmd/cmd.go | 37 ++ cmd/zero/cmd/petstore/README.md | 103 +++++ cmd/zero/cmd/petstore/cmd.go | 29 ++ cmd/zero/cmd/petstore/config.go | 146 +++++++ cmd/zero/cmd/petstore/serve.go | 93 ++++ cmd/zero/cmd/servicecontrol/check.go | 78 ++++ cmd/zero/cmd/servicecontrol/checkv2.go | 79 ++++ cmd/zero/cmd/servicecontrol/cmd.go | 53 +++ cmd/zero/cmd/servicecontrol/report.go | 281 ++++++++++++ cmd/zero/cmd/servicemanagement/cmd.go | 31 ++ .../cmd/servicemanagement/operations/cmd.go | 27 ++ .../cmd/servicemanagement/operations/get.go | 62 +++ .../cmd/servicemanagement/services/cmd.go | 34 ++ .../servicemanagement/services/configs/cmd.go | 29 ++ .../services/configs/create.go | 138 ++++++ .../servicemanagement/services/configs/get.go | 63 +++ .../services/configs/list.go | 78 ++++ .../cmd/servicemanagement/services/create.go | 67 +++ .../cmd/servicemanagement/services/delete.go | 62 +++ .../cmd/servicemanagement/services/get.go | 62 +++ .../cmd/servicemanagement/services/list.go | 80 ++++ .../services/rollouts/cmd.go | 29 ++ .../services/rollouts/create.go | 71 +++ .../services/rollouts/get.go | 63 +++ .../services/rollouts/list.go | 78 ++++ cmd/zero/main.go | 28 ++ cmd/zero/pkg/config/client.go | 47 ++ cmd/zero/pkg/config/config.go | 48 +++ cmd/zero/pkg/patch/patch.go | 132 ++++++ cmd/zero/pkg/servicebuilder/builder.go | 406 ++++++++++++++++++ cmd/zero/pkg/servicecontrol/control.go | 345 +++++++++++++++ 41 files changed, 3478 insertions(+) create mode 100644 cmd/zero/DOCUMENTATION.md create mode 100644 cmd/zero/README.md create mode 100644 cmd/zero/cmd/apikeys/cmd.go create mode 100644 cmd/zero/cmd/apikeys/create.go create mode 100644 cmd/zero/cmd/apikeys/delete.go create mode 100644 cmd/zero/cmd/apikeys/get.go create mode 100644 cmd/zero/cmd/apikeys/getKeyString.go create mode 100644 cmd/zero/cmd/apikeys/list.go create mode 100644 cmd/zero/cmd/apikeys/operations/cmd.go create mode 100644 cmd/zero/cmd/apikeys/operations/get.go create mode 100644 cmd/zero/cmd/cmd.go create mode 100644 cmd/zero/cmd/petstore/README.md create mode 100644 cmd/zero/cmd/petstore/cmd.go create mode 100644 cmd/zero/cmd/petstore/config.go create mode 100644 cmd/zero/cmd/petstore/serve.go create mode 100644 cmd/zero/cmd/servicecontrol/check.go create mode 100644 cmd/zero/cmd/servicecontrol/checkv2.go create mode 100644 cmd/zero/cmd/servicecontrol/cmd.go create mode 100644 cmd/zero/cmd/servicecontrol/report.go create mode 100644 cmd/zero/cmd/servicemanagement/cmd.go create mode 100644 cmd/zero/cmd/servicemanagement/operations/cmd.go create mode 100644 cmd/zero/cmd/servicemanagement/operations/get.go create mode 100644 cmd/zero/cmd/servicemanagement/services/cmd.go create mode 100644 cmd/zero/cmd/servicemanagement/services/configs/cmd.go create mode 100644 cmd/zero/cmd/servicemanagement/services/configs/create.go create mode 100644 cmd/zero/cmd/servicemanagement/services/configs/get.go create mode 100644 cmd/zero/cmd/servicemanagement/services/configs/list.go create mode 100644 cmd/zero/cmd/servicemanagement/services/create.go create mode 100644 cmd/zero/cmd/servicemanagement/services/delete.go create mode 100644 cmd/zero/cmd/servicemanagement/services/get.go create mode 100644 cmd/zero/cmd/servicemanagement/services/list.go create mode 100644 cmd/zero/cmd/servicemanagement/services/rollouts/cmd.go create mode 100644 cmd/zero/cmd/servicemanagement/services/rollouts/create.go create mode 100644 cmd/zero/cmd/servicemanagement/services/rollouts/get.go create mode 100644 cmd/zero/cmd/servicemanagement/services/rollouts/list.go create mode 100644 cmd/zero/main.go create mode 100644 cmd/zero/pkg/config/client.go create mode 100644 cmd/zero/pkg/config/config.go create mode 100644 cmd/zero/pkg/patch/patch.go create mode 100644 cmd/zero/pkg/servicebuilder/builder.go create mode 100644 cmd/zero/pkg/servicecontrol/control.go diff --git a/cmd/zero/DOCUMENTATION.md b/cmd/zero/DOCUMENTATION.md new file mode 100644 index 00000000..f0da79f1 --- /dev/null +++ b/cmd/zero/DOCUMENTATION.md @@ -0,0 +1,113 @@ +# Zero + +Simple API management without a gateway. Instead of using a gateway, we will +make direct calls to service control APIs from within our API server. + +Is this a good idea? + +Pros: + +- simple (no proxies to set up and manage) +- inexpensive (no additional sidecars to operate) +- runs anywhere (even when you run your server locally) + +Cons: + +- requires changes to your application +- hard to govern, there may be uncontrolled APIs that leak information + +## Demonstration + +### Preparation + +#### You need a domain + +To register a service with service manager, a domain is required. + +##### Use a domain you control + +We can register a domain with a registrar and prove to Google that we own it, +and then we can create services on that domain or any subdomain. +“example1.timbx.me” + +##### Get a domain from App Engine + +Alternately, we can use Google App Engine to get a domain that we can use. App +Engine apps are hosted at .appspot.com, where is usually the +project id. + +- create an app engine app for your project +- this will give you “appname.appspot.com”. For example, for my project, named + “nerdvana”, my domain name is “nerdvana.appspot.com” +- we can use this for our service name. +- we can also use subdomains of this domain. + +#### Create OAuth credentials + +These will be used to call the servicemanagement API + +store them in ~/.config/zero/credentials.json + +We could also use a service account for this. + +#### Set up the CLI + +create ~/.config/zero/zero.yaml for general configuration + +``` +serviceName: nerdvana.appspot.com +serviceConfig: 2023-10-06r1 +apiKey: XXX-REDACTED-XXX +producerProject: nerdvana +consumerProject: nerdvana +summary: "Namaste" +title: "Nerdvana" +``` + +### Service Management + +#### Create your service + +Create your service with a call to the service management API + +View the service in the endpoints console + +#### Configure your service + +Create your service config -- notice that we want to specify some things: + +- name / version +- description +- operations + +#### Rollout your configuration + +Rollout your service config + +Verify the rollout in the endpoints console + +### Service Control + +Create a service account to call the servicecontrol API + +Call the check service + +Call the check/allocatequota/report methods + +### The Sample API + +TODO + +## Capabilities and Limitations + +TODO + +## Using the Service Management API to build an API Catalog + +Get a list of managed services and the configurations for each. Each +configuration includes the list of operations that the API supports. These +lists can be used to build an index of APIs being provided by your Google +project. + +Not all APIs -- these are just the formally registered ones. Other indicators +of APIs are Load Balancers, Cloud Run endpoints, and GKE ingresses. diff --git a/cmd/zero/README.md b/cmd/zero/README.md new file mode 100644 index 00000000..f998caf7 --- /dev/null +++ b/cmd/zero/README.md @@ -0,0 +1,25 @@ +# zero + +This directory contains an experimental command-line tool that explores the +[Google Service Infrastructure](https://cloud.google.com/service-infrastructure/docs/overview). + +This tool is temporarily named `zero` to indicate that this is a minimalist +effort to explore the Service Infrastructure APIs out of the context of an API +gateway. + +Areas to explore include: + +- Automatically creating managed services and manage service configurations for + APIs in an API registry. +- Automatically importing information into an API registry from the + [Service Management API](https://cloud.google.com/service-infrastructure/docs/service-management/getting-started). +- Directly calling the + [Service Control API](https://cloud.google.com/service-infrastructure/docs/service-control/getting-started) + as an alternative to using an API proxy for the most basic API management + needs. + +Custom components used by the tool are in the `pkg` directory. Implementations +of CLI subcommands are in the `cmd` directory. + +Code is published for transparency and community review with no promises of +support or continued existence. diff --git a/cmd/zero/cmd/apikeys/cmd.go b/cmd/zero/cmd/apikeys/cmd.go new file mode 100644 index 00000000..6303f709 --- /dev/null +++ b/cmd/zero/cmd/apikeys/cmd.go @@ -0,0 +1,34 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 apikeys + +import ( + "github.com/apigee/registry-experimental/cmd/zero/cmd/apikeys/operations" + "github.com/spf13/cobra" +) + +func Cmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "api-keys", + Short: "API key management", + } + cmd.AddCommand(createCmd()) + cmd.AddCommand(deleteCmd()) + cmd.AddCommand(getCmd()) + cmd.AddCommand(getKeyStringCmd()) + cmd.AddCommand(listCmd()) + cmd.AddCommand(operations.Cmd()) + return cmd +} diff --git a/cmd/zero/cmd/apikeys/create.go b/cmd/zero/cmd/apikeys/create.go new file mode 100644 index 00000000..b4bf90d3 --- /dev/null +++ b/cmd/zero/cmd/apikeys/create.go @@ -0,0 +1,73 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 apikeys + +import ( + "context" + "fmt" + + "github.com/apigee/registry-experimental/cmd/zero/pkg/config" + "github.com/apigee/registry-experimental/cmd/zero/pkg/patch" + "github.com/spf13/cobra" + "google.golang.org/api/apikeys/v2" + "google.golang.org/api/option" +) + +func createCmd() *cobra.Command { + var output string + var producerProject string + cmd := &cobra.Command{ + Use: "create KEYID SERVICE", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + client, err := config.GetClient(ctx) + if err != nil { + return err + } + apikey := &apikeys.V2Key{ + Restrictions: &apikeys.V2Restrictions{ + ApiTargets: []*apikeys.V2ApiTarget{ + {Service: args[1]}, + }, + }, + } + srv, err := apikeys.NewService(ctx, option.WithHTTPClient(client)) + if err != nil { + return err + } + parent := fmt.Sprintf("projects/%s/locations/global", producerProject) + keyId := args[0] + item, err := apikeys.NewProjectsLocationsKeysService(srv). + Create(parent, apikey). + KeyId(keyId). + Do() + if err != nil { + return err + } + bytes, err := patch.MarshalAndWrap(item, "Operation", item.Name, output) + if err != nil { + return err + } + if _, err := cmd.OutOrStdout().Write(bytes); err != nil { + return err + } + return nil + }, + } + cmd.Flags().StringVarP(&output, "output", "o", "yaml", "Output format. One of: (yaml, json).") + cmd.Flags().StringVarP(&producerProject, "project", "p", "", "Producer project.") + return cmd +} diff --git a/cmd/zero/cmd/apikeys/delete.go b/cmd/zero/cmd/apikeys/delete.go new file mode 100644 index 00000000..6ae9ac8e --- /dev/null +++ b/cmd/zero/cmd/apikeys/delete.go @@ -0,0 +1,62 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 apikeys + +import ( + "context" + + "github.com/apigee/registry-experimental/cmd/zero/pkg/config" + "github.com/apigee/registry-experimental/cmd/zero/pkg/patch" + "google.golang.org/api/apikeys/v2" + "google.golang.org/api/option" + + "github.com/spf13/cobra" +) + +func deleteCmd() *cobra.Command { + var output string + cmd := &cobra.Command{ + Use: "delete APIKEY", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + client, err := config.GetClient(ctx) + if err != nil { + return err + } + apikey := args[0] + srv, err := apikeys.NewService(ctx, option.WithHTTPClient(client)) + if err != nil { + return err + } + item, err := apikeys.NewProjectsLocationsKeysService(srv). + Delete(apikey). + Do() + if err != nil { + return err + } + bytes, err := patch.MarshalAndWrap(item, "Operation", item.Name, output) + if err != nil { + return err + } + if _, err := cmd.OutOrStdout().Write(bytes); err != nil { + return err + } + return nil + }, + } + cmd.Flags().StringVarP(&output, "output", "o", "yaml", "Output format. One of: (yaml, json).") + return cmd +} diff --git a/cmd/zero/cmd/apikeys/get.go b/cmd/zero/cmd/apikeys/get.go new file mode 100644 index 00000000..581db80b --- /dev/null +++ b/cmd/zero/cmd/apikeys/get.go @@ -0,0 +1,62 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 apikeys + +import ( + "context" + + "github.com/apigee/registry-experimental/cmd/zero/pkg/config" + "github.com/apigee/registry-experimental/cmd/zero/pkg/patch" + "google.golang.org/api/apikeys/v2" + "google.golang.org/api/option" + + "github.com/spf13/cobra" +) + +func getCmd() *cobra.Command { + var output string + cmd := &cobra.Command{ + Use: "get APIKEY", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + client, err := config.GetClient(ctx) + if err != nil { + return err + } + apikey := args[0] + srv, err := apikeys.NewService(ctx, option.WithHTTPClient(client)) + if err != nil { + return err + } + item, err := apikeys.NewProjectsLocationsKeysService(srv). + Get(apikey). + Do() + if err != nil { + return err + } + bytes, err := patch.MarshalAndWrap(item, "ApiKey", item.Name, output) + if err != nil { + return err + } + if _, err := cmd.OutOrStdout().Write(bytes); err != nil { + return err + } + return nil + }, + } + cmd.Flags().StringVarP(&output, "output", "o", "yaml", "Output format. One of: (yaml, json).") + return cmd +} diff --git a/cmd/zero/cmd/apikeys/getKeyString.go b/cmd/zero/cmd/apikeys/getKeyString.go new file mode 100644 index 00000000..bc2c2c1c --- /dev/null +++ b/cmd/zero/cmd/apikeys/getKeyString.go @@ -0,0 +1,62 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 apikeys + +import ( + "context" + + "github.com/apigee/registry-experimental/cmd/zero/pkg/config" + "github.com/apigee/registry-experimental/cmd/zero/pkg/patch" + "google.golang.org/api/apikeys/v2" + "google.golang.org/api/option" + + "github.com/spf13/cobra" +) + +func getKeyStringCmd() *cobra.Command { + var output string + cmd := &cobra.Command{ + Use: "get-key-string APIKEY", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + client, err := config.GetClient(ctx) + if err != nil { + return err + } + apikey := args[0] + srv, err := apikeys.NewService(ctx, option.WithHTTPClient(client)) + if err != nil { + return err + } + item, err := apikeys.NewProjectsLocationsKeysService(srv). + GetKeyString(apikey). + Do() + if err != nil { + return err + } + bytes, err := patch.MarshalAndWrap(item, "ApiKeyString", apikey, output) + if err != nil { + return err + } + if _, err := cmd.OutOrStdout().Write(bytes); err != nil { + return err + } + return nil + }, + } + cmd.Flags().StringVarP(&output, "output", "o", "yaml", "Output format. One of: (yaml, json).") + return cmd +} diff --git a/cmd/zero/cmd/apikeys/list.go b/cmd/zero/cmd/apikeys/list.go new file mode 100644 index 00000000..33b0a3b1 --- /dev/null +++ b/cmd/zero/cmd/apikeys/list.go @@ -0,0 +1,79 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 apikeys + +import ( + "context" + "fmt" + + "github.com/apigee/registry-experimental/cmd/zero/pkg/config" + "github.com/apigee/registry-experimental/cmd/zero/pkg/patch" + "google.golang.org/api/apikeys/v2" + "google.golang.org/api/option" + + "github.com/spf13/cobra" +) + +func listCmd() *cobra.Command { + var output string + var producerProject string + cmd := &cobra.Command{ + Use: "list", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + client, err := config.GetClient(ctx) + if err != nil { + return err + } + srv, err := apikeys.NewService(ctx, option.WithHTTPClient(client)) + if err != nil { + return err + } + var cursor string + c := 0 + for i := 0; true; i++ { + items, err := apikeys.NewProjectsLocationsKeysService(srv). + List(fmt.Sprintf("projects/%s/locations/global", producerProject)). + PageToken(cursor). + Do() + if err != nil { + return err + } + for _, item := range items.Keys { + bytes, err := patch.MarshalAndWrap(item, "ApiKey", item.Name, output) + if err != nil { + return err + } + if c > 0 && output != "json" { + fmt.Printf("---\n") + } + c++ + if _, err := cmd.OutOrStdout().Write(bytes); err != nil { + return err + } + } + cursor = items.NextPageToken + if cursor == "" { + break + } + } + return nil + }, + } + cmd.Flags().StringVarP(&output, "output", "o", "yaml", "Output format. One of: (yaml, json).") + cmd.Flags().StringVarP(&producerProject, "project", "p", "", "Producer project.") + return cmd +} diff --git a/cmd/zero/cmd/apikeys/operations/cmd.go b/cmd/zero/cmd/apikeys/operations/cmd.go new file mode 100644 index 00000000..ef19ea82 --- /dev/null +++ b/cmd/zero/cmd/apikeys/operations/cmd.go @@ -0,0 +1,27 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 operations + +import ( + "github.com/spf13/cobra" +) + +func Cmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "operations", + } + cmd.AddCommand(getCmd()) + return cmd +} diff --git a/cmd/zero/cmd/apikeys/operations/get.go b/cmd/zero/cmd/apikeys/operations/get.go new file mode 100644 index 00000000..0e21234c --- /dev/null +++ b/cmd/zero/cmd/apikeys/operations/get.go @@ -0,0 +1,62 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 operations + +import ( + "context" + + "github.com/apigee/registry-experimental/cmd/zero/pkg/config" + "github.com/apigee/registry-experimental/cmd/zero/pkg/patch" + "google.golang.org/api/apikeys/v2" + "google.golang.org/api/option" + + "github.com/spf13/cobra" +) + +func getCmd() *cobra.Command { + var output string + cmd := &cobra.Command{ + Use: "get OPERATION", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + client, err := config.GetClient(ctx) + if err != nil { + return err + } + operation := args[0] + srv, err := apikeys.NewService(ctx, option.WithHTTPClient(client)) + if err != nil { + return err + } + item, err := srv.Operations. + Get(operation). + Do() + if err != nil { + return err + } + bytes, err := patch.MarshalAndWrap(item, "Operation", item.Name, output) + if err != nil { + return err + } + if _, err := cmd.OutOrStdout().Write(bytes); err != nil { + return err + } + return nil + }, + } + cmd.Flags().StringVarP(&output, "output", "o", "yaml", "Output format. One of: (yaml, json).") + return cmd +} diff --git a/cmd/zero/cmd/cmd.go b/cmd/zero/cmd/cmd.go new file mode 100644 index 00000000..fdabdb75 --- /dev/null +++ b/cmd/zero/cmd/cmd.go @@ -0,0 +1,37 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 ( + "context" + + "github.com/apigee/registry-experimental/cmd/zero/cmd/apikeys" + "github.com/apigee/registry-experimental/cmd/zero/cmd/petstore" + "github.com/apigee/registry-experimental/cmd/zero/cmd/servicecontrol" + "github.com/apigee/registry-experimental/cmd/zero/cmd/servicemanagement" + "github.com/spf13/cobra" +) + +func Cmd(ctx context.Context) *cobra.Command { + cmd := &cobra.Command{ + Use: "zero", + } + + cmd.AddCommand(apikeys.Cmd()) + cmd.AddCommand(petstore.Cmd()) + cmd.AddCommand(servicecontrol.Cmd()) + cmd.AddCommand(servicemanagement.Cmd()) + return cmd +} diff --git a/cmd/zero/cmd/petstore/README.md b/cmd/zero/cmd/petstore/README.md new file mode 100644 index 00000000..4a449a99 --- /dev/null +++ b/cmd/zero/cmd/petstore/README.md @@ -0,0 +1,103 @@ +# petstore + +This directory contains subcommands that provide a demonstration API server +that uses Service Control to check and log API requests. + +## Create credentials. + +To run the example, we'll need to call several Google APIs, and we'll +authenticate our calls using a service account. Create a service account in the +Cloud Console and download a JSON key. Move this file to +`~/.config/zero/control.json`. + +The service account needs the following roles: + +- Service Management Administrator, to register and configure our service +- Service Controller, to make calls to the Service Control API +- API Keys Admin, to create and manage API keys + +## Register the service. + +Next we will use the Service Management API to create a "managed service". This +is a top-level entity that we'll use to track service descriptions, access +controls, and logs. + +The Service Management API requires that service names be domains or subdomains +that are known to belong to us. That is because Service Infrastructure is often +used to manage services that Google hosts. If we're only using Service Control +that's not the case: Google is not hosting our service, but the requirement is +still enforced. + +To use a domain that we own as our service name, we can prove our ownership by +following the instructions +[here](https://cloud.google.com/endpoints/docs/openapi/verify-domain-name). +Once we've verified ownership, we can use our verified domain name or any +subdomain as a service name. + +A simpler alternative is to instead use a domain name that Google controls and +delegates to us. As described +[here](https://cloud.google.com/endpoints/docs/openapi/configure-endpoints#use_a_domain_managed_by_google), +these can be `*projectname*.appspot.com` or any subdomain of +`endpoints.*projectname*.cloud.goog`. Subdomains of `*projectname*.appspot.com` +are also ok, so if we had a project named `nerdvana`, we might create a service +named `petstore.nerdvana.appspot.com`. + +Again, when we're only using Service Control, this name is just an identifier +and doesn't need to match the hostname of our service. + +We can create the service using our CLI. + +``` +zero service-management services create petstore.nerdvana.appspot.com --project nerdvana +``` + +## Generate the Service Configuration. + +We describe our service to Service Infrastructure using a structure called +Service Config. Service Config can be generated by tools that read API +descriptions from various formats, but for our example, we will generate the +Service Config directly with code in our CLI. + +We can run the following to view the Service Config: + +``` +zero petstore config petstore.nerdvana.appspot.com +``` + +We can apply it by adding the `--apply` and `--project` flags. + +``` +zero petstore config petstore.nerdvana.appspot.com --project nerdvana +``` + +Note the contents of the `id` field in the result. We'll need that to roll out +our configuration. + +## Roll out the service configuration. + +``` +zero service-management services rollouts create petstore.nerdvana.appspot.com 2023-10-12r0 +``` + +Note that this is a long-running operation. We can check its status in the +Cloud Console. + +## Generate an API key. + +``` +gcloud alpha services api-keys create --api-target=service=petstore.nerdvana.appspot.com +``` + +``` +export APIKEY=`zero api-keys get-key-string projects/477308746250/locations/global/keys/k1 | yq .data.keyString -r` +``` + +## Run the server and make requests. + +``` +ab -n 50 -c 10 -H "X-Api-Key: $APIKEY" http://localhost:8080/v1/pets +ab -n 50 -c 10 -H "X-Api-Key: $APIKEY" http://localhost:8080/v1/pets/1 +ab -n 50 -c 10 -H "X-Api-Key: $APIKEY" -p pet.json -T application/json http://localhost:8080/v1/pets +``` + +## View the results in the Endpoints console. diff --git a/cmd/zero/cmd/petstore/cmd.go b/cmd/zero/cmd/petstore/cmd.go new file mode 100644 index 00000000..1235bacd --- /dev/null +++ b/cmd/zero/cmd/petstore/cmd.go @@ -0,0 +1,29 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 petstore + +import ( + "github.com/spf13/cobra" +) + +func Cmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "petstore", + Short: "petstore API", + } + cmd.AddCommand(configCmd()) + cmd.AddCommand(serveCmd()) + return cmd +} diff --git a/cmd/zero/cmd/petstore/config.go b/cmd/zero/cmd/petstore/config.go new file mode 100644 index 00000000..82712910 --- /dev/null +++ b/cmd/zero/cmd/petstore/config.go @@ -0,0 +1,146 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 petstore + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/apigee/registry-experimental/cmd/zero/pkg/config" + "github.com/apigee/registry-experimental/cmd/zero/pkg/patch" + "github.com/apigee/registry-experimental/cmd/zero/pkg/servicebuilder" + "google.golang.org/api/option" + "google.golang.org/api/servicemanagement/v1" + + "github.com/spf13/cobra" +) + +func configCmd() *cobra.Command { + var apply bool + var output string + var project string + cmd := &cobra.Command{ + Use: "config SERVICE", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + client, err := config.GetClient(ctx) + if err != nil { + return err + } + api := &servicebuilder.Api{ + Name: args[0], + Version: "1.0.0", + Operations: []servicebuilder.Operation{ + { + Id: "GetPets", + Method: "GET", + Path: "/v1/pets", + }, + { + Id: "GetPetById", + Method: "GET", + Path: "/v1/pets/:id", + }, + { + Id: "CreatePet", + Method: "POST", + Path: "/v1/pets", + }, + }, + } + apis := []*servicebuilder.Api{api} + service := &servicemanagement.Service{ + Apis: servicebuilder.Apis(apis), + Authentication: nil, + ConfigVersion: 3, + Control: servicebuilder.Control(), + Documentation: &servicemanagement.Documentation{ + Summary: "Petstore API", + }, + Endpoints: []*servicemanagement.Endpoint{ + { + Name: args[0], + }, + }, + Enums: servicebuilder.Enums(), + Http: servicebuilder.Http(apis), + Logging: servicebuilder.Logging(), + Logs: servicebuilder.Logs(), + Name: args[0], + ProducerProjectId: project, + SystemParameters: nil, + Title: "Petstore", + Metrics: servicebuilder.Metrics(), + MonitoredResources: servicebuilder.MonitoredResources(), + Monitoring: servicebuilder.Monitoring(), + // https://cloud.google.com/endpoints/docs/grpc/quotas-configure + Quota: &servicemanagement.Quota{ + Limits: []*servicemanagement.QuotaLimit{ + { + Name: "calls", + Metric: "calls", + Unit: "1/min/{project}", + Values: map[string]string{ + "STANDARD": "60", + }, + }, + }, + MetricRules: []*servicemanagement.MetricRule{ + { + MetricCosts: map[string]string{ + "calls": "1", + }, + Selector: "*", + }, + }, + }, + Types: servicebuilder.Types(), + Usage: servicebuilder.Usage(apis), + } + if !apply { + serviceJSON, err := json.MarshalIndent(service, "", " ") + if err != nil { + return err + } + fmt.Printf("%s", string(serviceJSON)) + return nil + } + srv, err := servicemanagement.NewService(ctx, option.WithHTTPClient(client)) + if err != nil { + return err + } + item, err := srv.Services.Configs. + Create(args[0], service). + Do() + if err != nil { + return err + } + bytes, err := patch.MarshalAndWrap(item, "ServiceConfig", item.Name, output) + if err != nil { + return err + } + if _, err := cmd.OutOrStdout().Write(bytes); err != nil { + return err + } + return nil + }, + } + cmd.Flags().StringVarP(&output, "output", "o", "yaml", "Output format. One of: (yaml, json).") + cmd.Flags().StringVarP(&project, "project", "p", "", "Project.") + cmd.Flags().BoolVar(&apply, "apply", false, "Apply config.") + return cmd +} diff --git a/cmd/zero/cmd/petstore/serve.go b/cmd/zero/cmd/petstore/serve.go new file mode 100644 index 00000000..85e08d38 --- /dev/null +++ b/cmd/zero/cmd/petstore/serve.go @@ -0,0 +1,93 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 petstore + +import ( + "net/http" + + "github.com/apigee/registry-experimental/cmd/zero/pkg/servicecontrol" + "github.com/gin-gonic/gin" + "github.com/spf13/cobra" +) + +func serveCmd() *cobra.Command { + var verbose bool + cmd := &cobra.Command{ + Use: "serve SERVICE", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + router := gin.Default() + router.Use(servicecontrol.Middleware(args[0], verbose)) + router.GET("/v1/pets", GetPets) + router.GET("/v1/pets/:id", GetPetById) + router.POST("/v1/pets", CreatePet) + return router.Run("0.0.0.0:8080") + }, + } + cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Verbose logging.") + return cmd +} + +// pet represents data about a pet. +type pet struct { + ID string `json:"id"` + Name string `json:"name"` + Tag string `json:"tag"` +} + +// pets slice to seed pet data. +var pets = []pet{ + {ID: "1", Name: "Tardar Sauce", Tag: "cat"}, + {ID: "2", Name: "Bo", Tag: "dog"}, + {ID: "3", Name: "Toto", Tag: "dog"}, +} + +// GetPets responds with the list of all pets as JSON. +func GetPets(c *gin.Context) { + c.IndentedJSON(http.StatusOK, pets) +} + +// CreatePet adds a pet from JSON received in the request body. +func CreatePet(c *gin.Context) { + var newpet pet + + // Call BindJSON to bind the received JSON to newpet. + if err := c.BindJSON(&newpet); err != nil { + return + } + + // Add the new pet to the slice. + pets = append(pets, newpet) + c.IndentedJSON(http.StatusCreated, newpet) +} + +// GetPetById locates the pet whose ID value matches the id +// parameter sent by the client, then returns that pet as a response. +func GetPetById(c *gin.Context) { + id := c.Param("id") + + // Loop through the list of pets, looking for + // a pet whose ID value matches the parameter. + for _, a := range pets { + if a.ID == id { + c.IndentedJSON(http.StatusOK, a) + return + } + } + c.IndentedJSON(http.StatusNotFound, gin.H{ + "code": http.StatusNotFound, + "message": "pet not found", + }) +} diff --git a/cmd/zero/cmd/servicecontrol/check.go b/cmd/zero/cmd/servicecontrol/check.go new file mode 100644 index 00000000..810e5e56 --- /dev/null +++ b/cmd/zero/cmd/servicecontrol/check.go @@ -0,0 +1,78 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 servicecontrol + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/apigee/registry-experimental/cmd/zero/pkg/config" + "google.golang.org/api/option" + "google.golang.org/api/servicecontrol/v1" + + "github.com/google/uuid" + "github.com/spf13/cobra" +) + +func checkCmd() *cobra.Command { + var output string + cmd := &cobra.Command{ + Use: "check", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + client, err := config.GetClient(ctx) + if err != nil { + return err + } + srv, err := servicecontrol.NewService(ctx, option.WithHTTPClient(client)) + if err != nil { + return err + } + now := time.Now() + timestamp := now.Format(time.RFC3339) + + request := &servicecontrol.CheckRequest{ + Operation: &servicecontrol.Operation{ + OperationId: uuid.New().String(), + OperationName: "/hello", + //ConsumerId: "project:" + producerProject, + ConsumerId: "api_key:" + apiKey, + StartTime: timestamp, + Labels: map[string]string{ + "cloud.googleapis.com/service": serviceName, + "serviceruntime.googleapis.com/api_method": "1.hello_nbuv3ljuva_uw_a_run_app.Hello", + "servicecontrol.googleapis.com/caller_ip": "172.125.77.209", + "servicecontrol.googleapis.com/user_agent": "ESPv2", + }, + }, + } + result, err := srv.Services.Check(serviceName, request).Do() + if err != nil { + return err + } + bytes, err := json.MarshalIndent(result, "", " ") + if err != nil { + return &json.UnsupportedValueError{} + } + fmt.Printf("%s", string(bytes)) + return nil + }, + } + cmd.Flags().StringVarP(&output, "output", "o", "yaml", "Output format. One of: (yaml, json).") + return cmd +} diff --git a/cmd/zero/cmd/servicecontrol/checkv2.go b/cmd/zero/cmd/servicecontrol/checkv2.go new file mode 100644 index 00000000..202bb094 --- /dev/null +++ b/cmd/zero/cmd/servicecontrol/checkv2.go @@ -0,0 +1,79 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 servicecontrol + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/apigee/registry-experimental/cmd/zero/pkg/config" + "google.golang.org/api/option" + "google.golang.org/api/servicecontrol/v2" + + "github.com/spf13/cobra" +) + +func checkV2Cmd() *cobra.Command { + var output string + cmd := &cobra.Command{ + Use: "checkv2", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + client, err := config.GetClient(ctx) + if err != nil { + return err + } + srv, err := servicecontrol.NewService(ctx, option.WithHTTPClient(client)) + if err != nil { + return err + } + //now := time.Now() + //timestamp := now.Format(time.RFC3339) + + request := &servicecontrol.CheckRequest{ + Attributes: &servicecontrol.AttributeContext{}, + /* + Operation: &servicecontrol.Operation{ + OperationId: uuid.New().String(), + OperationName: "/hello", + ConsumerId: "project:" + producerProject, + ConsumerId: "api_key:" + apiKey, + StartTime: timestamp, + Labels: map[string]string{ + "cloud.googleapis.com/service": serviceName, + "serviceruntime.googleapis.com/api_method": "1.hello_nbuv3ljuva_uw_a_run_app.Hello", + "servicecontrol.googleapis.com/caller_ip": "172.125.77.209", + "servicecontrol.googleapis.com/user_agent": "ESPv2", + }, + }, + */ + } + result, err := srv.Services.Check(serviceName, request).Do() + if err != nil { + return err + } + bytes, err := json.MarshalIndent(result, "", " ") + if err != nil { + return &json.UnsupportedValueError{} + } + fmt.Printf("%s", string(bytes)) + return nil + }, + } + cmd.Flags().StringVarP(&output, "output", "o", "yaml", "Output format. One of: (yaml, json).") + return cmd +} diff --git a/cmd/zero/cmd/servicecontrol/cmd.go b/cmd/zero/cmd/servicecontrol/cmd.go new file mode 100644 index 00000000..b9aa8460 --- /dev/null +++ b/cmd/zero/cmd/servicecontrol/cmd.go @@ -0,0 +1,53 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 servicecontrol + +import ( + "log" + + "github.com/apigee/registry-experimental/cmd/zero/pkg/config" + "github.com/spf13/cobra" +) + +var Verbose bool + +func Cmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "service-control", + Short: "service control", + } + cmd.AddCommand(checkCmd()) + cmd.AddCommand(reportCmd()) + cmd.AddCommand(checkV2Cmd()) + return cmd +} + +var serviceName = "" +var serviceConfig = "" +var apiKey = "" +var producerProject = "" +var consumerProject = "" + +func init() { + c, err := config.GetConfig() + if err != nil { + log.Fatalf("No config file: %s", err) + } + serviceName = c.ServiceName + serviceConfig = c.ServiceConfig + apiKey = c.ApiKey + producerProject = c.ProducerProject + consumerProject = c.ConsumerProject +} diff --git a/cmd/zero/cmd/servicecontrol/report.go b/cmd/zero/cmd/servicecontrol/report.go new file mode 100644 index 00000000..405aed23 --- /dev/null +++ b/cmd/zero/cmd/servicecontrol/report.go @@ -0,0 +1,281 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 servicecontrol + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "math" + "strings" + "time" + + "github.com/apigee/registry-experimental/cmd/zero/pkg/config" + "google.golang.org/api/option" + "google.golang.org/api/servicecontrol/v1" + + "github.com/google/uuid" + "github.com/spf13/cobra" +) + +type distOptions struct { + Buckets int64 + Growth float64 + Scale float64 +} + +func createInt64MetricSet(name string, value int64) *servicecontrol.MetricValueSet { + return &servicecontrol.MetricValueSet{ + MetricName: name, + MetricValues: []*servicecontrol.MetricValue{ + { + Int64Value: &value, + }, + }, + } +} + +var ( + timeDistOptions = distOptions{29, 2.0, 1e-6} + sizeDistOptions = distOptions{8, 10.0, 1} +) + +func createDistMetricSet(options *distOptions, name string, value int64) *servicecontrol.MetricValueSet { + buckets := make([]int64, options.Buckets+2) + fValue := float64(value) + idx := 0 + if fValue >= options.Scale { + idx = 1 + int(math.Log(fValue/options.Scale)/math.Log(options.Growth)) + if idx >= len(buckets) { + idx = len(buckets) - 1 + } + } + buckets[idx] = 1 + distValue := servicecontrol.Distribution{ + Count: 1, + BucketCounts: buckets, + ExponentialBuckets: &servicecontrol.ExponentialBuckets{ + NumFiniteBuckets: options.Buckets, + GrowthFactor: options.Growth, + Scale: options.Scale, + }, + } + if value != 0 { + distValue.Mean = fValue + distValue.Minimum = fValue + distValue.Maximum = fValue + } + return &servicecontrol.MetricValueSet{ + MetricName: name, + MetricValues: []*servicecontrol.MetricValue{ + { + DistributionValue: &distValue, + }, + }, + } +} + +func reportCmd() *cobra.Command { + var output string + var iterations int + cmd := &cobra.Command{ + Use: "report", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + client, err := config.GetClient(ctx) + if err != nil { + return err + } + srv, err := servicecontrol.NewService(ctx, option.WithHTTPClient(client)) + if err != nil { + return err + } + + for i := 0; i < iterations; i++ { + now := time.Now() + timestamp := now.Format(time.RFC3339Nano) + timestampfloat := float64(now.UnixNano()) / 1e9 + time.Sleep(100 * time.Millisecond) + timestamp2 := now.Format(time.RFC3339Nano) + + callerIP := "172.125.77.209" + apiName := "1." + strings.ReplaceAll( + strings.ReplaceAll(serviceName, ".", "_"), + "-", "_") + operationName := apiName + ".Unknown" + uid := uuid.New().String() + + status := 200 + if i%5 == 4 { + status = 404 + } else if i%7 == 6 { + status = 500 + } + operation := &servicecontrol.Operation{ + OperationId: uid, + // operation name seems unused but cannot be empty + OperationName: operationName, + //ConsumerId: "project:" + producerProject, + ConsumerId: "api_key:" + apiKey, + StartTime: timestamp, + Labels: map[string]string{ + "cloud.googleapis.com/location": "us-west1", + "serviceruntime.googleapis.com/api_method": operationName, + "cloud.googleapis.com/project": producerProject, + "cloud.googleapis.com/service": serviceName, + "serviceruntime.googleapis.com/api_version": "1.0.0", + // none of the following appear in the logs + // but they seem to be set in ESPv2 + // https://github.com/GoogleCloudPlatform/esp-v2/blob/9217d68484321aceb7f7fbdc63be9363c96ed722/tests/utils/service_control_utils.go#L245 + "cloud.googleapis.com/uid": uid, + "servicecontrol.googleapis.com/caller_ip": callerIP, + "servicecontrol.googleapis.com/service_agent": "ESPv2/2.45.0", + "servicecontrol.googleapis.com/platform": "Cloud Run", + "servicecontrol.googleapis.com/user_agent": "ESPv2", + "serviceruntime.googleapis.com/consumer_project": consumerProject, + "/response_code": fmt.Sprintf("%d", status), + "/response_code_class": fmt.Sprintf("%dxx", status/100), + "/status_code": fmt.Sprintf("%d", status), + "/protocol": "http", + }, + } + { + request := &servicecontrol.CheckRequest{ + Operation: operation, + } + start := time.Now() + result, err := srv.Services.Check(serviceName, request).Do() + elapsed := time.Since(start) + if err != nil { + return err + } + bytes, err := json.MarshalIndent(result, "", " ") + if err != nil { + return &json.UnsupportedValueError{} + } + fmt.Printf("CHECK (%fms) %d > %s\n", float64(elapsed)/1e6, i, string(bytes)) + } + + // allocate quota + { + request := &servicecontrol.AllocateQuotaRequest{ + ServiceConfigId: serviceConfig, + AllocateOperation: &servicecontrol.QuotaOperation{ + ConsumerId: "api_key:" + apiKey, + OperationId: uid, + QuotaMode: "NORMAL", + MethodName: "1.example5_apiregistry_dev.Unknown", + QuotaMetrics: []*servicecontrol.MetricValueSet{ + createInt64MetricSet("calls", 1), + }, + }, + } + start := time.Now() + result, err := srv.Services.AllocateQuota(serviceName, request).Do() + elapsed := time.Since(start) + if err != nil { + return err + } + bytes, err := json.MarshalIndent(result, "", " ") + if err != nil { + return &json.UnsupportedValueError{} + } + fmt.Printf("ALLOCATE QUOTA (%fms) %d > %s\n", float64(elapsed)/1e6, i, string(bytes)) + if result.AllocateErrors != nil { + return errors.New("out of quota") + } + } + + payload := map[string]interface{}{ + "api_key_state": "NOT CHECKED", + "api_key": apiKey, + "api_method": operationName, + "api_name": apiName, + "api_version": "1.0.0", + "http_status_code": status, + "location": "us-west1", + "log_message": operationName + " is called", + "producer_project_id": "nerdvana", + "response_code_detail": "via_upstream", + "service_agent": "ESPv2/2.45.0", + "service_config_id": serviceConfig, + "timestamp": timestampfloat, + "xtra": "extra info", + } + pbytes, err := json.Marshal(payload) + if err != nil { + return err + } + operation.EndTime = timestamp2 + operation.LogEntries = []*servicecontrol.LogEntry{ + { + Name: "endpoints_log", + Timestamp: timestamp, + Severity: "INFO", + HttpRequest: &servicecontrol.HttpRequest{ + RequestMethod: "GET", + RequestUrl: "/unknown", + RequestSize: 10, + Status: int64(status), + ResponseSize: 10, + RemoteIp: callerIP, + Latency: "10s", + Protocol: "http", + }, + StructPayload: pbytes, + }, + } + operation.MetricValueSets = []*servicecontrol.MetricValueSet{ + createInt64MetricSet("serviceruntime.googleapis.com/api/consumer/request_count", 1), + createInt64MetricSet("serviceruntime.googleapis.com/api/producer/request_count", 1), + createInt64MetricSet("serviceruntime.googleapis.com/api/consumer/quota_used_count", 1), + createDistMetricSet(&timeDistOptions, "serviceruntime.googleapis.com/api/consumer/total_latencies", 1), + createDistMetricSet(&timeDistOptions, "serviceruntime.googleapis.com/api/producer/total_latencies", 1), + createDistMetricSet(&sizeDistOptions, "serviceruntime.googleapis.com/api/consumer/request_sizes", 200), + createDistMetricSet(&sizeDistOptions, "serviceruntime.googleapis.com/api/consumer/response_sizes", 200), + createDistMetricSet(&timeDistOptions, "serviceruntime.googleapis.com/api/producer/request_overhead_latencies", 1), + createDistMetricSet(&timeDistOptions, "serviceruntime.googleapis.com/api/producer/backend_latencies", 1), + createDistMetricSet(&sizeDistOptions, "serviceruntime.googleapis.com/api/producer/request_sizes", 200), + createDistMetricSet(&sizeDistOptions, "serviceruntime.googleapis.com/api/producer/response_sizes", 200), + } + operation.QuotaProperties = nil + request := &servicecontrol.ReportRequest{ + Operations: []*servicecontrol.Operation{ + operation, + }, + } + + start := time.Now() + result, err := srv.Services.Report(serviceName, request).Do() + if err != nil { + return err + } + elapsed := time.Since(start) + + bytes, err := json.MarshalIndent(result, "", " ") + if err != nil { + return &json.UnsupportedValueError{} + } + fmt.Printf("REPORT (%fms) %d > %s\n", float64(elapsed)/1e6, i, string(bytes)) + } + return nil + }, + } + cmd.Flags().StringVarP(&output, "output", "o", "yaml", "Output format. One of: (yaml, json).") + cmd.Flags().IntVarP(&iterations, "iterations", "i", 1, "Number of times to call report.") + return cmd +} diff --git a/cmd/zero/cmd/servicemanagement/cmd.go b/cmd/zero/cmd/servicemanagement/cmd.go new file mode 100644 index 00000000..98b2b7d2 --- /dev/null +++ b/cmd/zero/cmd/servicemanagement/cmd.go @@ -0,0 +1,31 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 servicemanagement + +import ( + "github.com/apigee/registry-experimental/cmd/zero/cmd/servicemanagement/operations" + "github.com/apigee/registry-experimental/cmd/zero/cmd/servicemanagement/services" + "github.com/spf13/cobra" +) + +func Cmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "service-management", + Short: "service management", + } + cmd.AddCommand(services.Cmd()) + cmd.AddCommand(operations.Cmd()) + return cmd +} diff --git a/cmd/zero/cmd/servicemanagement/operations/cmd.go b/cmd/zero/cmd/servicemanagement/operations/cmd.go new file mode 100644 index 00000000..ef19ea82 --- /dev/null +++ b/cmd/zero/cmd/servicemanagement/operations/cmd.go @@ -0,0 +1,27 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 operations + +import ( + "github.com/spf13/cobra" +) + +func Cmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "operations", + } + cmd.AddCommand(getCmd()) + return cmd +} diff --git a/cmd/zero/cmd/servicemanagement/operations/get.go b/cmd/zero/cmd/servicemanagement/operations/get.go new file mode 100644 index 00000000..c0c24f18 --- /dev/null +++ b/cmd/zero/cmd/servicemanagement/operations/get.go @@ -0,0 +1,62 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 operations + +import ( + "context" + + "github.com/apigee/registry-experimental/cmd/zero/pkg/config" + "github.com/apigee/registry-experimental/cmd/zero/pkg/patch" + "google.golang.org/api/option" + "google.golang.org/api/servicemanagement/v1" + + "github.com/spf13/cobra" +) + +func getCmd() *cobra.Command { + var output string + cmd := &cobra.Command{ + Use: "get OPERATION", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + client, err := config.GetClient(ctx) + if err != nil { + return err + } + operation := args[0] + srv, err := servicemanagement.NewService(ctx, option.WithHTTPClient(client)) + if err != nil { + return err + } + item, err := srv.Operations. + Get(operation). + Do() + if err != nil { + return err + } + bytes, err := patch.MarshalAndWrap(item, "Operation", item.Name, output) + if err != nil { + return err + } + if _, err := cmd.OutOrStdout().Write(bytes); err != nil { + return err + } + return nil + }, + } + cmd.Flags().StringVarP(&output, "output", "o", "yaml", "Output format. One of: (yaml, json).") + return cmd +} diff --git a/cmd/zero/cmd/servicemanagement/services/cmd.go b/cmd/zero/cmd/servicemanagement/services/cmd.go new file mode 100644 index 00000000..afc59404 --- /dev/null +++ b/cmd/zero/cmd/servicemanagement/services/cmd.go @@ -0,0 +1,34 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 services + +import ( + "github.com/apigee/registry-experimental/cmd/zero/cmd/servicemanagement/services/configs" + "github.com/apigee/registry-experimental/cmd/zero/cmd/servicemanagement/services/rollouts" + "github.com/spf13/cobra" +) + +func Cmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "services", + } + cmd.AddCommand(createCmd()) + cmd.AddCommand(deleteCmd()) + cmd.AddCommand(getCmd()) + cmd.AddCommand(listCmd()) + cmd.AddCommand(configs.Cmd()) + cmd.AddCommand(rollouts.Cmd()) + return cmd +} diff --git a/cmd/zero/cmd/servicemanagement/services/configs/cmd.go b/cmd/zero/cmd/servicemanagement/services/configs/cmd.go new file mode 100644 index 00000000..97f9e580 --- /dev/null +++ b/cmd/zero/cmd/servicemanagement/services/configs/cmd.go @@ -0,0 +1,29 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 configs + +import ( + "github.com/spf13/cobra" +) + +func Cmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "configs", + } + cmd.AddCommand(createCmd()) + cmd.AddCommand(getCmd()) + cmd.AddCommand(listCmd()) + return cmd +} diff --git a/cmd/zero/cmd/servicemanagement/services/configs/create.go b/cmd/zero/cmd/servicemanagement/services/configs/create.go new file mode 100644 index 00000000..c95e4301 --- /dev/null +++ b/cmd/zero/cmd/servicemanagement/services/configs/create.go @@ -0,0 +1,138 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 configs + +import ( + "context" + + "github.com/apigee/registry-experimental/cmd/zero/pkg/config" + "github.com/apigee/registry-experimental/cmd/zero/pkg/patch" + "github.com/apigee/registry-experimental/cmd/zero/pkg/servicebuilder" + "google.golang.org/api/option" + "google.golang.org/api/servicemanagement/v1" + + "github.com/spf13/cobra" +) + +func createCmd() *cobra.Command { + var output string + var producerProject string + cmd := &cobra.Command{ + Use: "create SERVICE", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + client, err := config.GetClient(ctx) + if err != nil { + return nil + } + c, err := config.GetConfig() + if err != nil { + return nil + } + api := &servicebuilder.Api{ + Name: args[0], + Version: "1.0.0", + Operations: []servicebuilder.Operation{ + { + Id: "Hello", + Method: "GET", + Path: "/hello", + }, + { + Id: "Goodbye", + Method: "POST", + Path: "/goodbye", + }, + { + Id: "Unknown", + Method: "GET", + Path: "/unknown", + }, + }, + } + apis := []*servicebuilder.Api{api} + service := &servicemanagement.Service{ + Apis: servicebuilder.Apis(apis), + Authentication: nil, + ConfigVersion: 3, + Control: servicebuilder.Control(), + Documentation: &servicemanagement.Documentation{ + Summary: c.Summary, + }, + Endpoints: []*servicemanagement.Endpoint{ + { + Name: args[0], + }, + }, + Enums: servicebuilder.Enums(), + Http: servicebuilder.Http(apis), + Logging: servicebuilder.Logging(), + Logs: servicebuilder.Logs(), + Name: args[0], + ProducerProjectId: producerProject, + SystemParameters: nil, + Title: c.Title, + Metrics: servicebuilder.Metrics(), + MonitoredResources: servicebuilder.MonitoredResources(), + Monitoring: servicebuilder.Monitoring(), + // https://cloud.google.com/endpoints/docs/grpc/quotas-configure + Quota: &servicemanagement.Quota{ + Limits: []*servicemanagement.QuotaLimit{ + { + Name: "calls", + Metric: "calls", + Unit: "1/min/{project}", + Values: map[string]string{ + "STANDARD": "60", + }, + }, + }, + MetricRules: []*servicemanagement.MetricRule{ + { + MetricCosts: map[string]string{ + "calls": "1", + }, + Selector: "*", + }, + }, + }, + Types: servicebuilder.Types(), + Usage: servicebuilder.Usage(apis), + } + srv, err := servicemanagement.NewService(ctx, option.WithHTTPClient(client)) + if err != nil { + return nil + } + item, err := srv.Services.Configs. + Create(args[0], service). + Do() + if err != nil { + return nil + } + bytes, err := patch.MarshalAndWrap(item, "Operation", item.Name, output) + if err != nil { + return err + } + if _, err := cmd.OutOrStdout().Write(bytes); err != nil { + return err + } + return nil + }, + } + cmd.Flags().StringVarP(&output, "output", "o", "yaml", "Output format. One of: (yaml, json).") + cmd.Flags().StringVarP(&producerProject, "project", "p", "", "Producer project.") + return cmd +} diff --git a/cmd/zero/cmd/servicemanagement/services/configs/get.go b/cmd/zero/cmd/servicemanagement/services/configs/get.go new file mode 100644 index 00000000..aa2693e6 --- /dev/null +++ b/cmd/zero/cmd/servicemanagement/services/configs/get.go @@ -0,0 +1,63 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 configs + +import ( + "context" + + "github.com/apigee/registry-experimental/cmd/zero/pkg/config" + "github.com/apigee/registry-experimental/cmd/zero/pkg/patch" + "google.golang.org/api/option" + "google.golang.org/api/servicemanagement/v1" + + "github.com/spf13/cobra" +) + +func getCmd() *cobra.Command { + var output string + cmd := &cobra.Command{ + Use: "get SERVICE CONFIG", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + client, err := config.GetClient(ctx) + if err != nil { + return nil + } + service := args[0] + configId := args[1] + srv, err := servicemanagement.NewService(ctx, option.WithHTTPClient(client)) + if err != nil { + return nil + } + item, err := srv.Services.Configs. + Get(service, configId). + Do() + if err != nil { + return nil + } + bytes, err := patch.MarshalAndWrap(item, "ServiceConfig", item.Name, output) + if err != nil { + return err + } + if _, err := cmd.OutOrStdout().Write(bytes); err != nil { + return err + } + return nil + }, + } + cmd.Flags().StringVarP(&output, "output", "o", "yaml", "Output format. One of: (yaml, json).") + return cmd +} diff --git a/cmd/zero/cmd/servicemanagement/services/configs/list.go b/cmd/zero/cmd/servicemanagement/services/configs/list.go new file mode 100644 index 00000000..b803fb58 --- /dev/null +++ b/cmd/zero/cmd/servicemanagement/services/configs/list.go @@ -0,0 +1,78 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 configs + +import ( + "context" + "fmt" + + "github.com/apigee/registry-experimental/cmd/zero/pkg/config" + "github.com/apigee/registry-experimental/cmd/zero/pkg/patch" + "google.golang.org/api/option" + "google.golang.org/api/servicemanagement/v1" + + "github.com/spf13/cobra" +) + +func listCmd() *cobra.Command { + var output string + cmd := &cobra.Command{ + Use: "list SERVICE", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + client, err := config.GetClient(ctx) + if err != nil { + return nil + } + service := args[0] + srv, err := servicemanagement.NewService(ctx, option.WithHTTPClient(client)) + if err != nil { + return nil + } + var cursor string + c := 0 + for i := 0; true; i++ { + items, err := srv.Services.Configs. + List(service). + PageToken(cursor). + Do() + if err != nil { + return nil + } + for _, item := range items.ServiceConfigs { + bytes, err := patch.MarshalAndWrap(item, "ServiceConfig", item.Name, output) + if err != nil { + return err + } + if c > 0 && output != "json" { + fmt.Printf("---\n") + } + c++ + if _, err := cmd.OutOrStdout().Write(bytes); err != nil { + return err + } + } + cursor = items.NextPageToken + if cursor == "" { + break + } + } + return nil + }, + } + cmd.Flags().StringVarP(&output, "output", "o", "yaml", "Output format. One of: (yaml, json).") + return cmd +} diff --git a/cmd/zero/cmd/servicemanagement/services/create.go b/cmd/zero/cmd/servicemanagement/services/create.go new file mode 100644 index 00000000..7bdce207 --- /dev/null +++ b/cmd/zero/cmd/servicemanagement/services/create.go @@ -0,0 +1,67 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 services + +import ( + "context" + + "github.com/apigee/registry-experimental/cmd/zero/pkg/config" + "github.com/apigee/registry-experimental/cmd/zero/pkg/patch" + "google.golang.org/api/option" + "google.golang.org/api/servicemanagement/v1" + + "github.com/spf13/cobra" +) + +func createCmd() *cobra.Command { + var output string + var producerProject string + cmd := &cobra.Command{ + Use: "create SERVICE", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + client, err := config.GetClient(ctx) + if err != nil { + return err + } + service := &servicemanagement.ManagedService{ + ServiceName: args[0], + ProducerProjectId: producerProject, + } + srv, err := servicemanagement.NewService(ctx, option.WithHTTPClient(client)) + if err != nil { + return err + } + item, err := srv.Services. + Create(service). + Do() + if err != nil { + return err + } + bytes, err := patch.MarshalAndWrap(item, "Operation", item.Name, output) + if err != nil { + return err + } + if _, err := cmd.OutOrStdout().Write(bytes); err != nil { + return err + } + return nil + }, + } + cmd.Flags().StringVarP(&output, "output", "o", "yaml", "Output format. One of: (yaml, json).") + cmd.Flags().StringVarP(&producerProject, "project", "p", "", "Producer project.") + return cmd +} diff --git a/cmd/zero/cmd/servicemanagement/services/delete.go b/cmd/zero/cmd/servicemanagement/services/delete.go new file mode 100644 index 00000000..0ffc972f --- /dev/null +++ b/cmd/zero/cmd/servicemanagement/services/delete.go @@ -0,0 +1,62 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 services + +import ( + "context" + + "github.com/apigee/registry-experimental/cmd/zero/pkg/config" + "github.com/apigee/registry-experimental/cmd/zero/pkg/patch" + "google.golang.org/api/option" + "google.golang.org/api/servicemanagement/v1" + + "github.com/spf13/cobra" +) + +func deleteCmd() *cobra.Command { + var output string + cmd := &cobra.Command{ + Use: "delete SERVICE", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + client, err := config.GetClient(ctx) + if err != nil { + return err + } + service := args[0] + srv, err := servicemanagement.NewService(ctx, option.WithHTTPClient(client)) + if err != nil { + return err + } + item, err := srv.Services. + Delete(service). + Do() + if err != nil { + return err + } + bytes, err := patch.MarshalAndWrap(item, "Operation", item.Name, output) + if err != nil { + return err + } + if _, err := cmd.OutOrStdout().Write(bytes); err != nil { + return err + } + return nil + }, + } + cmd.Flags().StringVarP(&output, "output", "o", "yaml", "Output format. One of: (yaml, json).") + return cmd +} diff --git a/cmd/zero/cmd/servicemanagement/services/get.go b/cmd/zero/cmd/servicemanagement/services/get.go new file mode 100644 index 00000000..949b3079 --- /dev/null +++ b/cmd/zero/cmd/servicemanagement/services/get.go @@ -0,0 +1,62 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 services + +import ( + "context" + + "github.com/apigee/registry-experimental/cmd/zero/pkg/config" + "github.com/apigee/registry-experimental/cmd/zero/pkg/patch" + "google.golang.org/api/option" + "google.golang.org/api/servicemanagement/v1" + + "github.com/spf13/cobra" +) + +func getCmd() *cobra.Command { + var output string + cmd := &cobra.Command{ + Use: "get SERVICE", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + client, err := config.GetClient(ctx) + if err != nil { + return err + } + service := args[0] + srv, err := servicemanagement.NewService(ctx, option.WithHTTPClient(client)) + if err != nil { + return err + } + item, err := srv.Services. + Get(service). + Do() + if err != nil { + return err + } + bytes, err := patch.MarshalAndWrap(item, "Service", item.ServiceName, output) + if err != nil { + return err + } + if _, err := cmd.OutOrStdout().Write(bytes); err != nil { + return err + } + return nil + }, + } + cmd.Flags().StringVarP(&output, "output", "o", "yaml", "Output format. One of: (yaml, json).") + return cmd +} diff --git a/cmd/zero/cmd/servicemanagement/services/list.go b/cmd/zero/cmd/servicemanagement/services/list.go new file mode 100644 index 00000000..b28069e4 --- /dev/null +++ b/cmd/zero/cmd/servicemanagement/services/list.go @@ -0,0 +1,80 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 services + +import ( + "context" + "fmt" + + "github.com/apigee/registry-experimental/cmd/zero/pkg/config" + "github.com/apigee/registry-experimental/cmd/zero/pkg/patch" + "google.golang.org/api/option" + "google.golang.org/api/servicemanagement/v1" + + "github.com/spf13/cobra" +) + +func listCmd() *cobra.Command { + var output string + var producerProject string + cmd := &cobra.Command{ + Use: "list", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + client, err := config.GetClient(ctx) + if err != nil { + return err + } + srv, err := servicemanagement.NewService(ctx, option.WithHTTPClient(client)) + if err != nil { + return err + } + var cursor string + c := 0 + for i := 0; true; i++ { + items, err := srv.Services. + List(). + ProducerProjectId(producerProject). + PageToken(cursor). + Do() + if err != nil { + return err + } + for _, item := range items.Services { + bytes, err := patch.MarshalAndWrap(item, "Service", item.ServiceName, output) + if err != nil { + return err + } + if c > 0 && output != "json" { + fmt.Printf("---\n") + } + c++ + if _, err := cmd.OutOrStdout().Write(bytes); err != nil { + return err + } + } + cursor = items.NextPageToken + if cursor == "" { + break + } + } + return nil + }, + } + cmd.Flags().StringVarP(&output, "output", "o", "yaml", "Output format. One of: (yaml, json).") + cmd.Flags().StringVarP(&producerProject, "project", "p", "", "Producer project.") + return cmd +} diff --git a/cmd/zero/cmd/servicemanagement/services/rollouts/cmd.go b/cmd/zero/cmd/servicemanagement/services/rollouts/cmd.go new file mode 100644 index 00000000..33a5f256 --- /dev/null +++ b/cmd/zero/cmd/servicemanagement/services/rollouts/cmd.go @@ -0,0 +1,29 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 rollouts + +import ( + "github.com/spf13/cobra" +) + +func Cmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "rollouts", + } + cmd.AddCommand(createCmd()) + cmd.AddCommand(getCmd()) + cmd.AddCommand(listCmd()) + return cmd +} diff --git a/cmd/zero/cmd/servicemanagement/services/rollouts/create.go b/cmd/zero/cmd/servicemanagement/services/rollouts/create.go new file mode 100644 index 00000000..9697d06d --- /dev/null +++ b/cmd/zero/cmd/servicemanagement/services/rollouts/create.go @@ -0,0 +1,71 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 rollouts + +import ( + "context" + + "github.com/apigee/registry-experimental/cmd/zero/pkg/config" + "github.com/apigee/registry-experimental/cmd/zero/pkg/patch" + "google.golang.org/api/option" + "google.golang.org/api/servicemanagement/v1" + + "github.com/spf13/cobra" +) + +func createCmd() *cobra.Command { + var output string + cmd := &cobra.Command{ + Use: "create SERVICE CONFIG", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + client, err := config.GetClient(ctx) + if err != nil { + return err + } + serviceId := args[0] + rolloutId := args[1] + srv, err := servicemanagement.NewService(ctx, option.WithHTTPClient(client)) + if err != nil { + return err + } + rollout := &servicemanagement.Rollout{ + RolloutId: rolloutId, + TrafficPercentStrategy: &servicemanagement.TrafficPercentStrategy{ + Percentages: map[string]float64{ + rolloutId: 100.0, + }, + }, + } + item, err := srv.Services.Rollouts. + Create(serviceId, rollout). + Do() + if err != nil { + return err + } + bytes, err := patch.MarshalAndWrap(item, "Operation", item.Name, output) + if err != nil { + return err + } + if _, err := cmd.OutOrStdout().Write(bytes); err != nil { + return err + } + return nil + }, + } + cmd.Flags().StringVarP(&output, "output", "o", "yaml", "Output format. One of: (yaml, json).") + return cmd +} diff --git a/cmd/zero/cmd/servicemanagement/services/rollouts/get.go b/cmd/zero/cmd/servicemanagement/services/rollouts/get.go new file mode 100644 index 00000000..fabcdbb2 --- /dev/null +++ b/cmd/zero/cmd/servicemanagement/services/rollouts/get.go @@ -0,0 +1,63 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 rollouts + +import ( + "context" + + "github.com/apigee/registry-experimental/cmd/zero/pkg/config" + "github.com/apigee/registry-experimental/cmd/zero/pkg/patch" + "google.golang.org/api/option" + "google.golang.org/api/servicemanagement/v1" + + "github.com/spf13/cobra" +) + +func getCmd() *cobra.Command { + var output string + cmd := &cobra.Command{ + Use: "get SERVICE CONFIG", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + client, err := config.GetClient(ctx) + if err != nil { + return err + } + service := args[0] + rolloutId := args[1] + srv, err := servicemanagement.NewService(ctx, option.WithHTTPClient(client)) + if err != nil { + return err + } + item, err := srv.Services.Rollouts. + Get(service, rolloutId). + Do() + if err != nil { + return err + } + bytes, err := patch.MarshalAndWrap(item, "Rollout", item.RolloutId, output) + if err != nil { + return err + } + if _, err := cmd.OutOrStdout().Write(bytes); err != nil { + return err + } + return nil + }, + } + cmd.Flags().StringVarP(&output, "output", "o", "yaml", "Output format. One of: (yaml, json).") + return cmd +} diff --git a/cmd/zero/cmd/servicemanagement/services/rollouts/list.go b/cmd/zero/cmd/servicemanagement/services/rollouts/list.go new file mode 100644 index 00000000..260f1fd8 --- /dev/null +++ b/cmd/zero/cmd/servicemanagement/services/rollouts/list.go @@ -0,0 +1,78 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 rollouts + +import ( + "context" + "fmt" + + "github.com/apigee/registry-experimental/cmd/zero/pkg/config" + "github.com/apigee/registry-experimental/cmd/zero/pkg/patch" + "google.golang.org/api/option" + "google.golang.org/api/servicemanagement/v1" + + "github.com/spf13/cobra" +) + +func listCmd() *cobra.Command { + var output string + cmd := &cobra.Command{ + Use: "list SERVICE", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + client, err := config.GetClient(ctx) + if err != nil { + return err + } + service := args[0] + srv, err := servicemanagement.NewService(ctx, option.WithHTTPClient(client)) + if err != nil { + return err + } + var cursor string + c := 0 + for i := 0; true; i++ { + items, err := srv.Services.Rollouts. + List(service). + PageToken(cursor). + Do() + if err != nil { + return err + } + for _, item := range items.Rollouts { + bytes, err := patch.MarshalAndWrap(item, "Rollout", item.RolloutId, output) + if err != nil { + return err + } + if c > 0 && output != "json" { + fmt.Printf("---\n") + } + c++ + if _, err := cmd.OutOrStdout().Write(bytes); err != nil { + return err + } + } + cursor = items.NextPageToken + if cursor == "" { + break + } + } + return nil + }, + } + cmd.Flags().StringVarP(&output, "output", "o", "yaml", "Output format. One of: (yaml, json).") + return cmd +} diff --git a/cmd/zero/main.go b/cmd/zero/main.go new file mode 100644 index 00000000..3e63cf69 --- /dev/null +++ b/cmd/zero/main.go @@ -0,0 +1,28 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 main + +import ( + "context" + "os" + + "github.com/apigee/registry-experimental/cmd/zero/cmd" +) + +func main() { + if err := cmd.Cmd(context.Background()).Execute(); err != nil { + os.Exit(1) + } +} diff --git a/cmd/zero/pkg/config/client.go b/cmd/zero/pkg/config/client.go new file mode 100644 index 00000000..71528885 --- /dev/null +++ b/cmd/zero/pkg/config/client.go @@ -0,0 +1,47 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 config + +import ( + "context" + "net/http" + "os" + "path" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + "google.golang.org/api/servicecontrol/v2" +) + +const serviceAccountFile = ".config/zero/control.json" +const scope = servicecontrol.CloudPlatformScope + +func GetClient(ctx context.Context) (*http.Client, error) { + var client *http.Client + dirname, err := os.UserHomeDir() + if err != nil { + return nil, err + } + data, err := os.ReadFile(path.Join(dirname, serviceAccountFile)) + if err != nil { + return nil, err + } + creds, err := google.CredentialsFromJSON(ctx, data, scope) + if err != nil { + return nil, err + } + client = oauth2.NewClient(ctx, creds.TokenSource) + return client, nil +} diff --git a/cmd/zero/pkg/config/config.go b/cmd/zero/pkg/config/config.go new file mode 100644 index 00000000..bc73e862 --- /dev/null +++ b/cmd/zero/pkg/config/config.go @@ -0,0 +1,48 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 config + +import ( + "os" + "path" + + "gopkg.in/yaml.v3" +) + +const configFile = ".config/zero/zero.yaml" + +type Config struct { + ServiceName string `yaml:"serviceName"` + ServiceConfig string `yaml:"serviceConfig"` + ApiKey string `yaml:"apiKey"` + Summary string `yaml:"summary"` + Title string `yaml:"title"` + ProducerProject string `yaml:"producerProject"` + ConsumerProject string `yaml:"consumerProject"` +} + +func GetConfig() (*Config, error) { + var config Config + dirname, err := os.UserHomeDir() + if err != nil { + return nil, err + } + data, err := os.ReadFile(path.Join(dirname, configFile)) + if err != nil { + return nil, err + } + err = yaml.Unmarshal(data, &config) + return &config, err +} diff --git a/cmd/zero/pkg/patch/patch.go b/cmd/zero/pkg/patch/patch.go new file mode 100644 index 00000000..b7025beb --- /dev/null +++ b/cmd/zero/pkg/patch/patch.go @@ -0,0 +1,132 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 patch + +import ( + "encoding/json" + "time" + + "gopkg.in/yaml.v3" +) + +type Header struct { + ApiVersion string `yaml:"apiVersion" json:"apiVersion"` + Kind string `yaml:"kind" json:"kind"` + Metadata Metadata `yaml:"metadata" json:"metadata"` +} + +type Metadata struct { + Name string `yaml:"name" json:"name"` + Parent string `yaml:"parent,omitempty" json:"parent,omitempty"` + CreatedAt time.Time `yaml:"created_at,omitempty" json:"created_at,omitempty"` + UpdatedAt time.Time `yaml:"updated_at,omitempty" json:"updated_at,omitempty"` + Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"` + Annotations map[string]string `yaml:"annotations,omitempty" json:"annotations,omitempty"` +} + +type Patch struct { + Header `yaml:",inline" json:",inline"` + Data *yaml.Node `yaml:"data" json:"data"` +} + +func WrapJSON(kind, id string, bytes []byte) (*Patch, error) { + var m yaml.Node + if err := yaml.Unmarshal(bytes, &m); err != nil { + return nil, err + } + return &Patch{ + Header: Header{ + ApiVersion: "zero/v1", + Kind: kind, + Metadata: Metadata{ + Name: id, + }, + }, + Data: m.Content[0], + }, nil +} + +func MarshalYAML(wrapper *Patch) ([]byte, error) { + styleForYAML(wrapper.Data) + bytes, err := yaml.Marshal(wrapper) + if err != nil { + return nil, err + } + return bytes, err +} + +func MarshalJSON(wrapper *Patch) ([]byte, error) { + bytes, err := MarshalYAML(wrapper) + if err != nil { + return nil, err + } + var doc yaml.Node + err = yaml.Unmarshal(bytes, &doc) + if err != nil { + return nil, err + } + styleForJSON(&doc) + bytes, err = yaml.Marshal(doc.Content[0]) + if err != nil { + return nil, err + } + return bytes, err +} + +func Marshal(wrapper *Patch, output string) ([]byte, error) { + if output == "json" { + return MarshalJSON(wrapper) + } + return MarshalYAML(wrapper) +} + +func styleForYAML(node *yaml.Node) { + node.Style = 0 + for _, n := range node.Content { + styleForYAML(n) + } +} + +// styleForJSON sets the style field on a tree of yaml.Nodes for JSON export. +func styleForJSON(node *yaml.Node) { + switch node.Kind { + case yaml.DocumentNode, yaml.SequenceNode, yaml.MappingNode: + node.Style = yaml.FlowStyle + case yaml.ScalarNode: + switch node.Tag { + case "!!str": + node.Style = yaml.DoubleQuotedStyle + default: + node.Style = 0 + } + case yaml.AliasNode: + default: + } + for _, n := range node.Content { + styleForJSON(n) + } +} + +func MarshalAndWrap(item interface{}, kind, name, output string) ([]byte, error) { + bytes, err := json.Marshal(item) + if err != nil { + return nil, err + } + wrapper, err := WrapJSON(kind, name, bytes) + if err != nil { + return nil, err + } + return Marshal(wrapper, output) +} diff --git a/cmd/zero/pkg/servicebuilder/builder.go b/cmd/zero/pkg/servicebuilder/builder.go new file mode 100644 index 00000000..577501e4 --- /dev/null +++ b/cmd/zero/pkg/servicebuilder/builder.go @@ -0,0 +1,406 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 servicebuilder + +import ( + "fmt" + "strings" + + "google.golang.org/api/servicemanagement/v1" +) + +type Operation struct { + Id string + Method string + Path string +} + +type Api struct { + Name string + Version string + Operations []Operation +} + +func Apis(apis []*Api) []*servicemanagement.Api { + response := []*servicemanagement.Api{} + for i, a := range apis { + apiName := fmt.Sprintf("%d.%s", i+1, strings.ReplaceAll(a.Name, ".", "_")) + methods := []*servicemanagement.Method{} + for _, op := range a.Operations { + methods = append(methods, &servicemanagement.Method{ + Name: op.Id, + // config creation fails (with 500) if the following two fields are omitted + RequestTypeUrl: "type.googleapis.com/google.protobuf.Empty", + ResponseTypeUrl: "type.googleapis.com/google.protobuf.Value", + }) + } + response = append(response, &servicemanagement.Api{ + Name: apiName, + Methods: methods, + Version: a.Version, + }) + } + return response +} + +func Control() *servicemanagement.Control { + return &servicemanagement.Control{ + Environment: "servicecontrol.googleapis.com", + } +} + +func Enums() []*servicemanagement.Enum { + return []*servicemanagement.Enum{ + { + Enumvalue: []*servicemanagement.EnumValue{ + { + Name: "NULL_VALUE", + }, + }, + Name: "google.protobuf.NullValue", + SourceContext: &servicemanagement.SourceContext{ + FileName: "struct.proto", + }, + }, + } +} + +func Http(apis []*Api) *servicemanagement.Http { + rules := []*servicemanagement.HttpRule{} + for i, a := range apis { + apiName := fmt.Sprintf("%d.%s", i+1, strings.ReplaceAll(a.Name, ".", "_")) + for _, op := range a.Operations { + switch op.Method { + case "GET": + rules = append(rules, &servicemanagement.HttpRule{ + Get: op.Path, + Selector: apiName + "." + op.Id, + }) + case "POST": + rules = append(rules, &servicemanagement.HttpRule{ + Post: op.Path, + Selector: apiName + "." + op.Id, + }) + default: + panic(op.Method) + } + } + } + return &servicemanagement.Http{ + Rules: rules, + } +} +func Logging() *servicemanagement.Logging { + return &servicemanagement.Logging{ + ProducerDestinations: []*servicemanagement.LoggingDestination{ + { + Logs: []string{"endpoints_log"}, + MonitoredResource: "api", + }, + }, + } +} + +func Logs() []*servicemanagement.LogDescriptor { + return []*servicemanagement.LogDescriptor{ + { + Name: "endpoints_log", + }, + } +} + +func Metrics() []*servicemanagement.MetricDescriptor { + return []*servicemanagement.MetricDescriptor{ + { + Labels: []*servicemanagement.LabelDescriptor{ + {Key: "/credential_id"}, + {Key: "/protocol"}, + {Key: "/response_code"}, + {Key: "/response_code_class"}, + {Key: "/status_code"}, + }, + MetricKind: "DELTA", + Name: "serviceruntime.googleapis.com/api/consumer/request_count", + Type: "serviceruntime.googleapis.com/api/consumer/request_count", + ValueType: "INT64", + }, + { + Labels: []*servicemanagement.LabelDescriptor{ + {Key: "/credential_id"}, + }, + MetricKind: "DELTA", + Name: "serviceruntime.googleapis.com/api/consumer/total_latencies", + Type: "serviceruntime.googleapis.com/api/consumer/total_latencies", + ValueType: "DISTRIBUTION", + }, + { + Labels: []*servicemanagement.LabelDescriptor{ + {Key: "/protocol"}, + {Key: "/response_code"}, + {Key: "/response_code_class"}, + {Key: "/status_code"}, + }, + MetricKind: "DELTA", + Name: "serviceruntime.googleapis.com/api/producer/request_count", + Type: "serviceruntime.googleapis.com/api/producer/request_count", + ValueType: "INT64", + }, + { + MetricKind: "DELTA", + Name: "serviceruntime.googleapis.com/api/producer/total_latencies", + Type: "serviceruntime.googleapis.com/api/producer/total_latencies", + ValueType: "DISTRIBUTION", + }, + { + Labels: []*servicemanagement.LabelDescriptor{ + {Key: "/credential_id"}, + {Key: "/quota_group_name"}, + }, + MetricKind: "DELTA", + Name: "serviceruntime.googleapis.com/api/consumer/quota_used_count", + Type: "serviceruntime.googleapis.com/api/consumer/quota_used_count", + ValueType: "INT64", + }, + { + Labels: []*servicemanagement.LabelDescriptor{ + {Key: "/credential_id"}, + }, + MetricKind: "DELTA", + Name: "serviceruntime.googleapis.com/api/consumer/request_sizes", + Type: "serviceruntime.googleapis.com/api/consumer/request_sizes", + ValueType: "DISTRIBUTION", + }, + { + Labels: []*servicemanagement.LabelDescriptor{ + {Key: "/credential_id"}, + }, + MetricKind: "DELTA", + Name: "serviceruntime.googleapis.com/api/consumer/response_sizes", + Type: "serviceruntime.googleapis.com/api/consumer/response_sizes", + ValueType: "DISTRIBUTION", + }, + { + MetricKind: "DELTA", + Name: "serviceruntime.googleapis.com/api/producer/request_overhead_latencies", + Type: "serviceruntime.googleapis.com/api/producer/request_overhead_latencies", + ValueType: "DISTRIBUTION", + }, + { + MetricKind: "DELTA", + Name: "serviceruntime.googleapis.com/api/producer/backend_latencies", + Type: "serviceruntime.googleapis.com/api/producer/backend_latencies", + ValueType: "DISTRIBUTION", + }, + { + MetricKind: "DELTA", + Name: "serviceruntime.googleapis.com/api/producer/request_sizes", + Type: "serviceruntime.googleapis.com/api/producer/request_sizes", + ValueType: "DISTRIBUTION", + }, + { + MetricKind: "DELTA", + Name: "serviceruntime.googleapis.com/api/producer/response_sizes", + Type: "serviceruntime.googleapis.com/api/producer/response_sizes", + ValueType: "DISTRIBUTION", + }, + { + Name: "calls", + DisplayName: "Calls", + ValueType: "INT64", + MetricKind: "DELTA", + }, + } +} + +func MonitoredResources() []*servicemanagement.MonitoredResourceDescriptor { + return []*servicemanagement.MonitoredResourceDescriptor{ + { + Type: "api", + Labels: []*servicemanagement.LabelDescriptor{ + {Key: "cloud.googleapis.com/location"}, + {Key: "cloud.googleapis.com/uid"}, + {Key: "serviceruntime.googleapis.com/api_version"}, + {Key: "serviceruntime.googleapis.com/api_method"}, + {Key: "serviceruntime.googleapis.com/consumer_project"}, + {Key: "cloud.googleapis.com/project"}, + {Key: "cloud.googleapis.com/service"}, + }, + }, + } +} + +func Monitoring() *servicemanagement.Monitoring { + return &servicemanagement.Monitoring{ + ConsumerDestinations: []*servicemanagement.MonitoringDestination{ + { + Metrics: []string{ + "serviceruntime.googleapis.com/api/consumer/request_count", + "serviceruntime.googleapis.com/api/consumer/quota_used_count", + "serviceruntime.googleapis.com/api/consumer/total_latencies", + "serviceruntime.googleapis.com/api/consumer/request_sizes", + "serviceruntime.googleapis.com/api/consumer/response_sizes", + }, + MonitoredResource: "api", + }, + }, + ProducerDestinations: []*servicemanagement.MonitoringDestination{ + { + Metrics: []string{ + "serviceruntime.googleapis.com/api/producer/request_count", + "serviceruntime.googleapis.com/api/producer/total_latencies", + "serviceruntime.googleapis.com/api/producer/request_overhead_latencies", + "serviceruntime.googleapis.com/api/producer/backend_latencies", + "serviceruntime.googleapis.com/api/producer/request_sizes", + "serviceruntime.googleapis.com/api/producer/response_sizes", + }, + MonitoredResource: "api", + }, + }, + } +} + +func Types() []*servicemanagement.Type { + return []*servicemanagement.Type{ + { + Name: "google.protobuf.ListValue", + SourceContext: &servicemanagement.SourceContext{ + FileName: "struct.proto", + }, + Fields: []*servicemanagement.Field{ + { + Cardinality: "CARDINALITY_REPEATED", + JsonName: "values", + Kind: "TYPE_MESSAGE", + Name: "values", + Number: 1, + TypeUrl: "type.googleapis.com/google.protobuf.Value", + }, + }, + }, + { + Name: "google.protobuf.Struct", + SourceContext: &servicemanagement.SourceContext{ + FileName: "struct.proto", + }, + Fields: []*servicemanagement.Field{ + { + Cardinality: "CARDINALITY_REPEATED", + JsonName: "fields", + Kind: "TYPE_MESSAGE", + Name: "fields", + Number: 1, + TypeUrl: "type.googleapis.com/google.protobuf.Struct.FieldsEntry", + }, + }, + }, + { + Name: "google.protobuf.Struct.FieldsEntry", + SourceContext: &servicemanagement.SourceContext{ + FileName: "struct.proto", + }, + Fields: []*servicemanagement.Field{ + { + Cardinality: "CARDINALITY_OPTIONAL", + JsonName: "key", + Kind: "TYPE_STRING", + Name: "key", + Number: 1, + }, + { + Cardinality: "CARDINALITY_OPTIONAL", + JsonName: "value", + Kind: "TYPE_MESSAGE", + Name: "value", + Number: 2, + TypeUrl: "type.googleapis.com/google.protobuf.Value", + }, + }, + }, + { + Name: "google.protobuf.Empty", + SourceContext: &servicemanagement.SourceContext{ + FileName: "struct.proto", + }, + }, + { + Name: "google.protobuf.Value", + SourceContext: &servicemanagement.SourceContext{ + FileName: "struct.proto", + }, + Fields: []*servicemanagement.Field{ + { + Cardinality: "CARDINALITY_OPTIONAL", + JsonName: "nullValue", + Kind: "TYPE_ENUM", + Name: "null_value", + Number: 1, + TypeUrl: "type.googleapis.com/google.protobuf.NullValue", + }, + { + Cardinality: "CARDINALITY_OPTIONAL", + JsonName: "numberValue", + Kind: "TYPE_DOUBLE", + Name: "number_value", + Number: 2, + }, + { + Cardinality: "CARDINALITY_OPTIONAL", + JsonName: "stringValue", + Kind: "TYPE_STRING", + Name: "string_value", + Number: 3, + }, + { + Cardinality: "CARDINALITY_OPTIONAL", + JsonName: "boolValue", + Kind: "TYPE_BOOL", + Name: "bool_value", + Number: 4, + }, + { + Cardinality: "CARDINALITY_OPTIONAL", + JsonName: "structValue", + Kind: "TYPE_MESSAGE", + Name: "struct_value", + Number: 5, + TypeUrl: "type.googleapis.com/google.protobuf.Struct", + }, + { + Cardinality: "CARDINALITY_OPTIONAL", + JsonName: "listValue", + Kind: "TYPE_MESSAGE", + Name: "list_value", + Number: 6, + TypeUrl: "type.googleapis.com/google.protobuf.ListValue", + }, + }, + }, + } +} + +func Usage(apis []*Api) *servicemanagement.Usage { + rules := []*servicemanagement.UsageRule{} + for i, a := range apis { + apiName := fmt.Sprintf("%d.%s", i+1, strings.ReplaceAll(a.Name, ".", "_")) + for _, op := range a.Operations { + rules = append(rules, &servicemanagement.UsageRule{ + Selector: apiName + "." + op.Id, + }) + } + } + return &servicemanagement.Usage{ + Rules: rules, + } +} diff --git a/cmd/zero/pkg/servicecontrol/control.go b/cmd/zero/pkg/servicecontrol/control.go new file mode 100644 index 00000000..902ec522 --- /dev/null +++ b/cmd/zero/pkg/servicecontrol/control.go @@ -0,0 +1,345 @@ +// Copyright 2023 Google LLC. All Rights Reserved. +// +// 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 +// +// http://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 servicecontrol + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "math" + "net/http" + "strings" + "time" + + "github.com/apigee/registry-experimental/cmd/zero/pkg/config" + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "google.golang.org/api/option" + "google.golang.org/api/servicecontrol/v1" +) + +func Middleware(serviceName string, verbose bool) func(c *gin.Context) { + return func(c *gin.Context) { + t, err := NewTracker(c, serviceName) + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + t.Verbose = verbose + err = t.Check() + if err != nil { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + return + } + err = t.AllocateQuota() + if err != nil { + c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": err.Error()}) + return + } + t.CallHandler() + err = t.Report() + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + } +} + +type Tracker struct { + Context *gin.Context + ServiceName string + Client *http.Client + Service *servicecontrol.Service + StartTime time.Time + BackendDuration time.Duration + Operation *servicecontrol.Operation + Config *config.Config + Method string + ApiKey string + Verbose bool +} + +func NewTracker(gc *gin.Context, serviceName string) (*Tracker, error) { + apiName := "1." + strings.ReplaceAll( + strings.ReplaceAll(serviceName, ".", "_"), + "-", "_") + // this assumes/requires that the handler function name exactly matches the operation name + parts := strings.Split(gc.HandlerName(), ".") + method := parts[len(parts)-1] + var err error + tracker := &Tracker{ + Context: gc, + ServiceName: serviceName, + Method: method, + } + ctx := context.Background() + tracker.Client, err = config.GetClient(ctx) + if err != nil { + return nil, err + } + tracker.Service, err = servicecontrol.NewService(ctx, option.WithHTTPClient(tracker.Client)) + if err != nil { + return nil, err + } + tracker.ApiKey = gc.Request.Header.Get("X-Api-Key") + c, err := config.GetConfig() + if err != nil { + return nil, err + } + tracker.Config = c + tracker.StartTime = time.Now() + tracker.Operation = &servicecontrol.Operation{ + OperationId: uuid.New().String(), + OperationName: apiName + "." + method, + ConsumerId: "api_key:" + tracker.ApiKey, + StartTime: tracker.StartTime.Format(time.RFC3339Nano), + } + return tracker, nil +} + +func (t *Tracker) Check() error { + request := &servicecontrol.CheckRequest{ + Operation: t.Operation, + } + start := time.Now() + result, err := t.Service.Services.Check(t.ServiceName, request).Do() + elapsed := time.Since(start) + if err != nil { + return err + } + bytes, err := json.MarshalIndent(result, "", " ") + if err != nil { + return &json.UnsupportedValueError{} + } + if t.Verbose { + fmt.Printf("CHECK (%fms)> %s\n", float64(elapsed)/1e6, string(bytes)) + } + if result.CheckErrors != nil { + return fmt.Errorf("%s", result.CheckErrors[0].Code) + } + return nil +} + +func (t *Tracker) AllocateQuota() error { + apiName := "1." + strings.ReplaceAll( + strings.ReplaceAll(t.ServiceName, ".", "_"), + "-", "_") + operationName := apiName + "." + t.Method + request := &servicecontrol.AllocateQuotaRequest{ + ServiceConfigId: t.Config.ServiceConfig, + AllocateOperation: &servicecontrol.QuotaOperation{ + ConsumerId: "api_key:" + t.ApiKey, + OperationId: t.Operation.OperationId, + QuotaMode: "NORMAL", + MethodName: operationName, + QuotaMetrics: []*servicecontrol.MetricValueSet{ + createInt64MetricSet("calls", 1), + }, + }, + } + start := time.Now() + result, err := t.Service.Services.AllocateQuota(t.ServiceName, request).Do() + elapsed := time.Since(start) + if err != nil { + return err + } + bytes, err := json.MarshalIndent(result, "", " ") + if err != nil { + return &json.UnsupportedValueError{} + } + if t.Verbose { + fmt.Printf("ALLOCATE QUOTA (%fms) > %s\n", float64(elapsed)/1e6, string(bytes)) + } + if result.AllocateErrors != nil { + return errors.New("out of quota") + } + return nil +} + +func (t *Tracker) CallHandler() { + start := time.Now() + t.Context.Next() + t.BackendDuration = time.Since(start) +} + +func (t *Tracker) Report() error { + status := t.Context.Writer.Status() + now := time.Now() + timestampfloat := float64(now.UnixNano()) / 1e9 + timestamp2 := now.Format(time.RFC3339Nano) + + latency := time.Since(t.StartTime) + + requestSize := t.Context.Request.ContentLength + responseSize := t.Context.Writer.Size() + + callerIP := t.Context.RemoteIP() + apiName := "1." + strings.ReplaceAll( + strings.ReplaceAll(t.ServiceName, ".", "_"), + "-", "_") + operationName := apiName + "." + t.Method + operation := t.Operation + payload := map[string]interface{}{ + "api_key_state": "CHECKED", + "api_key": t.ApiKey, + "api_method": operationName, + "api_name": apiName, + "api_version": "1.0.0", + "http_status_code": status, + "location": "local", + "log_message": operationName + " is called", + "producer_project_id": t.Config.ProducerProject, + "response_code_detail": "via_upstream", + "service_agent": "Zero/0.0.1", + "service_config_id": t.Config.ServiceConfig, + "timestamp": timestampfloat, + "xtra": "extra info", + } + pbytes, err := json.Marshal(payload) + if err != nil { + return err + } + producerProject := t.Config.ProducerProject + operation.EndTime = timestamp2 + operation.Labels = map[string]string{ + "cloud.googleapis.com/location": "us-west1", + "serviceruntime.googleapis.com/api_method": operationName, + "cloud.googleapis.com/project": producerProject, + "cloud.googleapis.com/service": t.ServiceName, + "serviceruntime.googleapis.com/api_version": "1.0.0", + // none of the following appear in the logs + // but they seem to be set in ESPv2 + // https://github.com/GoogleCloudPlatform/esp-v2/blob/9217d68484321aceb7f7fbdc63be9363c96ed722/tests/utils/service_control_utils.go#L245 + "cloud.googleapis.com/uid": t.Operation.OperationId, + "servicecontrol.googleapis.com/caller_ip": callerIP, + "servicecontrol.googleapis.com/service_agent": "Zero/0.0.1", + "servicecontrol.googleapis.com/platform": "Custom", + "servicecontrol.googleapis.com/user_agent": "Zero", + "serviceruntime.googleapis.com/consumer_project": t.Config.ConsumerProject, + "/response_code": fmt.Sprintf("%d", status), + "/response_code_class": fmt.Sprintf("%dxx", status/100), + "/status_code": fmt.Sprintf("%d", status), + "/protocol": "http", + } + operation.LogEntries = []*servicecontrol.LogEntry{ + { + Name: "endpoints_log", + Timestamp: timestamp2, + Severity: "INFO", + HttpRequest: &servicecontrol.HttpRequest{ + RequestMethod: t.Context.Request.Method, + RequestUrl: t.Context.Request.URL.Path, + RequestSize: requestSize, + Status: int64(status), + ResponseSize: int64(responseSize), + RemoteIp: callerIP, + Latency: fmt.Sprintf("%fs", latency.Seconds()), + Protocol: "http", + }, + StructPayload: pbytes, + }, + } + operation.MetricValueSets = []*servicecontrol.MetricValueSet{ + createInt64MetricSet("serviceruntime.googleapis.com/api/consumer/request_count", 1), + createInt64MetricSet("serviceruntime.googleapis.com/api/producer/request_count", 1), + createInt64MetricSet("serviceruntime.googleapis.com/api/consumer/quota_used_count", 1), + createDistMetricSet(&timeDistOptions, "serviceruntime.googleapis.com/api/consumer/total_latencies", 1), + createDistMetricSet(&timeDistOptions, "serviceruntime.googleapis.com/api/producer/total_latencies", 1), + createDistMetricSet(&sizeDistOptions, "serviceruntime.googleapis.com/api/consumer/request_sizes", requestSize), + createDistMetricSet(&sizeDistOptions, "serviceruntime.googleapis.com/api/consumer/response_sizes", int64(responseSize)), + createDistMetricSet(&timeDistOptions, "serviceruntime.googleapis.com/api/producer/request_overhead_latencies", 1), + createDistMetricSet(&timeDistOptions, "serviceruntime.googleapis.com/api/producer/backend_latencies", t.BackendDuration.Milliseconds()), + createDistMetricSet(&sizeDistOptions, "serviceruntime.googleapis.com/api/producer/request_sizes", requestSize), + createDistMetricSet(&sizeDistOptions, "serviceruntime.googleapis.com/api/producer/response_sizes", int64(responseSize)), + } + operation.QuotaProperties = nil + request := &servicecontrol.ReportRequest{ + Operations: []*servicecontrol.Operation{ + operation, + }, + } + start := time.Now() + result, err := t.Service.Services.Report(t.ServiceName, request).Do() + if err != nil { + return err + } + elapsed := time.Since(start) + bytes, err := json.MarshalIndent(result, "", " ") + if err != nil { + return &json.UnsupportedValueError{} + } + if t.Verbose { + fmt.Printf("REPORT (%fms) > %s\n", float64(elapsed)/1e6, string(bytes)) + } + return nil +} + +type distOptions struct { + Buckets int64 + Growth float64 + Scale float64 +} + +func createInt64MetricSet(name string, value int64) *servicecontrol.MetricValueSet { + return &servicecontrol.MetricValueSet{ + MetricName: name, + MetricValues: []*servicecontrol.MetricValue{ + { + Int64Value: &value, + }, + }, + } +} + +var ( + timeDistOptions = distOptions{29, 2.0, 1e-6} + sizeDistOptions = distOptions{8, 10.0, 1} +) + +func createDistMetricSet(options *distOptions, name string, value int64) *servicecontrol.MetricValueSet { + buckets := make([]int64, options.Buckets+2) + fValue := float64(value) + idx := 0 + if fValue >= options.Scale { + idx = 1 + int(math.Log(fValue/options.Scale)/math.Log(options.Growth)) + if idx >= len(buckets) { + idx = len(buckets) - 1 + } + } + buckets[idx] = 1 + distValue := servicecontrol.Distribution{ + Count: 1, + BucketCounts: buckets, + ExponentialBuckets: &servicecontrol.ExponentialBuckets{ + NumFiniteBuckets: options.Buckets, + GrowthFactor: options.Growth, + Scale: options.Scale, + }, + } + if value != 0 { + distValue.Mean = fValue + distValue.Minimum = fValue + distValue.Maximum = fValue + } + return &servicecontrol.MetricValueSet{ + MetricName: name, + MetricValues: []*servicecontrol.MetricValue{ + { + DistributionValue: &distValue, + }, + }, + } +}