1package env
2
3import (
4 "bufio"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strings"
9
10 "gordon/internal/config"
11
12 "github.com/rs/zerolog/log"
13)
14
15type Loader struct {
16 cfg *config.Config
17 secretProviders map[string]SecretProvider
18}
19
20func NewLoader(cfg *config.Config) *Loader {
21 return &Loader{
22 cfg: cfg,
23 secretProviders: make(map[string]SecretProvider),
24 }
25}
26
27func (l *Loader) RegisterSecretProvider(name string, provider SecretProvider) {
28 l.secretProviders[name] = provider
29}
30
31func (l *Loader) LoadEnvForRoute(domain string) ([]string, error) {
32 envVars := []string{}
33
34 // Create domain-safe filename (replace dots and other chars with underscores)
35 safeDomain := strings.ReplaceAll(domain, ".", "_")
36 safeDomain = strings.ReplaceAll(safeDomain, ":", "_")
37 safeDomain = strings.ReplaceAll(safeDomain, "/", "_")
38
39 envFile := filepath.Join(l.cfg.Env.Dir, safeDomain+".env")
40
41 // Check if env file exists
42 if _, err := os.Stat(envFile); os.IsNotExist(err) {
43 log.Debug().
44 Str("domain", domain).
45 Str("env_file", envFile).
46 Msg("No env file found for route")
47 return envVars, nil
48 }
49
50 log.Debug().
51 Str("domain", domain).
52 Str("env_file", envFile).
53 Msg("Loading env file for route")
54
55 file, err := os.Open(envFile)
56 if err != nil {
57 return nil, fmt.Errorf("failed to open env file %s: %w", envFile, err)
58 }
59 defer file.Close()
60
61 scanner := bufio.NewScanner(file)
62 lineNum := 0
63
64 for scanner.Scan() {
65 lineNum++
66 line := strings.TrimSpace(scanner.Text())
67
68 // Skip empty lines and comments
69 if line == "" || strings.HasPrefix(line, "#") {
70 continue
71 }
72
73 // Parse KEY=VALUE format
74 parts := strings.SplitN(line, "=", 2)
75 if len(parts) != 2 {
76 log.Warn().
77 Str("file", envFile).
78 Int("line", lineNum).
79 Str("content", line).
80 Msg("Invalid env file line format, skipping")
81 continue
82 }
83
84 key := strings.TrimSpace(parts[0])
85 value := strings.TrimSpace(parts[1])
86
87 // Remove quotes if present
88 if len(value) >= 2 {
89 if (strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"")) ||
90 (strings.HasPrefix(value, "'") && strings.HasSuffix(value, "'")) {
91 value = value[1 : len(value)-1]
92 }
93 }
94
95 // Resolve secrets if value contains secret syntax
96 resolvedValue, err := l.resolveSecrets(value)
97 if err != nil {
98 log.Error().
99 Err(err).
100 Str("key", key).
101 Str("file", envFile).
102 Int("line", lineNum).
103 Msg("Failed to resolve secret")
104 return nil, fmt.Errorf("failed to resolve secret for %s in %s line %d: %w", key, envFile, lineNum, err)
105 }
106
107 envVars = append(envVars, fmt.Sprintf("%s=%s", key, resolvedValue))
108 }
109
110 if err := scanner.Err(); err != nil {
111 return nil, fmt.Errorf("error reading env file %s: %w", envFile, err)
112 }
113
114 log.Info().
115 Str("domain", domain).
116 Int("count", len(envVars)).
117 Msg("Loaded environment variables for route")
118
119 return envVars, nil
120}
121
122func (l *Loader) resolveSecrets(value string) (string, error) {
123 // Check if value contains secret syntax: ${provider:path}
124 if !strings.Contains(value, "${") {
125 return value, nil
126 }
127
128 result := value
129 for {
130 start := strings.Index(result, "${")
131 if start == -1 {
132 break
133 }
134
135 end := strings.Index(result[start:], "}")
136 if end == -1 {
137 return "", fmt.Errorf("unclosed secret syntax in value: %s", value)
138 }
139 end += start
140
141 secretRef := result[start+2 : end]
142 parts := strings.SplitN(secretRef, ":", 2)
143 if len(parts) != 2 {
144 return "", fmt.Errorf("invalid secret syntax: expected 'provider:path', got '%s'", secretRef)
145 }
146
147 providerName := parts[0]
148 secretPath := parts[1]
149
150 provider, exists := l.secretProviders[providerName]
151 if !exists {
152 return "", fmt.Errorf("unknown secret provider: %s", providerName)
153 }
154
155 secretValue, err := provider.GetSecret(secretPath)
156 if err != nil {
157 return "", fmt.Errorf("failed to get secret from provider %s: %w", providerName, err)
158 }
159
160 // Replace the secret reference with the actual value
161 result = result[:start] + secretValue + result[end+1:]
162 }
163
164 return result, nil
165}
166
167func (l *Loader) EnsureEnvDir() error {
168 if err := os.MkdirAll(l.cfg.Env.Dir, 0700); err != nil {
169 return fmt.Errorf("failed to create env directory %s: %w", l.cfg.Env.Dir, err)
170 }
171
172 log.Debug().
173 Str("env_dir", l.cfg.Env.Dir).
174 Msg("Ensured env directory exists")
175
176 return nil
177}
178
179func (l *Loader) CreateEnvFilesForRoutes() error {
180 routes := l.cfg.GetRoutes()
181 if len(routes) == 0 {
182 log.Debug().Msg("No routes configured, skipping env file creation")
183 return nil
184 }
185
186 createdCount := 0
187 for _, route := range routes {
188 // Create domain-safe filename (same logic as LoadEnvForRoute)
189 safeDomain := strings.ReplaceAll(route.Domain, ".", "_")
190 safeDomain = strings.ReplaceAll(safeDomain, ":", "_")
191 safeDomain = strings.ReplaceAll(safeDomain, "/", "_")
192
193 envFile := filepath.Join(l.cfg.Env.Dir, safeDomain+".env")
194
195 // Check if file already exists
196 if _, err := os.Stat(envFile); err == nil {
197 log.Debug().
198 Str("domain", route.Domain).
199 Str("env_file", envFile).
200 Msg("Env file already exists, skipping creation")
201 continue
202 } else if !os.IsNotExist(err) {
203 log.Warn().
204 Err(err).
205 Str("env_file", envFile).
206 Msg("Error checking env file, skipping creation")
207 continue
208 }
209
210 // Create empty env file with helpful comments
211 content := fmt.Sprintf(`# Environment variables for route: %s
212# Image: %s
213
214`, route.Domain, route.Image)
215
216 if err := os.WriteFile(envFile, []byte(content), 0600); err != nil {
217 log.Error().
218 Err(err).
219 Str("domain", route.Domain).
220 Str("env_file", envFile).
221 Msg("Failed to create env file")
222 continue
223 }
224
225 createdCount++
226 log.Info().
227 Str("domain", route.Domain).
228 Str("env_file", envFile).
229 Msg("Created empty env file for route")
230 }
231
232 if createdCount > 0 {
233 log.Info().
234 Int("created", createdCount).
235 Int("total_routes", len(routes)).
236 Str("env_dir", l.cfg.Env.Dir).
237 Msg("Created env files for routes")
238 }
239
240 return nil
241}
242
243func (l *Loader) CreateEnvFileForRoute(domain, image string) error {
244 // Create domain-safe filename (same logic as LoadEnvForRoute)
245 safeDomain := strings.ReplaceAll(domain, ".", "_")
246 safeDomain = strings.ReplaceAll(safeDomain, ":", "_")
247 safeDomain = strings.ReplaceAll(safeDomain, "/", "_")
248
249 envFile := filepath.Join(l.cfg.Env.Dir, safeDomain+".env")
250
251 // Check if file already exists
252 if _, err := os.Stat(envFile); err == nil {
253 log.Debug().
254 Str("domain", domain).
255 Str("env_file", envFile).
256 Msg("Env file already exists, skipping creation")
257 return nil
258 } else if !os.IsNotExist(err) {
259 return fmt.Errorf("error checking env file %s: %w", envFile, err)
260 }
261
262 // Ensure directory exists
263 if err := l.EnsureEnvDir(); err != nil {
264 return err
265 }
266
267 // Create empty env file with helpful comments
268 content := fmt.Sprintf(`# Environment variables for route: %s
269# Image: %s
270
271`, domain, image)
272
273 if err := os.WriteFile(envFile, []byte(content), 0600); err != nil {
274 return fmt.Errorf("failed to create env file %s: %w", envFile, err)
275 }
276
277 log.Info().
278 Str("domain", domain).
279 Str("env_file", envFile).
280 Msg("Created empty env file for new route")
281
282 return nil
283}
284
285func (l *Loader) UpdateConfig(cfg *config.Config) {
286 l.cfg = cfg
287}
288
289func (l *Loader) GetEnvFilePath(domain string) string {
290 safeDomain := strings.ReplaceAll(domain, ".", "_")
291 safeDomain = strings.ReplaceAll(safeDomain, ":", "_")
292 safeDomain = strings.ReplaceAll(safeDomain, "/", "_")
293 return filepath.Join(l.cfg.Env.Dir, safeDomain+".env")
294}