From babe4c111e6747a9a398911d2bcacc173af42108 Mon Sep 17 00:00:00 2001 From: Ben Moskovitz Date: Wed, 23 Nov 2022 14:22:55 +1300 Subject: [PATCH 1/3] Add example secrets pipeline --- .buildkite/secrets-pipeline-example.yml | 223 ++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 .buildkite/secrets-pipeline-example.yml diff --git a/.buildkite/secrets-pipeline-example.yml b/.buildkite/secrets-pipeline-example.yml new file mode 100644 index 0000000000..8e5cc9312c --- /dev/null +++ b/.buildkite/secrets-pipeline-example.yml @@ -0,0 +1,223 @@ +env: + DRY_RUN: false # set to true to disable publishing releases +agents: + queue: agent-runners-linux-amd64 + +secret_providers: + - id: ssm + type: aws-ssm + iam_role: arn:aws:iam::123456789012:role/buildkite-role + assume_via_oidc: true + + - id: ssm-in-another-account + type: aws-ssm + config: + iam_role: arn:aws:iam::555555555555:role/buildkite-role-2 + assume_via_oidc: true + + - id: vault + type: vault_kvv2 + config: + address: https://vault.example.com + mount_path: secret + auth_type: kubernetes + auth_config: + role: buildkite + service_account_token_path: /var/run/secrets/kubernetes.io/serviceaccount/token + +secrets: + - env_var: AWS_NEPTUNE_LOGIN_PASSWORD + key: path/to/neptune_login_password + provider_id: ssm + + - env_var: WIDGET_CORP_API_KEY + key: path/to/widget-corp-api-key + provider_id: vault + + - env_var: GADGET_CO_TOKEN + key: path/to/gadget-co-token + provider_id: ssm-in-another-account + + - file: ~/.ssh/id_rsa + key: path/to/ssh-key + provider_id: ssm + +steps: + - name: ":go: go fmt" + key: test-go-fmt + command: ".buildkite/steps/test-go-fmt.sh" + plugins: + docker-compose#v3.0.0: + config: .buildkite/docker-compose.yml + run: agent + + - name: ":linux: Linux AMD64 Tests" + key: test-linux-amd64 + command: ".buildkite/steps/tests.sh" + artifact_paths: junit-*.xml + plugins: + docker-compose#v3.0.0: + config: .buildkite/docker-compose.yml + run: agent + + - name: ":linux: Linux ARM64 Tests" + key: test-linux-arm64 + command: ".buildkite/steps/tests.sh" + artifact_paths: junit-*.xml + agents: + queue: agent-runners-linux-arm64 + plugins: + docker-compose#v3.0.0: + config: .buildkite/docker-compose.yml + run: agent + + - name: ":satellite: Detect Data Races" + key: test-race-linux-arm64 + command: ".buildkite/steps/tests.sh -race" + artifact_paths: junit-*.xml + agents: + queue: agent-runners-linux-arm64 + plugins: + docker-compose#v3.0.0: + config: .buildkite/docker-compose.yml + run: agent + + - name: ":windows: Windows AMD64 Tests" + key: test-windows + command: "bash .buildkite\\steps\\tests.sh" + artifact_paths: junit-*.xml + agents: + queue: agent-runners-windows-amd64 + + - label: ":writing_hand: Annotate with Test Failures" + depends_on: + - test-linux-amd64 + - test-race-linux-arm64 + - test-linux-arm64 + - test-windows + plugins: + - junit-annotate#v1.6.0: + artifacts: junit-*.xml + + - group: ":hammer_and_wrench: Binary builds" + steps: + - name: ":{{matrix.os}}: Build {{matrix.os}} {{matrix.arch}} binary" + command: ".buildkite/steps/build-binary.sh {{matrix.os}} {{matrix.arch}}" + key: build-binary + depends_on: + # don't wait for slower windows tests + - test-linux-amd64 + - test-linux-arm64 + artifact_paths: "pkg/*" + plugins: + docker-compose#v3.0.0: + config: .buildkite/docker-compose.yml + run: agent + matrix: + setup: + os: + - darwin + - freebsd + - linux + - openbsd + - windows + arch: + - "386" + - amd64 + - arm64 + adjustments: + - with: { os: darwin, arch: "386" } + skip: "macOS no longer supports x86 binaries" + + - with: { os: dragonflybsd, arch: amd64 } + + - with: { os: freebsd, arch: arm64 } + skip: "arm64 FreeBSD is not currently supported" + + - with: { os: linux, arch: arm } + - with: { os: linux, arch: armhf } + - with: { os: linux, arch: ppc64 } + - with: { os: linux, arch: ppc64le } + - with: { os: linux, arch: mips64le } + - with: { os: linux, arch: s390x } + + - with: { os: netbsd, arch: amd64 } + + - with: { os: openbsd, arch: arm64 } + skip: "arm64 OpenBSD is not currently supported" + + - name: ":technologist: Test bk cli + Agent cli" + key: test-bk-cli + depends_on: build-binary + command: ".buildkite/steps/test-bk.sh" + plugins: + docker-compose#v3.0.0: + config: .buildkite/docker-compose.yml + run: agent + env: + - BUILDKITE_AGENT_ACCESS_TOKEN + - BUILDKITE_BUILD_ID + - BUILDKITE_JOB_ID + volumes: + - "/usr/bin/buildkite-agent:/usr/bin/buildkite-agent" + + - name: ":mag: Extract Agent Version Metadata" + key: set-metadata + command: ".buildkite/steps/extract-agent-version-metadata.sh" + + - group: ":docker: Docker Container Builds" + steps: + - name: ":docker: {{matrix}} container build" + key: build-docker + agents: + queue: elastic-builders + depends_on: + - build-binary + - set-metadata + command: ".buildkite/steps/build-docker-image.sh {{matrix}}" + matrix: + setup: + - "alpine" + - "alpine-k8s" + - "ubuntu-18.04" + - "ubuntu-20.04" + - "sidecar" + + - name: ":debian: Debian package build" + key: build-debian-packages + depends_on: + - build-binary + - set-metadata + command: ".buildkite/steps/build-debian-packages.sh" + artifact_paths: "deb/**/*" + + - name: ":redhat: RPM Package build" + key: build-rpm-packages + depends_on: + - build-binary + - set-metadata + command: ".buildkite/steps/build-rpm-packages.sh" + artifact_paths: "rpm/**/*" + + - name: ":github: Build Github Release" + key: build-github-release + depends_on: + - build-binary + - set-metadata + command: ".buildkite/steps/build-github-release.sh" + artifact_paths: "releases/**/*" + plugins: + docker-compose#v1.8.0: + config: .buildkite/docker-compose.release.yml + run: github-release + + - name: ":pipeline: Upload Release Pipeline" + key: upload-release-steps + depends_on: + - test-windows + - test-bk-cli + - build-rpm-packages + - build-debian-packages + - build-docker + - build-github-release + command: ".buildkite/steps/upload-release-steps.sh" From 9028219a7596cb053bf5b20179206fefaebbb9c7 Mon Sep 17 00:00:00 2001 From: Ben Moskovitz Date: Thu, 24 Nov 2022 18:16:38 +1300 Subject: [PATCH 2/3] Secret experiment --- bootstrap/bootstrap.go | 76 ++++++++++++++++++++ go.mod | 1 + go.sum | 2 + secrets/provider.go | 37 ++++++++++ secrets/provider_aws_ssm.go | 75 ++++++++++++++++++++ secrets/provider_registry.go | 131 +++++++++++++++++++++++++++++++++++ secrets/secret.go | 69 ++++++++++++++++++ secrets/secret_config.go | 38 ++++++++++ 8 files changed, 429 insertions(+) create mode 100644 secrets/provider.go create mode 100644 secrets/provider_aws_ssm.go create mode 100644 secrets/provider_registry.go create mode 100644 secrets/secret.go create mode 100644 secrets/secret_config.go diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 472a9d0696..1c13e5d234 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -23,6 +23,7 @@ import ( "github.com/buildkite/agent/v3/hook" "github.com/buildkite/agent/v3/process" "github.com/buildkite/agent/v3/redaction" + "github.com/buildkite/agent/v3/secrets" "github.com/buildkite/agent/v3/tracetools" "github.com/buildkite/agent/v3/utils" "github.com/buildkite/roko" @@ -113,6 +114,81 @@ func (b *Bootstrap) Run(ctx context.Context) (exitCode int) { return shell.GetExitCode(err) } + // Just pretend that these two jsons live in the pipeline.yml, and get passed through as env vars by the backend + secretProviderRegistryJSON := `[ + { + "id": "ssm", + "type": "aws-ssm", + "config": {} + }, + { + "id": "other-ssm", + "type": "aws-ssm", + "config": { + "role_arn": "arn:aws:iam::555555555555:role/benno-test-role-delete-after-2022-11-29" + } + } +]` + + secretProviderRegistry, err := secrets.NewProviderRegistryFromJSON(secrets.ProviderRegistryConfig{Shell: b.shell}, secretProviderRegistryJSON) + if err != nil { + b.shell.Errorf("Error creating secret provider registry: %v", err) + return 1 + } + + secretsJSON := []byte(`[ + { + "env_var": "SUPER_SECRET_ENV_VAR", + "key": "/benno/secret/envar", + "provider_id": "ssm" + }, + { + "file": "/Users/ben/secret-file", + "key": "/benno/secret/file", + "provider_id": "other-ssm" + } +]`) + + var secretConfigs []secrets.SecretConfig + err = json.Unmarshal(secretsJSON, &secretConfigs) + if err != nil { + b.shell.Errorf("Error unmarshalling secrets: %v", err) + return 1 + } + + validationErrs := make([]error, 0, len(secretConfigs)) + for _, c := range secretConfigs { + err := c.Validate() + validationErrs = append(validationErrs, err) + } + + for _, err := range validationErrs { + b.shell.Errorf("Error validating secret config: %v", err) + } + + if len(validationErrs) > 0 { + return 1 + } + + secrets, errors := secretProviderRegistry.FetchAll(secretConfigs) + if len(errors) > 0 { + b.shell.Errorf("Errors fetching secrets:") + for _, err := range errors { + b.shell.Errorf(" %v", err) + } + return 1 + } + + for _, secret := range secrets { + // TODO: Automatically add env secrets to the redactor + err := secret.Store() + if err != nil { + b.shell.Errorf("Error storing secret: %v", err) + } + + defer secret.Cleanup() + } + var includePhase = func(phase string) bool { if len(b.Phases) == 0 { return true diff --git a/go.mod b/go.mod index eb8b643fd5..833449a8f9 100644 --- a/go.mod +++ b/go.mod @@ -75,6 +75,7 @@ require ( github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect github.com/philhofer/fwd v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/puzpuzpuz/xsync v1.5.2 github.com/qri-io/jsonpointer v0.0.0-20180309164927-168dd9e45cf2 // indirect github.com/russross/blackfriday/v2 v2.0.1 // indirect github.com/sasha-s/go-deadlock v0.0.0-20180226215254-237a9547c8a5 // indirect diff --git a/go.sum b/go.sum index b55eff19b1..9bd65ec4de 100644 --- a/go.sum +++ b/go.sum @@ -228,6 +228,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/puzpuzpuz/xsync v1.5.2 h1:yRAP4wqSOZG+/4pxJ08fPTwrfL0IzE/LKQ/cw509qGY= +github.com/puzpuzpuz/xsync v1.5.2/go.mod h1:K98BYhX3k1dQ2M63t1YNVDanbwUPmBCAhNmVrrxfiGg= github.com/qri-io/jsonpointer v0.0.0-20180309164927-168dd9e45cf2 h1:C8RRfIlExwwrXw28G8LkrpWiHUVT4uLowfvjUYJ2Iec= github.com/qri-io/jsonpointer v0.0.0-20180309164927-168dd9e45cf2/go.mod h1:DnJPaYgiKu56EuDp8TU5wFLdZIcAnb/uH9v37ZaMV64= github.com/qri-io/jsonschema v0.0.0-20180607150648-d0d3b10ec792 h1:vwTGeGWCew89DI4ZwKCaobGAN7ExvZiBzgn4LZHMVOc= diff --git a/secrets/provider.go b/secrets/provider.go new file mode 100644 index 0000000000..a161ff28c1 --- /dev/null +++ b/secrets/provider.go @@ -0,0 +1,37 @@ +package secrets + +import ( + "encoding/json" + "fmt" +) + +type providerCandidate struct { + Type string `json:"type"` + ID string `json:"id"` + Config json.RawMessage `json:"config"` +} + +func (r providerCandidate) Initialize() (Provider, error) { + switch r.Type { + case "aws-ssm": + var conf AWSSSMProviderConfig + err := json.Unmarshal(r.Config, &conf) + if err != nil { + return nil, fmt.Errorf("unmarshalling config for aws-ssm provider %s: %v", r.ID, err) + } + + ssm, err := NewAWSSSMProvider(r.ID, conf) + if err != nil { + return nil, fmt.Errorf("creating aws-ssm provider %s: %w", r.ID, err) + } + + return ssm, nil + default: + return nil, fmt.Errorf("invalid provider type %s for provider %s", r.Type, r.ID) + } +} + +// A Provider is a source of secrets, and must provide a way to fetch a secret given some key. Providers must be goroutine-safe. +type Provider interface { + Fetch(key string) (string, error) +} diff --git a/secrets/provider_aws_ssm.go b/secrets/provider_aws_ssm.go new file mode 100644 index 0000000000..c327882f6e --- /dev/null +++ b/secrets/provider_aws_ssm.go @@ -0,0 +1,75 @@ +package secrets + +import ( + "fmt" + "os" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/credentials/stscreds" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ssm" + "github.com/aws/aws-sdk-go/service/sts" +) + +type AWSSSMProviderConfig struct { + ID string `json:"id"` + RoleARN string `json:"role_arn"` + AssumeViaOIDC bool `json:"assume_via_oidc"` +} + +type AWSSSMProvider struct { + id string + ssmI *ssm.SSM +} + +func NewAWSSSMProvider(id string, config AWSSSMProviderConfig) (*AWSSSMProvider, error) { + sess, err := session.NewSession() + if err != nil { + return nil, fmt.Errorf("initialising AWS session: %w", err) + } + + return &AWSSSMProvider{ + id: id, + ssmI: generateSSMInstance(sess, config), + }, nil +} + +func (s *AWSSSMProvider) ID() string { + return s.id +} + +func (s *AWSSSMProvider) Type() string { + return "aws-ssm" +} + +func (s *AWSSSMProvider) Fetch(key string) (string, error) { + out, err := s.ssmI.GetParameter(&ssm.GetParameterInput{ + Name: aws.String(key), + WithDecryption: aws.Bool(true), + }) + + if err != nil { + return "", fmt.Errorf("retrieving secret %s from SSM Parameter Store: %w", key, err) + } + + return *out.Parameter.Value, nil +} + +func generateSSMInstance(sess *session.Session, config AWSSSMProviderConfig) *ssm.SSM { + if config.RoleARN == "" { + return ssm.New(sess) + } + + if config.AssumeViaOIDC { + stsClient := sts.New(sess) + sessionName := fmt.Sprintf("buildkite-agent-aws-ssm-secrets-provider-%s", os.Getenv("BUILDKITE_JOB_ID")) + // TODO: Use BK OIDC provider instead of some rando file + roleProvider := stscreds.NewWebIdentityRoleProviderWithOptions(stsClient, config.RoleARN, sessionName, stscreds.FetchTokenPath("/build/token")) + creds := credentials.NewCredentials(roleProvider) + return ssm.New(sess, &aws.Config{Credentials: creds}) + } + + creds := stscreds.NewCredentials(sess, config.RoleARN) + return ssm.New(sess, &aws.Config{Credentials: creds}) +} diff --git a/secrets/provider_registry.go b/secrets/provider_registry.go new file mode 100644 index 0000000000..3b712f830f --- /dev/null +++ b/secrets/provider_registry.go @@ -0,0 +1,131 @@ +package secrets + +import ( + "encoding/json" + "fmt" + "sync" + + "github.com/buildkite/agent/v3/bootstrap/shell" + "github.com/puzpuzpuz/xsync" +) + +type ProviderRegistry struct { + config ProviderRegistryConfig + // We have the two maps here because we only want to initialise a provider if a secret using that provider is used + // We shouldn't boot up any secrets providers that won't get used + candidates *xsync.MapOf[string, providerCandidate] // Candidates technically doesn't need to be a sync map as it's never altered after initialization, but i've made it one just for symmetry + providers *xsync.MapOf[string, Provider] +} + +type ProviderRegistryConfig struct { + Shell *shell.Shell +} + +// NewProviderRegistryFromJSON takes a JSON string representing a slice of secrets.RawProvider, and returns a ProviderRegistry, +// ready to be used to fetch secrets. +func NewProviderRegistryFromJSON(config ProviderRegistryConfig, jsonIn string) (*ProviderRegistry, error) { + var rawProviders []providerCandidate + err := json.Unmarshal([]byte(jsonIn), &rawProviders) + if err != nil { + return nil, fmt.Errorf("unmarshalling secret providers: %w", err) + } + + candidates := xsync.NewMapOf[providerCandidate]() + for _, provider := range rawProviders { + if _, ok := candidates.Load(provider.ID); ok { + return nil, fmt.Errorf("duplicate provider ID: %s. Provider IDs must be unique", provider.ID) + } + + candidates.Store(provider.ID, provider) + } + + return &ProviderRegistry{ + config: config, + candidates: candidates, + providers: xsync.NewMapOf[Provider](), + }, nil +} + +func (pr *ProviderRegistry) FetchAll(configs []SecretConfig) ([]Secret, []error) { + secrets := make([]Secret, 0, len(configs)) + secretsCh := make(chan Secret) + + errors := make([]error, 0, len(configs)) + errorsCh := make(chan error) + + var wg sync.WaitGroup + for _, c := range configs { + wg.Add(1) + go func(config SecretConfig) { + defer wg.Done() + + secret, err := pr.Fetch(config) + if err != nil { + + errorsCh <- err + return + } + secretsCh <- secret + }(c) + } + + go func() { + for err := range errorsCh { + errors = append(errors, err) + } + }() + + go func() { + for secret := range secretsCh { + secrets = append(secrets, secret) + } + }() + + wg.Wait() + close(secretsCh) + close(errorsCh) + + return secrets, errors +} + +// Fetch takes a SecretConfig, and attempts to fetch it from the provider specified in the config. +// This method is goroutine-safe. +func (pr *ProviderRegistry) Fetch(config SecretConfig) (Secret, error) { + if provider, ok := pr.providers.Load(config.ProviderID); ok { // We've used this provider before, it's already been initialized + value, err := provider.Fetch(config.Key) + if err != nil { + return nil, fmt.Errorf("fetching secret %s from provider %s: %w", config.Key, config.ProviderID, err) + } + + pr.config.Shell.Commentf("Secret %s fetched from provider %s", config.Key, config.ProviderID) + secret, err := newSecret(config, pr.config.Shell.Env, value) + if err != nil { + return nil, fmt.Errorf("creating secret %s from provider %s: %w", config.Key, config.ProviderID, err) + } + return secret, nil + } + + if candidate, ok := pr.candidates.Load(config.ProviderID); ok { // We haven't used this provider yet, so we need to initialize it + provider, err := candidate.Initialize() + if err != nil { + return nil, fmt.Errorf("initializing provider %s (type: %s) to fetch secret %s: %w", config.ProviderID, candidate.Type, config.Key, err) + } + + pr.providers.Store(config.ProviderID, provider) // Store the initialised provider + + value, err := provider.Fetch(config.Key) // Now fetch the actual secret. + if err != nil { + return nil, fmt.Errorf("fetching secret %s from provider %s: %w", config.Key, config.ProviderID, err) + } + + pr.config.Shell.Commentf("Secret %s fetched from provider %s", config.Key, config.ProviderID) + secret, err := newSecret(config, pr.config.Shell.Env, value) + if err != nil { + return nil, fmt.Errorf("creating secret %s from provider %s: %w", config.Key, config.ProviderID, err) + } + return secret, nil + } + + // If we've got to this point, the user has tried to use a provider ID that's not in the registry, so we can't give them their secret + return nil, fmt.Errorf("no secret provider with ID: %s", config.ProviderID) +} diff --git a/secrets/secret.go b/secrets/secret.go new file mode 100644 index 0000000000..c206613420 --- /dev/null +++ b/secrets/secret.go @@ -0,0 +1,69 @@ +package secrets + +import ( + "fmt" + "os" + + "github.com/buildkite/agent/v3/env" +) + +type Secret interface { + Store() error + Cleanup() func() +} + +func newSecret(config SecretConfig, environment env.Environment, value string) (Secret, error) { + switch config.Type() { + case "env": + return newEnvSecret(config, environment, value), nil + case "file": + return newFileSecret(config, value), nil + default: + return nil, fmt.Errorf("invalid secret type %s", config.Type()) + } +} + +type EnvSecret struct { + EnvVar string + Value string + Env env.Environment +} + +func newEnvSecret(config SecretConfig, environment env.Environment, value string) Secret { + return EnvSecret{ + EnvVar: config.EnvVar, + Value: value, + Env: environment, + } +} + +func (s EnvSecret) Store() error { + s.Env.Set(s.EnvVar, s.Value) + return nil +} + +func (s EnvSecret) Cleanup() func() { + return func() {} +} + +type FileSecret struct { + FilePath string + Value string +} + +func newFileSecret(config SecretConfig, value string) Secret { + return FileSecret{ + FilePath: config.File, + Value: value, + } +} + +func (s FileSecret) Store() error { + return os.WriteFile(s.FilePath, []byte(s.Value), 0777) +} + +func (s FileSecret) Cleanup() func() { + return func() { + os.Remove(s.FilePath) + } +} diff --git a/secrets/secret_config.go b/secrets/secret_config.go new file mode 100644 index 0000000000..2613db7ff1 --- /dev/null +++ b/secrets/secret_config.go @@ -0,0 +1,38 @@ +package secrets + +import "fmt" + +type SecretConfig struct { + Key string `json:"key"` + ProviderID string `json:"provider_id"` + EnvVar string `json:"env_var"` + File string `json:"file"` +} + +func (s SecretConfig) Validate() error { + if s.Key == "" { + return fmt.Errorf("secret: %s (provider: %s): key is required", s.Key, s.ProviderID) + } + + if s.ProviderID == "" { + return fmt.Errorf("secret: %s (provider: %s): provider_id is required", s.Key, s.ProviderID) + } + + if s.EnvVar == "" && s.File == "" { + return fmt.Errorf("secret: %s (provider: %s): must have either env_var or file set", s.Key, s.ProviderID) + } + + if s.EnvVar != "" && s.File != "" { + return fmt.Errorf("secret: %s (provider: %s): must only have one of env_var or file set", s.Key, s.ProviderID) + } + + return nil +} + +func (s SecretConfig) Type() string { + if s.EnvVar != "" { + return "env" + } + + return "file" +} From 177b438155c851f7664d27d83d0b8e4ef91be3b3 Mon Sep 17 00:00:00 2001 From: Ben Moskovitz Date: Tue, 29 Nov 2022 10:23:32 +1300 Subject: [PATCH 3/3] Fix race condition causing some secrets to never appear --- bootstrap/bootstrap.go | 17 +++++++++++++---- secrets/provider_registry.go | 34 ++++++++++++---------------------- secrets/secret.go | 12 +++++------- 3 files changed, 30 insertions(+), 33 deletions(-) diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 1c13e5d234..b476103e12 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -114,6 +114,8 @@ func (b *Bootstrap) Run(ctx context.Context) (exitCode int) { return shell.GetExitCode(err) } + b.shell.Headerf("🤫 Fetching Build Secrets") + // Just pretend that these two jsons live in the pipeline.yml, and get passed through as env vars by the backend secretProviderRegistryJSON := `[ { @@ -159,7 +161,9 @@ func (b *Bootstrap) Run(ctx context.Context) (exitCode int) { validationErrs := make([]error, 0, len(secretConfigs)) for _, c := range secretConfigs { err := c.Validate() - validationErrs = append(validationErrs, err) + if err != nil { + validationErrs = append(validationErrs, err) + } } for _, err := range validationErrs { @@ -170,7 +174,7 @@ func (b *Bootstrap) Run(ctx context.Context) (exitCode int) { return 1 } - secrets, errors := secretProviderRegistry.FetchAll(secretConfigs) + fetchedSecrets, errors := secretProviderRegistry.FetchAll(secretConfigs) if len(errors) > 0 { b.shell.Errorf("Errors fetching secrets:") for _, err := range errors { @@ -179,14 +183,19 @@ func (b *Bootstrap) Run(ctx context.Context) (exitCode int) { return 1 } - for _, secret := range secrets { + for _, secret := range fetchedSecrets { // TODO: Automatically add env secrets to the redactor err := secret.Store() if err != nil { b.shell.Errorf("Error storing secret: %v", err) } - defer secret.Cleanup() + defer func(secret secrets.Secret) { + err := secret.Cleanup() + if err != nil { + b.shell.Warningf("Error cleaning up secret: %s", err) + } + }(secret) } var includePhase = func(phase string) bool { diff --git a/secrets/provider_registry.go b/secrets/provider_registry.go index 3b712f830f..43658d2156 100644 --- a/secrets/provider_registry.go +++ b/secrets/provider_registry.go @@ -48,42 +48,32 @@ func NewProviderRegistryFromJSON(config ProviderRegistryConfig, jsonIn string) ( func (pr *ProviderRegistry) FetchAll(configs []SecretConfig) ([]Secret, []error) { secrets := make([]Secret, 0, len(configs)) - secretsCh := make(chan Secret) - errors := make([]error, 0, len(configs)) - errorsCh := make(chan error) - var wg sync.WaitGroup + var ( + wg sync.WaitGroup + mtx sync.Mutex + ) + + wg.Add(len(configs)) for _, c := range configs { - wg.Add(1) go func(config SecretConfig) { defer wg.Done() secret, err := pr.Fetch(config) - if err != nil { + mtx.Lock() + defer mtx.Unlock() - errorsCh <- err + if err != nil { + errors = append(errors, err) return } - secretsCh <- secret - }(c) - } - go func() { - for err := range errorsCh { - errors = append(errors, err) - } - }() - - go func() { - for secret := range secretsCh { secrets = append(secrets, secret) - } - }() + }(c) + } wg.Wait() - close(secretsCh) - close(errorsCh) return secrets, errors } diff --git a/secrets/secret.go b/secrets/secret.go index c206613420..17bb1b4540 100644 --- a/secrets/secret.go +++ b/secrets/secret.go @@ -9,7 +9,7 @@ import ( type Secret interface { Store() error - Cleanup() func() + Cleanup() error } func newSecret(config SecretConfig, environment env.Environment, value string) (Secret, error) { @@ -42,8 +42,8 @@ func (s EnvSecret) Store() error { return nil } -func (s EnvSecret) Cleanup() func() { - return func() {} +func (s EnvSecret) Cleanup() error { + return nil } type FileSecret struct { @@ -62,8 +62,6 @@ func (s FileSecret) Store() error { return os.WriteFile(s.FilePath, []byte(s.Value), 0777) } -func (s FileSecret) Cleanup() func() { - return func() { - os.Remove(s.FilePath) - } +func (s FileSecret) Cleanup() error { + return os.Remove(s.FilePath) }