···1111 enabled: true
1212 # Listen address for labeler (e.g., :5002).
1313 addr: :5002
1414- # Externally reachable labeler URL. Empty = derive from server.base_url.
1515- public_url: ""
1414+ # Externally reachable labeler URL (required, e.g. https://labeler.example.com).
1515+ public_url: https://labeler.example.com
1616+ # OAuth client display name (e.g., "ATCR Labeler").
1717+ client_name: ATCR Labeler
1818+ # Short brand label used in UI copy (e.g., "ATCR").
1919+ client_short_name: ATCR
1620 # DID of the labeler admin. Only this DID can log into the admin panel.
1721 owner_did: did:plc:your-did-here
1822 # Directory for labeler state (database, signing key, did.txt).
···3337 libsql_auth_token: ""
3438 # Embedded-replica pull interval (e.g. 30s). 0 = manual sync only.
3539 libsql_sync_interval: 0s
3636-# AppView server settings (shared config).
3737-server:
3838- base_url: https://atcr.io
3939- client_name: AT Container Registry
4040- client_short_name: ATCR
4141- test_mode: false
4240# Remote log shipping settings.
4341log_shipper:
4442 # Log shipping backend: "victoria", "opensearch", or "loki". Empty disables shipping.
+29-9
deploy/upcloud/cloudinit.go
···4949 S3SecretKey string
50505151 // Infrastructure (computed from zone + config)
5252- Zone string // e.g. "us-chi1"
5353- HoldDomain string // e.g. "us-chi1.cove.seamark.dev"
5454- HoldDid string // e.g. "did:web:us-chi1.cove.seamark.dev"
5555- BasePath string // e.g. "/var/lib/seamark"
5252+ Zone string // e.g. "us-chi1"
5353+ HoldDomain string // e.g. "us-chi1.cove.seamark.dev"
5454+ HoldDid string // e.g. "did:web:us-chi1.cove.seamark.dev"
5555+ LabelerDomain string // e.g. "labeler.seamark.dev"
5656+ BasePath string // e.g. "/var/lib/seamark"
56575758 // Scanner (auto-generated shared secret)
5859 ScannerSecret string // hex-encoded 32-byte secret; empty disables scanning
···373374}
374375375376// syncServiceUnit compares a rendered systemd service unit against what's on
376376-// the server. If they differ, it writes the new unit file. Returns true if the
377377-// unit was updated (caller should daemon-reload before restart).
377377+// the server. If they differ, it writes the new unit file. If the unit is
378378+// missing entirely, it installs it and runs `systemctl enable` so the service
379379+// starts on boot. Returns true if the unit was created or updated (caller
380380+// should daemon-reload before restart).
378381func syncServiceUnit(name, ip, serviceName, renderedUnit string) (bool, error) {
379382 unitPath := "/etc/systemd/system/" + serviceName + ".service"
380383···387390 rendered := strings.TrimSpace(renderedUnit)
388391389392 if remote == "__MISSING__" {
390390- fmt.Printf(" service unit: %s not found (cloud-init will handle it)\n", name)
391391- return false, nil
393393+ // First-time install: write file, daemon-reload, and enable so the
394394+ // service comes up on boot. The caller's restart will start it.
395395+ script := fmt.Sprintf("cat > %s << 'SVCEOF'\n%s\nSVCEOF\nsystemctl daemon-reload\nsystemctl enable %s",
396396+ unitPath, rendered, serviceName)
397397+ if _, err := runSSH(ip, script, false); err != nil {
398398+ return false, fmt.Errorf("install service unit: %w", err)
399399+ }
400400+ fmt.Printf(" service unit: %s installed and enabled\n", name)
401401+ return true, nil
392402 }
393403394404 if remote == rendered {
···416426 remote = strings.TrimSpace(remote)
417427418428 if remote == "__MISSING__" {
419419- fmt.Printf(" config sync: %s not yet created (cloud-init will handle it)\n", name)
429429+ // First-time install: write the rendered template as-is. Subsequent
430430+ // runs use the merge-keys path below to preserve operator edits.
431431+ dir := configPath[:strings.LastIndex(configPath, "/")]
432432+ if _, err := runSSH(ip, fmt.Sprintf("mkdir -p %s", dir), false); err != nil {
433433+ return fmt.Errorf("create config dir: %w", err)
434434+ }
435435+ script := fmt.Sprintf("cat > %s << 'CFGEOF'\n%s\nCFGEOF", configPath, strings.TrimRight(templateYAML, "\n"))
436436+ if _, err := runSSH(ip, script, false); err != nil {
437437+ return fmt.Errorf("write initial config: %w", err)
438438+ }
439439+ fmt.Printf(" config sync: %s installed\n", name)
420440 return nil
421441 }
422442
+4-2
deploy/upcloud/config.go
···9090 return clientName, baseDomain, registryDomains, nil
9191}
92929393-// readSSHPublicKey reads an SSH public key from a file path.
9393+// readSSHPublicKey reads an SSH public key from a file path. An empty path
9494+// returns an empty key without error — callers that need the key (e.g. when
9595+// creating new servers) must check for empty before use.
9496func readSSHPublicKey(path string) (string, error) {
9597 if path == "" {
9696- return "", fmt.Errorf("--ssh-key is required (path to SSH public key file)")
9898+ return "", nil
9799 }
98100 data, err := os.ReadFile(path)
99101 if err != nil {