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 <heschi@google.com>
Run-TryBot: Heschi Kreinick <heschi@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Auto-Submit: Heschi Kreinick <heschi@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/internal/secret/flag.go b/internal/secret/flag.go
new file mode 100644
index 0000000..6a860ca
--- /dev/null
+++ b/internal/secret/flag.go
@@ -0,0 +1,83 @@
+package secret
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"strings"
+
+	"cloud.google.com/go/compute/metadata"
+	secretmanager "cloud.google.com/go/secretmanager/apiv1"
+	secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1"
+)
+
+// FlagResolver contains the dependencies necessary to resolve a Secret flag.
+type FlagResolver struct {
+	Context          context.Context
+	Client           secretClient
+	DefaultProjectID string
+}
+
+// Flag declares a string flag on set that will be resolved using r.
+func (r *FlagResolver) Flag(set *flag.FlagSet, name string, usage string) *string {
+	var value string
+	suffixedUsage := usage + " [ specify `secret:[project name/]<secret name>` to read from Secret Manager ]"
+	set.Func(name, suffixedUsage, func(flagValue string) error {
+		if r.Client == nil || r.Context == nil {
+			return fmt.Errorf("secret resolver was not initialized")
+		}
+		if !strings.HasPrefix(flagValue, "secret:") {
+			value = flagValue
+			return nil
+		}
+
+		secretName := strings.TrimPrefix(flagValue, "secret:")
+		projectID := r.DefaultProjectID
+		if parts := strings.SplitN(secretName, "/", 2); len(parts) == 2 {
+			projectID, secretName = parts[0], parts[1]
+		}
+		if projectID == "" {
+			return fmt.Errorf("missing project ID: none specified in %q, and no default set (not on GCP?)", secretName)
+		}
+		r, err := r.Client.AccessSecretVersion(r.Context, &secretmanagerpb.AccessSecretVersionRequest{
+			Name: buildNamePath(projectID, secretName, "latest"),
+		})
+		if err != nil {
+			return fmt.Errorf("reading secret %q from project %v failed: %v", secretName, projectID, err)
+		}
+		value = string(r.Payload.GetData())
+		return nil
+	})
+	return &value
+}
+
+// DefaultResolver is the FlagResolver used by the convenience functions.
+var DefaultResolver FlagResolver
+
+// Flag declares a string flag on flag.CommandLine that supports Secret Manager
+// resolution for values like "secret:<secret name>". InitFlagSupport must be
+// called before flag.Parse.
+func Flag(name string, usage string) *string {
+	return DefaultResolver.Flag(flag.CommandLine, name, usage)
+}
+
+// InitFlagSupport initializes the dependencies for flags declared with Flag.
+func InitFlagSupport(ctx context.Context) error {
+	client, err := secretmanager.NewClient(ctx)
+	if err != nil {
+		return err
+	}
+	DefaultResolver = FlagResolver{
+		Context: ctx,
+		Client:  client,
+	}
+	if metadata.OnGCE() {
+		projectID, err := metadata.ProjectID()
+		if err != nil {
+			return err
+		}
+		DefaultResolver.DefaultProjectID = projectID
+	}
+
+	return nil
+}
diff --git a/internal/secret/gcp_secret_manager_test.go b/internal/secret/gcp_secret_manager_test.go
index 9759218..b4671c2 100644
--- a/internal/secret/gcp_secret_manager_test.go
+++ b/internal/secret/gcp_secret_manager_test.go
@@ -6,6 +6,7 @@
 
 import (
 	"context"
+	"flag"
 	"fmt"
 	"testing"
 
@@ -138,3 +139,47 @@
 		t.Errorf("BuildVersionNumber(%s, %s, %s) = %q; want=%q", "x", "y", "z", got, want)
 	}
 }
+
+func TestFlag(t *testing.T) {
+	r := &FlagResolver{
+		Context: context.Background(),
+		Client: &fakeSecretClient{
+			accessSecretMap: map[string]string{
+				buildNamePath("project1", "secret1", "latest"): "supersecret",
+				buildNamePath("project2", "secret2", "latest"): "tippytopsecret",
+			},
+		},
+		DefaultProjectID: "project1",
+	}
+
+	tests := []struct {
+		flagVal, wantVal string
+		wantErr          bool
+	}{
+		{"hey", "hey", false},
+		{"secret:secret1", "supersecret", false},
+		{"secret:project2/secret2", "tippytopsecret", false},
+		{"secret:foo", "", true},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.flagVal, func(t *testing.T) {
+			fs := flag.NewFlagSet("", flag.ContinueOnError)
+			fs.Usage = func() {} // Minimize console spam; can't prevent it entirely.
+			flagVal := r.Flag(fs, "testflag", "usage")
+			err := fs.Parse([]string{"--testflag", tt.flagVal})
+			if tt.wantErr {
+				if err == nil {
+					t.Fatalf("flag parsing succeeded, should have failed")
+				}
+				return
+			}
+			if err != nil {
+				t.Fatalf("flag parsing failed: %v", err)
+			}
+			if *flagVal != tt.wantVal {
+				t.Errorf("flag value = %q, want %q", *flagVal, "hey")
+			}
+		})
+	}
+}