├── .gitignore ├── cmdutil ├── cmdutil.go ├── untyped.go └── cmd.go ├── Dockerfile ├── cmd └── pvn-wrapper │ ├── fly │ ├── root.go │ ├── common.go │ ├── apply.go │ └── fetch.go │ ├── googlecloudrun │ ├── root.go │ ├── common.go │ ├── apply.go │ └── fetch.go │ ├── awsecs │ ├── root.go │ ├── common.go │ ├── fetch.go │ └── apply.go │ ├── pulumi │ ├── root.go │ └── up.go │ ├── terraform │ ├── root.go │ ├── apply.go │ └── plan.go │ ├── root.go │ └── exec.go ├── LICENSE ├── result ├── result_test.go └── result.go ├── .github └── workflows │ └── build-and-push-docker.yml ├── .goreleaser.yaml ├── go.mod └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | # Ignore generated credentials from google-github-actions/auth 3 | gha-creds-*.json 4 | -------------------------------------------------------------------------------- /cmdutil/cmdutil.go: -------------------------------------------------------------------------------- 1 | package cmdutil 2 | 3 | import ( 4 | "log" 5 | ) 6 | 7 | func Must(err error) { 8 | if err != nil { 9 | log.Fatal(err) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21 AS builder 2 | 3 | WORKDIR /src/ 4 | 5 | COPY . . 6 | ENV CGO_ENABLED=0 7 | RUN go build -v -o /usr/local/bin/pvn-wrapper ./cmd/pvn-wrapper 8 | 9 | FROM scratch 10 | COPY --from=builder /usr/local/bin/pvn-wrapper /pvn-wrapper 11 | ENTRYPOINT ["/pvn-wrapper"] 12 | -------------------------------------------------------------------------------- /cmd/pvn-wrapper/fly/root.go: -------------------------------------------------------------------------------- 1 | package fly 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var flyPath string 8 | 9 | var RootCmd = &cobra.Command{ 10 | Use: "fly ", 11 | Short: "Fly wrapper commands", 12 | } 13 | 14 | func init() { 15 | RootCmd.Flags().StringVar(&flyPath, "fly-path", "fly", "Path to fly binary") 16 | } 17 | -------------------------------------------------------------------------------- /cmd/pvn-wrapper/googlecloudrun/root.go: -------------------------------------------------------------------------------- 1 | package googlecloudrun 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var gcloudPath string 8 | 9 | var RootCmd = &cobra.Command{ 10 | Use: "google-cloud-run ", 11 | Short: "Google Cloud Run wrapper commands", 12 | } 13 | 14 | func init() { 15 | RootCmd.Flags().StringVar(&gcloudPath, "gcloud-path", "gcloud", "Path to gcloud binary") 16 | } 17 | -------------------------------------------------------------------------------- /cmd/pvn-wrapper/awsecs/root.go: -------------------------------------------------------------------------------- 1 | package awsecs 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var awsPath string 8 | 9 | var RootCmd = &cobra.Command{ 10 | Use: "aws-ecs ", 11 | Short: "AWS ECS wrapper commands", 12 | Long: `AWS ECS wrapper commands. 13 | 14 | pvn-wrapper awsecs apply ... 15 | `, 16 | } 17 | 18 | func init() { 19 | RootCmd.Flags().StringVar(&awsPath, "aws-path", "aws", "Path to aws binary") 20 | } 21 | -------------------------------------------------------------------------------- /cmd/pvn-wrapper/pulumi/root.go: -------------------------------------------------------------------------------- 1 | package pulumi 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var pulumiPath string 8 | 9 | var RootCmd = &cobra.Command{ 10 | Use: "pulumi ", 11 | Short: "Pulumi wrapper commands", 12 | Long: `Pulumi wrapper commands. 13 | 14 | pvn-wrapper pulumi preview ... 15 | `, 16 | } 17 | 18 | func init() { 19 | RootCmd.Flags().StringVar(&pulumiPath, "pulumi-path", "pulumi", "Path to the pulumi binary") 20 | } 21 | -------------------------------------------------------------------------------- /cmd/pvn-wrapper/terraform/root.go: -------------------------------------------------------------------------------- 1 | package terraform 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var terraformPath string 8 | 9 | var RootCmd = &cobra.Command{ 10 | Use: "terraform ", 11 | Short: "Terraform wrapper commands", 12 | Aliases: []string{"tf"}, 13 | Long: `Terraform wrapper commands. 14 | 15 | pvn-wrapper terraform plan ... 16 | `, 17 | } 18 | 19 | func init() { 20 | RootCmd.Flags().StringVar(&terraformPath, "terraform-path", "terraform", "Path to terraform binary") 21 | } 22 | -------------------------------------------------------------------------------- /cmdutil/untyped.go: -------------------------------------------------------------------------------- 1 | package cmdutil 2 | 3 | import "github.com/pkg/errors" 4 | 5 | func GetOrCreateUntypedMapFromStringMap(m map[string]interface{}, key string) (map[interface{}]interface{}, error) { 6 | if m[key] == nil { 7 | m[key] = map[interface{}]interface{}{} 8 | } 9 | typed, ok := m[key].(map[interface{}]interface{}) 10 | if !ok { 11 | return nil, errors.Errorf("unexpected type for %s: %T", key, m[key]) 12 | } 13 | return typed, nil 14 | } 15 | 16 | func GetOrCreateUntypedMap(m map[interface{}]interface{}, key string) (map[interface{}]interface{}, error) { 17 | if m[key] == nil { 18 | m[key] = map[interface{}]interface{}{} 19 | } 20 | typed, ok := m[key].(map[interface{}]interface{}) 21 | if !ok { 22 | return nil, errors.Errorf("unexpected type for %s: %T", key, m[key]) 23 | } 24 | return typed, nil 25 | } 26 | -------------------------------------------------------------------------------- /cmdutil/cmd.go: -------------------------------------------------------------------------------- 1 | package cmdutil 2 | 3 | import ( 4 | go_errors "errors" 5 | "log" 6 | "os" 7 | "os/exec" 8 | 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | func RunCmd(cmd *exec.Cmd) error { 13 | cmd.Stderr = os.Stderr 14 | cmd.Stdout = os.Stdout 15 | log.Printf("Running command: %s", cmd.String()) 16 | if err := cmd.Run(); err != nil { 17 | return errors.Wrapf(err, "Command failed:\n%s", cmd.String()) 18 | } 19 | return nil 20 | } 21 | 22 | func RunCmdOutput(cmd *exec.Cmd) ([]byte, error) { 23 | log.Printf("Running command: %s", cmd.String()) 24 | output, err := cmd.Output() 25 | log.Printf("Command output (%s):\n%s", cmd.String(), output) 26 | if err != nil { 27 | var exitErr *exec.ExitError 28 | if go_errors.As(err, &exitErr) { 29 | return nil, errors.Wrapf(err, "Command failed:\n%s\n%s", cmd.String(), string(exitErr.Stderr)) 30 | } 31 | return nil, errors.Wrapf(err, "Command failed for unknown reasons:\n%s", cmd.String()) 32 | } 33 | return output, nil 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Prodvana Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cmd/pvn-wrapper/root.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/prodvana/pvn-wrapper/cmd/pvn-wrapper/awsecs" 8 | "github.com/prodvana/pvn-wrapper/cmd/pvn-wrapper/fly" 9 | "github.com/prodvana/pvn-wrapper/cmd/pvn-wrapper/googlecloudrun" 10 | "github.com/prodvana/pvn-wrapper/cmd/pvn-wrapper/pulumi" 11 | "github.com/prodvana/pvn-wrapper/cmd/pvn-wrapper/terraform" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | var ( 16 | version = "dev" 17 | commit = "none" 18 | date = "unknown" 19 | ) 20 | 21 | var rootCmd = &cobra.Command{ 22 | Use: "pvn-wrapper", 23 | Short: "pvn-wrapper is used to facilitate executions of jobs in Prodvana.", 24 | Long: `pvn-wrapper is used to facilitate executions of jobs in Prodvana.`, 25 | TraverseChildren: true, 26 | } 27 | 28 | func init() { 29 | rootCmd.AddCommand(awsecs.RootCmd) 30 | rootCmd.AddCommand(terraform.RootCmd) 31 | rootCmd.AddCommand(pulumi.RootCmd) 32 | rootCmd.AddCommand(googlecloudrun.RootCmd) 33 | rootCmd.AddCommand(fly.RootCmd) 34 | rootCmd.Version = version 35 | rootCmd.SetVersionTemplate(fmt.Sprintf("{{ .Version }} (%s %s)\n", commit, date)) 36 | } 37 | 38 | func main() { 39 | log.SetFlags(log.Lmicroseconds | log.Lshortfile) 40 | if err := rootCmd.Execute(); err != nil { 41 | log.Fatal(err) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /result/result_test.go: -------------------------------------------------------------------------------- 1 | package result 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math/rand" 7 | "os" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 14 | 15 | func RandStringRunes(n int) string { 16 | b := make([]rune, n) 17 | for i := range b { 18 | b[i] = letterRunes[rand.Intn(len(letterRunes))] 19 | } 20 | return string(b) 21 | } 22 | 23 | func TestChunkFile(t *testing.T) { 24 | for _, length := range []int{ 25 | 0, 26 | 1, 27 | 10, 28 | 1024 * 1024, 29 | 1024*1024 + 1, 30 | 10 * 1024 * 1024, 31 | 10*1024*1024 + 20, 32 | } { 33 | t.Run(fmt.Sprintf("%d", length), func(t *testing.T) { 34 | content := RandStringRunes(length) 35 | tempfile, err := os.CreateTemp("", "") 36 | require.NoError(t, err) 37 | defer func() { 38 | require.NoError(t, os.Remove(tempfile.Name())) 39 | }() 40 | _, err = tempfile.WriteString(content) 41 | require.NoError(t, err) 42 | require.NoError(t, tempfile.Close()) 43 | 44 | buf := bytes.Buffer{} 45 | require.NoError(t, chunkFile(tempfile.Name(), func(b []byte) error { 46 | _, err := buf.Write(b) 47 | return err 48 | })) 49 | require.Equal(t, content, buf.String()) 50 | 51 | buf = bytes.Buffer{} 52 | require.NoError(t, chunkByte([]byte(content), func(b []byte) error { 53 | _, err := buf.Write(b) 54 | return err 55 | })) 56 | require.Equal(t, content, buf.String()) 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /cmd/pvn-wrapper/awsecs/common.go: -------------------------------------------------------------------------------- 1 | package awsecs 2 | 3 | import ( 4 | "github.com/prodvana/pvn-wrapper/cmdutil" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | var commonFlags = struct { 9 | taskDefinitionFile string 10 | serviceSpecFile string 11 | ecsClusterName string 12 | ecsServiceName string 13 | pvnServiceId string 14 | pvnServiceVersion string 15 | updateTaskDefinitionOnly bool 16 | }{} 17 | 18 | func registerCommonFlags(cmd *cobra.Command) { 19 | cmd.Flags().StringVar(&commonFlags.taskDefinitionFile, "task-definition-file", "", "Path to ECS task definition file") 20 | cmdutil.Must(cmd.MarkFlagRequired("task-definition-file")) 21 | cmd.Flags().StringVar(&commonFlags.serviceSpecFile, "service-spec-file", "", "Path to ECS service spec file") 22 | cmdutil.Must(cmd.MarkFlagRequired("service-spec-file")) 23 | cmd.Flags().StringVar(&commonFlags.ecsServiceName, "ecs-service-name", "", "Name of ECS service") 24 | cmdutil.Must(cmd.MarkFlagRequired("ecs-service-name")) 25 | cmd.Flags().StringVar(&commonFlags.ecsClusterName, "ecs-cluster-name", "", "Name of ECS cluster") 26 | cmdutil.Must(cmd.MarkFlagRequired("ecs-cluster-name")) 27 | cmd.Flags().StringVar(&commonFlags.pvnServiceId, "pvn-service-id", "", "Prodvana Service ID") 28 | cmdutil.Must(cmd.MarkFlagRequired("pvn-service-id")) 29 | cmd.Flags().StringVar(&commonFlags.pvnServiceVersion, "pvn-service-version", "", "Prodvana Service Version") 30 | cmdutil.Must(cmd.MarkFlagRequired("pvn-service-version")) 31 | cmd.Flags().BoolVar(&commonFlags.updateTaskDefinitionOnly, "update-task-definition-only", false, "Update task definition only") 32 | cmdutil.Must(cmd.MarkFlagRequired("update-task-definition-only")) 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/build-and-push-docker.yml: -------------------------------------------------------------------------------- 1 | name: build-docker-image 2 | 3 | on: 4 | push: 5 | # This GitHub action creates a release when a tag that matches the pattern 6 | # "v*" (e.g. v0.1.0) is created. 7 | tags: 8 | - 'v*' 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # content write needed to create releases 13 | permissions: 14 | id-token: write 15 | contents: write 16 | 17 | env: 18 | PUBLIC_REGISTRY: us-docker.pkg.dev/pvn-infra/pvn-public 19 | 20 | jobs: 21 | buildx: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v3 26 | - name: Set up QEMU 27 | uses: docker/setup-qemu-action@v2 28 | - name: Set up Docker Buildx 29 | id: buildx 30 | uses: docker/setup-buildx-action@v2 31 | - id: auth 32 | name: "Authenticate to Google Cloud" 33 | uses: "google-github-actions/auth@v0" 34 | with: 35 | workload_identity_provider: "projects/868387978158/locations/global/workloadIdentityPools/pvn-infra/providers/github" 36 | service_account: "public-docker-write@pvn-infra.iam.gserviceaccount.com" 37 | token_format: "access_token" 38 | create_credentials_file: true 39 | - uses: "docker/login-action@v1" 40 | with: 41 | registry: "us-docker.pkg.dev" 42 | username: "oauth2accesstoken" 43 | password: "${{ steps.auth.outputs.access_token }}" 44 | - uses: actions/setup-go@v4 45 | with: 46 | go-version: "1.21" 47 | - uses: goreleaser/goreleaser-action@v4 48 | with: 49 | distribution: goreleaser 50 | version: latest 51 | args: release --clean 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | -------------------------------------------------------------------------------- /cmd/pvn-wrapper/googlecloudrun/common.go: -------------------------------------------------------------------------------- 1 | package googlecloudrun 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/prodvana/pvn-wrapper/cmdutil" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var commonFlags = struct { 13 | gcpProject string 14 | region string 15 | specFile string 16 | pvnServiceId string 17 | pvnServiceVersion string 18 | }{} 19 | 20 | func registerCommonFlags(cmd *cobra.Command) { 21 | cmd.Flags().StringVar(&commonFlags.gcpProject, "gcp-project", "", "GCP project") 22 | cmdutil.Must(cmd.MarkFlagRequired("gcp-project")) 23 | cmd.Flags().StringVar(&commonFlags.region, "region", "", "GCP region") 24 | cmdutil.Must(cmd.MarkFlagRequired("region")) 25 | cmd.Flags().StringVar(&commonFlags.specFile, "spec-file", "", "Path to service spec file") 26 | cmdutil.Must(cmd.MarkFlagRequired("spec-file")) 27 | cmd.Flags().StringVar(&commonFlags.pvnServiceId, "pvn-service-id", "", "Prodvana Service ID") 28 | cmdutil.Must(cmd.MarkFlagRequired("pvn-service-id")) 29 | cmd.Flags().StringVar(&commonFlags.pvnServiceVersion, "pvn-service-version", "", "Prodvana Service Version") 30 | cmdutil.Must(cmd.MarkFlagRequired("pvn-service-version")) 31 | } 32 | 33 | func gcloudAuth() error { 34 | credentials := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") 35 | if credentials == "" { 36 | return errors.New("GOOGLE_APPLICATION_CREDENTIALS environment variable must be set") 37 | } 38 | tmpFile, err := os.CreateTemp("", "gcloud-credentials") 39 | if err != nil { 40 | return errors.Wrap(err, "failed to create temp file") 41 | } 42 | defer os.Remove(tmpFile.Name()) 43 | _, err = tmpFile.Write([]byte(credentials)) 44 | if err != nil { 45 | return errors.Wrap(err, "failed to write to temp file") 46 | } 47 | err = tmpFile.Close() 48 | if err != nil { 49 | return errors.Wrap(err, "failed to close temp file") 50 | } 51 | return cmdutil.RunCmd(exec.Command(gcloudPath, "auth", "activate-service-account", "--key-file", tmpFile.Name())) 52 | } 53 | -------------------------------------------------------------------------------- /cmd/pvn-wrapper/fly/common.go: -------------------------------------------------------------------------------- 1 | package fly 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/prodvana/prodvana-public/go/prodvana-sdk/proto/prodvana/fly" 9 | service_pb "github.com/prodvana/prodvana-public/go/prodvana-sdk/proto/prodvana/service" 10 | "github.com/prodvana/pvn-wrapper/cmdutil" 11 | "github.com/spf13/cobra" 12 | "google.golang.org/protobuf/proto" 13 | ) 14 | 15 | var commonFlags = struct { 16 | pvnCfgFile string 17 | pvnServiceId string 18 | pvnServiceVersion string 19 | }{} 20 | 21 | func registerCommonFlags(cmd *cobra.Command) { 22 | cmd.Flags().StringVar(&commonFlags.pvnCfgFile, "pvn-cfg-file", "", "Path to Prodvana Service Instance config file") 23 | cmdutil.Must(cmd.MarkFlagRequired("pvn-cfg-file")) 24 | cmd.Flags().StringVar(&commonFlags.pvnServiceId, "pvn-service-id", "", "Prodvana Service ID") 25 | cmdutil.Must(cmd.MarkFlagRequired("pvn-service-id")) 26 | cmd.Flags().StringVar(&commonFlags.pvnServiceVersion, "pvn-service-version", "", "Prodvana Service Version") 27 | cmdutil.Must(cmd.MarkFlagRequired("pvn-service-version")) 28 | } 29 | 30 | func getServiceConfig() (*service_pb.CompiledServiceInstanceConfig, error) { 31 | bytes, err := os.ReadFile(commonFlags.pvnCfgFile) 32 | if err != nil { 33 | return nil, errors.Wrap(err, "failed to read Prodvana config file") 34 | } 35 | var cfg service_pb.CompiledServiceInstanceConfig 36 | if err := proto.Unmarshal(bytes, &cfg); err != nil { 37 | return nil, errors.Wrap(err, "failed to unmarshal Prodvana config file") 38 | } 39 | return &cfg, nil 40 | } 41 | 42 | func makeTomlFile(cfg *service_pb.CompiledServiceInstanceConfig) (string, error) { 43 | tempFile, err := os.CreateTemp("", "*.fly.toml") 44 | if err != nil { 45 | return "", errors.Wrap(err, "failed to make tempfile") 46 | } 47 | var tomlBytes []byte 48 | switch inner := cfg.GetFly().GetTomlOneof().(type) { 49 | case *fly.FlyConfig_Inlined: 50 | tomlBytes = []byte(inner.Inlined) 51 | default: 52 | return "", errors.Errorf("unrecognized toml type: %T", inner) 53 | } 54 | if _, err := tempFile.Write(tomlBytes); err != nil { 55 | return "", errors.Wrap(err, "failed to write to tempfile") 56 | } 57 | if err := tempFile.Close(); err != nil { 58 | return "", errors.Wrap(err, "failed to close tempfile") 59 | } 60 | log.Printf("fly toml is:\n%s", string(tomlBytes)) 61 | return tempFile.Name(), nil 62 | } 63 | -------------------------------------------------------------------------------- /cmd/pvn-wrapper/pulumi/up.go: -------------------------------------------------------------------------------- 1 | package pulumi 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | go_errors "errors" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | 11 | "github.com/pkg/errors" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | var upFlags = struct { 16 | retryableExitCode int 17 | }{} 18 | 19 | var upCmd = &cobra.Command{ 20 | Use: "up", 21 | Short: "pulumi up wrapper", 22 | Long: `pulumi up wrapper. 23 | 24 | Takes all the same input that pulumi up would, but handles detecting retryable errors and exiting 25 | with a special exit code (default: 2). Retryable errors include: 26 | 27 | - lock acquisition errors 28 | 29 | Otherwise, pvn-wrapper will exit with the original exit code of pulumi up. Note that this command will buffer 30 | stderr to look at potential error messages, therefore, the order of stderr/stdout will be different 31 | than pulumi up's. 32 | 33 | pvn-wrapper pulumi up ... 34 | 35 | To pass flags to pulumi up, use -- 36 | 37 | pvn-wrapper terraform apply -- --show-sames 38 | 39 | pvn-wrapper will always pass --non-interactive. 40 | `, 41 | RunE: func(cmd *cobra.Command, args []string) error { 42 | ctx := context.Background() 43 | applyArgs := []string{"up"} 44 | applyArgs = append(applyArgs, args...) 45 | applyArgs = append(applyArgs, 46 | "--non-interactive", 47 | ) 48 | execCmd := exec.CommandContext(ctx, pulumiPath, applyArgs...) 49 | var stderr bytes.Buffer 50 | execCmd.Stdout = os.Stdout 51 | execCmd.Stderr = &stderr 52 | execErr := execCmd.Run() 53 | // write out stderr before doing any processing to be transparent 54 | _, err := os.Stderr.Write(stderr.Bytes()) 55 | if err != nil { 56 | return errors.Wrap(err, "failed to write to stderr") 57 | } 58 | if execErr != nil { 59 | stderrString := stderr.String() 60 | if strings.Contains(stderrString, "the stack is currently locked") { 61 | os.Exit(upFlags.retryableExitCode) 62 | } 63 | // other errors, try to match original exit code 64 | var exitErr *exec.ExitError 65 | if go_errors.As(execErr, &exitErr) { 66 | os.Exit(exitErr.ExitCode()) 67 | } else { 68 | return errors.Wrap(execErr, "up command failed unexpectedly") 69 | } 70 | } 71 | return nil 72 | }, 73 | } 74 | 75 | func init() { 76 | RootCmd.AddCommand(upCmd) 77 | 78 | upCmd.Flags().IntVar(&upFlags.retryableExitCode, "retryable-exit-code", 2, "Special exit code to use for retryable errors.") 79 | } 80 | -------------------------------------------------------------------------------- /cmd/pvn-wrapper/terraform/apply.go: -------------------------------------------------------------------------------- 1 | package terraform 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | go_errors "errors" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | 11 | "github.com/pkg/errors" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | var applyFlags = struct { 16 | retryableExitCode int 17 | }{} 18 | 19 | var applyCmd = &cobra.Command{ 20 | Use: "apply", 21 | Short: "terraform apply wrapper", 22 | Long: `terraform apply wrapper. 23 | 24 | Takes all the same input that terraform apply would, but handles detecting retryable errors and exiting 25 | with a special exit code (default: 2). Retryable errors include: 26 | 27 | - stale plan errors 28 | - lock acquisition errors 29 | 30 | Otherwise, pvn-wrapper will exit with the original exit code of terraform apply. Note that this command will buffer 31 | stderr to look at potential error messages, therefore, the order of stderr/stdout will be different 32 | than terraform apply's. 33 | 34 | pvn-wrapper terraform apply ... 35 | 36 | To pass flags to terraform apply, use -- 37 | 38 | pvn-wrapper terraform apply -- --lock=false plan.tfplan 39 | 40 | pvn-wrapper will always pass --no-color. 41 | `, 42 | RunE: func(cmd *cobra.Command, args []string) error { 43 | ctx := context.Background() 44 | applyArgs := []string{"apply"} 45 | applyArgs = append(applyArgs, args...) 46 | applyArgs = append(applyArgs, 47 | "-no-color", 48 | ) 49 | execCmd := exec.CommandContext(ctx, terraformPath, applyArgs...) 50 | var stderr bytes.Buffer 51 | execCmd.Stdout = os.Stdout 52 | execCmd.Stderr = &stderr 53 | execErr := execCmd.Run() 54 | // write out stderr before doing any processing to be transparent 55 | _, err := os.Stderr.Write(stderr.Bytes()) 56 | if err != nil { 57 | return errors.Wrap(err, "failed to write to stderr") 58 | } 59 | if execErr != nil { 60 | stderrString := stderr.String() 61 | if strings.Contains(stderrString, "Saved plan is stale") || strings.Contains(stderrString, "Error acquiring the state lock") { 62 | os.Exit(applyFlags.retryableExitCode) 63 | } 64 | // other errors, try to match original exit code 65 | var exitErr *exec.ExitError 66 | if go_errors.As(execErr, &exitErr) { 67 | os.Exit(exitErr.ExitCode()) 68 | } else { 69 | return errors.Wrap(execErr, "apply command failed unexpectedly") 70 | } 71 | } 72 | return nil 73 | }, 74 | } 75 | 76 | func init() { 77 | RootCmd.AddCommand(applyCmd) 78 | 79 | applyCmd.Flags().IntVar(&applyFlags.retryableExitCode, "retryable-exit-code", 2, "Special exit code to use for retryable errors.") 80 | } 81 | -------------------------------------------------------------------------------- /cmd/pvn-wrapper/fly/apply.go: -------------------------------------------------------------------------------- 1 | package fly 2 | 3 | import ( 4 | go_errors "errors" 5 | "os" 6 | "os/exec" 7 | 8 | "github.com/pelletier/go-toml" 9 | "github.com/pkg/errors" 10 | common_config_pb "github.com/prodvana/prodvana-public/go/prodvana-sdk/proto/prodvana/common_config" 11 | "github.com/prodvana/pvn-wrapper/cmdutil" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | type flyCfg struct { 16 | App string `toml:"app"` 17 | } 18 | 19 | func createIfNeeded(tomlFile string) error { 20 | bytes, err := os.ReadFile(tomlFile) 21 | if err != nil { 22 | return errors.Wrap(err, "failed to read toml file") 23 | } 24 | var cfg flyCfg 25 | if err := toml.Unmarshal(bytes, &cfg); err != nil { 26 | return errors.Wrap(err, "failed to unmarshal toml file") 27 | } 28 | _, err = flyStatus(tomlFile) 29 | if err == nil { 30 | return nil 31 | } 32 | if !go_errors.Is(err, errServiceNotFound) { 33 | return err 34 | } 35 | createCmd := exec.Command( 36 | flyPath, 37 | "app", 38 | "create", 39 | cfg.App, 40 | ) 41 | return cmdutil.RunCmd(createCmd) 42 | } 43 | 44 | var applyCmd = &cobra.Command{ 45 | Use: "apply", 46 | Short: "Create or update a Fly service", 47 | Args: cobra.NoArgs, 48 | RunE: func(cmd *cobra.Command, args []string) error { 49 | cfg, err := getServiceConfig() 50 | if err != nil { 51 | return err 52 | } 53 | tomlFile, err := makeTomlFile(cfg) 54 | if err != nil { 55 | return err 56 | } 57 | defer func() { _ = os.Remove(tomlFile) }() 58 | if err := createIfNeeded(tomlFile); err != nil { 59 | return err 60 | } 61 | envToInject := cfg.Env 62 | envToInject["PVN_SERVICE_ID"] = &common_config_pb.EnvValue{ 63 | ValueOneof: &common_config_pb.EnvValue_Value{Value: commonFlags.pvnServiceId}, 64 | } 65 | envToInject["PVN_SERVICE_VERSION"] = &common_config_pb.EnvValue{ 66 | ValueOneof: &common_config_pb.EnvValue_Value{Value: commonFlags.pvnServiceVersion}, 67 | } 68 | flyArgs := []string{ 69 | "deploy", 70 | "--config", 71 | tomlFile, 72 | } 73 | for k, v := range envToInject { 74 | switch v.GetValueOneof().(type) { 75 | case *common_config_pb.EnvValue_Value: 76 | flyArgs = append(flyArgs, "--env", k+"="+v.GetValue()) 77 | default: 78 | return errors.Errorf("unrecognized env value type for %s: %T", k, v) 79 | } 80 | } 81 | // TODO(naphat) disable waits 82 | createCmd := exec.Command( 83 | flyPath, 84 | flyArgs..., 85 | ) 86 | err = cmdutil.RunCmd(createCmd) 87 | if err != nil { 88 | return err 89 | } 90 | return nil 91 | }, 92 | } 93 | 94 | func init() { 95 | RootCmd.AddCommand(applyCmd) 96 | 97 | registerCommonFlags(applyCmd) 98 | } 99 | -------------------------------------------------------------------------------- /cmd/pvn-wrapper/googlecloudrun/apply.go: -------------------------------------------------------------------------------- 1 | package googlecloudrun 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/prodvana/pvn-wrapper/cmdutil" 9 | "github.com/spf13/cobra" 10 | "gopkg.in/yaml.v2" 11 | ) 12 | 13 | const ( 14 | idAnnotation = "prodvana.io/id" 15 | versionAnnotation = "prodvana.io/version" 16 | ) 17 | 18 | func patchSpecFile(specFilePath, pvnServiceId, pvnServiceVersion string) (string, error) { 19 | taskDef, err := os.ReadFile(specFilePath) 20 | if err != nil { 21 | return "", errors.Wrap(err, "failed to read task definition file") 22 | } 23 | var untypedDef map[string]interface{} 24 | if err := yaml.Unmarshal(taskDef, &untypedDef); err != nil { 25 | return "", errors.Wrapf(err, "failed to unmarshal task definition file: %s", string(taskDef)) 26 | } 27 | metadata, err := cmdutil.GetOrCreateUntypedMapFromStringMap(untypedDef, "metadata") 28 | if err != nil { 29 | return "", err 30 | } 31 | annotations, err := cmdutil.GetOrCreateUntypedMap(metadata, "annotations") 32 | if err != nil { 33 | return "", err 34 | } 35 | annotations[idAnnotation] = pvnServiceId 36 | annotations[versionAnnotation] = pvnServiceVersion 37 | 38 | updatedTaskDef, err := yaml.Marshal(untypedDef) 39 | if err != nil { 40 | return "", errors.Wrap(err, "failed to marshal") 41 | } 42 | 43 | tempFile, err := os.CreateTemp("", "google-cloud-run-spec") 44 | if err != nil { 45 | return "", errors.Wrap(err, "failed to make tempfile") 46 | } 47 | 48 | if _, err := tempFile.Write(updatedTaskDef); err != nil { 49 | return "", errors.Wrap(err, "failed to write to tempfile") 50 | } 51 | if err := tempFile.Close(); err != nil { 52 | return "", errors.Wrap(err, "failed to close tempfile") 53 | } 54 | 55 | return tempFile.Name(), nil 56 | } 57 | 58 | var applyCmd = &cobra.Command{ 59 | Use: "apply", 60 | Short: "Create or update a Google Cloud Run service", 61 | Args: cobra.NoArgs, 62 | RunE: func(cmd *cobra.Command, args []string) error { 63 | if err := gcloudAuth(); err != nil { 64 | return err 65 | } 66 | newSpecPath, err := patchSpecFile(commonFlags.specFile, commonFlags.pvnServiceId, commonFlags.pvnServiceVersion) 67 | if err != nil { 68 | return err 69 | } 70 | defer func() { _ = os.Remove(newSpecPath) }() 71 | createCmd := exec.Command( 72 | gcloudPath, 73 | "--project", 74 | commonFlags.gcpProject, 75 | "run", 76 | "services", 77 | "replace", 78 | "--region", 79 | commonFlags.region, 80 | newSpecPath, 81 | ) 82 | err = cmdutil.RunCmd(createCmd) 83 | if err != nil { 84 | return err 85 | } 86 | return nil 87 | }, 88 | } 89 | 90 | func init() { 91 | RootCmd.AddCommand(applyCmd) 92 | 93 | registerCommonFlags(applyCmd) 94 | } 95 | -------------------------------------------------------------------------------- /cmd/pvn-wrapper/terraform/plan.go: -------------------------------------------------------------------------------- 1 | package terraform 2 | 3 | import ( 4 | "context" 5 | go_errors "errors" 6 | "os" 7 | "os/exec" 8 | 9 | "github.com/pkg/errors" 10 | "github.com/prodvana/pvn-wrapper/cmdutil" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var planFlags = struct { 15 | planOut string 16 | planExplanationOut string 17 | }{} 18 | 19 | var planCmd = &cobra.Command{ 20 | Use: "plan", 21 | Short: "terraform plan wrapper", 22 | Long: `terraform plan wrapper. 23 | 24 | Takes all the same input that terraform plan would, but handles creating plan files and explanation, then exiting with 0, 1, or 2. 25 | Meant to be used as the fetch function of the terraform-runner runtime extension. 26 | 27 | 0 - No changes detected 28 | 1 - Unknown error 29 | 2 - Changes detected 30 | 31 | pvn-wrapper terraform plan ... 32 | 33 | To pass flags to terraform plan, use -- 34 | 35 | pvn-wrapper terraform plan -- --refresh=false 36 | 37 | pvn-wrapper will always pass --detailed-exitcode, --out, and --no-color. 38 | `, 39 | RunE: func(cmd *cobra.Command, args []string) error { 40 | ctx := context.Background() 41 | planArgs := []string{"plan"} 42 | planArgs = append(planArgs, args...) 43 | planArgs = append(planArgs, 44 | "-detailed-exitcode", 45 | "-no-color", 46 | "-out", 47 | planFlags.planOut, 48 | ) 49 | execCmd := exec.CommandContext(ctx, terraformPath, planArgs...) 50 | execCmd.Stdout = os.Stdout 51 | execCmd.Stderr = os.Stderr 52 | var exitCode int 53 | err := execCmd.Run() 54 | if err != nil { 55 | var exitErr *exec.ExitError 56 | if go_errors.As(err, &exitErr) { 57 | exitCode = exitErr.ExitCode() 58 | } else { 59 | return errors.Wrap(err, "plan command failed unexpectedly") 60 | } 61 | } 62 | if exitCode == 0 || exitCode == 2 { 63 | showCommand := exec.CommandContext(ctx, terraformPath, "show", "-no-color", planFlags.planOut) 64 | planExplanation, err := os.Create(planFlags.planExplanationOut) 65 | if err != nil { 66 | return errors.Wrapf(err, "failed to open %s", planFlags.planExplanationOut) 67 | } 68 | defer func() { _ = planExplanation.Close() }() 69 | showCommand.Stderr = os.Stderr 70 | showCommand.Stdout = planExplanation 71 | err = showCommand.Run() 72 | if err != nil { 73 | return errors.Wrap(err, "show command failed") 74 | } 75 | } 76 | os.Exit(exitCode) 77 | return nil 78 | }, 79 | } 80 | 81 | func init() { 82 | RootCmd.AddCommand(planCmd) 83 | 84 | planCmd.Flags().StringVar(&planFlags.planOut, "plan-out", "", "Plan file out location") 85 | cmdutil.Must(planCmd.MarkFlagRequired("plan-out")) 86 | planCmd.Flags().StringVar(&planFlags.planExplanationOut, "plan-explanation-out", "", "Plan file out location") 87 | cmdutil.Must(planCmd.MarkFlagRequired("plan-explanation-out")) 88 | } 89 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | before: 3 | hooks: 4 | - go mod tidy 5 | builds: 6 | - env: 7 | - CGO_ENABLED=0 8 | flags: 9 | - -trimpath 10 | main: ./cmd/pvn-wrapper 11 | goos: 12 | - linux 13 | - windows 14 | - darwin 15 | checksum: 16 | name_template: "checksums.txt" 17 | snapshot: 18 | version_template: "{{ incpatch .Version }}-next" 19 | changelog: 20 | disable: true 21 | 22 | release: 23 | github: 24 | owner: replit 25 | name: pvn-wrapper 26 | mode: replace 27 | 28 | dockers: 29 | - dockerfile: "build_template.docker" 30 | use: buildx 31 | image_templates: 32 | - "us-docker.pkg.dev/replit-prodvana-infra/pvn-public/pvn-wrapper:{{ .Tag }}-amd64" 33 | build_flag_templates: 34 | - "--pull" 35 | - "--label=org.opencontainers.image.created={{ .Date }}" 36 | - "--label=org.opencontainers.image.title={{ .ProjectName }}" 37 | - "--label=org.opencontainers.image.revision={{ .FullCommit }}" 38 | - "--label=org.opencontainers.image.version={{ .Version }}" 39 | - "--platform=linux/amd64" 40 | - dockerfile: "build_template.docker" 41 | use: buildx 42 | image_templates: 43 | - "us-docker.pkg.dev/replit-prodvana-infra/pvn-public/pvn-wrapper:{{ .Tag }}-arm64" 44 | build_flag_templates: 45 | - "--pull" 46 | - "--label=org.opencontainers.image.created={{ .Date }}" 47 | - "--label=org.opencontainers.image.title={{ .ProjectName }}" 48 | - "--label=org.opencontainers.image.revision={{ .FullCommit }}" 49 | - "--label=org.opencontainers.image.version={{ .Version }}" 50 | - "--platform=linux/arm64" 51 | docker_manifests: 52 | - name_template: "us-docker.pkg.dev/replit-prodvana-infra/pvn-public/pvn-wrapper:latest" 53 | image_templates: 54 | - "us-docker.pkg.dev/replit-prodvana-infra/pvn-public/pvn-wrapper:{{ .Tag }}-amd64" 55 | - "us-docker.pkg.dev/replit-prodvana-infra/pvn-public/pvn-wrapper:{{ .Tag }}-arm64" 56 | - name_template: "us-docker.pkg.dev/replit-prodvana-infra/pvn-public/pvn-wrapper:{{ .Tag }}" 57 | image_templates: 58 | - "us-docker.pkg.dev/replit-prodvana-infra/pvn-public/pvn-wrapper:{{ .Tag }}-amd64" 59 | - "us-docker.pkg.dev/replit-prodvana-infra/pvn-public/pvn-wrapper:{{ .Tag }}-arm64" 60 | - name_template: "us-docker.pkg.dev/replit-prodvana-infra/pvn-public/pvn-wrapper:v{{ .Major }}" 61 | image_templates: 62 | - "us-docker.pkg.dev/replit-prodvana-infra/pvn-public/pvn-wrapper:{{ .Tag }}-amd64" 63 | - "us-docker.pkg.dev/replit-prodvana-infra/pvn-public/pvn-wrapper:{{ .Tag }}-arm64" 64 | - name_template: "us-docker.pkg.dev/replit-prodvana-infra/pvn-public/pvn-wrapper:v{{ .Major }}.{{ .Minor }}" 65 | image_templates: 66 | - "us-docker.pkg.dev/replit-prodvana-infra/pvn-public/pvn-wrapper:{{ .Tag }}-amd64" 67 | - "us-docker.pkg.dev/replit-prodvana-infra/pvn-public/pvn-wrapper:{{ .Tag }}-arm64" 68 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/prodvana/pvn-wrapper 2 | 3 | go 1.22 4 | 5 | toolchain go1.22.1 6 | 7 | require ( 8 | github.com/spf13/cobra v1.7.0 9 | google.golang.org/grpc v1.59.0 10 | google.golang.org/protobuf v1.33.0 11 | k8s.io/api v0.27.6 12 | knative.dev/pkg v0.0.0-20231023151236-29775d7c9e5c 13 | knative.dev/serving v0.39.2 14 | sigs.k8s.io/yaml v1.3.0 15 | ) 16 | 17 | require ( 18 | github.com/blendle/zapdriver v1.3.1 // indirect 19 | github.com/davecgh/go-spew v1.1.1 // indirect 20 | github.com/envoyproxy/protoc-gen-validate v1.0.2 // indirect 21 | github.com/evanphx/json-patch/v5 v5.7.0 // indirect 22 | github.com/go-logr/logr v1.2.4 // indirect 23 | github.com/gogo/protobuf v1.3.2 // indirect 24 | github.com/golang/protobuf v1.5.3 // indirect 25 | github.com/google/go-cmp v0.6.0 // indirect 26 | github.com/google/go-containerregistry v0.13.0 // indirect 27 | github.com/google/gofuzz v1.2.0 // indirect 28 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 // indirect 29 | github.com/json-iterator/go v1.1.12 // indirect 30 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 31 | github.com/modern-go/reflect2 v1.0.2 // indirect 32 | github.com/opencontainers/go-digest v1.0.0 // indirect 33 | github.com/planetscale/vtprotobuf v0.6.0 // indirect 34 | github.com/pmezard/go-difflib v1.0.0 // indirect 35 | go.uber.org/multierr v1.10.0 // indirect 36 | go.uber.org/zap v1.26.0 // indirect 37 | golang.org/x/net v0.23.0 // indirect 38 | golang.org/x/oauth2 v0.13.0 // indirect 39 | golang.org/x/sys v0.18.0 // indirect 40 | golang.org/x/term v0.18.0 // indirect 41 | golang.org/x/text v0.14.0 // indirect 42 | golang.org/x/time v0.3.0 // indirect 43 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 44 | google.golang.org/appengine v1.6.7 // indirect 45 | google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f // indirect 46 | google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect 47 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 // indirect 48 | gopkg.in/inf.v0 v0.9.1 // indirect 49 | gopkg.in/yaml.v3 v3.0.1 // indirect 50 | k8s.io/apimachinery v0.27.6 // indirect 51 | k8s.io/client-go v0.27.6 // indirect 52 | k8s.io/klog/v2 v2.90.1 // indirect 53 | k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect 54 | knative.dev/networking v0.0.0-20231017124814-2a7676e912b7 // indirect 55 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 56 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 57 | ) 58 | 59 | require ( 60 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 61 | github.com/pelletier/go-toml v1.9.5 62 | github.com/pkg/errors v0.9.1 63 | github.com/prodvana/prodvana-public/go/prodvana-sdk v0.3.38 64 | github.com/spf13/pflag v1.0.5 // indirect 65 | github.com/stretchr/testify v1.8.4 66 | golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 67 | golang.org/x/sync v0.5.0 68 | gopkg.in/yaml.v2 v2.4.0 69 | ) 70 | -------------------------------------------------------------------------------- /cmd/pvn-wrapper/exec.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | 11 | "github.com/prodvana/prodvana-public/go/prodvana-sdk/proto/prodvana/pvn_wrapper" 12 | "github.com/prodvana/pvn-wrapper/result" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | var execFlags = struct { 17 | in []string 18 | out []string 19 | successExitCodes []int32 20 | }{} 21 | 22 | var execCmd = &cobra.Command{ 23 | Use: "exec", 24 | Short: "Execute a command then wrap its output in a format that Prodvana understands.", 25 | Long: `Execute a command then wrap its output in a format that Prodvana understands. 26 | The exit code matches the exit code of the underlying binary being executed. 27 | 28 | pvn-wrapper exec my-binary --my-flag=value my-args ... 29 | `, 30 | Args: cobra.MinimumNArgs(1), 31 | Run: func(cmd *cobra.Command, args []string) { 32 | inputFiles := make([]result.InputFile, 0, len(execFlags.in)) 33 | for _, in := range execFlags.in { 34 | components := strings.SplitN(in, "=", 2) 35 | if len(components) != 2 { 36 | log.Fatal("--in must be in the format input-file-path=input-blob-id") 37 | } 38 | inputFiles = append(inputFiles, result.InputFile{ 39 | Path: components[0], 40 | BlobId: components[1], 41 | }) 42 | } 43 | successExitCodes := execFlags.successExitCodes 44 | if len(successExitCodes) == 0 { 45 | successExitCodes = []int32{0} 46 | } 47 | result.RunWrapper(inputFiles, successExitCodes, func(ctx context.Context) (*pvn_wrapper.Output, []result.OutputFileUpload, error) { 48 | execCmd := exec.CommandContext(ctx, args[0], args[1:]...) 49 | execCmd.Env = os.Environ() 50 | 51 | outputs := make([]result.OutputFileUpload, 0, len(execFlags.out)) 52 | for _, out := range execFlags.out { 53 | components := strings.SplitN(out, "=", 2) 54 | if len(components) != 2 { 55 | return nil, nil, fmt.Errorf("--out must be in the format output-name=output-file") 56 | } 57 | outputs = append(outputs, result.OutputFileUpload{ 58 | Name: components[0], 59 | Path: components[1], 60 | }) 61 | } 62 | 63 | res, cmdOutputs, err := result.RunCmd(execCmd) 64 | outputs = append(outputs, cmdOutputs...) 65 | return res, outputs, err 66 | }) 67 | }, 68 | } 69 | 70 | func init() { 71 | rootCmd.AddCommand(execCmd) 72 | execCmd.Flags().StringArrayVar(&execFlags.in, "in", nil, "List of input files that should be created, in the format input-file-path=input-blob-id. These files will be downloaded from Prodvana and saved to the specified paths before the binary executes.") 73 | execCmd.Flags().StringArrayVar(&execFlags.out, "out", nil, "List of output files to capture, in the format of output-name=output-file-path. These files will be uploaded to Prodvana.") 74 | execCmd.Flags().Int32SliceVar(&execFlags.successExitCodes, "success-exit-codes", nil, "List of successful exit codes, used in the event that the program exited but an output file is missing. If the output file is missing and the exit code is a successful exit code as defined here, then the script will fail with an error (and no json output). Defaults to 0.") 75 | } 76 | -------------------------------------------------------------------------------- /cmd/pvn-wrapper/googlecloudrun/fetch.go: -------------------------------------------------------------------------------- 1 | package googlecloudrun 2 | 3 | import ( 4 | go_errors "errors" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | 10 | "github.com/pkg/errors" 11 | common_config_pb "github.com/prodvana/prodvana-public/go/prodvana-sdk/proto/prodvana/common_config" 12 | extensions_pb "github.com/prodvana/prodvana-public/go/prodvana-sdk/proto/prodvana/runtimes/extensions" 13 | "github.com/prodvana/pvn-wrapper/cmdutil" 14 | "github.com/spf13/cobra" 15 | "google.golang.org/protobuf/encoding/protojson" 16 | corev1 "k8s.io/api/core/v1" 17 | "knative.dev/pkg/apis" 18 | knative_serving "knative.dev/serving/pkg/apis/serving/v1" 19 | k8s_yaml "sigs.k8s.io/yaml" 20 | ) 21 | 22 | var errServiceNotFound = fmt.Errorf("service not found") 23 | 24 | func unmarshalKnativeService(bytes []byte) (*knative_serving.Service, error) { 25 | var serviceSpec knative_serving.Service 26 | err := k8s_yaml.Unmarshal(bytes, &serviceSpec) 27 | if err != nil { 28 | return nil, errors.Wrap(err, "failed to unmarshal yaml") 29 | } 30 | return &serviceSpec, nil 31 | } 32 | 33 | func describeService(service string) (*knative_serving.Service, error) { 34 | describeCmd := exec.Command( 35 | gcloudPath, 36 | "--project", 37 | commonFlags.gcpProject, 38 | "run", 39 | "services", 40 | "describe", 41 | "--region", 42 | commonFlags.region, 43 | service, 44 | "--format", 45 | "yaml", 46 | ) 47 | output, err := cmdutil.RunCmdOutput(describeCmd) 48 | if err != nil { 49 | if strings.Contains(err.Error(), "Cannot find service") { 50 | return nil, errServiceNotFound 51 | } 52 | return nil, err 53 | } 54 | return unmarshalKnativeService(output) 55 | } 56 | 57 | func getConditionStatus(conditions apis.Conditions, condType apis.ConditionType) corev1.ConditionStatus { 58 | for _, cond := range conditions { 59 | if cond.Type == condType { 60 | return cond.Status 61 | } 62 | } 63 | return corev1.ConditionUnknown 64 | } 65 | 66 | func runFetch() (*extensions_pb.FetchOutput, error) { 67 | specBytes, err := os.ReadFile(commonFlags.specFile) 68 | if err != nil { 69 | return nil, errors.Wrap(err, "failed to read spec file") 70 | } 71 | inputSpec, err := unmarshalKnativeService(specBytes) 72 | if err != nil { 73 | return nil, err 74 | } 75 | name := inputSpec.GetObjectMeta().GetName() 76 | cloudRunObj := &extensions_pb.ExternalObject{ 77 | Name: name, 78 | ObjectType: "CloudRun", 79 | ExternalLinks: []*common_config_pb.ExternalLink{ 80 | { 81 | Type: common_config_pb.ExternalLink_DETAIL, 82 | Name: "Cloud Run Console", 83 | Url: fmt.Sprintf( 84 | "https://console.cloud.google.com/run/detail/%[1]s/%[3]s/metrics?project=%[2]s", 85 | commonFlags.region, // 1 86 | commonFlags.gcpProject, // 2 87 | name, // 3 88 | ), 89 | }, 90 | }, 91 | } 92 | currentState, err := describeService(name) 93 | if err != nil { 94 | if go_errors.Is(err, errServiceNotFound) { 95 | return &extensions_pb.FetchOutput{ 96 | Objects: []*extensions_pb.ExternalObject{ 97 | cloudRunObj, 98 | }, 99 | }, nil 100 | } 101 | return nil, err 102 | } 103 | version := &extensions_pb.ExternalObjectVersion{ 104 | Active: true, 105 | } 106 | annotations := currentState.GetAnnotations() 107 | serviceId := annotations[idAnnotation] 108 | serviceVersion := annotations[versionAnnotation] 109 | if serviceId == commonFlags.pvnServiceId { 110 | version.Version = serviceVersion 111 | } // otherwise treat as unknown version 112 | cloudRunObj.Versions = []*extensions_pb.ExternalObjectVersion{version} 113 | if getConditionStatus(apis.Conditions(currentState.Status.Conditions), apis.ConditionReady) == corev1.ConditionTrue { 114 | cloudRunObj.Status = extensions_pb.ExternalObject_SUCCEEDED 115 | } 116 | // TODO(naphat) how to handle failures? 117 | return &extensions_pb.FetchOutput{ 118 | Objects: []*extensions_pb.ExternalObject{ 119 | cloudRunObj, 120 | }, 121 | }, nil 122 | } 123 | 124 | var fetchCmd = &cobra.Command{ 125 | Use: "fetch", 126 | Short: "Fetch current state of an Cloud Run service", 127 | Args: cobra.NoArgs, 128 | RunE: func(cmd *cobra.Command, args []string) error { 129 | if err := gcloudAuth(); err != nil { 130 | return err 131 | } 132 | fetchOutput, err := runFetch() 133 | if err != nil { 134 | return err 135 | } 136 | output, err := protojson.Marshal(fetchOutput) 137 | if err != nil { 138 | return errors.Wrap(err, "failed to marshal") 139 | } 140 | _, err = os.Stdout.Write(output) 141 | if err != nil { 142 | return errors.Wrap(err, "failed to write to stdout") 143 | } 144 | return nil 145 | }, 146 | } 147 | 148 | func init() { 149 | RootCmd.AddCommand(fetchCmd) 150 | 151 | registerCommonFlags(fetchCmd) 152 | } 153 | -------------------------------------------------------------------------------- /cmd/pvn-wrapper/fly/fetch.go: -------------------------------------------------------------------------------- 1 | package fly 2 | 3 | import ( 4 | "encoding/json" 5 | go_errors "errors" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "sort" 10 | "strings" 11 | 12 | "github.com/pkg/errors" 13 | common_config_pb "github.com/prodvana/prodvana-public/go/prodvana-sdk/proto/prodvana/common_config" 14 | extensions_pb "github.com/prodvana/prodvana-public/go/prodvana-sdk/proto/prodvana/runtimes/extensions" 15 | "github.com/prodvana/pvn-wrapper/cmdutil" 16 | "github.com/spf13/cobra" 17 | "golang.org/x/exp/maps" 18 | "google.golang.org/protobuf/encoding/protojson" 19 | ) 20 | 21 | type flyStatusOutput struct { 22 | ID string `json:"ID"` 23 | AppURL string `json:"AppURL"` 24 | Name string `json:"Name"` 25 | Hostname string `json:"Hostname"` 26 | Deployed bool `json:"Deployed"` 27 | Machines []*flyMachine `json:"Machines"` 28 | Version int `json:"Version"` 29 | } 30 | 31 | type flyMachine struct { 32 | Name string `json:"name"` 33 | State string `json:"state"` 34 | Config flyAppConfig `json:"config"` 35 | } 36 | 37 | type flyAppConfig struct { 38 | Env map[string]string `json:"env"` 39 | Metadata flyMetadata `json:"metadata"` 40 | } 41 | 42 | type flyMetadata struct { 43 | FlyReleaseVersion string `json:"fly_release_version"` 44 | } 45 | 46 | var errServiceNotFound = errors.New("service not found") 47 | 48 | func flyStatus(tomlFile string) (*flyStatusOutput, error) { 49 | 50 | describeCmd := exec.Command( 51 | flyPath, 52 | "status", 53 | "--config", 54 | tomlFile, 55 | "--json", 56 | ) 57 | output, err := cmdutil.RunCmdOutput(describeCmd) 58 | if err != nil { 59 | if strings.Contains(err.Error(), "Could not find") { 60 | return nil, errServiceNotFound 61 | } 62 | return nil, err 63 | } 64 | var statusOutput flyStatusOutput 65 | if err := json.Unmarshal(output, &statusOutput); err != nil { 66 | return nil, errors.Wrap(err, "failed to unmarshal fly status output") 67 | } 68 | return &statusOutput, nil 69 | } 70 | 71 | func runFetch() (*extensions_pb.FetchOutput, error) { 72 | cfg, err := getServiceConfig() 73 | if err != nil { 74 | return nil, err 75 | } 76 | tomlFile, err := makeTomlFile(cfg) 77 | if err != nil { 78 | return nil, err 79 | } 80 | defer func() { _ = os.Remove(tomlFile) }() 81 | status, err := flyStatus(tomlFile) 82 | if err != nil { 83 | if go_errors.Is(err, errServiceNotFound) { 84 | return &extensions_pb.FetchOutput{}, nil 85 | } 86 | return nil, err 87 | } 88 | 89 | versions := map[string]*extensions_pb.ExternalObjectVersion{} 90 | desiredFlyVersion := fmt.Sprintf("%d", status.Version) 91 | for _, machine := range status.Machines { 92 | var versionStr string 93 | if machine.Config.Env["PVN_SERVICE_ID"] == commonFlags.pvnServiceId { 94 | versionStr = machine.Config.Env["PVN_SERVICE_VERSION"] 95 | } 96 | if _, ok := versions[versionStr]; !ok { 97 | versions[versionStr] = &extensions_pb.ExternalObjectVersion{ 98 | Version: versionStr, 99 | Active: machine.Config.Metadata.FlyReleaseVersion == desiredFlyVersion, 100 | } 101 | } 102 | versions[versionStr].Replicas++ 103 | } 104 | 105 | versionsList := maps.Values(versions) 106 | sort.Slice(versionsList, func(i, j int) bool { 107 | return versionsList[i].Version < versionsList[j].Version 108 | }) 109 | 110 | cloudRunObj := &extensions_pb.ExternalObject{ 111 | Name: status.Name, 112 | ObjectType: "FlyApp", 113 | Versions: versionsList, 114 | ExternalLinks: []*common_config_pb.ExternalLink{ 115 | { 116 | Type: common_config_pb.ExternalLink_DETAIL, 117 | Name: "Fly Console", 118 | Url: fmt.Sprintf( 119 | "https://fly.io/apps/%[1]s", 120 | status.ID, // 1 121 | ), 122 | }, 123 | { 124 | Type: common_config_pb.ExternalLink_APP, 125 | Name: "App", 126 | Url: status.AppURL, 127 | }, 128 | }, 129 | } 130 | if status.Deployed { 131 | // TODO(naphat) failure? 132 | cloudRunObj.Status = extensions_pb.ExternalObject_SUCCEEDED 133 | } 134 | 135 | return &extensions_pb.FetchOutput{ 136 | Objects: []*extensions_pb.ExternalObject{ 137 | cloudRunObj, 138 | }, 139 | }, nil 140 | } 141 | 142 | var fetchCmd = &cobra.Command{ 143 | Use: "fetch", 144 | Short: "Fetch current state of an Cloud Run service", 145 | Args: cobra.NoArgs, 146 | RunE: func(cmd *cobra.Command, args []string) error { 147 | fetchOutput, err := runFetch() 148 | if err != nil { 149 | return err 150 | } 151 | output, err := protojson.Marshal(fetchOutput) 152 | if err != nil { 153 | return errors.Wrap(err, "failed to marshal") 154 | } 155 | _, err = os.Stdout.Write(output) 156 | if err != nil { 157 | return errors.Wrap(err, "failed to write to stdout") 158 | } 159 | return nil 160 | }, 161 | } 162 | 163 | func init() { 164 | RootCmd.AddCommand(fetchCmd) 165 | 166 | registerCommonFlags(fetchCmd) 167 | } 168 | -------------------------------------------------------------------------------- /cmd/pvn-wrapper/awsecs/fetch.go: -------------------------------------------------------------------------------- 1 | package awsecs 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "sort" 8 | 9 | "github.com/pkg/errors" 10 | common_config_pb "github.com/prodvana/prodvana-public/go/prodvana-sdk/proto/prodvana/common_config" 11 | runtimes_pb "github.com/prodvana/prodvana-public/go/prodvana-sdk/proto/prodvana/runtimes" 12 | extensions_pb "github.com/prodvana/prodvana-public/go/prodvana-sdk/proto/prodvana/runtimes/extensions" 13 | "github.com/spf13/cobra" 14 | "golang.org/x/sync/errgroup" 15 | "google.golang.org/protobuf/encoding/protojson" 16 | "google.golang.org/protobuf/types/known/timestamppb" 17 | ) 18 | 19 | func runFetch() (*extensions_pb.FetchOutput, error) { 20 | serviceOutput, err := describeService(commonFlags.ecsClusterName, commonFlags.ecsServiceName) 21 | if err != nil { 22 | return nil, err 23 | } 24 | region := os.Getenv("AWS_DEFAULT_REGION") 25 | if region == "" { 26 | return nil, errors.Errorf("AWS_DEFAULT_REGION not set") 27 | } 28 | ecsServiceObj := &extensions_pb.ExternalObject{ 29 | Name: commonFlags.ecsServiceName, 30 | ObjectType: "ECS", 31 | ExternalLinks: []*common_config_pb.ExternalLink{ 32 | { 33 | Type: common_config_pb.ExternalLink_DETAIL, 34 | Name: "ECS Console", 35 | Url: fmt.Sprintf( 36 | "https://%[1]s.console.aws.amazon.com/ecs/v2/clusters/%[2]s/services/%[3]s?region=%[1]s", 37 | region, 38 | commonFlags.ecsClusterName, 39 | commonFlags.ecsServiceName, 40 | ), 41 | }, 42 | }, 43 | } 44 | if serviceMissing(serviceOutput) { 45 | ecsServiceObj.Status = extensions_pb.ExternalObject_PENDING 46 | return &extensions_pb.FetchOutput{ 47 | Objects: []*extensions_pb.ExternalObject{ 48 | ecsServiceObj, 49 | }, 50 | }, nil 51 | } 52 | versionChan := make(chan *extensions_pb.ExternalObjectVersion) 53 | errg := errgroup.Group{} 54 | for _, depl := range serviceOutput.Services[0].Deployments { 55 | depl := depl 56 | errg.Go(func() error { 57 | def, err := describeTaskDefinition(depl.TaskDefinition) 58 | if err != nil { 59 | return err 60 | } 61 | tags := tagsToMap(def.Tags) 62 | version := &extensions_pb.ExternalObjectVersion{ 63 | Replicas: int32(depl.PendingCount) + int32(depl.RunningCount), 64 | Active: depl.Status == "PRIMARY", 65 | AvailableReplicas: int32(depl.RunningCount), 66 | TargetReplicas: int32(depl.DesiredCount), 67 | // TODO(naphat) today we use the service version string to detect drift. 68 | // It is currently not possible to change ECS-service-level settings like desired count 69 | // without also creating a new version string, so this works. 70 | } 71 | if version.Replicas == 0 { 72 | // skip, this deployment is no longer active and has no replicas left 73 | return nil 74 | } 75 | if tags[serviceIdTagKey] == commonFlags.pvnServiceId { 76 | // if the service ID doesn't match, we leave the version unset, essentially treating it as unknown 77 | version.Version = tags[serviceVersionTagKey] 78 | } 79 | versionChan <- version 80 | return nil 81 | }) 82 | } 83 | var versions []*extensions_pb.ExternalObjectVersion 84 | done := make(chan struct{}) 85 | go func() { 86 | defer close(done) 87 | for ver := range versionChan { 88 | versions = append(versions, ver) 89 | } 90 | }() 91 | err = errg.Wait() 92 | close(versionChan) 93 | if err != nil { 94 | return nil, err 95 | } 96 | <-done 97 | ecsServiceObj.Versions = versions 98 | foundCount := 0 99 | var debugMessage string 100 | var debugEvents []*runtimes_pb.DebugEvent 101 | for _, depl := range serviceOutput.Services[0].Deployments { 102 | if depl.Status == "PRIMARY" { 103 | switch depl.RolloutState { 104 | case "COMPLETED": 105 | ecsServiceObj.Status = extensions_pb.ExternalObject_SUCCEEDED 106 | case "FAILED": 107 | ecsServiceObj.Status = extensions_pb.ExternalObject_FAILED 108 | } 109 | foundCount++ 110 | debugEvents = append(debugEvents, &runtimes_pb.DebugEvent{ 111 | Timestamp: timestamppb.New(depl.CreatedAt), 112 | Message: fmt.Sprintf("Deployment %s started.", depl.Id), 113 | }) 114 | if depl.FailedTasks > 0 { 115 | debugEvents = append(debugEvents, &runtimes_pb.DebugEvent{ 116 | Timestamp: timestamppb.New(depl.UpdatedAt), 117 | Message: fmt.Sprintf("Deployment %s has %d failing tasks.", depl.Id, depl.FailedTasks), 118 | }) 119 | } 120 | } 121 | } 122 | if foundCount != 1 { 123 | log.Printf("Found multiple PRIMARY deployments for service %s, marking it as PENDING", commonFlags.ecsServiceName) 124 | ecsServiceObj.Status = extensions_pb.ExternalObject_PENDING 125 | debugMessage = "Found multiple PRIMARY deployments" 126 | } 127 | sort.Slice(debugEvents, func(i, j int) bool { 128 | // sort descending order 129 | return debugEvents[i].Timestamp.AsTime().After(debugEvents[j].Timestamp.AsTime()) 130 | }) 131 | if ecsServiceObj.Status == extensions_pb.ExternalObject_PENDING { 132 | ecsServiceObj.Message = debugMessage 133 | ecsServiceObj.DebugEvents = debugEvents 134 | } 135 | return &extensions_pb.FetchOutput{ 136 | Objects: []*extensions_pb.ExternalObject{ 137 | ecsServiceObj, 138 | }, 139 | }, nil 140 | } 141 | 142 | var fetchCmd = &cobra.Command{ 143 | Use: "fetch", 144 | Short: "Fetch current state of an ECS service", 145 | Args: cobra.NoArgs, 146 | RunE: func(cmd *cobra.Command, args []string) error { 147 | fetchOutput, err := runFetch() 148 | if err != nil { 149 | return err 150 | } 151 | output, err := protojson.Marshal(fetchOutput) 152 | if err != nil { 153 | return errors.Wrap(err, "failed to marshal") 154 | } 155 | _, err = os.Stdout.Write(output) 156 | if err != nil { 157 | return errors.Wrap(err, "failed to write to stdout") 158 | } 159 | return nil 160 | }, 161 | } 162 | 163 | func init() { 164 | RootCmd.AddCommand(fetchCmd) 165 | 166 | registerCommonFlags(fetchCmd) 167 | } 168 | -------------------------------------------------------------------------------- /result/result.go: -------------------------------------------------------------------------------- 1 | package result 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | go_errors "errors" 8 | "fmt" 9 | "io" 10 | "log" 11 | "os" 12 | "os/exec" 13 | "time" 14 | 15 | "github.com/pkg/errors" 16 | "github.com/prodvana/prodvana-public/go/prodvana-sdk/client" 17 | blobs_pb "github.com/prodvana/prodvana-public/go/prodvana-sdk/proto/prodvana/blobs" 18 | pvn_wrapper_pb "github.com/prodvana/prodvana-public/go/prodvana-sdk/proto/prodvana/pvn_wrapper" 19 | "google.golang.org/grpc" 20 | "google.golang.org/protobuf/encoding/protojson" 21 | ) 22 | 23 | type OutputFileUpload struct { 24 | Name string 25 | Stdout bool 26 | Stderr bool 27 | 28 | // only one or the other can be specified 29 | Path string 30 | Content []byte 31 | } 32 | 33 | type InputFile struct { 34 | Path string 35 | BlobId string 36 | } 37 | 38 | const ( 39 | PvnWrapperVersion = "0.0.2" 40 | ) 41 | 42 | func chunkReader(reader io.Reader, process func([]byte) error) error { 43 | reader = bufio.NewReader(reader) 44 | const chunkSize = 1024 * 1024 45 | buf := make([]byte, chunkSize) 46 | sentOnce := false 47 | for { 48 | n, err := reader.Read(buf) 49 | if err != nil { 50 | if go_errors.Is(err, io.EOF) { 51 | break 52 | } 53 | return err 54 | } 55 | sentOnce = true 56 | err = process(buf[:n]) 57 | if err != nil { 58 | return err 59 | } 60 | } 61 | if !sentOnce { 62 | // HACK(naphat) somewhere our handling of empty files is incorrect, requiring us to send a no-op upload req once to avoid hanging 63 | err := process(nil) 64 | if err != nil { 65 | return err 66 | } 67 | } 68 | return nil 69 | } 70 | 71 | func chunkFile(path string, process func([]byte) error) error { 72 | file, err := os.Open(path) 73 | if err != nil { 74 | return err 75 | } 76 | defer func() { _ = file.Close() }() 77 | return chunkReader(file, process) 78 | } 79 | 80 | func chunkByte(content []byte, process func([]byte) error) error { 81 | reader := bytes.NewReader(content) 82 | return chunkReader(reader, process) 83 | } 84 | 85 | func uploadOutput(ctx context.Context, blobsClient blobs_pb.BlobsManagerClient, file OutputFileUpload) (string, error) { 86 | strm, err := blobsClient.UploadCasBlob(ctx) 87 | if err != nil { 88 | return "", err 89 | } 90 | process := func(b []byte) error { 91 | return strm.Send(&blobs_pb.UploadCasBlobReq{ 92 | Bytes: b, 93 | }) 94 | } 95 | if file.Path != "" { 96 | err = chunkFile(file.Path, process) 97 | } else { 98 | err = chunkByte(file.Content, process) 99 | } 100 | if err != nil { 101 | return "", err 102 | } 103 | resp, err := strm.CloseAndRecv() 104 | if err != nil { 105 | return "", err 106 | } 107 | return resp.Id, nil 108 | } 109 | 110 | func downloadBlob(ctx context.Context, blobsClient blobs_pb.BlobsManagerClient, file InputFile) error { 111 | strm, err := blobsClient.GetCasBlob(ctx, &blobs_pb.GetCasBlobReq{ 112 | Id: file.BlobId, 113 | }) 114 | if err != nil { 115 | return errors.Wrapf(err, "failed to initiate download of blob %s", file.BlobId) 116 | } 117 | defer func() { _ = strm.CloseSend() }() 118 | f, err := os.Create(file.Path) 119 | if err != nil { 120 | return errors.Wrapf(err, "failed to open %s", file.Path) 121 | } 122 | defer func() { _ = f.Close() }() 123 | for { 124 | resp, err := strm.Recv() 125 | if err != nil { 126 | if go_errors.Is(err, io.EOF) { 127 | break 128 | } 129 | return errors.Wrapf(err, "failed to download blob %s", file.BlobId) 130 | } 131 | _, err = f.Write(resp.Bytes) 132 | if err != nil { 133 | return errors.Wrapf(err, "failed to write to %s", file.Path) 134 | } 135 | } 136 | return nil 137 | } 138 | 139 | // Handle the "main" function of wrapper commands. 140 | // This function never returns. 141 | func RunWrapper(inputFiles []InputFile, successExitCodes []int32, run func(context.Context) (*pvn_wrapper_pb.Output, []OutputFileUpload, error)) { 142 | ctx := context.Background() 143 | var conn *grpc.ClientConn 144 | getProdvanaConnection := func() *grpc.ClientConn { 145 | var err error 146 | conn, err = client.MakeProdvanaConnection(client.DefaultConnectionOptions()) 147 | if err != nil { 148 | // TODO(naphat) should we return json in the event of infra errors too? 149 | log.Fatal(err) 150 | } 151 | return conn 152 | } 153 | var blobsClient blobs_pb.BlobsManagerClient 154 | getBlobsClient := func() blobs_pb.BlobsManagerClient { 155 | if blobsClient == nil { 156 | blobsClient = blobs_pb.NewBlobsManagerClient(getProdvanaConnection()) 157 | } 158 | return blobsClient 159 | } 160 | var jobClient pvn_wrapper_pb.JobManagerClient 161 | getJobClient := func() pvn_wrapper_pb.JobManagerClient { 162 | if jobClient == nil { 163 | jobClient = pvn_wrapper_pb.NewJobManagerClient(getProdvanaConnection()) 164 | } 165 | return jobClient 166 | } 167 | defer func() { 168 | if conn != nil { 169 | _ = conn.Close() 170 | } 171 | }() 172 | for _, input := range inputFiles { 173 | if err := downloadBlob(ctx, getBlobsClient(), input); err != nil { 174 | log.Fatal(err) 175 | } 176 | } 177 | startTs := time.Now() 178 | result, outputFiles, err := run(ctx) 179 | duration := time.Since(startTs) 180 | if err != nil { 181 | result = &pvn_wrapper_pb.Output{} 182 | result.ExecError = err.Error() 183 | result.ExitCode = -1 184 | } 185 | hostname, err := os.Hostname() 186 | if err == nil { 187 | result.Hostname = hostname 188 | } 189 | result.StartTimestampNs = startTs.UnixNano() 190 | result.DurationNs = duration.Nanoseconds() 191 | result.Version = PvnWrapperVersion 192 | isSuccessful := false 193 | for _, exitCode := range successExitCodes { 194 | if exitCode == result.ExitCode { 195 | isSuccessful = true 196 | break 197 | } 198 | } 199 | if len(outputFiles) > 0 { 200 | defer func() { _ = conn.Close() }() 201 | for _, file := range outputFiles { 202 | id, uploadErr := uploadOutput(ctx, getBlobsClient(), file) 203 | if uploadErr != nil { 204 | if !os.IsNotExist(uploadErr) || isSuccessful { 205 | // for IsNotExist errors in the event the program did not exit successfully, do not hard error on missing output file. 206 | // TODO(naphat) should we return json in the event of infra errors too? 207 | // for now, print out every output file so that we have the output for debugging 208 | for _, file := range outputFiles { 209 | if file.Stderr { 210 | fmt.Printf("Stderr:\n") 211 | } else if file.Stdout { 212 | fmt.Printf("Stdout:\n") 213 | } else { 214 | fmt.Printf("Output file: %s\n", file.Name) 215 | } 216 | var bytes []byte 217 | var err error 218 | if file.Path != "" { 219 | bytes, err = os.ReadFile(file.Path) 220 | if err != nil { 221 | log.Printf("Failed to read file %s: %v", file.Path, err) 222 | } 223 | } else { 224 | bytes = file.Content 225 | } 226 | _, err = os.Stdout.Write(bytes) 227 | if err != nil { 228 | log.Printf("Failed to write output %s for debugging: %+v", file.Name, err) 229 | } 230 | } 231 | fileName := file.Path 232 | if file.Stderr { 233 | fileName = "stderr" 234 | } 235 | if file.Stdout { 236 | fileName = "stdout" 237 | } 238 | log.Fatalf("failed to upload file %s: %+v\n", fileName, uploadErr) 239 | } 240 | continue 241 | } 242 | if file.Stdout { 243 | if result.StdoutBlobId != "" { 244 | log.Fatal("internal error: multiple stdout provided") 245 | } 246 | result.StdoutBlobId = id 247 | } else if file.Stderr { 248 | if result.StderrBlobId != "" { 249 | log.Fatal("internal error: multiple stderr provided") 250 | } 251 | result.StderrBlobId = id 252 | } else { 253 | result.Files = append(result.Files, &pvn_wrapper_pb.OutputFile{ 254 | Name: file.Name, 255 | ContentBlobId: id, 256 | }) 257 | } 258 | } 259 | } 260 | 261 | jobId := os.Getenv("PVN_JOB_ID") 262 | if jobId != "" { 263 | _, err := getJobClient().ReportJobResult(ctx, &pvn_wrapper_pb.ReportJobResultReq{ 264 | JobId: jobId, 265 | Output: result, 266 | }) 267 | if err != nil { 268 | log.Fatal(err) 269 | } 270 | } 271 | 272 | output, err := protojson.Marshal(result) 273 | if err != nil { 274 | // If something went wrong during encode/write to stdout, indicate that in stderr and exit non-zero. 275 | log.Fatal(err) 276 | } 277 | _, err = os.Stdout.Write(output) 278 | if err != nil { 279 | log.Fatal(err) 280 | } 281 | 282 | // If the wrapped process fails, make sure this process has a non-zero exit code. 283 | // This is to maintain compatibility with existing task execution infrastructure. 284 | // Once we enforce the use of this wrapper, we can safely exit 0 here. 285 | os.Exit(int(result.ExitCode)) 286 | } 287 | 288 | func RunCmd(cmd *exec.Cmd) (*pvn_wrapper_pb.Output, []OutputFileUpload, error) { 289 | stdout := new(bytes.Buffer) 290 | stderr := new(bytes.Buffer) 291 | cmd.Stdout = stdout 292 | cmd.Stderr = stderr 293 | 294 | var result pvn_wrapper_pb.Output 295 | 296 | err := cmd.Run() 297 | 298 | if err != nil { 299 | var exitErr *exec.ExitError 300 | if go_errors.As(err, &exitErr) { 301 | result.ExitCode = int32(exitErr.ExitCode()) 302 | } else { 303 | return nil, nil, err 304 | } 305 | } 306 | 307 | return &result, []OutputFileUpload{ 308 | { 309 | Stdout: true, 310 | Content: stdout.Bytes(), 311 | }, 312 | { 313 | Stderr: true, 314 | Content: stderr.Bytes(), 315 | }, 316 | }, nil 317 | } 318 | -------------------------------------------------------------------------------- /cmd/pvn-wrapper/awsecs/apply.go: -------------------------------------------------------------------------------- 1 | package awsecs 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "time" 11 | 12 | "github.com/pkg/errors" 13 | "github.com/prodvana/pvn-wrapper/cmdutil" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | const ( 18 | serviceIdTagKey = "pvn:id" 19 | serviceVersionTagKey = "pvn:version" 20 | ) 21 | 22 | type tagPair struct { 23 | Key string `json:"key"` 24 | Value string `json:"value"` 25 | } 26 | 27 | type describeTaskDefinitionOutput struct { 28 | Tags []tagPair `json:"tags"` 29 | } 30 | 31 | func describeTaskDefinition(definition string) (*describeTaskDefinitionOutput, error) { 32 | describeCmd := exec.Command( 33 | awsPath, 34 | "ecs", 35 | "describe-task-definition", 36 | "--include=TAGS", 37 | "--task-definition", 38 | definition, 39 | ) 40 | output, err := cmdutil.RunCmdOutput(describeCmd) 41 | if err != nil { 42 | return nil, err 43 | } 44 | var describeOutput describeTaskDefinitionOutput 45 | if err := json.Unmarshal(output, &describeOutput); err != nil { 46 | return nil, errors.Wrap(err, "failed to unmarsal describe-task-definition output") 47 | } 48 | return &describeOutput, nil 49 | } 50 | 51 | func tagsToMap(tags []tagPair) map[string]string { 52 | tagMap := make(map[string]string) 53 | for _, tag := range tags { 54 | tagMap[tag.Key] = tag.Value 55 | } 56 | return tagMap 57 | } 58 | 59 | type getResourcesOutput struct { 60 | ResourceTagMappingList []struct { 61 | ResourceARN string `json:"ResourceARN"` 62 | // this endpoint uses capital CamelCase so it cannot use the same struct as the other endpoints 63 | Tags []struct { 64 | Key string `json:"Key"` 65 | Value string `json:"Value"` 66 | } `json:"Tags"` 67 | } `json:"ResourceTagMappingList"` 68 | } 69 | 70 | func getValidTaskDefinitionArns(pvnServiceId, pvnServiceVersion string) ([]string, error) { 71 | output, err := cmdutil.RunCmdOutput(exec.Command( 72 | awsPath, 73 | "resourcegroupstaggingapi", 74 | "get-resources", 75 | "--resource-type-filters", "ecs:task-definition", 76 | "--tag-filters", 77 | fmt.Sprintf("Key=%s,Values=%s", serviceIdTagKey, pvnServiceId), 78 | fmt.Sprintf("Key=%s,Values=%s", serviceVersionTagKey, pvnServiceVersion), 79 | )) 80 | if err != nil { 81 | return nil, err 82 | } 83 | var outputParsed getResourcesOutput 84 | if err := json.Unmarshal(output, &outputParsed); err != nil { 85 | return nil, errors.Wrap(err, "failed to unmarshal get-resources output") 86 | } 87 | var validArns []string 88 | for _, resource := range outputParsed.ResourceTagMappingList { 89 | validArns = append(validArns, resource.ResourceARN) 90 | } 91 | return validArns, nil 92 | } 93 | 94 | type registerTaskDefinitionOutput struct { 95 | TaskDefinition struct { 96 | TaskDefinitionArn string `json:"taskDefinitionArn"` 97 | } `json:"taskDefinition"` 98 | } 99 | 100 | func registerTaskDefinitionIfNeeded(taskDefPath, pvnServiceId, pvnServiceVersion string, serviceOutput *describeServicesOutput) (string, error) { 101 | validArns, err := getValidTaskDefinitionArns(pvnServiceId, pvnServiceVersion) 102 | if err != nil { 103 | return "", err 104 | } 105 | if len(serviceOutput.Services) > 0 { 106 | service := serviceOutput.Services[0] 107 | for _, arn := range validArns { 108 | if arn == service.TaskDefinition { 109 | // prioritize returning the service's existing task arn 110 | log.Printf("Using existing task definition %s already present in service definition", arn) 111 | return arn, nil 112 | } 113 | } 114 | } 115 | if len(validArns) > 0 { 116 | log.Printf("Using existing task definition %s", validArns[0]) 117 | return validArns[0], nil 118 | } 119 | 120 | taskDefPath, err = filepath.Abs(taskDefPath) 121 | if err != nil { 122 | return "", errors.Wrap(err, "failed to make abs path") 123 | } 124 | 125 | taskDefContents, err := os.ReadFile(taskDefPath) 126 | if err != nil { 127 | return "", errors.Wrap(err, "failed to read task definition file") 128 | } 129 | // Printing task definition contents to help with debugging. 130 | log.Printf("Task definition:\n%s", taskDefContents) 131 | log.Printf("Registering new task definition for %s:%s", pvnServiceId, pvnServiceVersion) 132 | 133 | registerCmd := exec.Command( 134 | awsPath, 135 | "ecs", 136 | "register-task-definition", 137 | "--cli-input-json", 138 | fmt.Sprintf("file://%s", taskDefPath), 139 | ) 140 | output, err := cmdutil.RunCmdOutput(registerCmd) 141 | if err != nil { 142 | return "", err 143 | } 144 | var registerOutput registerTaskDefinitionOutput 145 | if err := json.Unmarshal(output, ®isterOutput); err != nil { 146 | return "", errors.Wrap(err, "failed to unmarshal register-task-definition output") 147 | } 148 | taskArn := registerOutput.TaskDefinition.TaskDefinitionArn 149 | if taskArn == "" { 150 | return "", errors.Errorf("got empty task definition arn. Register output: %s", string(output)) 151 | } 152 | return taskArn, nil 153 | } 154 | 155 | type networkConfiguration struct { 156 | AwsvpcConfiguration *struct { 157 | Subnets []string `json:"subnets"` 158 | SecurityGroups []string `json:"securityGroups"` 159 | AssignPublicIp string `json:"assignPublicIp"` 160 | } `json:"awsvpcConfiguration"` 161 | } 162 | 163 | type describeServicesOutput struct { 164 | Services []struct { 165 | Status string `json:"status"` 166 | TaskDefinition string `json:"taskDefinition"` 167 | Deployments []struct { 168 | Status string `json:"status"` 169 | TaskDefinition string `json:"taskDefinition"` 170 | DesiredCount int `json:"desiredCount"` 171 | PendingCount int `json:"pendingCount"` 172 | RunningCount int `json:"runningCount"` 173 | FailedTasks int `json:"failedTasks"` 174 | Id string `json:"id"` 175 | CreatedAt time.Time `json:"createdAt"` 176 | UpdatedAt time.Time `json:"updatedAt"` 177 | NetworkConfiguration networkConfiguration `json:"networkConfiguration"` 178 | RolloutState string `json:"rolloutState"` 179 | RolloutStateReason string `json:"rolloutStateReason"` 180 | } `json:"deployments"` 181 | } `json:"services"` 182 | Failures []struct { 183 | Reason string `json:"reason"` 184 | } `json:"failures"` 185 | } 186 | 187 | func describeService(clusterName, serviceName string) (*describeServicesOutput, error) { 188 | describeCmd := exec.Command( 189 | awsPath, 190 | "ecs", 191 | "describe-services", 192 | "--cluster", 193 | clusterName, 194 | "--services", 195 | serviceName, 196 | ) 197 | output, err := cmdutil.RunCmdOutput(describeCmd) 198 | if err != nil { 199 | return nil, err 200 | } 201 | var describeOutput describeServicesOutput 202 | if err := json.Unmarshal(output, &describeOutput); err != nil { 203 | return nil, errors.Wrap(err, "failed to unmarsal describe-services output") 204 | } 205 | if len(describeOutput.Failures) > 0 { 206 | if describeOutput.Failures[0].Reason != "MISSING" { 207 | return nil, errors.Errorf("unexpected failure reason: %s", describeOutput.Failures[0].Reason) 208 | } 209 | } else { 210 | if len(describeOutput.Services) != 1 { 211 | return nil, errors.Errorf("unexpected number of services: %d", len(describeOutput.Services)) 212 | } 213 | } 214 | return &describeOutput, nil 215 | } 216 | 217 | func patchTaskDefinition(taskDefPath, pvnServiceId, pvnServiceVersion string) (string, error) { 218 | taskDef, err := os.ReadFile(taskDefPath) 219 | if err != nil { 220 | return "", errors.Wrap(err, "failed to read task definition file") 221 | } 222 | var untypedDef map[string]interface{} 223 | if err := json.Unmarshal(taskDef, &untypedDef); err != nil { 224 | return "", errors.Wrapf(err, "failed to unmarshal task definition file: %s", string(taskDef)) 225 | } 226 | var tagsList []interface{} 227 | tags, hasTags := untypedDef["tags"] 228 | if hasTags { 229 | var ok bool 230 | tagsList, ok = tags.([]interface{}) 231 | if !ok { 232 | return "", errors.Wrapf(err, "unexpected type for tags: %T", tags) 233 | } 234 | } 235 | tagsList = append(tagsList, map[string]string{ 236 | "key": serviceIdTagKey, 237 | "value": pvnServiceId, 238 | }, map[string]string{ 239 | "key": serviceVersionTagKey, 240 | "value": pvnServiceVersion, 241 | }) 242 | untypedDef["tags"] = tagsList 243 | 244 | updatedTaskDef, err := json.Marshal(untypedDef) 245 | if err != nil { 246 | return "", errors.Wrap(err, "failed to marshal") 247 | } 248 | 249 | tempFile, err := os.CreateTemp("", "ecs-task-definition") 250 | if err != nil { 251 | return "", errors.Wrap(err, "failed to make tempfile") 252 | } 253 | 254 | if _, err := tempFile.Write(updatedTaskDef); err != nil { 255 | return "", errors.Wrap(err, "failed to write to tempfile") 256 | } 257 | if err := tempFile.Close(); err != nil { 258 | return "", errors.Wrap(err, "failed to close tempfile") 259 | } 260 | 261 | return tempFile.Name(), nil 262 | } 263 | 264 | func patchServiceSpec(serviceSpecPath string, ecsServiceName, ecsCluster, taskArn string, forUpdate bool) (string, error) { 265 | serviceSpec, err := os.ReadFile(serviceSpecPath) 266 | if err != nil { 267 | return "", errors.Wrap(err, "failed to read service spec file") 268 | } 269 | var untypedDef map[string]interface{} 270 | if err := json.Unmarshal(serviceSpec, &untypedDef); err != nil { 271 | return "", errors.Wrapf(err, "failed to unmarshal service spec file: %s", string(serviceSpec)) 272 | } 273 | 274 | serviceName, hasServiceName := untypedDef["serviceName"] 275 | if hasServiceName { 276 | serviceNameString, ok := serviceName.(string) 277 | if !ok { 278 | return "", errors.Wrapf(err, "unexpected type for serviceName: %T", serviceName) 279 | } 280 | if serviceNameString != ecsServiceName { 281 | return "", errors.Errorf("serviceName in service spec file does not match ECS service name from Prodvana Service config. Got %s, want %s", serviceNameString, ecsServiceName) 282 | } 283 | } 284 | // delete the service field, as it's passed differently on update vs. create and handled on cli 285 | delete(untypedDef, "serviceName") 286 | delete(untypedDef, "service") 287 | cluster, hasCluster := untypedDef["cluster"] 288 | if hasCluster { 289 | clusterString, ok := cluster.(string) 290 | if !ok { 291 | return "", errors.Wrapf(err, "unexpected type for cluster: %T", cluster) 292 | } 293 | if clusterString != ecsCluster { 294 | return "", errors.Errorf("cluster in service spec file does not match ECS cluster name from Prodvana Runtime config. Got %s, want %s", clusterString, ecsCluster) 295 | } 296 | } else { 297 | untypedDef["cluster"] = ecsCluster 298 | } 299 | 300 | untypedDef["taskDefinition"] = taskArn 301 | 302 | if forUpdate { 303 | delete(untypedDef, "launchType") 304 | } 305 | 306 | updatedTaskDef, err := json.Marshal(untypedDef) 307 | if err != nil { 308 | return "", errors.Wrap(err, "failed to marshal") 309 | } 310 | 311 | tempFile, err := os.CreateTemp("", "ecs-task-definition") 312 | if err != nil { 313 | return "", errors.Wrap(err, "failed to make tempfile") 314 | } 315 | 316 | if _, err := tempFile.Write(updatedTaskDef); err != nil { 317 | return "", errors.Wrap(err, "failed to write to tempfile") 318 | } 319 | if err := tempFile.Close(); err != nil { 320 | return "", errors.Wrap(err, "failed to close tempfile") 321 | } 322 | 323 | return tempFile.Name(), nil 324 | } 325 | 326 | func serviceMissing(output *describeServicesOutput) bool { 327 | if len(output.Failures) > 0 { 328 | return output.Failures[0].Reason == "MISSING" 329 | } 330 | return output.Services[0].Status == "INACTIVE" 331 | } 332 | 333 | var applyCmd = &cobra.Command{ 334 | Use: "apply", 335 | Short: "Create or update an ECS service", 336 | Args: cobra.NoArgs, 337 | RunE: func(cmd *cobra.Command, args []string) error { 338 | newTaskDefPath, err := patchTaskDefinition(commonFlags.taskDefinitionFile, commonFlags.pvnServiceId, commonFlags.pvnServiceVersion) 339 | if err != nil { 340 | return err 341 | } 342 | defer func() { _ = os.Remove(newTaskDefPath) }() 343 | serviceOutput, err := describeService(commonFlags.ecsClusterName, commonFlags.ecsServiceName) 344 | if err != nil { 345 | return err 346 | } 347 | taskArn, err := registerTaskDefinitionIfNeeded(newTaskDefPath, commonFlags.pvnServiceId, commonFlags.pvnServiceVersion, serviceOutput) 348 | if err != nil { 349 | return err 350 | } 351 | commonArgs := []string{ 352 | "--propagate-tags=TASK_DEFINITION", 353 | "--cluster", // must be set regardless of serviceSpec, in case updateTaskDefinitionOnly is set 354 | commonFlags.ecsClusterName, 355 | } 356 | if commonFlags.updateTaskDefinitionOnly { 357 | commonArgs = append(commonArgs, 358 | "--task-definition", 359 | taskArn, 360 | ) 361 | } else { 362 | newServiceSpecPath, err := patchServiceSpec( 363 | commonFlags.serviceSpecFile, 364 | commonFlags.ecsServiceName, 365 | commonFlags.ecsClusterName, 366 | taskArn, 367 | !serviceMissing(serviceOutput), 368 | ) 369 | if err != nil { 370 | return err 371 | } 372 | defer func() { _ = os.Remove(newServiceSpecPath) }() 373 | commonArgs = append(commonArgs, 374 | "--cli-input-json", 375 | fmt.Sprintf("file://%s", newServiceSpecPath), 376 | ) 377 | 378 | serviceDefContents, err := os.ReadFile(newServiceSpecPath) 379 | if err != nil { 380 | return errors.Wrap(err, "failed to read service definition file") 381 | } 382 | // Printing service definition contents to help with debugging. 383 | log.Printf("Service definition:\n%s", serviceDefContents) 384 | } 385 | if serviceMissing(serviceOutput) { 386 | if commonFlags.updateTaskDefinitionOnly { 387 | return errors.Errorf("cannot update task definition only when ECS service does not exist. ECS service: %s", commonFlags.ecsServiceName) 388 | } 389 | log.Printf("Creating service %s on cluster %s with task ARN %s\n", commonFlags.ecsServiceName, commonFlags.ecsClusterName, taskArn) 390 | // create service 391 | createCmd := exec.Command(awsPath, append([]string{ 392 | "ecs", 393 | "create-service", 394 | "--service-name", 395 | commonFlags.ecsServiceName, 396 | }, commonArgs...)...) 397 | err := cmdutil.RunCmd(createCmd) 398 | if err != nil { 399 | return err 400 | } 401 | } else { 402 | log.Printf("Updating service %s on cluster %s with task ARN %s\n", commonFlags.ecsServiceName, commonFlags.ecsClusterName, taskArn) 403 | // update service 404 | updateCmd := exec.Command(awsPath, append([]string{ 405 | "ecs", 406 | "update-service", 407 | "--service", 408 | commonFlags.ecsServiceName, // must be set regardless of serviceSpec, in case updateTaskDefinitionOnly is set 409 | }, commonArgs...)...) 410 | err := cmdutil.RunCmd(updateCmd) 411 | if err != nil { 412 | return err 413 | } 414 | } 415 | return nil 416 | }, 417 | } 418 | 419 | func init() { 420 | RootCmd.AddCommand(applyCmd) 421 | 422 | registerCommonFlags(applyCmd) 423 | } 424 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | contrib.go.opencensus.io/exporter/ocagent v0.7.1-0.20200907061046-05415f1de66d h1:LblfooH1lKOpp1hIhukktmSAxFkqMPFk9KR6iZ0MJNI= 2 | contrib.go.opencensus.io/exporter/ocagent v0.7.1-0.20200907061046-05415f1de66d/go.mod h1:IshRmMJBhDfFj5Y67nVhMYTTIze91RUeT73ipWKs/GY= 3 | contrib.go.opencensus.io/exporter/prometheus v0.4.2 h1:sqfsYl5GIY/L570iT+l93ehxaWJs2/OwXtiWwew3oAg= 4 | contrib.go.opencensus.io/exporter/prometheus v0.4.2/go.mod h1:dvEHbiKmgvbr5pjaF9fpw1KeYcjrnC1J8B+JKjsZyRQ= 5 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 6 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 7 | github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHfpE= 8 | github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc= 9 | github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= 10 | github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= 11 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 12 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 13 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 14 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 16 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= 18 | github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 19 | github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= 20 | github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= 21 | github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= 22 | github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 23 | github.com/evanphx/json-patch/v5 v5.7.0 h1:nJqP7uwL84RJInrohHfW0Fx3awjbm8qZeFv0nW9SYGc= 24 | github.com/evanphx/json-patch/v5 v5.7.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= 25 | github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= 26 | github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= 27 | github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= 28 | github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 29 | github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 30 | github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= 31 | github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 32 | github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= 33 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 34 | github.com/go-openapi/jsonreference v0.20.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTrLC1F86HID8= 35 | github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 36 | github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= 37 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 38 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 39 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 40 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 41 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 42 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 43 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 44 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 45 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 46 | github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0= 47 | github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E= 48 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 49 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 50 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 51 | github.com/google/go-containerregistry v0.13.0 h1:y1C7Z3e149OJbOPDBxLYR8ITPz8dTKqQwjErKVHJC8k= 52 | github.com/google/go-containerregistry v0.13.0/go.mod h1:J9FQ+eSS4a1aC2GNZxvNpbWhgp0487v+cgiilB4FqDo= 53 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 54 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 55 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 56 | github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= 57 | github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 58 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 h1:6UKoz5ujsI55KNpsJH3UwCq3T8kKbZwNZBNPuTTje8U= 59 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1/go.mod h1:YvJ2f6MplWDhfxiUC3KpyTy76kYUZA4W3pTv/wdKQ9Y= 60 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 61 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 62 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 63 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 64 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 65 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 66 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 67 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 68 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 69 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 70 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 71 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 72 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 73 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 74 | github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= 75 | github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 76 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 77 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 78 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 79 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 80 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 81 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 82 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 83 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 84 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 85 | github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= 86 | github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 87 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 88 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 89 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 90 | github.com/planetscale/vtprotobuf v0.6.0 h1:nBeETjudeJ5ZgBHUz1fVHvbqUKnYOXNhsIEabROxmNA= 91 | github.com/planetscale/vtprotobuf v0.6.0/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= 92 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 93 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 94 | github.com/prodvana/prodvana-public/go/prodvana-sdk v0.3.38 h1:tjYsKfCOoirIWJKSpUp00r2YPWaftizM6CLuU36DRvg= 95 | github.com/prodvana/prodvana-public/go/prodvana-sdk v0.3.38/go.mod h1:FzjJuPW6kjapJfmOqSjrPC19uMmNJuIdozPR6C/ZTsQ= 96 | github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= 97 | github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= 98 | github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM= 99 | github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= 100 | github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= 101 | github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= 102 | github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI= 103 | github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= 104 | github.com/prometheus/statsd_exporter v0.22.7 h1:7Pji/i2GuhK6Lu7DHrtTkFmNBCudCPT1pX2CziuyQR0= 105 | github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9dFqnUakOjnEuMPJJJnI= 106 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 107 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 108 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 109 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= 110 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 111 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 112 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 113 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 114 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 115 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 116 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 117 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 118 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 119 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 120 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 121 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 122 | go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= 123 | go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= 124 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 125 | go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= 126 | go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 127 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 128 | go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= 129 | go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= 130 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 131 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 132 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 133 | golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= 134 | golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= 135 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 136 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 137 | golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= 138 | golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 139 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 140 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 141 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 142 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 143 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 144 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= 145 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 146 | golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= 147 | golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= 148 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 149 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 150 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 151 | golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= 152 | golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 153 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 154 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 155 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 156 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= 157 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 158 | golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= 159 | golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= 160 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 161 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 162 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 163 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 164 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 165 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 166 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 167 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 168 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 169 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 170 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 171 | golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= 172 | golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= 173 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 174 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 175 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 176 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 177 | gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= 178 | gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= 179 | google.golang.org/api v0.147.0 h1:Can3FaQo9LlVqxJCodNmeZW/ib3/qKAY3rFeXiHo5gc= 180 | google.golang.org/api v0.147.0/go.mod h1:pQ/9j83DcmPd/5C9e2nFOdjjNkDZ1G+zkbK2uvdkJMs= 181 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 182 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 183 | google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f h1:Vn+VyHU5guc9KjB5KrjI2q0wCOWEOIh0OEsleqakHJg= 184 | google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f/go.mod h1:nWSwAFPb+qfNJXsoeO3Io7zf4tMSfN8EA8RlDA04GhY= 185 | google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo= 186 | google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4= 187 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 h1:DC7wcm+i+P1rN3Ff07vL+OndGg5OhNddHyTA+ocPqYE= 188 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4/go.mod h1:eJVxU6o+4G1PSczBr85xmyvSNYAKvAYgkub40YGomFM= 189 | google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= 190 | google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= 191 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 192 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 193 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 194 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 195 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 196 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 197 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 198 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 199 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 200 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 201 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 202 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 203 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 204 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 205 | k8s.io/api v0.27.6 h1:PBWu/lywJe2qQcshMjubzcBg7+XDZOo7O8JJAWuYtUo= 206 | k8s.io/api v0.27.6/go.mod h1:AQYj0UsFCp3qJE7bOVnUuy4orCsXVkvHefnbYQiNWgk= 207 | k8s.io/apimachinery v0.27.6 h1:mGU8jmBq5o8mWBov+mLjdTBcU+etTE19waies4AQ6NE= 208 | k8s.io/apimachinery v0.27.6/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E= 209 | k8s.io/client-go v0.27.6 h1:vzI8804gpUtpMCNaFjIFyJrifH7u//LJCJPy8fQuYQg= 210 | k8s.io/client-go v0.27.6/go.mod h1:PMsXcDKiJTW7PHJ64oEsIUJF319wm+EFlCj76oE5QXM= 211 | k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= 212 | k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= 213 | k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg= 214 | k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg= 215 | k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY= 216 | k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 217 | knative.dev/networking v0.0.0-20231017124814-2a7676e912b7 h1:6+1icZuxiZO1paFZ4d/ysKWVG2M4WB7OxNJNyLG0P/E= 218 | knative.dev/networking v0.0.0-20231017124814-2a7676e912b7/go.mod h1:1gcHoIVG47ekQWjkddqRq+/7tWRh+CB9W4k/NAcdRbk= 219 | knative.dev/pkg v0.0.0-20231023151236-29775d7c9e5c h1:xyPoEToTWeBdn6tinhLxXfnhJhTNQt5WzHiTNiFphRw= 220 | knative.dev/pkg v0.0.0-20231023151236-29775d7c9e5c/go.mod h1:HHRXEd7ZlFpthgE+rwAZ6MUVnuJOAeolnaFSthXloUQ= 221 | knative.dev/serving v0.39.2 h1:qxUdop3fGNAuBwAzutCU1HSszoO1nezKbV2I0HgTh4k= 222 | knative.dev/serving v0.39.2/go.mod h1:0QIp5mvgWa1oUC2MxMf+Q/JWgG8JhAsSdJKc6iTRlvE= 223 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= 224 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 225 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= 226 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= 227 | sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= 228 | sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= 229 | --------------------------------------------------------------------------------