carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostops
2
3import (
4 "context"
5 "reflect"
6 "testing"
7
8 "github.com/carlosframework/platform/internal/hostclaim"
9 "github.com/carlosframework/platform/internal/moves"
10 "github.com/carlosframework/platform/internal/provision"
11)
12
13// A source that never had a session (claim spec §6.4a) is the paired legacy
14// identity {session 0, generation ""}. The move's cut then names session 0
15// and an empty source generation; a mismatched pair or a missing journal
16// key is still refused.
17
18// legacySource makes the holder the legacy source before the target's first
19// admission, with the move's cut at the legacy identity.
20func legacySource(covered bool, afterAdmission bool) func(*hostclaim.Claim, *provision.Record, *AppAdmission) {
21 return func(c *hostclaim.Claim, _ *provision.Record, _ *AppAdmission) {
22 h := c.Holder
23 h.WriterBox, h.Session, h.Generation = "ie-box", 0, ""
24 h.Admissions = nil
25 if afterAdmission {
26 h.WriterBox, h.Session, h.Generation = "uk-box", 1, "mv"
27 h.Admissions = []hostclaim.AdmissionRecord{{Session: 1, Box: "uk-box", Generation: "mv", Predecessor: "", Region: "uk", PredecessorRegion: "ie", Mode: "cold", Coverage: "sealed"}}
28 }
29 c.Op.Journal[hostclaim.JournalCutSession] = "0"
30 c.Op.Journal[hostclaim.JournalSrcGen] = ""
31 if covered {
32 seal, err := moves.DecodeSeal(c.Op.Journal[hostclaim.JournalSeal])
33 if err != nil {
34 panic(err)
35 }
36 seal.SrcCovered = true
37 c.Op.Journal[hostclaim.JournalSeal] = seal.Encode()
38 }
39 }
40}
41
42func declareLegacy(t *testing.T, w *lossWorld) []hostclaim.WriteSource {
43 t.Helper()
44 in := w.inspect(t, "uk-label")
45 if _, err := w.declare(w.submission(in)); err != nil {
46 t.Fatal(err)
47 }
48 loss, err := hostclaim.ReadLossRecord(context.Background(), w.raw, w.pub, in.Details.ID)
49 if err != nil || loss.Record.State != "committed" {
50 t.Fatalf("loss record = %+v %v", loss.Record, err)
51 }
52 return loss.Record.WriteSources
53}
54
55func TestJ4LegacySourceCutDeclares(t *testing.T) {
56 sourceCut := hostclaim.WriteSource{Reason: "source-cut", Box: "ie-box", Generation: "", Session: 0, Region: "ie", Baseline: "seal:" + lossOp}
57 cases := map[string]struct {
58 covered, admitted bool
59 want []hostclaim.WriteSource
60 }{
61 "before first admission": {false, false, []hostclaim.WriteSource{sourceCut}},
62 "after first admission": {false, true, []hostclaim.WriteSource{
63 {Reason: "target", Box: "uk-box", Generation: "mv", Session: 1, Region: "uk", Baseline: "replica:uk/mv"}, sourceCut}},
64 "covered after first admission": {true, true, []hostclaim.WriteSource{
65 {Reason: "target", Box: "uk-box", Generation: "mv", Session: 1, Region: "uk", Baseline: "replica:uk/mv"}}},
66 }
67 for name, tc := range cases {
68 t.Run(name, func(t *testing.T) {
69 w := newLossWorld(t, "landing", legacySource(tc.covered, tc.admitted))
70 if got := declareLegacy(t, w); !reflect.DeepEqual(got, tc.want) {
71 t.Fatalf("write sources = %+v, want %+v", got, tc.want)
72 }
73 })
74 }
75}
76
77func TestJ4LegacyCoveredBeforeAdmissionDeclares(t *testing.T) {
78 w := newLossWorld(t, "landing", legacySource(true, false))
79 if got := declareLegacy(t, w); len(got) != 0 {
80 t.Fatalf("write sources = %+v", got)
81 }
82}
83
84func TestJ4LegacyJournalRefusedBeforeAcceptance(t *testing.T) {
85 set := func(key, v string) func(*hostclaim.Claim, *provision.Record, *AppAdmission) {
86 return func(c *hostclaim.Claim, _ *provision.Record, _ *AppAdmission) { c.Op.Journal[key] = v }
87 }
88 del := func(key string) func(*hostclaim.Claim, *provision.Record, *AppAdmission) {
89 return func(c *hostclaim.Claim, _ *provision.Record, _ *AppAdmission) { delete(c.Op.Journal, key) }
90 }
91 both := func(fs ...func(*hostclaim.Claim, *provision.Record, *AppAdmission)) func(*hostclaim.Claim, *provision.Record, *AppAdmission) {
92 return func(c *hostclaim.Claim, r *provision.Record, a *AppAdmission) {
93 for _, f := range fs {
94 f(c, r, a)
95 }
96 }
97 }
98 legacy := legacySource(false, false)
99 cases := map[string]func(*hostclaim.Claim, *provision.Record, *AppAdmission){
100 "missing cut session": both(legacy, del(hostclaim.JournalCutSession)),
101 "malformed cut session": both(legacy, set(hostclaim.JournalCutSession, "x")),
102 "negative cut session": both(legacy, set(hostclaim.JournalCutSession, "-1")),
103 "empty cut session": both(legacy, set(hostclaim.JournalCutSession, "")),
104 "missing source generation": both(legacy, del(hostclaim.JournalSrcGen)),
105 "session 0 with generation": both(legacy, set(hostclaim.JournalSrcGen, "src")),
106 "session with empty generation": both(legacy, set(hostclaim.JournalCutSession, "15")),
107 "covered missing cut session": both(legacySource(true, false), del(hostclaim.JournalCutSession)),
108 "covered malformed cut": both(legacySource(true, false), set(hostclaim.JournalCutSession, "x")),
109 }
110 for name, mutate := range cases {
111 t.Run(name, func(t *testing.T) {
112 w := newLossWorld(t, "landing", mutate)
113 snap := lossSnapshot(t, w)
114 if _, err := w.control.InspectMoveLoss(context.Background(), w.signer, lossBucket, "operator", "bab", "app", lossHost, "uk-box"); err == nil {
115 t.Fatal("inspection accepted")
116 }
117 if lossSnapshot(t, w) != snap {
118 t.Fatal("a refused inspection wrote")
119 }
120 if len(w.acceptances(t)) != 0 || w.roster(t).Roster.Boxes["uk-box"].State == "quarantined" {
121 t.Fatal("accepted or quarantined")
122 }
123 })
124 }
125}
126