v2.0.0 ae3e752

loader.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close