v2.10.2 705579a

migrate_env.go RawHistoryFolder

1package app
2
3import (
4	"fmt"
5	"os"
6	"path/filepath"
7	"strings"
8
9	"github.com/bnema/zerowrap"
10
11	"github.com/bnema/gordon/internal/adapters/out/domainsecrets"
12	"github.com/bnema/gordon/internal/domain"
13)
14
15func migrateEnvFilesToPass(envDir string, passStore *domainsecrets.PassStore, log zerowrap.Logger) error {
16	entries, err := readEnvDir(envDir)
17	if err != nil {
18		return err
19	}
20
21	for _, entry := range entries {
22		if entry.IsDir() {
23			continue
24		}
25		name := entry.Name()
26		if !isPlainEnvFile(name) {
27			continue
28		}
29		if err := migrateEnvFile(envDir, name, passStore, log); err != nil {
30			return err
31		}
32	}
33
34	if err := migrateAttachmentEnvFilesToPass(envDir, passStore, log); err != nil {
35		return err
36	}
37
38	return nil
39}
40
41func readEnvDir(envDir string) ([]os.DirEntry, error) {
42	entries, err := os.ReadDir(envDir)
43	if err != nil {
44		if os.IsNotExist(err) {
45			return nil, nil
46		}
47		return nil, fmt.Errorf("failed to read env directory: %w", err)
48	}
49	return entries, nil
50}
51
52func isPlainEnvFile(name string) bool {
53	if !strings.HasSuffix(name, ".env") || strings.HasSuffix(name, ".env.migrated") {
54		return false
55	}
56	// Skip attachment env files: gordon-<sanitized-domain>-<service>.env
57	if strings.HasPrefix(name, "gordon-") {
58		return false
59	}
60	return true
61}
62
63func migrateEnvFile(envDir, name string, passStore *domainsecrets.PassStore, log zerowrap.Logger) error {
64	filePath := filepath.Join(envDir, name)
65	data, err := os.ReadFile(filePath)
66	if err != nil {
67		return fmt.Errorf("failed to read env file %s: %w", filePath, err)
68	}
69
70	domainName := strings.TrimSuffix(name, ".env")
71	secrets, err := domain.ParseEnvData(data)
72	if err != nil {
73		return fmt.Errorf("failed to parse env file %s: %w", filePath, err)
74	}
75	if len(secrets) == 0 {
76		log.Info().
77			Str("file", filePath).
78			Msg("no secrets found in env file; skipping migration")
79		return nil
80	}
81
82	existingKeys, err := passStore.ListKeys(domainName)
83	if err != nil {
84		return fmt.Errorf("failed to read pass keys for %s: %w", domainName, err)
85	}
86
87	missing := missingSecrets(existingKeys, secrets)
88	if len(missing) > 0 {
89		if err := passStore.Set(domainName, missing); err != nil {
90			return fmt.Errorf("failed to migrate secrets for %s: %w", domainName, err)
91		}
92	}
93
94	migratedPath := filePath + ".migrated"
95	if err := os.Rename(filePath, migratedPath); err != nil {
96		return fmt.Errorf("failed to rename env file %s: %w", filePath, err)
97	}
98
99	log.Info().
100		Int(zerowrap.FieldCount, len(missing)).
101		Str("domain", domainName).
102		Msg("migrated secrets for domain from plain text to pass")
103	log.Info().
104		Str("file", migratedPath).
105		Msg("original file renamed to .env.migrated - you can safely remove it")
106
107	return nil
108}
109
110func missingSecrets(existingKeys []string, secrets map[string]string) map[string]string {
111	if len(secrets) == 0 {
112		return nil
113	}
114
115	existingSet := make(map[string]struct{}, len(existingKeys))
116	for _, key := range existingKeys {
117		existingSet[key] = struct{}{}
118	}
119
120	missing := make(map[string]string)
121	for key, value := range secrets {
122		if _, exists := existingSet[key]; exists {
123			continue
124		}
125		missing[key] = value
126	}
127
128	if len(missing) == 0 {
129		return nil
130	}
131
132	return missing
133}
134
135func migrateAttachmentEnvFilesToPass(envDir string, passStore *domainsecrets.PassStore, log zerowrap.Logger) error {
136	entries, err := readEnvDir(envDir)
137	if err != nil {
138		return err
139	}
140
141	for _, entry := range entries {
142		if entry.IsDir() {
143			continue
144		}
145		name := entry.Name()
146		if !isAttachmentEnvFile(name) {
147			continue
148		}
149		if err := migrateAttachmentEnvFile(envDir, name, passStore, log); err != nil {
150			return err
151		}
152	}
153
154	return nil
155}
156
157func isAttachmentEnvFile(name string) bool {
158	if !strings.HasSuffix(name, ".env") || strings.HasSuffix(name, ".env.migrated") {
159		return false
160	}
161	return strings.HasPrefix(name, "gordon-")
162}
163
164func migrateAttachmentEnvFile(envDir, name string, passStore *domainsecrets.PassStore, log zerowrap.Logger) error {
165	filePath := filepath.Join(envDir, name)
166	data, err := os.ReadFile(filePath)
167	if err != nil {
168		return fmt.Errorf("failed to read attachment env file %s: %w", filePath, err)
169	}
170
171	containerName := extractContainerNameFromAttachmentFile(name)
172	secrets, err := domain.ParseEnvData(data)
173	if err != nil {
174		return fmt.Errorf("failed to parse attachment env file %s: %w", filePath, err)
175	}
176	if len(secrets) == 0 {
177		log.Info().
178			Str("file", filePath).
179			Msg("no secrets found in attachment env file; skipping migration")
180		return nil
181	}
182
183	existingSecrets, err := passStore.GetAllAttachment(containerName)
184	if err != nil {
185		return fmt.Errorf("failed to read pass secrets for attachment %s: %w", containerName, err)
186	}
187
188	existingKeys := make([]string, 0, len(existingSecrets))
189	for key := range existingSecrets {
190		existingKeys = append(existingKeys, key)
191	}
192
193	missing := missingSecrets(existingKeys, secrets)
194	if len(missing) > 0 {
195		if err := passStore.SetAttachment(containerName, missing); err != nil {
196			return fmt.Errorf("failed to migrate attachment secrets for %s: %w", containerName, err)
197		}
198		log.Info().
199			Int(zerowrap.FieldCount, len(missing)).
200			Str("container", containerName).
201			Msg("migrated secrets for attachment container from plain text to pass")
202	} else {
203		log.Info().
204			Str("container", containerName).
205			Msg("all attachment secrets already exist in pass; no migration needed")
206	}
207
208	migratedPath := filePath + ".migrated"
209	if err := os.Rename(filePath, migratedPath); err != nil {
210		return fmt.Errorf("failed to rename attachment env file %s: %w", filePath, err)
211	}
212
213	log.Info().
214		Str("file", migratedPath).
215		Msg("original attachment file renamed to .env.migrated - you can safely remove it")
216
217	return nil
218}
219
220func extractContainerNameFromAttachmentFile(filename string) string {
221	name := strings.TrimSuffix(filename, ".env")
222	return name
223}

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