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
3import (
4 "context"
5 "net/http"
6 "strings"
7 "testing"
8 "time"
9
10 "github.com/carlosframework/platform/internal/hostclaim"
11 "github.com/carlosframework/platform/internal/provision"
12 "github.com/carlosframework/platform/internal/release"
13)
14
15// A move request is refused while a run is open (run spec §5.1, codex 1).
16// The legacy door, live while CARLOS_MOVES_PHASE1 is off, says RUN-C43.
17// The target is armed with a staging bucket the way TestAPIInstanceMoveVerbs
18// (apiinstancemove_test.go:27) arms it, so the only refusal left is the run.
19func TestMoveDoorRefusesWhileARunIsOpen(t *testing.T) {
20 fx := runFixture(t)
21 id := fx.startRun(t)
22 if _, err := fx.rel.Put(context.Background(), "control/edges/uk-box.json",
23 []byte(`{"v":1,"box":"uk-box","eip":"1.2.3.4","placement":"uk","data_bucket":"data-uk","data_bucket_region":"eu-west-2","hosts":{}}`), ""); err != nil {
24 t.Fatal(err)
25 }
26 u := fx.ts.URL + "/api/cli/apps/bab/shop/instances/" + runTestHost + "/move"
27 status, _, body := rawReq(t, http.MethodPost, u, fx.member, `{"region":"uk"}`)
28 if status != http.StatusConflict || !strings.Contains(body, "a run is open on this instance: "+id+". wait for it to finish or cancel it, then move the instance") {
29 t.Fatalf("legacy move: %d %s", status, body)
30 }
31 if _, err := provision.SetRecordRun(context.Background(), fx.rel, release.OpsAccount, "shop", runTestHost, provision.RunExpire(id)); err != nil {
32 t.Fatal(err)
33 }
34 if status, _, body := rawReq(t, http.MethodPost, u, fx.member, `{"region":"uk"}`); status != http.StatusAccepted {
35 t.Fatalf("a move after the run ended: %d %s", status, body)
36 }
37}
38
39// movePhase1RunFixture is an operator fixture with acme created in ie, both
40// boxes armed, acme's serving claim written and the Phase 1 switch on.
41func movePhase1RunFixture(t *testing.T) (fx *consoleFixture, token, moveURL string) {
42 t.Helper()
43 fx, token = operatorInstanceFixture(t, "code-inst-move-run")
44 m := titoManifest()
45 m.Domain = "example.com"
46 m.Regions = nil
47 m.Pool = ""
48 enableInstances(t, fx, "titogo", m)
49 u := instancesURL(fx, "bab", "titogo")
50 if code, _, out := rawReq(t, http.MethodPost, u, token, `{"host":"acme.example.com","region":"ie"}`); code != http.StatusCreated {
51 t.Fatalf("create = %d %s", code, out)
52 }
53 armMoveFleet(t, fx, ieBox("ie-box"), ukBox("uk-box"))
54 claimFor(t, fx, "acme.example.com", "ie-box", "ie")
55 return fx, token, u + "/acme.example.com/move"
56}
57
58func admitRun(t *testing.T, rel release.Store, id string) {
59 t.Helper()
60 if _, err := provision.SetRecordRun(context.Background(), rel, "bab", "titogo", "acme.example.com",
61 provision.RunAdmit(provision.RunAdmission{ID: id, By: "alice@keymail.dev", Deadline: time.Now().Add(time.Minute)})); err != nil {
62 t.Fatal(err)
63 }
64}
65
66// The Phase 1 door (switch on) refuses a move while a run is open before it
67// touches the claim, with its generic sentence (index item 11): the run's id
68// is not a sentence the Phase 1 table knows.
69func TestPhase1MoveDoorRefusesWhileARunIsOpen(t *testing.T) {
70 fx, token, moveURL := movePhase1RunFixture(t)
71 ctx := context.Background()
72 admitRun(t, fx.rel, "run-p1")
73 code, _, out := rawReq(t, http.MethodPost, moveURL, token, `{"region":"uk"}`)
74 if code != http.StatusConflict || errorText(out) != "this move's state does not allow that. read its state and try again" {
75 t.Fatalf("phase 1 move with a run open = %d %s", code, out)
76 }
77 obs, err := hostclaim.Read(ctx, fx.rel, "acme.example.com")
78 if err != nil {
79 t.Fatal(err)
80 }
81 if obs.Claim.Op.Kind != hostclaim.KindServing {
82 t.Fatalf("claim op = %+v, want it untouched (serving)", obs.Claim.Op)
83 }
84 if _, err := provision.SetRecordRun(ctx, fx.rel, "bab", "titogo", "acme.example.com", provision.RunExpire("run-p1")); err != nil {
85 t.Fatal(err)
86 }
87 if code, _, out := rawReq(t, http.MethodPost, moveURL, token, `{"region":"uk"}`); code != http.StatusAccepted {
88 t.Fatalf("phase 1 move after the run ended = %d %s", code, out)
89 }
90}
91
92// beforePut runs fn once, just before the first write of key.
93type beforePut struct {
94 release.Store
95 key string
96 fn func()
97 done bool
98}
99
100func (b *beforePut) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) {
101 if key == b.key && !b.done {
102 b.done = true
103 b.fn()
104 }
105 return b.Store.Put(ctx, key, body, ifMatch)
106}
107
108// A run admitted after the Phase 1 door's probe passed and before its
109// record write (here: just before the claim CAS) makes MoveStartTransition
110// refuse inside SetRecordMove. That is "not applied": the moving claim goes
111// back to serving, no move is journaled, and the run is left alone.
112func TestPhase1MoveDoorUnwindsWhenARunIsAdmittedAfterTheProbe(t *testing.T) {
113 fx, token, moveURL := movePhase1RunFixture(t)
114 ctx := context.Background()
115 rel := fx.rel
116 hook := &beforePut{Store: rel, key: "control/hosts/acme.example.com.json", fn: func() { admitRun(t, rel, "run-race") }}
117 fx.srv.cfg.Releases = hook
118 code, _, out := rawReq(t, http.MethodPost, moveURL, token, `{"region":"uk"}`)
119 fx.srv.cfg.Releases = rel
120 if !hook.done {
121 t.Fatal("the run was never admitted")
122 }
123 if code != http.StatusBadGateway || errorText(out) != "could not record the move. try again" {
124 t.Fatalf("move racing a run = %d %s", code, out)
125 }
126 obs, err := hostclaim.Read(ctx, rel, "acme.example.com")
127 if err != nil {
128 t.Fatal(err)
129 }
130 if obs.Claim.Op.Kind != hostclaim.KindServing || obs.Claim.Op.Exec != "ie-box" || obs.Claim.Op.Journal["released_move"] == "" {
131 t.Fatalf("claim op = %+v, want serving under ie-box with the move released", obs.Claim.Op)
132 }
133 rec, err := provision.GetRecord(ctx, rel, "bab", "titogo", "acme.example.com")
134 if err != nil {
135 t.Fatal(err)
136 }
137 if rec.Move != nil || rec.ActiveRun == nil || rec.ActiveRun.ID != "run-race" || !rec.ActiveRun.Open() {
138 t.Fatalf("record move %+v run %+v, want no move and the run still open", rec.Move, rec.ActiveRun)
139 }
140}
141