A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
80
fork

Configure Feed

Select the types of activity you want to include in your feed.

add missing config keys on provision

+126 -1
+105
deploy/upcloud/cloudinit.go
··· 6 6 "fmt" 7 7 "strings" 8 8 "text/template" 9 + 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 + 195 + // syncConfigKeys fetches the existing config from a server and merges in any 196 + // missing keys from the rendered template. Existing values are never overwritten. 197 + func syncConfigKeys(name, ip, configPath, templateYAML string) error { 198 + remote, err := runSSH(ip, fmt.Sprintf("cat %s 2>/dev/null || echo '__MISSING__'", configPath), false) 199 + if err != nil { 200 + fmt.Printf(" config sync: could not reach %s (%v)\n", name, err) 201 + return nil 202 + } 203 + remote = strings.TrimSpace(remote) 204 + 205 + if remote == "__MISSING__" { 206 + fmt.Printf(" config sync: %s not yet created (cloud-init will handle it)\n", name) 207 + return nil 208 + } 209 + 210 + // Parse both into yaml.Node trees 211 + var templateDoc yaml.Node 212 + if err := yaml.Unmarshal([]byte(templateYAML), &templateDoc); err != nil { 213 + return fmt.Errorf("parse template yaml: %w", err) 214 + } 215 + var existingDoc yaml.Node 216 + if err := yaml.Unmarshal([]byte(remote), &existingDoc); err != nil { 217 + return fmt.Errorf("parse remote yaml: %w", err) 218 + } 219 + 220 + // Unwrap document nodes to get the root mapping 221 + templateRoot := unwrapDocNode(&templateDoc) 222 + existingRoot := unwrapDocNode(&existingDoc) 223 + if templateRoot == nil || existingRoot == nil { 224 + fmt.Printf(" config sync: %s skipped (unexpected YAML structure)\n", name) 225 + return nil 226 + } 227 + 228 + added := mergeYAMLNodes(templateRoot, existingRoot) 229 + if !added { 230 + fmt.Printf(" config sync: %s up to date\n", name) 231 + return nil 232 + } 233 + 234 + // Marshal the modified tree back 235 + merged, err := yaml.Marshal(&existingDoc) 236 + if err != nil { 237 + return fmt.Errorf("marshal merged yaml: %w", err) 238 + } 239 + 240 + // Write back to server 241 + script := fmt.Sprintf("cat > %s << 'CFGEOF'\n%sCFGEOF", configPath, string(merged)) 242 + if _, err := runSSH(ip, script, false); err != nil { 243 + return fmt.Errorf("write merged config: %w", err) 244 + } 245 + fmt.Printf(" config sync: %s updated with new keys\n", name) 246 + return nil 247 + } 248 + 249 + // unwrapDocNode returns the root mapping node, unwrapping a DocumentNode wrapper if present. 250 + func unwrapDocNode(n *yaml.Node) *yaml.Node { 251 + if n.Kind == yaml.DocumentNode && len(n.Content) > 0 { 252 + return n.Content[0] 253 + } 254 + if n.Kind == yaml.MappingNode { 255 + return n 256 + } 257 + return nil 258 + } 259 + 260 + // mergeYAMLNodes recursively adds keys from base into existing that are not 261 + // already present. Existing values are never overwritten. Returns true if any 262 + // new keys were added. 263 + func mergeYAMLNodes(base, existing *yaml.Node) bool { 264 + if base.Kind != yaml.MappingNode || existing.Kind != yaml.MappingNode { 265 + return false 266 + } 267 + 268 + added := false 269 + for i := 0; i+1 < len(base.Content); i += 2 { 270 + baseKey := base.Content[i] 271 + baseVal := base.Content[i+1] 272 + 273 + // Look for this key in existing 274 + found := false 275 + for j := 0; j+1 < len(existing.Content); j += 2 { 276 + if existing.Content[j].Value == baseKey.Value { 277 + found = true 278 + // If both are mappings, recurse to merge sub-keys 279 + if baseVal.Kind == yaml.MappingNode && existing.Content[j+1].Kind == yaml.MappingNode { 280 + if mergeYAMLNodes(baseVal, existing.Content[j+1]) { 281 + added = true 282 + } 283 + } 284 + break 285 + } 286 + } 287 + 288 + if !found { 289 + // Append the missing key+value pair 290 + existing.Content = append(existing.Content, baseKey, baseVal) 291 + added = true 292 + } 293 + } 294 + 295 + return added 296 + }
+1 -1
deploy/upcloud/configs/cloudinit.sh.tmpl
··· 58 58 {{.ConfigYAML}} 59 59 CFGEOF 60 60 else 61 - echo "Config {{.ConfigPath}} already exists, skipping" 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 + appviewConfigYAML, err := renderConfig(appviewConfigTmpl, vals) 193 + if err != nil { 194 + return fmt.Errorf("render appview config: %w", err) 195 + } 196 + if err := syncConfigKeys("appview", state.Appview.PublicIP, naming.AppviewConfigPath(), appviewConfigYAML); err != nil { 197 + return fmt.Errorf("appview config sync: %w", err) 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 + } 224 + holdConfigYAML, err := renderConfig(holdConfigTmpl, vals) 225 + if err != nil { 226 + return fmt.Errorf("render hold config: %w", err) 227 + } 228 + if err := syncConfigKeys("hold", state.Hold.PublicIP, naming.HoldConfigPath(), holdConfigYAML); err != nil { 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 + 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 + 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 + 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 + 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 + 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 + 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=