Skip to content

Commit 55bc65c

Browse files
committed
internal/secret: add flag support
To avoid hardcoding secret names everywhere, we want to pass them via flags instead. As a convenience, introduce a new flag type that resolves values of the form "secret:[project name/]<secret name>" using Secret Manager. This is a bit janky in the name of convenience: we need a SM client before calling flag.Parse, which I decided should be initialized by the user rather than implicitly. Typical usage will look like: var token = secret.Flag("token", "token used to do the thing") func main() { if err := secret.InitFlagSupport(context.Background()); err != nil { log.Fatal(err) } flag.Parse() fmt.Printf("My token is %v\n", *token) } Supporting literal values might be unnecessary but I think it might be helpful for local testing, and we can extend it with a file: prefix to read from local files too. For golang/go#51122. Change-Id: Ie6102453c2242baf2e91b873e62e035f72a82584 Reviewed-on: https://go-review.googlesource.com/c/build/+/385185 Trust: Heschi Kreinick <[email protected]> Run-TryBot: Heschi Kreinick <[email protected]> Reviewed-by: Dmitri Shuralyov <[email protected]> Auto-Submit: Heschi Kreinick <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent 8613bfa commit 55bc65c

File tree

2 files changed

+128
-0
lines changed

2 files changed

+128
-0
lines changed

internal/secret/flag.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package secret
2+
3+
import (
4+
"context"
5+
"flag"
6+
"fmt"
7+
"strings"
8+
9+
"cloud.google.com/go/compute/metadata"
10+
secretmanager "cloud.google.com/go/secretmanager/apiv1"
11+
secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1"
12+
)
13+
14+
// FlagResolver contains the dependencies necessary to resolve a Secret flag.
15+
type FlagResolver struct {
16+
Context context.Context
17+
Client secretClient
18+
DefaultProjectID string
19+
}
20+
21+
// Flag declares a string flag on set that will be resolved using r.
22+
func (r *FlagResolver) Flag(set *flag.FlagSet, name string, usage string) *string {
23+
var value string
24+
suffixedUsage := usage + " [ specify `secret:[project name/]<secret name>` to read from Secret Manager ]"
25+
set.Func(name, suffixedUsage, func(flagValue string) error {
26+
if r.Client == nil || r.Context == nil {
27+
return fmt.Errorf("secret resolver was not initialized")
28+
}
29+
if !strings.HasPrefix(flagValue, "secret:") {
30+
value = flagValue
31+
return nil
32+
}
33+
34+
secretName := strings.TrimPrefix(flagValue, "secret:")
35+
projectID := r.DefaultProjectID
36+
if parts := strings.SplitN(secretName, "/", 2); len(parts) == 2 {
37+
projectID, secretName = parts[0], parts[1]
38+
}
39+
if projectID == "" {
40+
return fmt.Errorf("missing project ID: none specified in %q, and no default set (not on GCP?)", secretName)
41+
}
42+
r, err := r.Client.AccessSecretVersion(r.Context, &secretmanagerpb.AccessSecretVersionRequest{
43+
Name: buildNamePath(projectID, secretName, "latest"),
44+
})
45+
if err != nil {
46+
return fmt.Errorf("reading secret %q from project %v failed: %v", secretName, projectID, err)
47+
}
48+
value = string(r.Payload.GetData())
49+
return nil
50+
})
51+
return &value
52+
}
53+
54+
// DefaultResolver is the FlagResolver used by the convenience functions.
55+
var DefaultResolver FlagResolver
56+
57+
// Flag declares a string flag on flag.CommandLine that supports Secret Manager
58+
// resolution for values like "secret:<secret name>". InitFlagSupport must be
59+
// called before flag.Parse.
60+
func Flag(name string, usage string) *string {
61+
return DefaultResolver.Flag(flag.CommandLine, name, usage)
62+
}
63+
64+
// InitFlagSupport initializes the dependencies for flags declared with Flag.
65+
func InitFlagSupport(ctx context.Context) error {
66+
client, err := secretmanager.NewClient(ctx)
67+
if err != nil {
68+
return err
69+
}
70+
DefaultResolver = FlagResolver{
71+
Context: ctx,
72+
Client: client,
73+
}
74+
if metadata.OnGCE() {
75+
projectID, err := metadata.ProjectID()
76+
if err != nil {
77+
return err
78+
}
79+
DefaultResolver.DefaultProjectID = projectID
80+
}
81+
82+
return nil
83+
}

internal/secret/gcp_secret_manager_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package secret
66

77
import (
88
"context"
9+
"flag"
910
"fmt"
1011
"testing"
1112

@@ -138,3 +139,47 @@ func TestBuildNamePath(t *testing.T) {
138139
t.Errorf("BuildVersionNumber(%s, %s, %s) = %q; want=%q", "x", "y", "z", got, want)
139140
}
140141
}
142+
143+
func TestFlag(t *testing.T) {
144+
r := &FlagResolver{
145+
Context: context.Background(),
146+
Client: &fakeSecretClient{
147+
accessSecretMap: map[string]string{
148+
buildNamePath("project1", "secret1", "latest"): "supersecret",
149+
buildNamePath("project2", "secret2", "latest"): "tippytopsecret",
150+
},
151+
},
152+
DefaultProjectID: "project1",
153+
}
154+
155+
tests := []struct {
156+
flagVal, wantVal string
157+
wantErr bool
158+
}{
159+
{"hey", "hey", false},
160+
{"secret:secret1", "supersecret", false},
161+
{"secret:project2/secret2", "tippytopsecret", false},
162+
{"secret:foo", "", true},
163+
}
164+
165+
for _, tt := range tests {
166+
t.Run(tt.flagVal, func(t *testing.T) {
167+
fs := flag.NewFlagSet("", flag.ContinueOnError)
168+
fs.Usage = func() {} // Minimize console spam; can't prevent it entirely.
169+
flagVal := r.Flag(fs, "testflag", "usage")
170+
err := fs.Parse([]string{"--testflag", tt.flagVal})
171+
if tt.wantErr {
172+
if err == nil {
173+
t.Fatalf("flag parsing succeeded, should have failed")
174+
}
175+
return
176+
}
177+
if err != nil {
178+
t.Fatalf("flag parsing failed: %v", err)
179+
}
180+
if *flagVal != tt.wantVal {
181+
t.Errorf("flag value = %q, want %q", *flagVal, "hey")
182+
}
183+
})
184+
}
185+
}

0 commit comments

Comments
 (0)