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}