package app import ( "fmt" "log/slog" "os" "path/filepath" "regexp" "strings" "gitea.maximumdirect.net/eric/narratio/internal/config" ) var envVarNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) type secretsLoadStats struct { Dir string Loaded int PreservedExisting int Skipped int } func loadSecretsFromConfig(cfg *config.Config, logger *slog.Logger) (*secretsLoadStats, error) { if cfg == nil || cfg.Pipeline == nil || cfg.Pipeline.Secrets == nil { return nil, nil } rawDir := strings.TrimSpace(cfg.Pipeline.Secrets.EnvDir) if rawDir == "" { return nil, nil } resolvedDir := rawDir if !filepath.IsAbs(resolvedDir) { cwd, err := os.Getwd() if err != nil { return nil, fmt.Errorf("resolve secrets env_dir %q from current working directory: %w", rawDir, err) } resolvedDir = filepath.Join(cwd, resolvedDir) } resolvedDir = filepath.Clean(resolvedDir) entries, err := os.ReadDir(resolvedDir) if err != nil { return nil, fmt.Errorf("read secrets env_dir %q: %w", resolvedDir, err) } stats := &secretsLoadStats{Dir: resolvedDir} for _, entry := range entries { name := entry.Name() if !envVarNamePattern.MatchString(name) { stats.Skipped++ continue } if entry.IsDir() { stats.Skipped++ continue } secretPath := filepath.Join(resolvedDir, name) bytes, err := os.ReadFile(secretPath) if err != nil { return nil, fmt.Errorf("read secret file %q: %w", secretPath, err) } value := strings.TrimRight(string(bytes), "\r\n") if _, exists := os.LookupEnv(name); exists { stats.PreservedExisting++ continue } if err := os.Setenv(name, value); err != nil { return nil, fmt.Errorf("set environment variable %q from %q: %w", name, secretPath, err) } stats.Loaded++ } if logger != nil { logger.Info( "loaded secret environment variables from filesystem", "secrets_env_dir", stats.Dir, "loaded", stats.Loaded, "preserved_existing", stats.PreservedExisting, "skipped", stats.Skipped, ) } return stats, nil }