package secrets import ( "context" "fmt" "os" "os/exec" "strings" "gopkg.in/yaml.v3" ) // SOPSBackend reads secrets from SOPS-encrypted YAML files using the sops CLI. // This is the DR fallback after Infisical migration. type SOPSBackend struct { secretsDir string secrets map[string]string loaded bool } // NewSOPSBackend creates a SOPS backend reading from a directory of .yaml files. func NewSOPSBackend(secretsDir string) *SOPSBackend { return &SOPSBackend{secretsDir: secretsDir} } func (s *SOPSBackend) Name() string { return "sops" } func (s *SOPSBackend) load() error { if s.loaded { return nil } s.secrets = make(map[string]string) entries, err := os.ReadDir(s.secretsDir) if err != nil { return fmt.Errorf("read secrets dir: %w", err) } for _, entry := range entries { if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yaml") { continue } path := s.secretsDir + "/" + entry.Name() cmd := exec.Command("sops", "-d", path) out, err := cmd.Output() if err != nil { continue // skip unreadable files } var data map[string]any if err := yaml.Unmarshal(out, &data); err != nil { continue } base := strings.TrimSuffix(entry.Name(), ".yaml") for k, v := range data { s.secrets[base+"/"+k] = fmt.Sprintf("%v", v) } } s.loaded = true return nil } func (s *SOPSBackend) Get(ctx context.Context, key string) (string, error) { if err := s.load(); err != nil { return "", err } val, ok := s.secrets[key] if !ok { return "", ErrNotFound } return val, nil } func (s *SOPSBackend) List(ctx context.Context) ([]string, error) { if err := s.load(); err != nil { return nil, err } keys := make([]string, 0, len(s.secrets)) for k := range s.secrets { keys = append(keys, k) } return keys, nil } func (s *SOPSBackend) Set(ctx context.Context, key string, value string) error { return fmt.Errorf("SOPS backend is read-only; use Infisical for writes or sops CLI manually") }