Skip to content

Commit c458e49

Browse files
committed
access secrets in GCP concurrently
1 parent 32e483b commit c458e49

File tree

7 files changed

+81
-33
lines changed

7 files changed

+81
-33
lines changed

Diff for: Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# syntax = docker/dockerfile:experimental
22

3-
FROM --platform=${BUILDPLATFORM} golang:1.21-alpine as builder
3+
FROM --platform=${BUILDPLATFORM} golang:1.22-alpine as builder
44
# passed by buildkit
55
ARG TARGETOS
66
ARG TARGETARCH

Diff for: Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ platfrom-build: clean lint test ; $(info $(M) building binaries for multiple os/
5252
setup-tools: setup-lint setup-gocov setup-gocov-xml setup-go2xunit setup-mockery setup-ghr
5353

5454
setup-lint:
55-
$(GO) install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.54.2
55+
$(GO) install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.59.1
5656
setup-gocov:
5757
$(GO) install github.com/axw/gocov/...
5858
setup-gocov-xml:

Diff for: go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module secrets-init
22

3-
go 1.21
3+
go 1.22
44

55
require (
66
cloud.google.com/go/compute v1.10.0

Diff for: go.sum

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
22
cloud.google.com/go v0.104.0 h1:gSmWO7DY1vOm0MVU6DNXM11BWHHsTUmsC5cv1fuW5X8=
3+
cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA=
34
cloud.google.com/go/compute v1.10.0 h1:aoLIYaA1fX3ywihqpBk2APQKOo20nXsp1GEZQbx5Jk4=
45
cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU=
56
cloud.google.com/go/iam v0.5.0 h1:fz9X5zyTWBmamZsqvqZqD7khbifcZF/q+Z1J8pfhIUg=

Diff for: main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func main() {
8080
Action: mainCmd,
8181
Version: Version,
8282
}
83-
cli.VersionPrinter = func(c *cli.Context) {
83+
cli.VersionPrinter = func(_ *cli.Context) {
8484
fmt.Printf("version: %s\n", Version)
8585
fmt.Printf(" build date: %s\n", BuildDate)
8686
fmt.Printf(" commit: %s\n", GitCommit)

Diff for: pkg/secrets/google/secrets.go

+74-27
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"regexp"
77
"strings"
8+
"sync"
89

910
"secrets-init/pkg/secrets" //nolint:gci
1011

@@ -15,6 +16,13 @@ import (
1516
secretspb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1" //nolint:gci
1617
)
1718

19+
var fullSecretRe = regexp.MustCompile(`projects/[^/]+/secrets/[^/+](/version/[^/+])?`)
20+
21+
type result struct {
22+
Env string
23+
Err error
24+
}
25+
1826
// SecretsProvider Google Cloud secrets provider
1927
type SecretsProvider struct {
2028
sm SecretsManagerAPI
@@ -53,40 +61,79 @@ func NewGoogleSecretsProvider(ctx context.Context, projectID string) (secrets.Pr
5361
func (sp SecretsProvider) ResolveSecrets(ctx context.Context, vars []string) ([]string, error) {
5462
envs := make([]string, 0, len(vars))
5563

56-
fullSecretRe := regexp.MustCompile("projects/[^/]+/secrets/[^/+](/version/[^/+])?")
64+
// Create a channel to collect the results
65+
results := make(chan result, len(vars))
5766

67+
// Start a goroutine for each secret
68+
var wg sync.WaitGroup
5869
for _, env := range vars {
59-
kv := strings.Split(env, "=")
60-
key, value := kv[0], kv[1]
61-
if strings.HasPrefix(value, "gcp:secretmanager:") {
62-
// construct valid secret name
63-
name := strings.TrimPrefix(value, "gcp:secretmanager:")
64-
65-
isLong := fullSecretRe.MatchString(name)
66-
67-
if !isLong {
68-
if sp.projectID == "" {
69-
return vars, errors.Errorf("failed to get secret \"%s\" from Google Secret Manager (unknown project)", name)
70+
wg.Add(1)
71+
go func(env string) {
72+
defer wg.Done()
73+
select {
74+
case <-ctx.Done():
75+
results <- result{Err: ctx.Err()}
76+
return
77+
default:
78+
val, err := sp.processEnvironmentVariable(ctx, env)
79+
if err != nil {
80+
results <- result{Err: err}
81+
return
7082
}
71-
name = fmt.Sprintf("projects/%s/secrets/%s", sp.projectID, name)
83+
results <- result{Env: val}
7284
}
85+
}(env)
86+
}
7387

74-
// if no version specified add latest
75-
if !strings.Contains(name, "/versions/") {
76-
name += "/versions/latest"
77-
}
78-
// get secret value
79-
req := &secretspb.AccessSecretVersionRequest{
80-
Name: name,
81-
}
82-
secret, err := sp.sm.AccessSecretVersion(ctx, req)
83-
if err != nil {
84-
return vars, errors.Wrap(err, "failed to get secret from Google Secret Manager")
85-
}
86-
env = key + "=" + string(secret.Payload.GetData())
88+
// Start another goroutine to close the results channel when all fetch goroutines are done
89+
go func() {
90+
wg.Wait()
91+
close(results)
92+
}()
93+
94+
// Collect the results
95+
for res := range results {
96+
if res.Err != nil {
97+
return vars, res.Err
8798
}
88-
envs = append(envs, env)
99+
envs = append(envs, res.Env)
89100
}
90101

91102
return envs, nil
92103
}
104+
105+
// processEnvironmentVariable processes the environment variable and replaces the value with the secret value
106+
func (sp SecretsProvider) processEnvironmentVariable(ctx context.Context, env string) (string, error) {
107+
kv := strings.Split(env, "=")
108+
key, value := kv[0], kv[1]
109+
if !strings.HasPrefix(value, "gcp:secretmanager:") {
110+
return env, nil
111+
}
112+
113+
// construct valid secret name
114+
name := strings.TrimPrefix(value, "gcp:secretmanager:")
115+
116+
isLong := fullSecretRe.MatchString(name)
117+
118+
if !isLong {
119+
if sp.projectID == "" {
120+
return "", errors.Errorf("failed to get secret \"%s\" from Google Secret Manager (unknown project)", name)
121+
}
122+
name = fmt.Sprintf("projects/%s/secrets/%s", sp.projectID, name)
123+
}
124+
125+
// if no version specified add latest
126+
if !strings.Contains(name, "/versions/") {
127+
name += "/versions/latest"
128+
}
129+
130+
// get secret value
131+
req := &secretspb.AccessSecretVersionRequest{
132+
Name: name,
133+
}
134+
secret, err := sp.sm.AccessSecretVersion(ctx, req)
135+
if err != nil {
136+
return "", fmt.Errorf("failed to get secret from Google Secret Manager: %w", err)
137+
}
138+
return key + "=" + string(secret.Payload.GetData()), nil
139+
}

Diff for: pkg/secrets/google/secrets_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ package google
44
import (
55
"context"
66
"errors"
7-
"reflect"
87
"testing"
98

109
"secrets-init/mocks"
1110
"secrets-init/pkg/secrets"
1211

12+
"github.com/stretchr/testify/assert"
1313
secretspb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1"
1414
)
1515

@@ -205,7 +205,7 @@ func TestSecretsProvider_ResolveSecrets(t *testing.T) {
205205
t.Errorf("SecretsProvider.ResolveSecrets() error = %v, wantErr %v", err, tt.wantErr)
206206
return
207207
}
208-
if !reflect.DeepEqual(got, tt.want) {
208+
if !assert.ElementsMatch(t, got, tt.want) {
209209
t.Errorf("SecretsProvider.ResolveSecrets() = %v, want %v", got, tt.want)
210210
}
211211
mockSM.AssertExpectations(t)

0 commit comments

Comments
 (0)