Files
at-container-registry/deploy/upcloud/teardown.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

121 lines
2.9 KiB
Go

package main
import (
"context"
"fmt"
"time"
"github.com/UpCloudLtd/upcloud-go-api/v8/upcloud/request"
"github.com/spf13/cobra"
)
var teardownCmd = &cobra.Command{
Use: "teardown",
Short: "Destroy all infrastructure",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
token, _ := cmd.Root().PersistentFlags().GetString("token")
return cmdTeardown(token)
},
}
func init() {
rootCmd.AddCommand(teardownCmd)
}
func cmdTeardown(token string) error {
state, err := loadState()
if err != nil {
return err
}
naming := state.Naming()
// Confirmation prompt
fmt.Printf("This will DESTROY all %s infrastructure:\n", naming.DisplayName())
fmt.Printf(" Zone: %s\n", state.Zone)
fmt.Printf(" Appview: %s (%s)\n", state.Appview.UUID, state.Appview.PublicIP)
fmt.Printf(" Hold: %s (%s)\n", state.Hold.UUID, state.Hold.PublicIP)
if state.Scanner.UUID != "" {
fmt.Printf(" Scanner: %s (%s)\n", state.Scanner.UUID, state.Scanner.PublicIP)
}
fmt.Printf(" Network: %s\n", state.Network.UUID)
fmt.Printf(" LB: %s\n", state.LB.UUID)
fmt.Println()
fmt.Print("Type 'yes' to confirm: ")
if answer, _ := readLine(); answer != "yes" {
fmt.Println("Aborted.")
return nil
}
svc, err := newService(token)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
// Delete LB first (depends on network)
if state.LB.UUID != "" {
fmt.Printf("Deleting load balancer %s...\n", state.LB.UUID)
if err := svc.DeleteLoadBalancer(ctx, &request.DeleteLoadBalancerRequest{
UUID: state.LB.UUID,
}); err != nil {
fmt.Printf(" Warning: %v\n", err)
}
}
// Stop and delete servers (must stop before delete, and delete storage)
for _, s := range []struct {
name string
uuid string
}{
{"appview", state.Appview.UUID},
{"hold", state.Hold.UUID},
{"scanner", state.Scanner.UUID},
} {
if s.uuid == "" {
continue
}
fmt.Printf("Stopping server %s (%s)...\n", s.name, s.uuid)
_, err := svc.StopServer(ctx, &request.StopServerRequest{
UUID: s.uuid,
})
if err != nil {
fmt.Printf(" Warning (stop): %v\n", err)
} else {
_, _ = svc.WaitForServerState(ctx, &request.WaitForServerStateRequest{
UUID: s.uuid,
DesiredState: "stopped",
})
}
fmt.Printf("Deleting server %s...\n", s.name)
if err := svc.DeleteServerAndStorages(ctx, &request.DeleteServerAndStoragesRequest{
UUID: s.uuid,
}); err != nil {
fmt.Printf(" Warning (delete): %v\n", err)
}
}
// Delete network (after servers are gone)
if state.Network.UUID != "" {
fmt.Printf("Deleting network %s...\n", state.Network.UUID)
if err := svc.DeleteNetwork(ctx, &request.DeleteNetworkRequest{
UUID: state.Network.UUID,
}); err != nil {
fmt.Printf(" Warning: %v\n", err)
}
}
// Remove state file
if err := deleteState(); err != nil {
return err
}
fmt.Println("\nTeardown complete. All infrastructure destroyed.")
return nil
}