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 "bytes"
5 "context"
6 "crypto/ed25519"
7 "encoding/json"
8 "errors"
9 "strings"
10 "testing"
11 "time"
12
13 "github.com/carlosframework/platform/internal/hostactors"
14 "github.com/carlosframework/platform/internal/hostfloor"
15 "github.com/carlosframework/platform/internal/release"
16 "github.com/carlosframework/platform/internal/status"
17)
18
19type activationFaultStore struct {
20 release.Store
21 fail bool
22 commit bool
23 hide bool
24}
25
26func (s *activationFaultStore) Put(ctx context.Context, key string, raw []byte, etag string) (string, error) {
27 if key != release.SystemPointerKey("router", "stable") || !s.fail {
28 return s.Store.Put(ctx, key, raw, etag)
29 }
30 s.fail = false
31 if s.commit {
32 if _, err := s.Store.Put(ctx, key, raw, etag); err != nil {
33 return "", err
34 }
35 }
36 return "", errors.New("publication response lost")
37}
38func (s *activationFaultStore) Get(ctx context.Context, key string) ([]byte, string, error) {
39 if s.hide && key == release.SystemPointerKey("router", "stable") && !s.fail {
40 s.hide = false
41 return nil, "", errors.New("publication read unavailable")
42 }
43 return s.Store.Get(ctx, key)
44}
45
46func activationFixture(t *testing.T) (*Server, hostactors.Directory, release.SystemRelease) {
47 t.Helper()
48 ctx := t.Context()
49 pub, key, err := ed25519.GenerateKey(nil)
50 if err != nil {
51 t.Fatal(err)
52 }
53 store := release.NewDirStore(t.TempDir())
54 s := &Server{cfg: Config{Releases: store, Key: key, Bucket: "bucket"}}
55 sg := release.RootSigner(key)
56 old := release.SystemRelease{Version: "v1.0.0", Source: "release:v1.0.0", ReleaseTag: "v1.0.0", MovesProtocol: 1, Artifacts: map[string]release.SystemArtifact{"linux-amd64": {Key: release.SystemArtifactKey("v1.0.0", "linux-amd64"), SHA256: strings.Repeat("a", 64)}}}
57 next := release.SystemRelease{Version: "v1.1.0", Source: "release:v1.1.0", ReleaseTag: "v1.1.0", MovesProtocol: 1, Artifacts: map[string]release.SystemArtifact{"linux-amd64": {Key: release.SystemArtifactKey("v1.1.0", "linux-amd64"), SHA256: strings.Repeat("b", 64)}}}
58 for _, rel := range []release.SystemRelease{next, old} {
59 if _, err := release.PromoteSystem(ctx, store, sg, "router", "stable", rel, release.SystemPromoteOptions{}); err != nil {
60 t.Fatal(err)
61 }
62 raw, _, err := store.Get(ctx, release.SystemPointerKey("router", "stable"))
63 if err != nil {
64 t.Fatal(err)
65 }
66 if err := release.PublishSystemIdentity(ctx, store, pub, raw); err != nil {
67 t.Fatal(err)
68 }
69 }
70 if err := hostfloor.Publish(ctx, store, sg, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.0.0"}); err != nil {
71 t.Fatal(err)
72 }
73 floor, err := hostfloor.Read(ctx, store, pub, "bucket")
74 if err != nil {
75 t.Fatal(err)
76 }
77 enrollment := strings.Repeat("e", 32)
78 roster := hostactors.Roster{V: 1, DeploymentBucket: "bucket", Revision: 1, Publication: "published", FloorSHA256: floor.Digest, Boxes: map[string]hostactors.Box{"box": {BoxID: "box", Namespace: "platform", Label: "box", Region: "ie", Pool: "default", Cloud: hostactors.Cloud{Provider: "aws", Account: "123", Region: "eu-west-1", InstanceID: "i-box"}, Enrollment: enrollment, EvidenceSHA256: strings.Repeat("f", 64), State: "active"}}, Consoles: map[string]hostactors.Console{}, Holds: map[string]hostactors.Reservation{}, Activations: map[string]hostactors.Reservation{}}
79 raw, err := release.SignRootEnvelope(sg, roster)
80 if err != nil {
81 t.Fatal(err)
82 }
83 if _, err := store.Put(ctx, hostactors.Key, raw, ""); err != nil {
84 t.Fatal(err)
85 }
86 box := status.Box{GeneratedAt: time.Now(), Metrics: &status.BoxMetrics{Enrollment: enrollment, InstanceID: "i-box", Release: old.Version, ReleaseTag: old.ReleaseTag, Platform: "linux-amd64", BinarySHA256: old.Artifacts["linux-amd64"].SHA256, MovesProtocol: 1, BootID: "boot", FloorSHA256: floor.Digest, FloorGuardReady: true, HostClaimEnabled: true, SystemChannel: "stable"}}
87 raw, err = json.Marshal(box)
88 if err != nil {
89 t.Fatal(err)
90 }
91 if _, err := store.Put(ctx, "status/boxes/box.json", raw, ""); err != nil {
92 t.Fatal(err)
93 }
94 return s, hostactors.Directory{Store: store, PublicKey: pub, Bucket: "bucket"}, next
95}
96
97// Legacy promotion needs both floor objects absent; either half alone is
98// an unfinished publication (review round 4).
99func TestHostUpdateReservationRefusesIncompleteFloorPair(t *testing.T) {
100 for _, drop := range []string{hostfloor.Key, hostfloor.AuthKey} {
101 t.Run(drop, func(t *testing.T) {
102 s, _, target := activationFixture(t)
103 if err := s.cfg.Releases.Delete(t.Context(), drop); err != nil {
104 t.Fatal(err)
105 }
106 if id, err := s.reserveHostUpdate(t.Context(), &target, "stable", "promote", release.SystemPromoteOptions{}); !errors.Is(err, hostfloor.ErrFloor) || id != "" {
107 t.Fatalf("incomplete pair without %s = %q, %v; want ErrFloor", drop, id, err)
108 }
109 })
110 }
111}
112
113func TestHostUpdateReservationRetriesPublication(t *testing.T) {
114 for _, fault := range []string{"crash", "failed", "committed", "unreadable"} {
115 t.Run(fault, func(t *testing.T) {
116 s, d, target := activationFixture(t)
117 id, err := s.reserveHostUpdate(t.Context(), &target, "stable", "promote", release.SystemPromoteOptions{})
118 if err != nil || id == "" {
119 t.Fatal(id, err)
120 }
121 first, err := d.Read(t.Context())
122 if err != nil {
123 t.Fatal(err)
124 }
125 if fault != "crash" {
126 broken := &activationFaultStore{Store: d.Store, fail: true, commit: fault != "failed", hide: fault == "unreadable"}
127 s.cfg.Releases = broken
128 _, err = s.promoteHostUpdate(t.Context(), target, "router", "stable", release.SystemPromoteOptions{})
129 if (err == nil) != (fault == "committed") {
130 t.Fatalf("first publication: %v", err)
131 }
132 }
133 // A new console process has no in-memory job or reservation state.
134 s = &Server{cfg: Config{Releases: d.Store, Key: s.cfg.Key, Bucket: "bucket"}}
135 p, err := s.promoteHostUpdate(t.Context(), target, "router", "stable", release.SystemPromoteOptions{})
136 if err != nil || p.ActivationID != id || p.Current.Version != target.Version {
137 t.Fatal(p, err)
138 }
139 after, err := d.Read(t.Context())
140 if err != nil || len(after.Roster.Activations) != 1 {
141 t.Fatal(after, err)
142 }
143 if !bytes.Equal(first.Roster.Activations[id].Publication.After, after.Roster.Activations[id].Publication.After) {
144 t.Fatal("retry replaced publication identity")
145 }
146 })
147 }
148}
149
150func TestHostRollbackResumesWithoutPoppingAgain(t *testing.T) {
151 s, d, _ := activationFixture(t)
152 broken := &activationFaultStore{Store: d.Store, fail: true}
153 s.cfg.Releases = broken
154 if _, err := s.rollbackHostUpdate(t.Context(), "router", "stable", "operator"); err == nil {
155 t.Fatal("missing injected failure")
156 }
157 before, err := d.Read(t.Context())
158 if err != nil || len(before.Roster.Activations) != 1 {
159 t.Fatal(before, err)
160 }
161 var id string
162 for id = range before.Roster.Activations {
163 }
164 s.cfg.Releases = d.Store
165 first, err := s.rollbackHostUpdate(t.Context(), "router", "stable", "operator")
166 if err != nil || first.ActivationID != id || first.Current.Version != "v1.1.0" {
167 t.Fatal(first, err)
168 }
169 second, err := s.rollbackHostUpdate(t.Context(), "router", "stable", "operator")
170 if err != nil || second.ActivationID != id || len(second.History) != len(first.History) {
171 t.Fatal(second, err)
172 }
173}
174
175func TestHostUpdateCancellationBlocksDelayedPublication(t *testing.T) {
176 s, d, target := activationFixture(t)
177 id, err := s.reserveHostUpdate(t.Context(), &target, "stable", "promote", release.SystemPromoteOptions{})
178 if err != nil {
179 t.Fatal(err)
180 }
181 before, err := d.Read(t.Context())
182 if err != nil {
183 t.Fatal(err)
184 }
185 stale := *before.Roster.Activations[id].Publication
186 if err := d.CancelActivation(t.Context(), release.RootSigner(s.cfg.Key), id); err != nil {
187 t.Fatal(err)
188 }
189 if _, err := release.PublishSystemPublication(t.Context(), d.Store, d.PublicKey, stale); !errors.Is(err, release.ErrCASConflict) {
190 t.Fatalf("delayed publication: %v", err)
191 }
192 after, err := d.Read(t.Context())
193 if err != nil || len(after.Roster.Activations) != 0 {
194 t.Fatal(after, err)
195 }
196 p, err := s.promoteHostUpdate(t.Context(), target, "router", "stable", release.SystemPromoteOptions{})
197 if err != nil || p.ActivationID == id {
198 t.Fatal(p, err)
199 }
200 if err := d.CancelActivation(t.Context(), release.RootSigner(s.cfg.Key), p.ActivationID); !errors.Is(err, release.ErrCASConflict) {
201 t.Fatalf("cancelled published activation: %v", err)
202 }
203 after, err = d.Read(t.Context())
204 if err != nil || len(after.Roster.Activations) != 1 {
205 t.Fatal(after, err)
206 }
207}
208