1// Package envloader implements the environment variable loader adapter.
2package envloader
3
4import (
5 "context"
6 "fmt"
7 "sort"
8 "strings"
9
10 "github.com/bnema/zerowrap"
11
12 "github.com/bnema/gordon/internal/adapters/out/domainsecrets"
13)
14
15// PassLoader implements the EnvLoader interface using pass-backed secrets.
16type PassLoader struct {
17 store *domainsecrets.PassStore
18 log zerowrap.Logger
19}
20
21// NewPassLoader creates a new pass-based environment loader.
22func NewPassLoader(store *domainsecrets.PassStore, log zerowrap.Logger) (*PassLoader, error) {
23 if store == nil {
24 return nil, fmt.Errorf("pass store is required")
25 }
26
27 log.Debug().
28 Str(zerowrap.FieldLayer, "adapter").
29 Str(zerowrap.FieldAdapter, "envloader").
30 Str("provider", "pass").
31 Msg("env loader initialized")
32
33 return &PassLoader{
34 store: store,
35 log: log,
36 }, nil
37}
38
39// LoadEnv loads environment variables for a given domain.
40func (l *PassLoader) LoadEnv(ctx context.Context, domain string) ([]string, error) {
41 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
42 zerowrap.FieldLayer: "adapter",
43 zerowrap.FieldAdapter: "envloader",
44 zerowrap.FieldAction: "LoadEnv",
45 "domain": domain,
46 })
47 log := zerowrap.FromCtx(ctx)
48
49 var secretsMap map[string]string
50 var err error
51
52 if isAttachmentContainer(domain) {
53 secretsMap, err = l.store.GetAllAttachment(domain)
54 if err != nil {
55 return nil, log.WrapErr(err, "failed to load attachment env from pass")
56 }
57 } else {
58 secretsMap, err = l.store.GetAll(domain)
59 if err != nil {
60 return nil, log.WrapErr(err, "failed to load env from pass")
61 }
62 }
63
64 keys := make([]string, 0, len(secretsMap))
65 for key := range secretsMap {
66 keys = append(keys, key)
67 }
68 sort.Strings(keys)
69
70 envVars := make([]string, 0, len(keys))
71 for _, key := range keys {
72 envVars = append(envVars, fmt.Sprintf("%s=%s", key, secretsMap[key]))
73 }
74
75 log.Info().Int(zerowrap.FieldCount, len(envVars)).Msg("loaded environment variables for route")
76
77 return envVars, nil
78}
79
80// isAttachmentContainer determines whether a given name refers to an attachment container
81// or a domain. It uses a simple heuristic: attachment container names always start with
82// the "gordon-" prefix (e.g., "gordon-git-example-com-gitea-postgres").
83//
84// LIMITATION: This heuristic assumes domains never start with "gordon-". A domain like
85// "gordon-app.example.com" would be incorrectly classified as an attachment container.
86// This is an acceptable trade-off for simplicity, but if such domains become common,
87// this should be replaced with a more robust detection mechanism (e.g., checking both
88// GetAll() and GetAllAttachment() and seeing which returns results).
89func isAttachmentContainer(domain string) bool {
90 return strings.HasPrefix(domain, "gordon-")
91}
92
93// CreateEnvFile is a no-op for pass-backed loader.
94func (l *PassLoader) CreateEnvFile(ctx context.Context, domain string) error {
95 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
96 zerowrap.FieldLayer: "adapter",
97 zerowrap.FieldAdapter: "envloader",
98 zerowrap.FieldAction: "CreateEnvFile",
99 "domain": domain,
100 })
101 log := zerowrap.FromCtx(ctx)
102 log.Debug().Msg("pass loader does not create env files")
103 return nil
104}
105
106// EnvFileExists checks if a manifest exists for a domain in pass.
107func (l *PassLoader) EnvFileExists(domain string) bool {
108 exists, err := l.store.ManifestExists(domain)
109 if err != nil {
110 l.log.Warn().Err(err).Str("domain", domain).Msg("failed to check pass manifest")
111 return false
112 }
113 return exists
114}