diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 054a1ba6..cbe4c13b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,10 +4,17 @@ jobs: cloudshell_open: runs-on: ubuntu-latest steps: + - name: Free disk space + run: | + docker system prune -a -f + sudo rm -rf /usr/local/lib/android + df /var/ + df -h - uses: actions/checkout@master - - uses: actions/setup-go@v1 + - uses: actions/setup-go@v5 with: - go-version: 1.14 + go-version: 1.23 + - run: git config --global init.defaultBranch main # TODO remove later as git updates to a newer version - run: go mod download - name: Set up git run: | @@ -21,13 +28,14 @@ jobs: working-directory: ./cmd/cloudshell_open - name: Build docker image run: docker build -t runbutton . + redirector: runs-on: ubuntu-latest steps: - uses: actions/checkout@master - - uses: actions/setup-go@v1 + - uses: actions/setup-go@v5 with: - go-version: 1.14 + go-version: 1.23 - run: go mod download working-directory: ./cmd/redirector - name: Validate formatting with go fmt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 78dfe6b0..1444bc8f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ ## Test Cloud Run Button's Underlying Command Locally with Local Go -1. Download Go 1.14.x +1. Download Go 1.14 (see [`Dockerfile`](Dockerfile) for the exact version used) 1. Run the tests: ``` go test ./cmd/cloudshell_open @@ -14,24 +14,31 @@ 1. To test the command: 1. [Enable the cloudresourcemanager API](https://console.developers.google.com/apis/api/cloudresourcemanager.googleapis.com/overview) 1. [Enable the billing API](https://console.developers.google.com/apis/api/cloudbilling.googleapis.com/overview) - 1. Create a Service Account with the *Cloud Run Admin*, *ServiceEnabler*, *Service Account User*, and *Storage Admin* roles - 1. Download the JSON key + 1. Create a Service Account with the following roles: + * Cloud Run Admin (`roles/run.admin`), + * Service Usage Admin (`roles/serviceusage.serviceUsageAdmin`), + * Service Account User (`roles/iam.serviceAccountUser`), and + * Storage Admin (`roles/storage.admin`). + 1. [Download the Service Account key as a JSON file](https://cloud.google.com/iam/docs/creating-managing-service-account-keys#creating) 1. Authenticate gcloud as the service account: ``` export GOOGLE_APPLICATION_CREDENTIALS=PATH_TO_YOUR_SERVICE_ACCOUNT_KEY_FILE gcloud auth activate-service-account --key-file=$GOOGLE_APPLICATION_CREDENTIALS export TRUSTED_ENVIRONMENT=true + export SKIP_CLONE_REPORTING=true ``` 1. Run the button: ``` - (cd /tmp; ./cloudshell_open --repo_url=https://github.com/GoogleCloudPlatform/cloud-run-hello.git; rm -rf cloud-run-hello) + (cd /tmp; rm -rf cloud-run-hello; ./cloudshell_open --repo_url=https://github.com/GoogleCloudPlatform/cloud-run-hello.git; rm -rf cloud-run-hello) ``` Other `cloudshell_open` flags: `--git_branch`, `--dir`, `--context` - Env vars you can set to avoid STDIN questions: `GOOGLE_CLOUD_PROJECT` `GOOGLE_CLOUD_REGION` + Optionally, you can set to `GOOGLE_CLOUD_PROJECT` `GOOGLE_CLOUD_REGION` to avoid being prompted for these. ## Test Cloud Run Button's Underlying Command Locally in a Container +โš ๏ธ This will download very large Docker images to your system. + 1. [Create a Service Account in a test account](https://console.cloud.google.com/iam-admin/serviceaccounts) 1. Download the key json file for the new service account 1. Build the Container @@ -52,6 +59,7 @@ -v $KEY_FILE:/root/user.json \ -e GOOGLE_APPLICATION_CREDENTIALS=/root/user.json \ -e TRUSTED_ENVIRONMENT=true \ + -e SKIP_CLONE_REPORTING=true \ --entrypoint=/bin/sh cloud-run-button -c \ "gcloud auth activate-service-account --key-file=/root/user.json \ --quiet && gcloud auth configure-docker --quiet && \ @@ -59,6 +67,13 @@ --repo_url=https://github.com/GoogleCloudPlatform/cloud-run-hello.git" ``` +## Test Instrumentless + +Test getting a coupon from the instrumentless API: +``` +go run ./cmd/instrumentless_test YOUR_EVENT $(gcloud auth print-access-token) +``` + ## Contributor License Agreement Contributions to this project must be accompanied by a Contributor License diff --git a/Dockerfile b/Dockerfile index 173b5566..568670df 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.14-alpine AS build +FROM golang:1.23-alpine AS build RUN apk add --no-cache git WORKDIR /src COPY go.mod go.sum ./ diff --git a/README.md b/README.md index 3fb73737..4df7d574 100644 --- a/README.md +++ b/README.md @@ -63,13 +63,20 @@ For example, a fully populated `app.json` file looks like this: }, "APP_SECRET": { "generator": "secret" + }, + "ORDERED_ENV": { + "description": "control the order env variables are prompted", + "order": 100 } }, "options": { "allow-unauthenticated": false, "memory": "512Mi", "cpu": "1", - "port": "80" + "port": 80, + "http2": false, + "concurrency": 80, + "max-instances": 10 }, "build": { "skip": false, @@ -114,17 +121,23 @@ Reference: - `required`, _(optional, default: `true`)_ indicates if they user must provide a value for this variable. - `generator`, _(optional)_ use a generator for the value, currently only support `secret` + - `order`, _(optional)_ if specified, used to indicate the order in which the + variable is prompted to the user. If some variables specify this and some + don't, then the unspecified ones are prompted last. - `options`: _(optional)_ Options when deploying the service - `allow-unauthenticated`: _(optional, default: `true`)_ allow unauthenticated requests - `memory`: _(optional)_ memory for each instance - `cpu`: _(optional)_ cpu for each instance - `port`: _(optional)_ if your application doesn't respect the PORT environment - variable provided by Cloud Run, specify the port number it listens on. + variable provided by Cloud Run, specify the port number it listens on + - `http2`: _(optional)_ use http2 for the connection + - `concurrency`: _(optional)_ concurrent requests for each instance + - `max-instances`: _(optional)_ autoscaling limit (max 1000) - `build`: _(optional)_ Build configuration - `skip`: _(optional, default: `false`)_ skips the built-in build methods (`docker build`, `Maven Jib`, and `buildpacks`), but still allows for `prebuild` and `postbuild` hooks to be run in order to build the container image manually - - `buildpacks`: _(optional)_ buildpacks config + - `buildpacks`: _(optional)_ buildpacks config (Note: Additional Buildpack config can be specified using a `project.toml` file. [See the spec for details](https://buildpacks.io/docs/reference/config/project-descriptor/).) - `builder`: _(optional, default: `gcr.io/buildpacks/builder:v1`)_ overrides the buildpack builder image - `hooks`: _(optional)_ Run commands in separate bash shells with the environment variables configured for the application and environment variables `GOOGLE_CLOUD_PROJECT` (Google Cloud project), `GOOGLE_CLOUD_REGION` @@ -138,7 +151,7 @@ Reference: - `commands`: _(array of strings)_ The list of commands to run - `precreate`: _(optional)_ Runs the specified commands before the service has been created - `commands`: _(array of strings)_ The list of commands to run - - `postcreate`: _(optional)_ Runs the specified commands after the service has been created + - `postcreate`: _(optional)_ Runs the specified commands after the service has been created; the `SERVICE_URL` environment variable provides the URL of the deployed Cloud Run service - `commands`: _(array of strings)_ The list of commands to run ### Notes diff --git a/cloudbuild.yaml b/cloudbuild.yaml index bd719825..0994e46c 100644 --- a/cloudbuild.yaml +++ b/cloudbuild.yaml @@ -1,12 +1,36 @@ +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + # Default cloudbuild.yaml for Cloud Shell custom images. # This file is used by the Cloud Build trigger that builds this image. steps: -- name: 'gcr.io/cloud-builders/docker' - args: ['build', '-t', 'gcr.io/$PROJECT_ID/button', '.'] -- name: 'gcr.io/cloudshell-images/custom-image-validation' - args: ['image_test.py', '--image', 'gcr.io/$PROJECT_ID/button'] -images: ['gcr.io/$PROJECT_ID/button'] -timeout: '3600s' + - name: "gcr.io/cloud-builders/docker" + args: + [ + "build", + "-t", + "gcr.io/$PROJECT_ID/button", + "-t", + "gcr.io/$PROJECT_ID/button:public-image-${COMMIT_SHA}", + ".", + ] + - name: "gcr.io/cloudshell-images/custom-image-validation" + args: ["image_test.py", "--image", "gcr.io/$PROJECT_ID/button"] +images: + - "gcr.io/$PROJECT_ID/button" + - "gcr.io/$PROJECT_ID/button:public-image-${COMMIT_SHA}" +timeout: "3600s" options: - machineType: 'N1_HIGHCPU_8' + machineType: "N1_HIGHCPU_8" diff --git a/cmd/cloudshell_open/api.go b/cmd/cloudshell_open/api.go index 68557e70..358bf33c 100644 --- a/cmd/cloudshell_open/api.go +++ b/cmd/cloudshell_open/api.go @@ -27,10 +27,6 @@ func enableAPIs(project string, apis []string) error { return fmt.Errorf("failed to create resource manager client: %w", err) } - // TODO(ahmetb) specify this explicitly, otherwise for some reason this becomes serviceusage.mtls.googleapis.com (and 404s) - // while querying the Operation. investigate later with the client library teams. - client.BasePath = "https://serviceusage.googleapis.com/" - enabled, err := enabledAPIs(client, project) if err != nil { return err diff --git a/cmd/cloudshell_open/appfile.go b/cmd/cloudshell_open/appfile.go index d9c914b2..b0a5470a 100644 --- a/cmd/cloudshell_open/appfile.go +++ b/cmd/cloudshell_open/appfile.go @@ -22,6 +22,7 @@ import ( "io" "os" "path/filepath" + "sort" "github.com/fatih/color" @@ -33,6 +34,7 @@ type env struct { Value string `json:"value"` Required *bool `json:"required"` Generator string `json:"generator"` + Order *int `json:"order"` } type options struct { @@ -40,6 +42,9 @@ type options struct { Memory string `json:"memory"` CPU string `json:"cpu"` Port int `json:"port"` + HTTP2 *bool `json:"http2"` + Concurrency int `json:"concurrency"` + MaxInstances int `json:"max-instances"` } type hook struct { @@ -190,7 +195,6 @@ func promptOrGenerateEnvs(list map[string]env) ([]string, error) { } func generateEnvs(keys []string) ([]string, error) { - for i, key := range keys { resp, err := rand64String() if err != nil { @@ -202,15 +206,50 @@ func generateEnvs(keys []string) ([]string, error) { return keys, nil } -func promptEnv(list map[string]env) ([]string, error) { - // TODO(ahmetb): remove these defers and make customizations at the - // individual prompt-level once survey lib allows non-global settings. +type envKeyValuePair struct { + k string + v env +} + +type envKeyValuePairs []envKeyValuePair + +func (e envKeyValuePairs) Len() int { return len(e) } + +func (e envKeyValuePairs) Swap(i, j int) { + e[i], e[j] = e[j], e[i] +} + +func (e envKeyValuePairs) Less(i, j int) bool { + // if env.Order is unspecified, it should appear less. + // otherwise, less values show earlier. + if e[i].v.Order == nil { + return false + } + if e[j].v.Order == nil { + return true + } + return *e[i].v.Order < *e[j].v.Order +} +func sortedEnvs(envs map[string]env) []string { + var v envKeyValuePairs + for key, value := range envs { + v = append(v, envKeyValuePair{key, value}) + } + sort.Sort(v) + var keys []string + for _, vv := range v { + keys = append(keys, vv.k) + } + return keys +} + +func promptEnv(list map[string]env) ([]string, error) { var out []string - // TODO(ahmetb): we should ideally use an ordered map structure for Env - // field and prompt the questions as they appear in the app.json file as - // opposed to random order we do here. - for k, e := range list { + sortedKeys := sortedEnvs(list) + + for _, k := range sortedKeys { + e := list[k] var resp string if err := survey.AskOne(&survey.Input{ diff --git a/cmd/cloudshell_open/appfile_test.go b/cmd/cloudshell_open/appfile_test.go index 0d7bdac8..afc5bcea 100644 --- a/cmd/cloudshell_open/appfile_test.go +++ b/cmd/cloudshell_open/appfile_test.go @@ -233,3 +233,25 @@ func TestGetAppFile(t *testing.T) { t.Fatalf("wrong parsed value: got=%#v, expected=%#v", v, expected) } } + +func Test_sortedEnvs(t *testing.T) { + envs := map[string]env{ + "NIL_ORDER": {}, + "ORDER_100": {Order: mkInt(100)}, + "ORDER_0": {Order: mkInt(0)}, + "ORDER_-10": {Order: mkInt(-10)}, + "ORDER_50": {Order: mkInt(50)}, + } + got := sortedEnvs(envs) + expected := []string{ + "ORDER_-10", "ORDER_0", "ORDER_50", "ORDER_100", "NIL_ORDER", + } + + if !reflect.DeepEqual(got, expected) { + t.Fatalf("sorted envs in wrong order: expected:%v\ngot=%v", expected, got) + } +} + +func mkInt(i int) *int { + return &i +} diff --git a/cmd/cloudshell_open/artifactregistry.go b/cmd/cloudshell_open/artifactregistry.go new file mode 100644 index 00000000..21ba8148 --- /dev/null +++ b/cmd/cloudshell_open/artifactregistry.go @@ -0,0 +1,74 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "errors" + "fmt" + + artifactregistry "cloud.google.com/go/artifactregistry/apiv1" + artifactregistrypb "cloud.google.com/go/artifactregistry/apiv1/artifactregistrypb" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// Create a "Cloud Run Source Deploy" repository in Artifact Registry (if it doesn't already exist) +func createArtifactRegistry(project string, region string, repoName string) error { + + repoPrefix := fmt.Sprintf("projects/%s/locations/%s", project, region) + repoFull := fmt.Sprintf("%s/repositories/%s", repoPrefix, repoName) + + ctx := context.Background() + + client, err := artifactregistry.NewClient(ctx) + if err != nil { + return fmt.Errorf("failed to create artifact registry client: %w", err) + } + + // Check for existing repo + req := &artifactregistrypb.GetRepositoryRequest{ + Name: repoFull, + } + existingRepo, err := client.GetRepository(ctx, req) + + if err != nil { + // The repo might not already exist, so allow that specific grpc error + notFoundError := status.Error(codes.NotFound, "Requested entity was not found.") + if !(errors.Is(err, notFoundError)) { + return fmt.Errorf("failed to retrieve existing artifact registry client: %w", err) + } + } + + // If the existing repo doesn't exist, create it + if existingRepo == nil { + req := &artifactregistrypb.CreateRepositoryRequest{ + Parent: repoPrefix, + RepositoryId: repoName, + Repository: &artifactregistrypb.Repository{ + Name: repoFull, + Format: artifactregistrypb.Repository_DOCKER, + }, + } + + _, err := client.CreateRepository(context.TODO(), req) + if err != nil { + return fmt.Errorf("failed to create artifact registry: %w", err) + } + } + + return nil + +} diff --git a/cmd/cloudshell_open/billing.go b/cmd/cloudshell_open/billing.go index d6582050..2f140cc9 100644 --- a/cmd/cloudshell_open/billing.go +++ b/cmd/cloudshell_open/billing.go @@ -34,3 +34,24 @@ func checkBillingEnabled(projectID string) (bool, error) { } return bo.BillingEnabled, nil } + +func billingAccounts() ([]cloudbilling.BillingAccount, error) { + var out []cloudbilling.BillingAccount + + client, err := cloudbilling.NewService(context.TODO()) + if err != nil { + return nil, fmt.Errorf("failed to initialize cloud billing client: %w", err) + } + billingAccounts, err := client.BillingAccounts.List().Context(context.TODO()).Do() + if err != nil { + return nil, fmt.Errorf("failed to query billing accounts: %w", err) + } + + for _, p := range billingAccounts.BillingAccounts { + if p.Open { + out = append(out, *p) + } + } + + return out, nil +} diff --git a/cmd/cloudshell_open/clone.go b/cmd/cloudshell_open/clone.go index 47cf8167..86b27230 100644 --- a/cmd/cloudshell_open/clone.go +++ b/cmd/cloudshell_open/clone.go @@ -16,6 +16,7 @@ package main import ( "fmt" + "net" "os/exec" "regexp" "strings" @@ -78,3 +79,27 @@ func gitCheckout(dir, rev string) error { } return nil } + +// signalRepoCloneStatus signals to the cloudshell host that the repo is +// cloned or not (bug/178009327). +func signalRepoCloneStatus(success bool) error { + c, err := net.Dial("tcp", net.JoinHostPort("localhost", "8998")) + if err != nil { + return fmt.Errorf("failed to connect to cloudshell host: %w", err) + } + msgFmt := `[null,null,null,[null,null,null,null,[%d]]]` + var msg string + if success { + msg = fmt.Sprintf(msgFmt, 1) + } else { + msg = fmt.Sprintf(msgFmt, 0) + } + msg = fmt.Sprintf("%d\n%s", len(msg), msg) + if _, err := c.Write([]byte(msg)); err != nil { + return fmt.Errorf("failed to send data to cloudshell host: %w", nil) + } + if err := c.Close(); err != nil { + return fmt.Errorf("failed to close conn to cloudshell host: %w", nil) + } + return nil +} diff --git a/cmd/cloudshell_open/clone_test.go b/cmd/cloudshell_open/clone_test.go index 6cbaddc4..b6ec0b99 100644 --- a/cmd/cloudshell_open/clone_test.go +++ b/cmd/cloudshell_open/clone_test.go @@ -106,7 +106,7 @@ func TestGitCheckout(t *testing.T) { run(t, "git", "commit", "--allow-empty", "--message", "initial commit") run(t, "git", "branch", "foo") - if err := gitCheckout(tmpDir, "master"); err != nil { + if err := gitCheckout(tmpDir, "main"); err != nil { t.Fatal(err) } if err := gitCheckout(tmpDir, "foo"); err != nil { diff --git a/cmd/cloudshell_open/cloudrun.go b/cmd/cloudshell_open/cloudrun.go index 9f1f8a8c..19e42022 100644 --- a/cmd/cloudshell_open/cloudrun.go +++ b/cmd/cloudshell_open/cloudrun.go @@ -115,12 +115,12 @@ func envVars(project, name, region string) (map[string]struct{}, error) { // prevent deployment failures due to Cloud Run service naming constraints such // as: // -// * names with a leading non-letter (e.g. digit or '-') are prefixed -// * names over 63 characters are truncated -// * names ending with a '-' have the suffix trimmed -func tryFixServiceName(name string) string { +// - names with a leading non-letter (e.g. digit or '-') are prefixed +// - names over 63 characters are truncated +// - names ending with a '-' have the suffix trimmed +func tryFixServiceName(name string) (string, error) { if name == "" { - return name + return "", fmt.Errorf("service name can't be empty") } name = strings.ToLower(name) @@ -145,5 +145,5 @@ func tryFixServiceName(name string) string { name = name[:len(name)-1] } - return name + return name, nil } diff --git a/cmd/cloudshell_open/cloudrun_test.go b/cmd/cloudshell_open/cloudrun_test.go index 2fbe1f18..50fc5ec9 100644 --- a/cmd/cloudshell_open/cloudrun_test.go +++ b/cmd/cloudshell_open/cloudrun_test.go @@ -46,7 +46,7 @@ func Test_tryFixServiceName(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := tryFixServiceName(tt.in); got != tt.want { + if got, _ := tryFixServiceName(tt.in); got != tt.want { t.Errorf("tryFixServiceName(%s) = %v, want %v", tt.in, got, tt.want) } }) diff --git a/cmd/cloudshell_open/deploy.go b/cmd/cloudshell_open/deploy.go index 6a3beca1..f108f990 100644 --- a/cmd/cloudshell_open/deploy.go +++ b/cmd/cloudshell_open/deploy.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "math/rand" + "strconv" "strings" "time" @@ -34,7 +35,7 @@ func deploy(project, name, image, region string, envs []string, options options) svc, err := getService(project, name, region) if err == nil { // existing service - svc = patchService(svc, envVars, image) + svc = patchService(svc, envVars, image, options) _, err = client.Namespaces.Services.ReplaceService("namespaces/"+project+"/services/"+name, svc).Do() if err != nil { if e, ok := err.(*googleapi.Error); ok { @@ -79,9 +80,23 @@ func optionsToResourceRequirements(options options) *runapi.ResourceRequirements if options.CPU != "" { limits["cpu"] = options.CPU } + return &runapi.ResourceRequirements{Limits: limits} } +func optionsToContainerSpec(options options) *runapi.ContainerPort { + var containerPortName = "http1" + if options.HTTP2 != nil && *options.HTTP2 { + containerPortName = "h2c" + } + + var containerPort = 8080 + if options.Port > 0 { + containerPort = options.Port + } + return &runapi.ContainerPort{ContainerPort: int64(containerPort), Name: containerPortName} +} + // newService initializes a new Knative Service object with given properties. func newService(name, project, image string, envs map[string]string, options options) *runapi.Service { var envVars []*runapi.EnvVar @@ -104,11 +119,13 @@ func newService(name, project, image string, envs map[string]string, options opt Annotations: make(map[string]string), }, Spec: &runapi.RevisionSpec{ + ContainerConcurrency: int64(options.Concurrency), Containers: []*runapi.Container{ { Image: image, Env: envVars, Resources: optionsToResourceRequirements(options), + Ports: []*runapi.ContainerPort{optionsToContainerSpec(options)}, }, }, }, @@ -117,17 +134,15 @@ func newService(name, project, image string, envs map[string]string, options opt }, }, } - if options.Port > 0 { - svc.Spec.Template.Spec.Containers[0].Ports = append(svc.Spec.Template.Spec.Containers[0].Ports, - &runapi.ContainerPort{ContainerPort: int64(options.Port)}) - } + applyMeta(svc.Metadata, image) applyMeta(svc.Spec.Template.Metadata, image) + applyScaleMeta(svc.Spec.Template.Metadata, "maxScale", options.MaxInstances) return svc } -// applyMeta applies optional annotations to the specified Metadata.Annotation field. +// applyMeta applies optional annotations to the specified Metadata.Annotation fields func applyMeta(meta *runapi.ObjectMeta, userImage string) { if meta.Annotations == nil { meta.Annotations = make(map[string]string) @@ -136,6 +151,13 @@ func applyMeta(meta *runapi.ObjectMeta, userImage string) { meta.Annotations["run.googleapis.com/client-name"] = "cloud-run-button" } +// applyScaleMeta optional annotations for scale commands +func applyScaleMeta(meta *runapi.ObjectMeta, scaleType string, scaleValue int) { + if scaleValue > 0 { + meta.Annotations["autoscaling.knative.dev"+scaleType] = strconv.Itoa(scaleValue) + } +} + // generateRevisionName attempts to generate a random revision name that is alphabetically increasing but also has // a random suffix. objectGeneration is the current object generation. func generateRevisionName(name string, objectGeneration int64) string { @@ -143,23 +165,29 @@ func generateRevisionName(name string, objectGeneration int64) string { out := name + "-" + num + "-" r := rand.New(rand.NewSource(time.Now().UnixNano())) for i := 0; i < 3; i++ { - out += string(int('a') + r.Intn(26)) + out += string(rune(int('a') + r.Intn(26))) } return out } // patchService modifies an existing Service with requested changes. -func patchService(svc *runapi.Service, envs map[string]string, image string) *runapi.Service { +func patchService(svc *runapi.Service, envs map[string]string, image string, options options) *runapi.Service { // merge env vars svc.Spec.Template.Spec.Containers[0].Env = mergeEnvs(svc.Spec.Template.Spec.Containers[0].Env, envs) // update container image svc.Spec.Template.Spec.Containers[0].Image = image + // update container port + svc.Spec.Template.Spec.Containers[0].Ports[0] = optionsToContainerSpec(options) + // apply metadata annotations applyMeta(svc.Metadata, image) applyMeta(svc.Spec.Template.Metadata, image) + // apply scale metadata annotations + applyScaleMeta(svc.Spec.Template.Metadata, "maxScale", options.MaxInstances) + // update revision name svc.Spec.Template.Metadata.Name = generateRevisionName(svc.Metadata.Name, svc.Metadata.Generation) diff --git a/cmd/cloudshell_open/main.go b/cmd/cloudshell_open/main.go index 5d15d2af..4c66b39a 100644 --- a/cmd/cloudshell_open/main.go +++ b/cmd/cloudshell_open/main.go @@ -25,22 +25,30 @@ import ( "strings" "time" + "github.com/AlecAivazis/survey/v2" + "github.com/GoogleCloudPlatform/cloud-run-button/cmd/instrumentless" + "google.golang.org/api/transport" + "cloud.google.com/go/compute/metadata" "github.com/briandowns/spinner" "github.com/fatih/color" ) const ( - flRepoURL = "repo_url" - flGitBranch = "git_branch" - flSubDir = "dir" - flPage = "page" - flContext = "context" + flRepoURL = "repo_url" + flGitBranch = "git_branch" + flSubDir = "dir" + flPage = "page" + flForceNewClone = "force_new_clone" + flContext = "context" reauthCredentialsWaitTimeout = time.Minute * 2 reauthCredentialsPollingInterval = time.Second - projectCreateURL = "https://console.cloud.google.com/cloud-resource-manager" + billingCreateURL = "https://console.cloud.google.com/billing/create" + trygcpURL = "https://console.cloud.google.com/trygcp" + instrumentlessEvent = "crbutton" + artifactRegistry = "cloud-run-source-deploy" ) var ( @@ -70,6 +78,7 @@ func init() { flags.StringVar(&opts.context, flContext, "", "(optional) arbitrary context") _ = flags.String(flPage, "", "ignored") + _ = flags.Bool(flForceNewClone, false, "ignored") } func main() { usage := flags.Usage @@ -149,6 +158,13 @@ func run(opts runOpts) error { fmt.Sprintf("Cloned git repository %s.", highlight(repo)), fmt.Sprintf("Failed to clone git repository %s", highlight(repo))) cloneDir, err := handleRepo(repo) + if trusted && os.Getenv("SKIP_CLONE_REPORTING") == "" { + // TODO(ahmetb) had to introduce SKIP_CLONE_REPORTING env var here + // to skip connecting to :8998 while testing locally if this var is set. + if err := signalRepoCloneStatus(err == nil); err != nil { + return err + } + } end(err == nil) if err != nil { return err @@ -181,13 +197,13 @@ func run(opts runOpts) error { project := os.Getenv("GOOGLE_CLOUD_PROJECT") - if project == "" { + for project == "" { var projects []string for len(projects) == 0 { - end = logProgress("Retrieving your GCP projects...", - "Queried list of your GCP projects", - "Failed to retrieve your GCP projects.", + end = logProgress("Retrieving your projects...", + "Queried list of your projects", + "Failed to retrieve your projects.", ) projects, err = listProjects() end(err == nil) @@ -196,14 +212,7 @@ func run(opts runOpts) error { } if len(projects) == 0 { - fmt.Print(errorPrefix+" "+ - warningLabel.Sprint("You don't have any GCP projects to deploy into!")+ - "\n 1. Visit "+linkLabel.Sprint(projectCreateURL), - "\n 2. Create a new GCP project with a billing account", - "\n 3. Once you're done, press "+parameterLabel.Sprint("Enter")+" to continue: ") - if _, err := bufio.NewReader(os.Stdin).ReadBytes('\n'); err != nil { - return err - } + fmt.Print(errorPrefix + " " + warningLabel.Sprint("You don't have any projects to deploy into.")) } } @@ -214,19 +223,51 @@ func run(opts runOpts) error { project, err = promptProject(projects) if err != nil { - return err + fmt.Println(errorPrefix + " " + warningLabel.Sprint("You need to create a project")) + err := promptInstrumentless() + if err != nil { + return err + } } } if err := waitForBilling(project, func(p string) error { - fmt.Print(errorPrefix+" "+ - warningLabel.Sprint("GCP project you chose does not have an active billing account!")+ - "\n 1. Visit "+linkLabel.Sprint(projectCreateURL), - "\n 2. Associate a billing account for project "+parameterLabel.Sprint(p), - "\n 3. Once you're done, press "+parameterLabel.Sprint("Enter")+" to continue: ") + projectLabel := color.New(color.Bold, color.FgHiCyan).Sprint(project) + + fmt.Println(fmt.Sprintf(errorPrefix+" Project %s does not have an active billing account!", projectLabel)) + + billingAccounts, err := billingAccounts() + if err != nil { + return fmt.Errorf("could not get billing accounts: %v", err) + } + + useExisting := false + + if len(billingAccounts) > 0 { + useExisting, err = prompUseExistingBillingAccount(project) + if err != nil { + return err + } + } + + if !useExisting { + err := promptInstrumentless() + if err != nil { + return err + } + } + + fmt.Println(infoPrefix + " Link the billing account to the project:" + + "\n " + linkLabel.Sprintf("https://console.cloud.google.com/billing?project=%s", project)) + + fmt.Println(questionPrefix + " " + "Once you're done, press " + parameterLabel.Sprint("Enter") + " to continue: ") + if _, err := bufio.NewReader(os.Stdin).ReadBytes('\n'); err != nil { return err } + + // TODO(jamesward) automatically set billing account on project + return nil }); err != nil { return err @@ -236,7 +277,7 @@ func run(opts runOpts) error { fmt.Sprintf("Enabling Cloud Run API on project %s...", highlight(project)), fmt.Sprintf("Enabled Cloud Run API on project %s.", highlight(project)), fmt.Sprintf("Failed to enable required APIs on project %s.", highlight(project))) - err = enableAPIs(project, []string{"run.googleapis.com", "containerregistry.googleapis.com"}) + err = enableAPIs(project, []string{"run.googleapis.com", "artifactregistry.googleapis.com"}) end(err == nil) if err != nil { return err @@ -251,14 +292,27 @@ func run(opts runOpts) error { } } + end = logProgress( + fmt.Sprintf("Setting up %s in region %s (if it doesn't already exist)", highlight(artifactRegistry), highlight(region)), + fmt.Sprintf("Set up %s in region %s (if it doesn't already exist)", highlight(artifactRegistry), highlight(region)), + "Failed to setup artifact registry.") + err = createArtifactRegistry(project, region, artifactRegistry) + end(err == nil) + if err != nil { + return err + } + repoName := filepath.Base(appDir) serviceName := repoName if appFile.Name != "" { serviceName = appFile.Name } - serviceName = tryFixServiceName(serviceName) + serviceName, err = tryFixServiceName(serviceName) + if err != nil { + return err + } - image := fmt.Sprintf("gcr.io/%s/%s", project, serviceName) + image := fmt.Sprintf("%s-docker.pkg.dev/%s/%s/%s", region, project, artifactRegistry, serviceName) existingEnvVars := make(map[string]struct{}) // todo(jamesward) actually determine if the service exists instead of assuming it doesn't if we get an error @@ -403,6 +457,8 @@ func run(opts runOpts) error { return err } + hookEnvs = append(hookEnvs, fmt.Sprintf("SERVICE_URL=%s", url)) + if existingService == nil { err = runScripts(appDir, appFile.Hooks.PostCreate.Commands, hookEnvs) if err != nil { @@ -440,6 +496,24 @@ func optionsToFlags(options options) []string { flags = append(flags, cpuSetting) } + if options.HTTP2 != nil { + if *options.HTTP2 == false { + flags = append(flags, "--no-use-http2") + } else { + flags = append(flags, "--use-http2") + } + } + + if options.Concurrency > 0 { + concurrencySetting := fmt.Sprintf("--concurrency=%d", options.Concurrency) + flags = append(flags, concurrencySetting) + } + + if options.MaxInstances > 0 { + maxInstancesSetting := fmt.Sprintf("--max-instances=%d", options.MaxInstances) + flags = append(flags, maxInstancesSetting) + } + return flags } @@ -523,3 +597,69 @@ func isSubPath(a, b string) (bool, error) { } return !strings.HasPrefix(v, ".."+string(os.PathSeparator)), nil } + +func instrumentlessCoupon() (*instrumentless.Coupon, error) { + ctx := context.TODO() + + creds, err := transport.Creds(ctx) + if err != nil { + return nil, fmt.Errorf("could not get user credentials: %v", err) + } + + token, err := creds.TokenSource.Token() + if err != nil { + return nil, fmt.Errorf("could not get an auth token: %v", err) + } + + return instrumentless.GetCoupon(instrumentlessEvent, token.AccessToken) +} + +func promptInstrumentless() error { + coupon, err := instrumentlessCoupon() + + if err != nil || coupon == nil { + fmt.Println(infoPrefix + " Create a new billing account:") + fmt.Println(" " + linkLabel.Sprint(billingCreateURL)) + fmt.Println(questionPrefix + " " + "Once you're done, press " + parameterLabel.Sprint("Enter") + " to continue: ") + + if _, err := bufio.NewReader(os.Stdin).ReadBytes('\n'); err != nil { + return err + } + + return nil + } + + code := "" + parts := strings.Split(coupon.URL, "code=") + if len(parts) == 2 { + code = parts[1] + } else { + return fmt.Errorf("could not get a coupon code") + } + + fmt.Println(infoPrefix + " Open this page:\n " + linkLabel.Sprint(trygcpURL)) + + fmt.Println(infoPrefix + " Use this coupon code:\n " + code) + + fmt.Println(questionPrefix + " Once you're done, press " + parameterLabel.Sprint("Enter") + " to continue: ") + + if _, err := bufio.NewReader(os.Stdin).ReadBytes('\n'); err != nil { + return err + } + + return nil +} + +func prompUseExistingBillingAccount(project string) (bool, error) { + useExisting := false + + projectLabel := color.New(color.Bold, color.FgHiCyan).Sprint(project) + + if err := survey.AskOne(&survey.Confirm{ + Default: false, + Message: fmt.Sprintf("Would you like to use an existing billing account with project %s?", projectLabel), + }, &useExisting, surveyIconOpts); err != nil { + return false, fmt.Errorf("could not prompt for confirmation %+v", err) + } + return useExisting, nil +} diff --git a/cmd/cloudshell_open/project.go b/cmd/cloudshell_open/project.go index 20975b99..2fdf244b 100644 --- a/cmd/cloudshell_open/project.go +++ b/cmd/cloudshell_open/project.go @@ -42,7 +42,9 @@ func listProjects() ([]string, error) { var out []string if err := client.Projects.List().PageSize(1000).Pages(context.TODO(), func(resp *cloudresourcemanager.ListProjectsResponse) error { for _, p := range resp.Projects { - out = append(out, p.ProjectId) + if p.LifecycleState == "ACTIVE" { + out = append(out, p.ProjectId) + } } return nil }); err != nil { @@ -82,11 +84,10 @@ func confirmProject(project string) (bool, error) { func promptMultipleProjects(projects []string) (string, error) { var p string if err := survey.AskOne(&survey.Select{ - Message: "Choose a project to deploy this application:", + Message: "Choose a project or press ctrl-c to create a new project:", Options: projects, }, &p, surveyIconOpts, - survey.WithValidator(survey.Required), ); err != nil { return p, fmt.Errorf("could not choose a project: %+v", err) } diff --git a/cmd/instrumentless/instrumentless.go b/cmd/instrumentless/instrumentless.go new file mode 100644 index 00000000..42e94b07 --- /dev/null +++ b/cmd/instrumentless/instrumentless.go @@ -0,0 +1,58 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package instrumentless + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" +) + +type Coupon struct { + URL string `json:"url"` +} + +func GetCoupon(event string, bearerToken string) (*Coupon, error) { + url := fmt.Sprintf("https://api.gcpcredits.com/%s", event) + + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", bearerToken)) + + res, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + + if res.StatusCode != 200 { + return nil, errors.New(res.Status) + } + + if res.Body != nil { + defer res.Body.Close() + } + + coupon := Coupon{} + err = json.NewDecoder(res.Body).Decode(&coupon) + if err != nil { + return nil, fmt.Errorf("failed to read and parse the response: %v", err) + } + + return &coupon, nil +} diff --git a/cmd/instrumentless_test/main.go b/cmd/instrumentless_test/main.go new file mode 100644 index 00000000..21061e44 --- /dev/null +++ b/cmd/instrumentless_test/main.go @@ -0,0 +1,42 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "os" + + "github.com/GoogleCloudPlatform/cloud-run-button/cmd/instrumentless" +) + +func main() { + + if len(os.Args) < 3 { + fmt.Fprintln(os.Stderr, "Not enough args") + os.Exit(1) + } + + event := os.Args[1] + token := os.Args[2] + + coupon, err := instrumentless.GetCoupon(event, token) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + fmt.Printf("Got coupon: %s\n", coupon.URL) + +} diff --git a/go.mod b/go.mod index b2d7be17..de439955 100644 --- a/go.mod +++ b/go.mod @@ -1,36 +1,52 @@ module github.com/GoogleCloudPlatform/cloud-run-button -go 1.14 +go 1.23.0 + +toolchain go1.24.1 + +require ( + cloud.google.com/go/artifactregistry v1.16.3 + cloud.google.com/go/compute/metadata v0.6.0 + github.com/AlecAivazis/survey/v2 v2.3.7 + github.com/briandowns/spinner v1.23.2 + github.com/fatih/color v1.18.0 + google.golang.org/api v0.228.0 + google.golang.org/grpc v1.71.0 +) require ( - cloud.google.com/go v0.55.0 - cloud.google.com/go/pubsub v1.3.1 // indirect - dmitri.shuralyov.com/gpu/mtl v0.0.0-20191203043605-d42048ed14fd // indirect - github.com/AlecAivazis/survey/v2 v2.0.7 - github.com/BurntSushi/xgb v0.0.0-20200324125942-20f126ea2843 // indirect - github.com/Netflix/go-expect v0.0.0-20200312175327-da48e75238e2 // indirect - github.com/briandowns/spinner v1.9.0 - github.com/cncf/udpa/go v0.0.0-20200324003616-bae28a880fdb // indirect - github.com/fatih/color v1.9.0 - github.com/hashicorp/golang-lru v0.5.4 // indirect - github.com/hinshun/vt10x v0.0.0-20180809195222-d55458df857c // indirect - github.com/kr/pretty v0.2.0 // indirect - github.com/kr/pty v1.1.8 // indirect - github.com/kr/text v0.2.0 // indirect - github.com/mattn/go-colorable v0.1.6 // indirect - github.com/prometheus/client_model v0.2.0 // indirect - github.com/rogpeppe/go-internal v1.5.2 // indirect - github.com/stretchr/objx v0.2.0 // indirect - github.com/stretchr/testify v1.5.1 // indirect - golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 // indirect - golang.org/x/exp v0.0.0-20200320212757-167ffe94c325 // indirect - golang.org/x/image v0.0.0-20200119044424-58c23975cae1 // indirect - golang.org/x/mobile v0.0.0-20200222142934-3c8601c510d0 // indirect - golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect - golang.org/x/tools v0.0.0-20200324175852-6fb6f5a9fc59 // indirect - google.golang.org/api v0.20.0 - google.golang.org/genproto v0.0.0-20200323114720-3f67cca34472 // indirect - gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect - gopkg.in/yaml.v2 v2.2.8 // indirect - rsc.io/sampler v1.99.99 // indirect + cloud.google.com/go v0.120.0 // indirect + cloud.google.com/go/auth v0.15.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/iam v1.4.2 // indirect + cloud.google.com/go/longrunning v0.6.6 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.14.1 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/oauth2 v0.28.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/term v0.30.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/time v0.11.0 // indirect + google.golang.org/genproto v0.0.0-20250324211829-b45e905df463 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect + google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/go.sum b/go.sum index 210317cc..2e851723 100644 --- a/go.sum +++ b/go.sum @@ -1,382 +1,139 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.55.0 h1:eoz/lYxKSL4CNAiaUJ0ZfD1J3bfMYbU5B3rwM1C1EIU= -cloud.google.com/go v0.55.0/go.mod h1:ZHmoY+/lIMNkN2+fBmuTiqZ4inFhvQad8ft7MT8IV5Y= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20191203043605-d42048ed14fd/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/AlecAivazis/survey/v2 v2.0.1 h1:fQuEpIq5H/rJMaGqiOtPBgnRZP1oexFYRi6bonYTi0w= -github.com/AlecAivazis/survey/v2 v2.0.1/go.mod h1:WYBhg6f0y/fNYUuesWQc0PKbJcEliGcYHB9sNT3Bg74= -github.com/AlecAivazis/survey/v2 v2.0.7 h1:+f825XHLse/hWd2tE/V5df04WFGimk34Eyg/z35w/rc= -github.com/AlecAivazis/survey/v2 v2.0.7/go.mod h1:mlizQTaPjnR4jcpwRSaSlkbsRfYFEyKgLQvYTzxxiHA= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/BurntSushi/xgb v0.0.0-20200324125942-20f126ea2843/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw= -github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= -github.com/Netflix/go-expect v0.0.0-20200312175327-da48e75238e2/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= -github.com/briandowns/spinner v0.0.0-20190319032542-ac46072a5a91 h1:GMmnK0dvr0Sf0gx3DvTbln0c8DE07B7sPVD9dgHOqo4= -github.com/briandowns/spinner v0.0.0-20190319032542-ac46072a5a91/go.mod h1:hw/JEQBIE+c/BLI4aKM8UU8v+ZqrD3h7HC27kKt8JQU= -github.com/briandowns/spinner v1.9.0 h1:+OMAisemaHar1hjuJ3Z2hIvNhQl9Y7GLPWUwwz2Pxo8= -github.com/briandowns/spinner v1.9.0/go.mod h1://Zf9tMcxfRUA36V23M6YGEAv+kECGfvpnLTnb8n4XQ= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200324003616-bae28a880fdb/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA= +cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= +cloud.google.com/go/artifactregistry v1.16.3 h1:FPTr8KmB+K29uOg+3IITqdXHd1sP+Y+hxQtMvxW0MfM= +cloud.google.com/go/artifactregistry v1.16.3/go.mod h1:eiLO70Qh5Z9Jbwctl0KdW5VzJ5HncWgNaYN0NdF8lmM= +cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= +cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/iam v1.4.2 h1:4AckGYAYsowXeHzsn/LCKWIwSWLkdb0eGjH8wWkd27Q= +cloud.google.com/go/iam v1.4.2/go.mod h1:REGlrt8vSlh4dfCJfSEcNjLGq75wW75c5aU3FLOYq34= +cloud.google.com/go/longrunning v0.6.6 h1:XJNDo5MUfMM05xK3ewpbSdmt7R2Zw+aQEMbdQR65Rbw= +cloud.google.com/go/longrunning v0.6.6/go.mod h1:hyeGJUrPHcx0u2Uu1UFSoYZLn4lkMrccJig0t4FI7yw= +github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= +github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= +github.com/briandowns/spinner v1.23.2 h1:Zc6ecUnI+YzLmJniCfDNaMbW0Wid1d5+qcTq4L2FW8w= +github.com/briandowns/spinner v1.23.2/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= +github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= +github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ= -github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= -github.com/hinshun/vt10x v0.0.0-20180809195222-d55458df857c/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= +github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= +github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ= -github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/testify v1.2.1 h1:52QO5WkIUcHGIR7EnGagH88x1bUzqGXTC5/1bDTUQ7U= -github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 h1:8dUaAV7K4uHsF56JQWkprecIQKdPHtR9jCHF5nB8uzc= -golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20200320212757-167ffe94c325/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mobile v0.0.0-20200222142934-3c8601c510d0/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200320220750-118fecf932d8 h1:1+zQlQqEEhUeStBTi653GZAnAuivZq/2hz+Iz+OP7rg= -golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= +golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1 h1:R4dVlxdmKenVdMRS/tTspEpSTRWINYrHD8ySIU9yCIU= -golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200317113312-5766fd39f98d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200320181252-af34d8274f85 h1:fD99hd4ciR6T3oPhr2EkmuKe9oHixHx9Hj/hND89j3g= -golang.org/x/sys v0.0.0-20200320181252-af34d8274f85/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200317043434-63da46f3035e/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200324175852-6fb6f5a9fc59/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.7.1-0.20190723010802-069bea57b1be h1:E/Spp2CUo60yVoPmkhX19wXoHBH+sPiyVFjSulU7EUE= -google.golang.org/api v0.7.1-0.20190723010802-069bea57b1be/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0 h1:jz2KixHX7EcCPiQrySzPdnYT7DbINAypCqKZ1Z7GM40= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873 h1:nfPFGzJkUDX6uBmpN/pSw7MbOAWegH5QDQuoXFHedLg= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200317114155-1f3552e48f24/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200319113533-08878b785e9c h1:5aI3/f/3eCZps9xwoEnmgfDJDhMbnJpfqeGpjVNgVEI= -google.golang.org/genproto v0.0.0-20200319113533-08878b785e9c/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200323114720-3f67cca34472 h1:XRuIAeTRoXziYGYTVer+YGxVXQBiOhZ8+SpNELP73oQ= -google.golang.org/genproto v0.0.0-20200323114720-3f67cca34472/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0 h1:bO/TA4OxCOummhSf10siHuG7vJOiwh7SpRpFZDkOgl4= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/api v0.228.0 h1:X2DJ/uoWGnY5obVjewbp8icSL5U4FzuCfy9OjbLSnLs= +google.golang.org/api v0.228.0/go.mod h1:wNvRS1Pbe8r4+IfBIniV8fwCpGwTrYa+kMUDiC5z5a4= +google.golang.org/genproto v0.0.0-20250324211829-b45e905df463 h1:qEFnJI6AnfZk0NNe8YTyXQh5i//Zxi4gBHwRgp76qpw= +google.golang.org/genproto v0.0.0-20250324211829-b45e905df463/go.mod h1:SqIx1NV9hcvqdLHo7uNZDS5lrUJybQ3evo3+z/WBfA0= +google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM= +google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -rsc.io/sampler v1.99.99/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/integration.cloudbuild.yaml b/integration.cloudbuild.yaml new file mode 100644 index 00000000..1a56ba50 --- /dev/null +++ b/integration.cloudbuild.yaml @@ -0,0 +1,88 @@ +steps: + - id: build go + name: golang:${_VERSION} + env: + - CGO_ENABLED=0 + - GOOS=linux + args: + [ + "go", + "build", + "-o", + "/workspace/cloudshell_open", + "./cmd/cloudshell_open", + ] + + - id: docker build + name: "gcr.io/cloud-builders/docker" + args: ["build", "-t", "gcr.io/$PROJECT_ID/button", "."] + + - id: docker push + name: "gcr.io/cloud-builders/docker" + args: ["push", "gcr.io/$PROJECT_ID/button"] + + - id: image validation + name: "gcr.io/cloudshell-images/custom-image-validation" + args: ["image_test.py", "--image", "gcr.io/$PROJECT_ID/button"] + + - id: run tests + name: gcr.io/$PROJECT_ID/button + env: + - "DEBUG=${_DEBUG}" + - "GOOGLE_CLOUD_PROJECT=$PROJECT_ID" + - "GOOGLE_CLOUD_REGION=${_REGION}" + - "GIT_BRANCH=${BRANCH_NAME}" + - "PYTHONUNBUFFERED=true" + - "TEST_CMD=python3 tests/run_integration_test.py deploy --repo_url $_REPO_URL --repo_branch $_REPO_BRANCH" + script: | + #!/bin/bash -e + + gcloud version + + pip3 install -r tests/requirements.txt --user + + $TEST_CMD --description "no config" --directory empty-appjson --expected_text 'hello, world' + + $TEST_CMD --description "inline hooks" --directory hooks-prepostcreate-inline --expected_text 'AB' + + $TEST_CMD --description "external hooks" --directory hooks-prepostcreate-external --expected_text '3' + $TEST_CMD --description "external hooks, dirty" --directory hooks-prepostcreate-external --expected_text '3' --dirty + + $TEST_CMD --description "generator" --directory envvars-generated + GEN_URL=$(gcloud run services describe envvars-generated --region $GOOGLE_CLOUD_REGION --format "value(status.url)") + OUTPUT=$(curl $GEN_URL --silent) + $TEST_CMD --description "generator, dirty" --directory envvars-generated --dirty --expected_text $OUTPUT + + $TEST_CMD --description "options" --directory options --expected_text 'hello, world' + $TEST_CMD --description "options - http2" --directory options-http2 --expected_text 'hello, world' + $TEST_CMD --description "options - require auth" --directory options-require-auth --expected_status 403 + $TEST_CMD --description "custom buildpacks" --directory buildpacks-builder --expected_text 'hello, world' + + + - id: cleanup + name: 'gcr.io/google.com/cloudsdktool/cloud-sdk:slim' + env: + - "GOOGLE_CLOUD_REGION=${_REGION}" + script: | + #!/bin/bash -e + gcloud run services list --region $GOOGLE_CLOUD_REGION + + for service in $(gcloud run services list --region $GOOGLE_CLOUD_REGION --format "value(name)"); do + echo "๐Ÿงน Cleaning up $service" + gcloud run services delete $service --region $GOOGLE_CLOUD_REGION --quiet + done; + +timeout: "3600s" + +substitutions: + _VERSION: "1.23" + _REGION: us-central1 + _REPO_URL: https://github.com/GoogleCloudPlatform/cloud-run-button + _REPO_BRANCH: master + _DEBUG: "" + +options: + machineType: "N1_HIGHCPU_32" + dynamicSubstitutions: true + +logsBucket: ${PROJECT_ID}-buildlogs diff --git a/renovate.json b/renovate.json new file mode 100644 index 00000000..de881db3 --- /dev/null +++ b/renovate.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended", + "schedule:weekly", + ":semanticCommits" + ], + "postUpdateOptions": [ + "gomodTidy" + ], + "commitMessagePrefix": "chore(all): ", + "commitMessageAction": "update", + "groupName": "all", + "packageRules": [ + { + "matchUpdateTypes": [ + "major" + ], + "enabled": false + } + ], + "constraints": { + "go": "1.23" + } +} diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..698fc9b2 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,12 @@ +# Integration tests + +The service account that runs these tests (either the default Cloud Build Service Account, or a custom account) requires the same permissions as in the [CONTRIBUTING](../CONTRIBUTING.md) guide. + +Run `../integration.cloudbuild.yaml` to test variations on deployments. Open any test folder in GitHub in the browser and click the Run on Google Cloud to test in isolation. + +When adding new features, you should: + + * create a new folder in `test/` + * append the test to the `run tests` step in `../integration.cloudbuild.yaml` + +Note: new tests must be run separately, as this harness might use the PR for the configuration, but each tests pulls from the main branch. diff --git a/tests/buildpacks-builder/README.md b/tests/buildpacks-builder/README.md new file mode 100644 index 00000000..7cd2bbb5 --- /dev/null +++ b/tests/buildpacks-builder/README.md @@ -0,0 +1,5 @@ +# buildpacks builder test + +Use a custom buildpacks builder to make the image. + +[![Run on Google Cloud](https://deploy.cloud.run/button.svg)](https://deploy.cloud.run) diff --git a/tests/buildpacks-builder/app.json b/tests/buildpacks-builder/app.json new file mode 100644 index 00000000..b3be2f37 --- /dev/null +++ b/tests/buildpacks-builder/app.json @@ -0,0 +1,7 @@ +{ + "build": { + "buildpacks": { + "builder": "paketobuildpacks/builder:full" + } + } +} diff --git a/tests/buildpacks-builder/index.php b/tests/buildpacks-builder/index.php new file mode 100644 index 00000000..4b5fa637 --- /dev/null +++ b/tests/buildpacks-builder/index.php @@ -0,0 +1 @@ +hello, world diff --git a/tests/empty-appjson/Dockerfile b/tests/empty-appjson/Dockerfile new file mode 100644 index 00000000..03281627 --- /dev/null +++ b/tests/empty-appjson/Dockerfile @@ -0,0 +1,3 @@ +FROM alpine + +ENTRYPOINT while :; do nc -k -l -p $PORT -e sh -c 'echo -e "HTTP/1.1 200 OK\n\nhello, world"'; done diff --git a/tests/empty-appjson/README.md b/tests/empty-appjson/README.md new file mode 100644 index 00000000..8e9a2890 --- /dev/null +++ b/tests/empty-appjson/README.md @@ -0,0 +1,5 @@ +# empty config + +This folder has an empty config. + +[![Run on Google Cloud](https://deploy.cloud.run/button.svg)](https://deploy.cloud.run) diff --git a/tests/empty-appjson/app.json b/tests/empty-appjson/app.json new file mode 100644 index 00000000..2c63c085 --- /dev/null +++ b/tests/empty-appjson/app.json @@ -0,0 +1,2 @@ +{ +} diff --git a/tests/envvars-generated/Dockerfile b/tests/envvars-generated/Dockerfile new file mode 100644 index 00000000..b4f18a0f --- /dev/null +++ b/tests/envvars-generated/Dockerfile @@ -0,0 +1,3 @@ +FROM alpine + +ENTRYPOINT while :; do nc -k -l -p $PORT -e sh -c 'echo -e "HTTP/1.1 200 OK\n\n$FOO"'; done diff --git a/tests/envvars-generated/README.md b/tests/envvars-generated/README.md new file mode 100644 index 00000000..69fcd611 --- /dev/null +++ b/tests/envvars-generated/README.md @@ -0,0 +1,5 @@ +# Generated envvars + +This service generates it's own envvars. Re-deploy to ensure they don't get overridden again. + +[![Run on Google Cloud](https://deploy.cloud.run/button.svg)](https://deploy.cloud.run) diff --git a/tests/envvars-generated/app.json b/tests/envvars-generated/app.json new file mode 100644 index 00000000..32301227 --- /dev/null +++ b/tests/envvars-generated/app.json @@ -0,0 +1,7 @@ +{ + "env": { + "FOO": { + "generator": "secret" + } + } +} diff --git a/tests/hooks-prepostcreate-external/Dockerfile b/tests/hooks-prepostcreate-external/Dockerfile new file mode 100644 index 00000000..cd207e46 --- /dev/null +++ b/tests/hooks-prepostcreate-external/Dockerfile @@ -0,0 +1,3 @@ +FROM alpine + +ENTRYPOINT while :; do nc -k -l -p $PORT -e sh -c 'echo -e "HTTP/1.1 200 OK\n\n$GEN"'; done diff --git a/tests/hooks-prepostcreate-external/README.md b/tests/hooks-prepostcreate-external/README.md new file mode 100644 index 00000000..1bfc7733 --- /dev/null +++ b/tests/hooks-prepostcreate-external/README.md @@ -0,0 +1,5 @@ +# External Hooks test + +Run the shell scripts as in this folder in pre and post create steps + +[![Run on Google Cloud](https://deploy.cloud.run/button.svg)](https://deploy.cloud.run) diff --git a/tests/hooks-prepostcreate-external/app.json b/tests/hooks-prepostcreate-external/app.json new file mode 100644 index 00000000..cc3a4058 --- /dev/null +++ b/tests/hooks-prepostcreate-external/app.json @@ -0,0 +1,14 @@ +{ + "hooks": { + "precreate": { + "commands": [ + "./precreate.sh" + ] + }, + "postcreate": { + "commands": [ + "./postcreate.sh" + ] + } + } +} diff --git a/tests/hooks-prepostcreate-external/postcreate.sh b/tests/hooks-prepostcreate-external/postcreate.sh new file mode 100755 index 00000000..e3c5dc31 --- /dev/null +++ b/tests/hooks-prepostcreate-external/postcreate.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +GEN=$(gcloud run services describe --platform=managed --project=$GOOGLE_CLOUD_PROJECT --region=$GOOGLE_CLOUD_REGION --format='value(status.observedGeneration)' $K_SERVICE) +gcloud run services update --platform=managed --project=$GOOGLE_CLOUD_PROJECT --region=$GOOGLE_CLOUD_REGION --update-env-vars=GEN=$GEN $K_SERVICE diff --git a/tests/hooks-prepostcreate-external/precreate.sh b/tests/hooks-prepostcreate-external/precreate.sh new file mode 100755 index 00000000..87b06257 --- /dev/null +++ b/tests/hooks-prepostcreate-external/precreate.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +gcloud run deploy --image=gcr.io/cloudrun/hello --platform=managed --project=$GOOGLE_CLOUD_PROJECT --region=$GOOGLE_CLOUD_REGION --no-allow-unauthenticated $K_SERVICE +GEN=$(gcloud run services describe --platform=managed --project=$GOOGLE_CLOUD_PROJECT --region=$GOOGLE_CLOUD_REGION --format='value(status.observedGeneration)' $K_SERVICE) +gcloud run services update --platform=managed --project=$GOOGLE_CLOUD_PROJECT --region=$GOOGLE_CLOUD_REGION --update-env-vars=GEN=$GEN $K_SERVICE diff --git a/tests/hooks-prepostcreate-inline/Dockerfile b/tests/hooks-prepostcreate-inline/Dockerfile new file mode 100644 index 00000000..51793c24 --- /dev/null +++ b/tests/hooks-prepostcreate-inline/Dockerfile @@ -0,0 +1,3 @@ +FROM alpine + +ENTRYPOINT while :; do nc -k -l -p $PORT -e sh -c 'echo -e "HTTP/1.1 200 OK\n\n$A$B"'; done diff --git a/tests/hooks-prepostcreate-inline/README.md b/tests/hooks-prepostcreate-inline/README.md new file mode 100644 index 00000000..dfba9a1a --- /dev/null +++ b/tests/hooks-prepostcreate-inline/README.md @@ -0,0 +1,5 @@ +# Inline Hooks test + +Run the shell scripts inline in pre and post create steps + +[![Run on Google Cloud](https://deploy.cloud.run/button.svg)](https://deploy.cloud.run) diff --git a/tests/hooks-prepostcreate-inline/app.json b/tests/hooks-prepostcreate-inline/app.json new file mode 100644 index 00000000..dfdc41d6 --- /dev/null +++ b/tests/hooks-prepostcreate-inline/app.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "precreate": { + "commands": [ + "gcloud run deploy --image=gcr.io/cloudrun/hello --platform=managed --project=$GOOGLE_CLOUD_PROJECT --region=$GOOGLE_CLOUD_REGION --no-allow-unauthenticated $K_SERVICE", + "gcloud run services update --platform=managed --project=$GOOGLE_CLOUD_PROJECT --region=$GOOGLE_CLOUD_REGION --update-env-vars=A=A $K_SERVICE" + ] + }, + "postcreate": { + "commands": [ + "gcloud run services update --platform=managed --project=$GOOGLE_CLOUD_PROJECT --region=$GOOGLE_CLOUD_REGION --update-env-vars=B=B $K_SERVICE" + ] + } + } +} diff --git a/tests/options-http2/Dockerfile b/tests/options-http2/Dockerfile new file mode 100644 index 00000000..1ef641f6 --- /dev/null +++ b/tests/options-http2/Dockerfile @@ -0,0 +1,4 @@ +FROM lkwg82/h2o-http2-server + +COPY h2o.conf h2o.conf +COPY index.html /www/ diff --git a/tests/options-http2/README.md b/tests/options-http2/README.md new file mode 100644 index 00000000..71099d18 --- /dev/null +++ b/tests/options-http2/README.md @@ -0,0 +1,5 @@ +# HTTP2 Options + +Deploy with HTTP2 + +[![Run on Google Cloud](https://deploy.cloud.run/button.svg)](https://deploy.cloud.run) diff --git a/tests/options-http2/app.json b/tests/options-http2/app.json new file mode 100644 index 00000000..282912c0 --- /dev/null +++ b/tests/options-http2/app.json @@ -0,0 +1,5 @@ +{ + "options": { + "http2": true + } +} diff --git a/tests/options-http2/h2o.conf b/tests/options-http2/h2o.conf new file mode 100644 index 00000000..bf78d636 --- /dev/null +++ b/tests/options-http2/h2o.conf @@ -0,0 +1,9 @@ +hosts: + default: + listen: + port: 8080 + paths: + /: + file.dir: /www +access-log: /dev/stdout +error-log: /dev/stderr diff --git a/tests/options-http2/index.html b/tests/options-http2/index.html new file mode 100644 index 00000000..4b5fa637 --- /dev/null +++ b/tests/options-http2/index.html @@ -0,0 +1 @@ +hello, world diff --git a/tests/options-require-auth/Dockerfile b/tests/options-require-auth/Dockerfile new file mode 100644 index 00000000..03281627 --- /dev/null +++ b/tests/options-require-auth/Dockerfile @@ -0,0 +1,3 @@ +FROM alpine + +ENTRYPOINT while :; do nc -k -l -p $PORT -e sh -c 'echo -e "HTTP/1.1 200 OK\n\nhello, world"'; done diff --git a/tests/options-require-auth/README.md b/tests/options-require-auth/README.md new file mode 100644 index 00000000..2aa2f7fa --- /dev/null +++ b/tests/options-require-auth/README.md @@ -0,0 +1,5 @@ +# Authenticated check + +Deploy as an authenticated service + +[![Run on Google Cloud](https://deploy.cloud.run/button.svg)](https://deploy.cloud.run) diff --git a/tests/options-require-auth/app.json b/tests/options-require-auth/app.json new file mode 100644 index 00000000..fce5b4d2 --- /dev/null +++ b/tests/options-require-auth/app.json @@ -0,0 +1,5 @@ +{ + "options": { + "allow-unauthenticated": false + } +} diff --git a/tests/options/Dockerfile b/tests/options/Dockerfile new file mode 100644 index 00000000..03281627 --- /dev/null +++ b/tests/options/Dockerfile @@ -0,0 +1,3 @@ +FROM alpine + +ENTRYPOINT while :; do nc -k -l -p $PORT -e sh -c 'echo -e "HTTP/1.1 200 OK\n\nhello, world"'; done diff --git a/tests/options/README.md b/tests/options/README.md new file mode 100644 index 00000000..527a81dd --- /dev/null +++ b/tests/options/README.md @@ -0,0 +1,5 @@ +# Memory and CPU options + +Deploy with custom memory and CPU options + +[![Run on Google Cloud](https://deploy.cloud.run/button.svg)](https://deploy.cloud.run) diff --git a/tests/options/app.json b/tests/options/app.json new file mode 100644 index 00000000..4ffb410e --- /dev/null +++ b/tests/options/app.json @@ -0,0 +1,8 @@ +{ + "options": { + "memory": "512Mi", + "cpu": "2", + "max-instances": 10, + "concurrency": 10 + } +} \ No newline at end of file diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 00000000..d7266abc --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,3 @@ + +google-api-python-client +click \ No newline at end of file diff --git a/tests/run_integration_test.py b/tests/run_integration_test.py new file mode 100644 index 00000000..16be3c1b --- /dev/null +++ b/tests/run_integration_test.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +import click +import os +import shutil +import subprocess +import time +from urllib import request, error + +from googleapiclient.discovery import build as api + + +GIT_URL = os.environ.get( + "GIT_URL", "https://github.com/GoogleCloudPlatform/cloud-run-button" +) +GIT_BRANCH = os.environ.get("GIT_BRANCH", "master") +TESTS_DIR = "tests" + +# Keep to Python 3.7 systems (gcloud image currently Python 3.7.3) +GOOGLE_CLOUD_PROJECT = os.environ.get("GOOGLE_CLOUD_PROJECT", None) +if not GOOGLE_CLOUD_PROJECT: + raise Exception("'GOOGLE_CLOUD_PROJECT' env var not found") + +GOOGLE_CLOUD_REGION = os.environ.get("GOOGLE_CLOUD_REGION", None) +if not GOOGLE_CLOUD_REGION: + raise Exception("'GOOGLE_CLOUD_REGION' env var not found") + +WORKING_DIR = os.environ.get("WORKING_DIR", ".") + +DEBUG = os.environ.get("DEBUG", False) +if DEBUG == "": + DEBUG = False + +############################################################################### + + +def debugging(*args): + c = click.get_current_context() + output = " ".join([str(k) for k in args]) + if DEBUG: + print(f"๐Ÿž {output}") + + +def print_help_msg(command): + with click.Context(command) as ctx: + click.echo(command.get_help(ctx)) + + +def gcloud(*args): + """Invoke the gcloud executable""" + return run_shell( + ["gcloud"] + + list(args) + + [ + "--platform", + "managed", + "--project", + GOOGLE_CLOUD_PROJECT, + "--region", + GOOGLE_CLOUD_REGION, + ] + ) + + +def cloudshell_open(directory, repo_url, git_branch): + """Invoke the cloudshell_open executable.""" + params = [ + f"{WORKING_DIR}/cloudshell_open", + f"--repo_url={repo_url}", + f"--git_branch={git_branch}", + ] + + if directory: + params += [f"--dir={TESTS_DIR}/{directory}"] + return run_shell(params) + + +def run_shell(params): + """Invoke the given subproceess, capturing output status and returning stdout""" + debugging("Running:", " ".join(params)) + + env = {} + env.update(os.environ) + env.update({"TRUSTED_ENVIRONMENT": "true", "SKIP_CLONE_REPORTING": "true"}) + + resp = subprocess.run(params, capture_output=True, env=env) + + output = resp.stdout.decode("utf-8") + error = resp.stderr.decode("utf-8") + + if DEBUG: + debugging("stdout:", output or "") + debugging("stderr:", error or "") + + if resp.returncode != 0: + raise ValueError( + f"Command error.\nCommand '{' '.join(params)}' returned {resp.returncode}.\nError: {error}\nOutput: {output}" + ) + return output + + +def clean_clone(folder_name): + """Remove the cloned code""" + if os.path.isdir(folder_name): + debugging(f"๐ŸŸจ Removing old {folder_name} code clone") + shutil.rmtree(folder_name) + + +############################################################################### + + +def deploy_service(directory, repo_url, repo_branch, dirty): + """Deploy a Cloud Run service using the Cloud Run Button""" + + # The repo name is the last part of a github URL, without the org/user. + # The service name will be either the directory name, or the repo name + # The folder name will be the repo name + repo_name = repo_url.split("/")[-1] + service_name = directory or repo_name + folder_name = repo_name + + clean_clone(folder_name) + + if not dirty: + debugging(f"๐ŸŸจ Removing old service {service_name} (if it exists)") + delete_service(service_name) + else: + print(f"๐Ÿ™ˆ Keeping the old service {service_name} (if it exists)") + + print("๐ŸŸฆ Pressing the Cloud Run button...") + cloudshell_open(directory=directory, repo_url=repo_url, git_branch=repo_branch) + + run = api("run", "v1") + service_fqdn = f"projects/{GOOGLE_CLOUD_PROJECT}/locations/{GOOGLE_CLOUD_REGION}/services/{service_name}" + service_obj = run.projects().locations().services().get(name=service_fqdn).execute() + + service_url = service_obj["status"]["url"] + + clean_clone(folder_name) + return service_url + + +def delete_service(service_name): + try: + gcloud( + "run", + "services", + "delete", + service_name, + "--quiet", + ) + debugging(f"Service {service_name} deleted.") + except ValueError: + debugging(f"Service {service_name} not deleted, as it does not exist. ") + pass + + +def get_url(service_url, expected_status=200): + """GET a URL, returning the status and body""" + debugging(f"Service: {service_url}") + status, body = "","" + + try: + request.urlcleanup() # reset cache + resp = request.urlopen(service_url) + status = resp.status + body = resp.read().decode("utf-8") + + except error.HTTPError as e: + # Sometimes errors are OK + if e.code == expected_status: + status = e.code + body = e.msg + + debugging(f"Status: {status}") + debugging(f"Body: {body[-100:]}") + return status, body + + +############################################################################### + + +@click.group() +def cli() -> None: + """Tool for testing Cloud Run Button deployments""" + pass + + +@cli.command() +@click.option("--description", help="Test description") +@click.option("--repo_url", help="Repo URL to deploy") +@click.option("--repo_branch", default=GIT_BRANCH, help="Branch in Repo URL to deploy") +@click.option("--directory", help="Directory in repo to deploy") +@click.option("--expected_status", default=200, help="Status code to expect") +@click.option("--expected_text", help="Text in service to expect") +@click.option("--dirty", is_flag=True, default=False, help="Keep existing service") +def deploy( + description, + directory, + repo_url, + repo_branch, + expected_status, + expected_text, + dirty, +): + """Run service tests. + + Takes a repo url (defaulting to the button's own repo), and an optional directory. + Deploys the service with the Cloud Run Button, and checks the body and status of the resulting service.""" + + if not directory and not repo_url: + print_help_msg(deploy) + raise ValueError( + f"Must supply either a directory for the default repo ({GIT_URL}) or a custom repo.\n" + ) + + if not repo_url: + repo_url = GIT_URL + + print( + f"\nRunning {description or directory or 'a test'}\nConfig: {directory or 'root'} in {repo_url} on branch {repo_branch}." + ) + service_url = deploy_service(directory, repo_url, repo_branch, dirty) + time.sleep(2) + status, body = get_url(service_url, expected_status) + print(f"โฌœ Service deployed to {service_url}.") + + details = { + "Service URL": service_url, + "Expected Status": expected_status, + "Status": status, + "Expected": expected_text, + "Text": body, + } + debugging_details = "\n".join([f"{k}: {v}" for k, v in details.items()]) + + if expected_status == status: + print(f"๐ŸŸข Service returned expected status {expected_status}.") + else: + + print( + f"โŒ Service did not return expected status (got {status}, expected {expected_status})." + ) + raise ValueError(f"Error: Expected status not found:\n{debugging_details}") + + if expected_text: + if expected_text in body: + print(f'๐ŸŸข Service returned expected content ("{expected_text}").') + else: + print( + f"โŒ Service did not return expected content ({expected_text} not in body).\nBody: {body}" + ) + raise ValueError(f"Error: Expected value not found.\n{debugging_details}") + print(f"โœ… Test successful.") + + time.sleep(60) ## avoid quota issues + + +if __name__ == "__main__": + cli()