carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3// The seam between the two halves of instance provisioning: the console
4// verbs that write records to the bucket (apiinstances.go) and the agent
5// pass that turns them into routes (internal/provision). They share no code
6// path and no process — only key shapes and an account sqid — which is
7// exactly the kind of agreement that breaks silently.
8
9import (
10 "context"
11 "net/http"
12 "path/filepath"
13 "testing"
14 "time"
15
16 "github.com/carlosframework/platform/internal/provision"
17 "github.com/carlosframework/platform/internal/registry"
18)
19
20// TestAPIInstanceReconcileSeam is the seam spec §13 names: the console's
21// verbs and the agent's reconcile pass, joined by nothing but the bucket.
22// Every literal that could drift between them is computed, never typed —
23// the account segment comes from console's own accountSqid (domains.Sqid),
24// the record key from provision.RecordKey, the route's paths from the
25// provisioner's shared helpers — because the failure this pins is silent:
26// a console writing apps/<sqidA>/... and an agent parsing apps/<sqidB>/...
27// both "work", and instances simply never appear.
28//
29// It runs on a NON-ops account deliberately: ops's sqid is hardcoded known
30// on the agent side (domains.OpsSqid), so an ops-only test would pass with
31// the sqid agreement completely broken.
32func TestAPIInstanceReconcileSeam(t *testing.T) {
33 fx := newConsoleFixture(t)
34 token := cliToken(t, fx, "alice@keymail.dev", "code-inst-seam")
35 now := time.Now()
36 const acctID int64 = 4242
37 sqid := accountSqid(acctID)
38 if err := fx.st.CreateAccount(acctID, sqid, "Tito", "alice@keymail.dev", now); err != nil {
39 t.Fatal(err)
40 }
41 if _, err := fx.st.CreateAppIn(acctID, "titogo", "alice@keymail.dev", now); err != nil {
42 t.Fatal(err)
43 }
44 // The account manifest is how the agent learns this sqid exists at all
45 // (ParseRecordKey refuses keys under an unknown account).
46 if err := WriteAccountManifest(context.Background(), fx.rel, acctID, sqid, "Tito", now); err != nil {
47 t.Fatal(err)
48 }
49 if err := provision.PutManifest(context.Background(), fx.rel, sqid, "titogo", titoManifest()); err != nil {
50 t.Fatal(err)
51 }
52
53 u := instancesURL(fx, sqid, "titogo")
54 const host = "acme.go.tito.io"
55 if status, _, out := rawReq(t, http.MethodPost, u, token,
56 `{"host":"`+host+`","region":"ie"}`); status != http.StatusCreated {
57 t.Fatalf("create = %d %s", status, out)
58 }
59
60 // A hand-added neighbour in the SAME scope: not provisioned, so the
61 // removal phase has no authority over it however the rest of the pass
62 // goes.
63 dataDir := t.TempDir()
64 neighbour := registry.Route{
65 Host: "hand.go.tito.io", Slug: "hand", Kind: "instance",
66 Socket: provision.DefaultSocket("hand.go.tito.io"),
67 DBPath: provision.DefaultDBPath(dataDir, "hand.go.tito.io"),
68 App: "titogo", Account: sqid, Pool: "tito", Region: "ie",
69 Channel: "stable", Status: "ready",
70 }
71 reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db"))
72 if err != nil {
73 t.Fatal(err)
74 }
75 t.Cleanup(func() { reg.Close() })
76 if err := reg.Upsert(neighbour); err != nil {
77 t.Fatal(err)
78 }
79 before, err := reg.Get(neighbour.Host)
80 if err != nil || before == nil {
81 t.Fatalf("neighbour route: %v", err)
82 }
83
84 cfg := provision.ReconcileConfig{Region: "ie", Pools: []string{"tito"}, DataDir: dataDir}
85 if err := provision.Reconcile(context.Background(), fx.rel, reg, cfg); err != nil {
86 t.Fatalf("reconcile: %v", err)
87 }
88 got, err := reg.Get(host)
89 if err != nil {
90 t.Fatal(err)
91 }
92 if got == nil {
93 t.Fatalf("the console's record never materialized as a route — the sqid or key shapes disagree")
94 }
95 want := registry.Route{
96 Host: host, Slug: "acme", Kind: "instance",
97 Socket: provision.DefaultSocket(host), DBPath: provision.DefaultDBPath(dataDir, host),
98 App: "titogo", Account: sqid, Pool: "tito", Region: "ie", Channel: "stable",
99 Hibernate: true, Provisioned: true, Seed: `["seed"]`, Status: "ready",
100 CreatedAt: got.CreatedAt,
101 }
102 if *got != want {
103 t.Fatalf("minted route =\n %+v\nwant\n %+v", *got, want)
104 }
105
106 // And back the other way: the DELETE verb is what retires the route.
107 if status, _, out := rawReq(t, http.MethodDelete, u+"/"+host, token, ""); status != http.StatusOK {
108 t.Fatalf("delete = %d %s", status, out)
109 }
110 if err := provision.Reconcile(context.Background(), fx.rel, reg, cfg); err != nil {
111 t.Fatalf("reconcile after delete: %v", err)
112 }
113 if gone, err := reg.Get(host); err != nil {
114 t.Fatal(err)
115 } else if gone != nil {
116 t.Fatalf("the route survived its record's deletion: %+v", gone)
117 }
118 after, err := reg.Get(neighbour.Host)
119 if err != nil || after == nil {
120 t.Fatalf("the hand-added neighbour was removed by a pass that never minted it: %v", err)
121 }
122 if *after != *before {
123 t.Fatalf("the hand-added neighbour changed under reconciliation:\n got %+v\nwant %+v", *after, *before)
124 }
125}
126