Files
at-container-registry/deploy/upcloud/state.go
T
Evan JarrettandClaude Fable 5.1 83092d9aee deploy: give the scanner its own server, and stop the tool from undoing production fixes on provision
The scanner shared the hold's 1 GB host and thrashed it twice: 11 hours on
2026-09-12 and again on the 13th (644 MiB resident plus 1.9 GB of swap, 504
on every repo page). It is memory-bound, not CPU-bound, so it now gets a
dedicated STARTER-2xCPU-4GB server: own state entry, own plan flag (pinned
name, shape match if UpCloud renames the tier again, picker last), own
cloud-init, firewall, `update scanner`, `ssh scanner`, status, backup and
teardown. Its config reaches the hold over the private network and its unit
sets MemorySwapMax=0 so an overshoot is an OOM kill and a restart, not a
wedged host. The hold's cloud-init and update paths no longer carry it.

Three defects the first provision run exposed, all fixed here:

- Frontend HTTP/2 defaulted to on and was reconciled onto the LB every run.
  Re-enabling it on the 12th stranded the appview<->hold connections for
  25 minutes. Default is now off and reconciled off, with a guard test.
- The TLS step requested Let's Encrypt bundles for every registry domain,
  re-adding the .cr ones that were removed when those moved behind Bunny.
  It now skips any domain whose DNS does not resolve to the LB.
- Each prompt built its own bufio.Scanner on stdin, so the first swallowed
  every piped answer and the second read EOF and took the default, which
  re-ran cloud-init on the production hold. One shared reader, and no answer
  now means skip.

Also: STARTER- plans take standard storage (maxiops fails with TIER_INVALID),
and the cloud-init wait polls for up to 20 minutes instead of one SSH call
capped at five, which a first boot with npm exceeds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hho5da4daoCoPBJ9tCrL7s
2026-09-12 21:49:26 -05:00

96 lines
2.6 KiB
Go

package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"runtime"
)
// InfraState persists infrastructure resource UUIDs between commands.
type InfraState struct {
Zone string `json:"zone"`
ClientName string `json:"client_name,omitempty"`
RepoBranch string `json:"repo_branch,omitempty"`
Network StateRef `json:"network"`
Appview ServerState `json:"appview"`
Hold ServerState `json:"hold"`
Scanner ServerState `json:"scanner,omitempty"`
LB StateRef `json:"loadbalancer"`
ObjectStorage ObjectStorageState `json:"object_storage"`
ScannerEnabled bool `json:"scanner_enabled,omitempty"`
ScannerSecret string `json:"scanner_secret,omitempty"`
LabelerEnabled bool `json:"labeler_enabled,omitempty"`
}
// Naming returns a Naming helper, defaulting to "seamark" if ClientName is empty.
func (s *InfraState) Naming() Naming {
name := s.ClientName
if name == "" {
name = "seamark"
}
return Naming{ClientName: name}
}
// Branch returns the repo branch, defaulting to "main" if empty.
func (s *InfraState) Branch() string {
if s.RepoBranch == "" {
return "main"
}
return s.RepoBranch
}
type StateRef struct {
UUID string `json:"uuid"`
}
type ServerState struct {
UUID string `json:"server_uuid"`
PublicIP string `json:"public_ip"`
PrivateIP string `json:"private_ip"`
}
type ObjectStorageState struct {
UUID string `json:"uuid"`
Endpoint string `json:"endpoint"`
Region string `json:"region"`
Bucket string `json:"bucket"`
AccessKeyID string `json:"access_key_id"`
}
func statePath() string {
_, thisFile, _, _ := runtime.Caller(0)
return filepath.Join(filepath.Dir(thisFile), "state.json")
}
func loadState() (*InfraState, error) {
data, err := os.ReadFile(statePath())
if err != nil {
return nil, fmt.Errorf("read state.json: %w (run 'provision' first)", err)
}
var st InfraState
if err := json.Unmarshal(data, &st); err != nil {
return nil, fmt.Errorf("parse state.json: %w", err)
}
return &st, nil
}
func saveState(st *InfraState) error {
data, err := json.MarshalIndent(st, "", " ")
if err != nil {
return fmt.Errorf("marshal state: %w", err)
}
if err := os.WriteFile(statePath(), data, 0644); err != nil {
return fmt.Errorf("write state.json: %w", err)
}
return nil
}
func deleteState() error {
if err := os.Remove(statePath()); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("remove state.json: %w", err)
}
return nil
}