From b25a4edc05d5027422455fa3680355cb17b762ba Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 16:06:14 +0000 Subject: [PATCH] refactor: Use Google Secret Manager for secrets Removes the dependency on `secrets.yaml` and implements a new secret management strategy. - If `GCLOUD_PROJECT_ID` is set, secrets are fetched exclusively from Google Secret Manager. - If `GCLOUD_PROJECT_ID` is not set, secrets are fetched from environment variables for local development. This change also fixes the Firestore client initialization error by ensuring the `GCLOUD_PROJECT_ID` is correctly propagated to the datastore client. The `BotPlatform` dependency's `SecretsData` struct is now correctly populated to ensure compatibility. --- main.go | 12 +++--- pkg/bot/telegram.go | 13 +++++-- pkg/secrets/secrets.go | 86 +++++++++++++++++++++++++++++++----------- 3 files changed, 78 insertions(+), 33 deletions(-) diff --git a/main.go b/main.go index b032ea3..cfc537c 100644 --- a/main.go +++ b/main.go @@ -10,15 +10,14 @@ import ( "os" "strings" - "github.com/julwrites/BotPlatform/pkg/secrets" "github.com/julwrites/ScriptureBot/pkg/bot" + "github.com/julwrites/ScriptureBot/pkg/secrets" ) func bothandler(res http.ResponseWriter, req *http.Request) { - secretsPath := "/go/bin/secrets.yaml" - secretsData, err := secrets.LoadSecrets(secretsPath) + secretsData, err := secrets.LoadSecrets() if err != nil { - panic(err) + log.Fatalf("Failed to load secrets: %v", err) } switch strings.Trim(req.URL.EscapedPath(), "\n") { @@ -32,10 +31,9 @@ func bothandler(res http.ResponseWriter, req *http.Request) { } func subscriptionhandler() { - secretsPath := "/go/bin/secrets.yaml" - secretsData, err := secrets.LoadSecrets(secretsPath) + secretsData, err := secrets.LoadSecrets() if err != nil { - panic(err) + log.Fatalf("Failed to load secrets: %v", err) } bot.SubscriptionHandler(&secretsData) diff --git a/pkg/bot/telegram.go b/pkg/bot/telegram.go index 69b36a6..cac7227 100644 --- a/pkg/bot/telegram.go +++ b/pkg/bot/telegram.go @@ -7,11 +7,11 @@ import ( "log" "net/http" - "github.com/julwrites/BotPlatform/pkg/secrets" - "github.com/julwrites/ScriptureBot/pkg/utils" - "github.com/julwrites/BotPlatform/pkg/def" "github.com/julwrites/BotPlatform/pkg/platform" + bpsecrets "github.com/julwrites/BotPlatform/pkg/secrets" + "github.com/julwrites/ScriptureBot/pkg/secrets" + "github.com/julwrites/ScriptureBot/pkg/utils" ) func TelegramHandler(res http.ResponseWriter, req *http.Request, secrets *secrets.SecretsData) { @@ -22,7 +22,12 @@ func TelegramHandler(res http.ResponseWriter, req *http.Request, secrets *secret return } - env.Secrets = *secrets + // Create a BotPlatform-compatible SecretsData struct using an import alias + platformSecrets := bpsecrets.SecretsData{ + TELEGRAM_ID: secrets.TELEGRAM_ID, + PROJECT_ID: secrets.PROJECT_ID, + } + env.Secrets = platformSecrets // log.Printf("Loaded secrets...") env.ResourcePath = "/go/bin/" diff --git a/pkg/secrets/secrets.go b/pkg/secrets/secrets.go index 417bd5e..b76b86a 100644 --- a/pkg/secrets/secrets.go +++ b/pkg/secrets/secrets.go @@ -5,62 +5,104 @@ import ( "fmt" "log" "os" + "sync" secretmanager "cloud.google.com/go/secretmanager/apiv1" "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" "github.com/joho/godotenv" ) +// SecretsData holds all the secrets for the application. +type SecretsData struct { + TELEGRAM_ID string + PROJECT_ID string + // Add other secrets here as needed +} + func init() { LoadAndLog() } // LoadAndLog loads environment variables from a .env file (if present) and logs -// the status of the GCLOUD_PROJECT_ID. This function is called automatically on package initialization. -// It is also exported to allow for re-loading in test environments. +// the status of the GCLOUD_PROJECT_ID. func LoadAndLog() { - // godotenv.Overload will read your .env file and set the environment variables. - // It will OVERWRITE any existing environment variables. err := godotenv.Overload() if err != nil { - log.Println("No .env file found, continuing with environment variables") + log.Println("No .env file found, using environment variables.") } - - // Log the status of the GCLOUD_PROJECT_ID for debugging purposes. if projectID, ok := os.LookupEnv("GCLOUD_PROJECT_ID"); ok { log.Printf("GCLOUD_PROJECT_ID is set: %s", projectID) } else { - log.Println("GCLOUD_PROJECT_ID is not set. Google Secret Manager will not be used.") + log.Println("GCLOUD_PROJECT_ID is not set. Assuming local development.") + } +} + +// LoadSecrets populates the SecretsData struct by fetching secrets. +func LoadSecrets() (SecretsData, error) { + projectID := os.Getenv("GCLOUD_PROJECT_ID") + + var secrets SecretsData + secrets.PROJECT_ID = projectID + + var wg sync.WaitGroup + var errs = make(chan error, 1) // Buffer to hold the first error + + // List of secret names to fetch + secretNames := []string{"TELEGRAM_ID"} // Add other secret names here + + for _, secretName := range secretNames { + wg.Add(1) + go func(name string) { + defer wg.Done() + value, err := Get(name) + if err != nil { + select { + case errs <- fmt.Errorf("failed to load secret '%s': %v", name, err): + default: + } + return + } + switch name { + case "TELEGRAM_ID": + secrets.TELEGRAM_ID = value + } + }(secretName) } + + wg.Wait() + close(errs) + + if err := <-errs; err != nil { + return SecretsData{}, err + } + + return secrets, nil } -// Get retrieves a secret. It follows a specific order of precedence: -// 1. Google Secret Manager (if GCLOUD_PROJECT_ID is set) -// 2. Environment variables (which includes those loaded from a .env file) -// -// If the secret is not found in any of these locations, it returns an error. +// Get retrieves a secret. +// If GCLOUD_PROJECT_ID is set, it exclusively fetches from Google Secret Manager. +// Otherwise, it falls back to environment variables for local development. func Get(secretName string) (string, error) { - // Attempt to get the secret from Google Secret Manager first. projectID, isCloudRun := os.LookupEnv("GCLOUD_PROJECT_ID") if isCloudRun && projectID != "" { + // Cloud environment: Use Secret Manager exclusively. secretValue, err := getFromSecretManager(projectID, secretName) - if err == nil { - log.Printf("Loaded '%s' from Secret Manager", secretName) - return secretValue, nil + if err != nil { + return "", fmt.Errorf("failed to get secret '%s' from Secret Manager: %v", secretName, err) } - log.Printf("Could not fetch '%s' from Secret Manager, falling back to environment variables: %v", secretName, err) + log.Printf("Loaded '%s' from Secret Manager", secretName) + return secretValue, nil } - // Fallback to environment variables. + // Local environment: Use environment variables. if value, ok := os.LookupEnv(secretName); ok { - log.Printf("Loaded '%s' from .env file or environment", secretName) + log.Printf("Loaded '%s' from environment", secretName) return value, nil } - return "", fmt.Errorf("secret '%s' not found in Secret Manager, .env file, or environment variables", secretName) + return "", fmt.Errorf("secret '%s' not found in environment variables", secretName) } -// getFromSecretManager fetches a secret from Google Secret Manager. func getFromSecretManager(projectID, secretName string) (string, error) { ctx := context.Background() client, err := secretmanager.NewClient(ctx)