tangled
alpha
login
or
join now
evan.jarrett.net
/
at-container-registry
66
fork
atom
A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
atcr.io
docker
container
atproto
go
66
fork
atom
overview
issues
1
pulls
pipelines
add missing config keys on provision
evan.jarrett.net
1 month ago
fbe73384
bc034e34
verified
This commit was signed with the committer's
known signature
.
evan.jarrett.net
SSH Key Fingerprint:
SHA256:bznk0uVPp7XFOl67P0uTM1pCjf2A4ojeP/lsUE7uauQ=
+126
-1
4 changed files
expand all
collapse all
unified
split
deploy
upcloud
cloudinit.go
configs
cloudinit.sh.tmpl
provision.go
go.sum
+105
deploy/upcloud/cloudinit.go
···
6
6
"fmt"
7
7
"strings"
8
8
"text/template"
9
9
+
10
10
+
"go.yaml.in/yaml/v3"
9
11
)
10
12
11
13
//go:embed systemd/appview.service.tmpl
···
189
191
}
190
192
return buf.String(), nil
191
193
}
194
194
+
195
195
+
// syncConfigKeys fetches the existing config from a server and merges in any
196
196
+
// missing keys from the rendered template. Existing values are never overwritten.
197
197
+
func syncConfigKeys(name, ip, configPath, templateYAML string) error {
198
198
+
remote, err := runSSH(ip, fmt.Sprintf("cat %s 2>/dev/null || echo '__MISSING__'", configPath), false)
199
199
+
if err != nil {
200
200
+
fmt.Printf(" config sync: could not reach %s (%v)\n", name, err)
201
201
+
return nil
202
202
+
}
203
203
+
remote = strings.TrimSpace(remote)
204
204
+
205
205
+
if remote == "__MISSING__" {
206
206
+
fmt.Printf(" config sync: %s not yet created (cloud-init will handle it)\n", name)
207
207
+
return nil
208
208
+
}
209
209
+
210
210
+
// Parse both into yaml.Node trees
211
211
+
var templateDoc yaml.Node
212
212
+
if err := yaml.Unmarshal([]byte(templateYAML), &templateDoc); err != nil {
213
213
+
return fmt.Errorf("parse template yaml: %w", err)
214
214
+
}
215
215
+
var existingDoc yaml.Node
216
216
+
if err := yaml.Unmarshal([]byte(remote), &existingDoc); err != nil {
217
217
+
return fmt.Errorf("parse remote yaml: %w", err)
218
218
+
}
219
219
+
220
220
+
// Unwrap document nodes to get the root mapping
221
221
+
templateRoot := unwrapDocNode(&templateDoc)
222
222
+
existingRoot := unwrapDocNode(&existingDoc)
223
223
+
if templateRoot == nil || existingRoot == nil {
224
224
+
fmt.Printf(" config sync: %s skipped (unexpected YAML structure)\n", name)
225
225
+
return nil
226
226
+
}
227
227
+
228
228
+
added := mergeYAMLNodes(templateRoot, existingRoot)
229
229
+
if !added {
230
230
+
fmt.Printf(" config sync: %s up to date\n", name)
231
231
+
return nil
232
232
+
}
233
233
+
234
234
+
// Marshal the modified tree back
235
235
+
merged, err := yaml.Marshal(&existingDoc)
236
236
+
if err != nil {
237
237
+
return fmt.Errorf("marshal merged yaml: %w", err)
238
238
+
}
239
239
+
240
240
+
// Write back to server
241
241
+
script := fmt.Sprintf("cat > %s << 'CFGEOF'\n%sCFGEOF", configPath, string(merged))
242
242
+
if _, err := runSSH(ip, script, false); err != nil {
243
243
+
return fmt.Errorf("write merged config: %w", err)
244
244
+
}
245
245
+
fmt.Printf(" config sync: %s updated with new keys\n", name)
246
246
+
return nil
247
247
+
}
248
248
+
249
249
+
// unwrapDocNode returns the root mapping node, unwrapping a DocumentNode wrapper if present.
250
250
+
func unwrapDocNode(n *yaml.Node) *yaml.Node {
251
251
+
if n.Kind == yaml.DocumentNode && len(n.Content) > 0 {
252
252
+
return n.Content[0]
253
253
+
}
254
254
+
if n.Kind == yaml.MappingNode {
255
255
+
return n
256
256
+
}
257
257
+
return nil
258
258
+
}
259
259
+
260
260
+
// mergeYAMLNodes recursively adds keys from base into existing that are not
261
261
+
// already present. Existing values are never overwritten. Returns true if any
262
262
+
// new keys were added.
263
263
+
func mergeYAMLNodes(base, existing *yaml.Node) bool {
264
264
+
if base.Kind != yaml.MappingNode || existing.Kind != yaml.MappingNode {
265
265
+
return false
266
266
+
}
267
267
+
268
268
+
added := false
269
269
+
for i := 0; i+1 < len(base.Content); i += 2 {
270
270
+
baseKey := base.Content[i]
271
271
+
baseVal := base.Content[i+1]
272
272
+
273
273
+
// Look for this key in existing
274
274
+
found := false
275
275
+
for j := 0; j+1 < len(existing.Content); j += 2 {
276
276
+
if existing.Content[j].Value == baseKey.Value {
277
277
+
found = true
278
278
+
// If both are mappings, recurse to merge sub-keys
279
279
+
if baseVal.Kind == yaml.MappingNode && existing.Content[j+1].Kind == yaml.MappingNode {
280
280
+
if mergeYAMLNodes(baseVal, existing.Content[j+1]) {
281
281
+
added = true
282
282
+
}
283
283
+
}
284
284
+
break
285
285
+
}
286
286
+
}
287
287
+
288
288
+
if !found {
289
289
+
// Append the missing key+value pair
290
290
+
existing.Content = append(existing.Content, baseKey, baseVal)
291
291
+
added = true
292
292
+
}
293
293
+
}
294
294
+
295
295
+
return added
296
296
+
}
+1
-1
deploy/upcloud/configs/cloudinit.sh.tmpl
···
58
58
{{.ConfigYAML}}
59
59
CFGEOF
60
60
else
61
61
-
echo "Config {{.ConfigPath}} already exists, skipping"
61
61
+
echo "Config {{.ConfigPath}} already exists, skipping overwrite (missing keys merged separately)"
62
62
fi
63
63
64
64
# Systemd service
+14
deploy/upcloud/provision.go
···
189
189
if err := syncCloudInit("appview", state.Appview.PublicIP, appviewScript); err != nil {
190
190
return err
191
191
}
192
192
+
appviewConfigYAML, err := renderConfig(appviewConfigTmpl, vals)
193
193
+
if err != nil {
194
194
+
return fmt.Errorf("render appview config: %w", err)
195
195
+
}
196
196
+
if err := syncConfigKeys("appview", state.Appview.PublicIP, naming.AppviewConfigPath(), appviewConfigYAML); err != nil {
197
197
+
return fmt.Errorf("appview config sync: %w", err)
198
198
+
}
192
199
} else {
193
200
fmt.Println("Creating appview server...")
194
201
appviewUserData, err := generateAppviewCloudInit(cfg, vals, goVersion)
···
213
220
}
214
221
if err := syncCloudInit("hold", state.Hold.PublicIP, holdScript); err != nil {
215
222
return err
223
223
+
}
224
224
+
holdConfigYAML, err := renderConfig(holdConfigTmpl, vals)
225
225
+
if err != nil {
226
226
+
return fmt.Errorf("render hold config: %w", err)
227
227
+
}
228
228
+
if err := syncConfigKeys("hold", state.Hold.PublicIP, naming.HoldConfigPath(), holdConfigYAML); err != nil {
229
229
+
return fmt.Errorf("hold config sync: %w", err)
216
230
}
217
231
} else {
218
232
fmt.Println("Creating hold server...")
+6
go.sum
···
1
1
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
2
2
+
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
2
3
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
3
4
github.com/RussellLuo/slidingwindow v0.0.0-20200528002341-535bb99d338b h1:5/++qT1/z812ZqBvqQt6ToRswSuPZ/B33m6xVHRzADU=
4
5
github.com/RussellLuo/slidingwindow v0.0.0-20200528002341-535bb99d338b/go.mod h1:4+EPqMRApwwE/6yo6CxiHoSnBzjRr3jsqer7frxP8y4=
···
78
79
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
79
80
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
80
81
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
82
82
+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
81
83
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
82
84
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
83
85
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
···
95
97
github.com/earthboundkid/versioninfo/v2 v2.24.1 h1:SJTMHaoUx3GzjjnUO1QzP3ZXK6Ee/nbWyCm58eY3oUg=
96
98
github.com/earthboundkid/versioninfo/v2 v2.24.1/go.mod h1:VcWEooDEuyUJnMfbdTh0uFN4cfEIg+kHMuWB2CDCLjw=
97
99
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
100
100
+
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
98
101
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
99
102
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
100
103
github.com/filecoin-project/go-clock v0.1.0 h1:SFbYIM75M8NnFm1yMHhN9Ahy3W5bEZV9gd6MPfXbKVU=
···
161
164
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
162
165
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
163
166
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
167
167
+
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
164
168
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
165
169
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
166
170
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
···
291
295
github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 h1:JLvn7D+wXjH9g4Jsjo+VqmzTUpl/LX7vfr6VOfSWTdM=
292
296
github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06/go.mod h1:FUkZ5OHjlGPjnM2UyGJz9TypXQFgYqw6AFNO1UiROTM=
293
297
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
298
298
+
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
294
299
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
295
300
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
296
301
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
···
345
350
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
346
351
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
347
352
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
353
353
+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
348
354
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0=
349
355
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw=
350
356
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=