package secrets import ( "context" "log/slog" ) // NewManagerFromConfig creates a secrets Manager from the Infisical connection // parameters in cfg, with an optional SOPS fallback from cfg.SecretsDir. // Returns nil if Infisical is not configured. func NewManagerFromConfig(siteURL, clientID, clientSecret, projectID, env, secretsDir string) *Manager { if siteURL == "" { return nil } infCfg := InfisicalConfig{ SiteURL: siteURL, ClientID: clientID, ClientSecret: clientSecret, ProjectID: projectID, SecretPath: "/", Env: env, } if infCfg.Env == "" { infCfg.Env = "dev" } primary := NewInfisicalBackend(infCfg) var fallback Backend if secretsDir != "" { fallback = NewSOPSBackend(secretsDir) } return NewManager(primary, fallback) } // VerifyExpectedSecrets checks that a list of expected keys are present // in the backend. Logs a summary and returns the count of missing keys. // Use at startup to detect incomplete Infisical migration. func VerifyExpectedSecrets(ctx context.Context, sec Backend, expected []string) int { keys, err := sec.List(ctx) if err != nil { slog.Warn("secrets: cannot verify expected secrets, list failed", "error", err) return len(expected) } keySet := make(map[string]struct{}, len(keys)) for _, k := range keys { keySet[k] = struct{}{} } missing := 0 for _, exp := range expected { if _, ok := keySet[exp]; !ok { missing++ slog.Warn("secrets: expected key missing from Infisical", "key", exp) } } if missing == 0 { slog.Info("secrets: all expected keys present", "count", len(expected)) } else { slog.Warn("secrets: some expected keys missing from Infisical", "missing", missing, "total", len(expected)) } return missing } // OverlayConfig fetches secrets from the backend and returns a function that // applies them to config fields. Each entry maps an Infisical key to a setter; // if the key is found and non-empty, the setter is called; if not found or // empty, the env-derived value is left unchanged and a warning is logged. // Returns the number of secrets resolved from Infisical (useful for logging). func OverlayConfig(ctx context.Context, sec Backend, overlays []secretOverlay) int { resolved := 0 for _, o := range overlays { val, err := sec.Get(ctx, o.infisicalKey) if err != nil { slog.Warn("secret not resolved from Infisical, using env fallback", "key", o.infisicalKey, "error", err) continue } if val == "" { slog.Warn("Infisical returned empty value, keeping env-derived value", "key", o.infisicalKey) continue } o.apply(val) resolved++ } return resolved } // ConfigOverlays returns the standard set of Infisical → config overlays for // the oikos binary. Each overlay is attempted at startup; if the key exists // in Infisical, it overrides the env-derived value. func ConfigOverlays(cfg map[string]func(string)) []secretOverlay { overlays := make([]secretOverlay, 0, len(cfg)) for key, setter := range cfg { overlays = append(overlays, secretOverlay{infisicalKey: key, apply: setter}) } return overlays } // ResolveSecret attempts to fetch a single secret from the backend. Returns // the Infisical value if found and non-empty, otherwise falls back to the // env-derived value. Warnings are logged for failures. func ResolveSecret(ctx context.Context, sec Backend, infisicalKey, fallback string) string { if sec == nil { return fallback } val, err := sec.Get(ctx, infisicalKey) if err != nil { slog.Warn("secret not resolved from Infisical, using env fallback", "key", infisicalKey, "error", err) return fallback } if val == "" { slog.Warn("Infisical returned empty value, keeping env-derived value", "key", infisicalKey) return fallback } return val }