1+ // Package secrets provides integration with Google Secret Manager for fetching configuration.
2+ package secrets
3+
4+ import (
5+ "context"
6+ "fmt"
7+ "log"
8+ "os"
9+ "time"
10+
11+ secretmanager "cloud.google.com/go/secretmanager/apiv1"
12+ "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
13+ "google.golang.org/api/option"
14+ )
15+
16+ const (
17+ // secretManagerTimeout prevents indefinite hangs when accessing secrets.
18+ secretManagerTimeout = 10 * time .Second
19+ )
20+
21+ // Manager handles fetching secrets from Google Secret Manager.
22+ type Manager struct {
23+ client * secretmanager.Client
24+ projectID string
25+ }
26+
27+ // New creates a new secrets manager with optional credentials.
28+ // If credentialsPath is empty, it uses Application Default Credentials.
29+ func New (ctx context.Context , projectID , credentialsPath string ) (* Manager , error ) {
30+ var opts []option.ClientOption
31+ if credentialsPath != "" {
32+ opts = append (opts , option .WithCredentialsFile (credentialsPath ))
33+ }
34+
35+ client , err := secretmanager .NewClient (ctx , opts ... )
36+ if err != nil {
37+ return nil , fmt .Errorf ("failed to create secret manager client: %w" , err )
38+ }
39+
40+ return & Manager {
41+ client : client ,
42+ projectID : projectID ,
43+ }, nil
44+ }
45+
46+ // GetWithEnvOverride fetches a secret value from Google Secret Manager,
47+ // but returns the environment variable value if it exists (env vars take precedence).
48+ // The secretName should be the same as the environment variable name (e.g., "GITHUB_WEBHOOK_SECRET").
49+ func (m * Manager ) GetWithEnvOverride (ctx context.Context , envVar , secretName string ) (string , error ) {
50+ // Check environment variable first (takes precedence)
51+ if value := os .Getenv (envVar ); value != "" {
52+ log .Printf ("using environment variable instead of secret: env_var=%s source=environment" , envVar )
53+ return value , nil
54+ }
55+
56+ // Build the resource name for Secret Manager
57+ resourceName := fmt .Sprintf ("projects/%s/secrets/%s/versions/latest" , m .projectID , secretName )
58+
59+ log .Printf ("attempting to access secret from Secret Manager: env_var=%s secret_name=%s project_id=%s" , envVar , secretName , m .projectID )
60+
61+ // Create a context with timeout to prevent indefinite hangs
62+ timeoutCtx , cancel := context .WithTimeout (ctx , secretManagerTimeout )
63+ defer cancel ()
64+
65+ // Access the secret version
66+ req := & secretmanagerpb.AccessSecretVersionRequest {
67+ Name : resourceName ,
68+ }
69+
70+ result , err := m .client .AccessSecretVersion (timeoutCtx , req )
71+ if err != nil {
72+ log .Printf ("failed to access secret from Secret Manager: env_var=%s secret_name=%s error=%v" , envVar , secretName , err )
73+ return "" , fmt .Errorf ("failed to access secret %s: %w" , resourceName , err )
74+ }
75+
76+ secretValue := string (result .GetPayload ().GetData ())
77+ log .Printf ("successfully fetched secret from Google Secret Manager: env_var=%s secret_name=%s has_value=%v" , envVar , secretName , secretValue != "" )
78+ return secretValue , nil
79+ }
80+
81+ // Close closes the Secret Manager client connection.
82+ func (m * Manager ) Close () error {
83+ if m .client != nil {
84+ return m .client .Close ()
85+ }
86+ return nil
87+ }
0 commit comments