carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package host
4
5import (
6 "bytes"
7 "context"
8 "crypto/ed25519"
9 "log"
10 "os"
11 "path/filepath"
12 "slices"
13 "strings"
14 "testing"
15 "time"
16
17 "github.com/carlosframework/platform/internal/genselect"
18 "github.com/carlosframework/platform/internal/hostactors"
19 "github.com/carlosframework/platform/internal/hostbox"
20 "github.com/carlosframework/platform/internal/hostclaim"
21 "github.com/carlosframework/platform/internal/hostops"
22 "github.com/carlosframework/platform/internal/hostwrite"
23 "github.com/carlosframework/platform/internal/lease"
24 "github.com/carlosframework/platform/internal/park"
25 "github.com/carlosframework/platform/internal/registry"
26 "github.com/carlosframework/platform/internal/release"
27 "github.com/carlosframework/platform/internal/rootstate"
28)
29
30type fix4Owners struct{ ClaimOwnerBindings }
31
32func (o fix4Owners) OpenClaimOwners(context.Context, release.Store, ClaimRuntimeResources) (ClaimOwnerBindings, error) {
33 return o.ClaimOwnerBindings, nil
34}
35
36type fix4UnusedDependencies struct {
37 hostbox.LocalAuthority
38 StopControllerReader
39 lease.VersionedStore
40 ClaimWakeInputs
41 hostbox.RegionalEvidence
42}
43
44func fix4JournalFixture(t *testing.T) (release.Store, ClaimRuntimeResources, fix4Owners, string) {
45 t.Helper()
46 root := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{1}, ed25519.SeedSize))
47 evidence := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{2}, ed25519.SeedSize))
48 store := release.NewDirStore(t.TempDir())
49 identity := handoverRecoveryIdentity(t, hostbox.Actor{Box: "box", Enrollment: strings.Repeat("a", 32), Revision: 1, Binding: "1", Region: "ie", ControlBucket: "deployment"})
50 actor, err := identity.Verify(t.Context())
51 if err != nil {
52 t.Fatal(err)
53 }
54 row := hostactors.Box{BoxID: actor.Box, Enrollment: actor.Enrollment, Namespace: actor.Namespace, Label: actor.Box, Region: actor.Region, Pool: "default", Cloud: hostactors.Cloud{Provider: actor.Cloud.Provider, Account: actor.Cloud.Account, Region: actor.Cloud.Region, InstanceID: actor.Cloud.InstanceID}, EvidenceSHA256: strings.Repeat("c", 64), CensusPublicKey: evidence.Public().(ed25519.PublicKey), State: "active"}
55 roster := hostactors.Roster{V: 1, DeploymentBucket: actor.ControlBucket, Revision: 1, RequestLimitNS: int64(hostbox.RequestLimit), Boxes: map[string]hostactors.Box{actor.Box: row}, Consoles: map[string]hostactors.Console{}, Holds: map[string]hostactors.Reservation{}, Activations: map[string]hostactors.Reservation{}}
56 body, err := release.SignRootEnvelope(release.RootSigner(root), roster)
57 if err != nil {
58 t.Fatal(err)
59 }
60 if _, err = store.Put(t.Context(), hostactors.Key, body, ""); err != nil {
61 t.Fatal(err)
62 }
63 directory := hostactors.Directory{Store: store, PublicKey: root.Public().(ed25519.PublicKey), Bucket: actor.ControlBucket}
64 unused := fix4UnusedDependencies{}
65 owners := fix4Owners{ClaimOwnerBindings{
66 Identity: identity, RetirementAuthority: &hostops.ProductionAuthority{Store: store, Directory: directory, Now: time.Now}, EvidenceKey: evidence,
67 Local: unused, Controllers: unused, Data: unused, Inputs: unused, Evidence: unused,
68 InputPolicy: func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error { return nil },
69 MovesPolicy: func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error { return nil },
70 LeaseSet: func(context.Context, string, string) ([]string, error) { return nil, nil },
71 View: func(context.Context, hostclaim.Observed) (park.View, error) { return park.View{}, nil },
72 Authorize: func(context.Context, hostclaim.Observed, hostclaim.Obligation) error { return nil },
73 AdmissionEvidence: func(context.Context, hostclaim.Observed, hostclaim.Admission, *genselect.Plan) error { return nil },
74 PublicationEvidence: func(context.Context, hostclaim.Observed) error { return nil },
75 Close: func(context.Context) error { return nil },
76 }}
77 reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db"))
78 if err != nil {
79 t.Fatal(err)
80 }
81 t.Cleanup(func() { reg.Close() })
82 resources := ClaimRuntimeResources{Mode: "instance-control", Registry: reg, DataDir: t.TempDir(), OptDir: t.TempDir(), UnitsDir: t.TempDir()}
83 legacy := filepath.Join(resources.DataDir, ".hostclaims")
84 if err = os.Mkdir(legacy, 0700); err != nil {
85 t.Fatal(err)
86 }
87 journalPath := filepath.Join(t.TempDir(), "journals.db")
88 authorityDir := t.TempDir()
89 originalAuthority := openClaimRootAuthority
90 t.Cleanup(func() { openClaimRootAuthority = originalAuthority })
91 openClaimRootAuthority = func() (rootstate.Store, error) { return &rollbackDisk{dir: authorityDir}, nil }
92 original := openClaimRootJournal
93 t.Cleanup(func() { openClaimRootJournal = original })
94 openClaimRootJournal = func() (*hostbox.LaunchJournal, error) { return hostbox.OpenLaunchJournal(journalPath) }
95 return store, resources, owners, legacy
96}
97
98func TestPlan9Fix4RootAssemblyIgnoresReplacedLegacyDirectory(t *testing.T) {
99 fixtureWithoutRootMigrationCensus(t)
100 for _, mode := range []string{"instance-control", "adopt"} {
101 for _, replacement := range []string{"missing", "renamed", "symlink", "file"} {
102 t.Run(mode+"/"+replacement, func(t *testing.T) {
103 store, resources, owners, legacy := fix4JournalFixture(t)
104 resources.Mode = mode
105 if mode == "adopt" {
106 owners.EvidenceKey = nil
107 }
108 journal, err := openClaimRootJournal()
109 if err != nil {
110 t.Fatal(err)
111 }
112 service, err := hostbox.NewJournalService(journal)
113 if err != nil {
114 t.Fatal(err)
115 }
116 if alert, err := service.CheckLegacyMigration(t.Context(), legacy); alert != nil || err != nil {
117 t.Fatalf("marker: %v %v", alert, err)
118 }
119 if err = journal.Close(); err != nil {
120 t.Fatal(err)
121 }
122 target := t.TempDir()
123 if replacement == "renamed" {
124 err = os.Rename(legacy, legacy+"-moved")
125 } else {
126 err = os.Remove(legacy)
127 }
128 if err != nil {
129 t.Fatal(err)
130 }
131 switch replacement {
132 case "symlink":
133 err = os.Symlink(target, legacy)
134 case "file":
135 err = os.WriteFile(legacy, []byte("agent replacement"), 0640)
136 }
137 if err != nil {
138 t.Fatal(err)
139 }
140 var logs bytes.Buffer
141 previous := log.Writer()
142 log.SetOutput(&logs)
143 defer log.SetOutput(previous)
144 assembly, err := OpenProductionClaimAssembly(t.Context(), store, resources, owners)
145 if err != nil {
146 t.Fatalf("agent-controlled legacy directory prevented root startup: %v", err)
147 }
148 if err = assembly.Close(t.Context()); err != nil {
149 t.Fatal(err)
150 }
151 if !strings.Contains(logs.String(), "ignored after migration") {
152 t.Fatalf("missing legacy directory alert: %q", logs.String())
153 }
154 if replacement == "missing" || replacement == "renamed" {
155 if _, err = os.Lstat(legacy); !os.IsNotExist(err) {
156 t.Fatalf("root recreated agent directory: %v", err)
157 }
158 }
159 if replacement == "symlink" {
160 if got, err := os.Readlink(legacy); err != nil || got != target {
161 t.Fatalf("root changed symlink: %q %v", got, err)
162 }
163 }
164 if replacement == "file" {
165 if got, err := os.ReadFile(legacy); err != nil || string(got) != "agent replacement" {
166 t.Fatalf("root changed file: %q %v", got, err)
167 }
168 }
169 })
170 }
171 }
172}
173
174func TestPlan9Fix4MigrationRetryAfterCommitRecoversOwnedMask(t *testing.T) {
175 for _, removeLegacy := range []bool{false, true} {
176 t.Run(map[bool]string{false: "unchanged", true: "removed"}[removeLegacy], func(t *testing.T) {
177 store, resources, owners, legacy := fix4JournalFixture(t)
178 var calls []string
179 plan9Fix2Systemctl(t, "enabled", &calls)
180 owner := plan9Fix2MaskOwner(t)
181 launch, err := hostbox.OpenLaunchJournal(filepath.Join(legacy, "launches.db"))
182 if err != nil {
183 t.Fatal(err)
184 }
185 if err = launch.Close(); err != nil {
186 t.Fatal(err)
187 }
188 inventory, err := openLegacyInventory(t, filepath.Join(legacy, "inventory.db"))
189 if err != nil {
190 t.Fatal(err)
191 }
192 if err = inventory.Close(); err != nil {
193 t.Fatal(err)
194 }
195 journal, err := openClaimRootJournal()
196 if err != nil {
197 t.Fatal(err)
198 }
199 service, err := hostbox.NewJournalService(journal)
200 if err != nil {
201 t.Fatal(err)
202 }
203 err = service.MigrateLegacy(t.Context(), legacy, "box", strings.Repeat("a", 32), func(ctx context.Context) (func(), error) {
204 _, err := quiesceLegacyClaimJournals(ctx)
205 return func() {}, err
206 }, nil)
207 if err != nil {
208 t.Fatal(err)
209 }
210 if err = journal.Close(); err != nil {
211 t.Fatal(err)
212 }
213 raw, err := os.ReadFile(owner)
214 record, recorded := decodeClaimEdgeMaskRecord(raw, plan9Fix2Boot(t))
215 if err != nil || !recorded {
216 t.Fatalf("commit lost crash mask identity: %q %v", raw, err)
217 }
218 if ours, identityErr := claimEdgeMaskStillOwned(record); identityErr != nil || !ours {
219 t.Fatalf("commit changed crash mask identity: %v %v", ours, identityErr)
220 }
221 if removeLegacy {
222 if err = os.RemoveAll(legacy); err != nil {
223 t.Fatal(err)
224 }
225 }
226 calls = nil
227 plan9Fix2Systemctl(t, "masked-runtime", &calls)
228 if err = migrateClaimJournals(t.Context(), store, resources, owners); err != nil {
229 t.Fatal(err)
230 }
231 if !slices.Contains(calls, "unmask --runtime carlos-edge.service") {
232 t.Fatalf("committed retry left edge masked: %v", calls)
233 }
234 if _, err = os.Stat(owner); !os.IsNotExist(err) {
235 t.Fatalf("migration mask record remains: %v", err)
236 }
237 for _, call := range calls {
238 if strings.HasPrefix(call, "mask ") {
239 t.Fatalf("completed retry masked edge again: %v", calls)
240 }
241 }
242 })
243 }
244}
245
246func TestPlan9Fix4CompletedMigrationKeepsOperatorMask(t *testing.T) {
247 for _, test := range []struct{ name, state, record string }{
248 {"unrecorded-runtime", "masked-runtime", ""},
249 {"different-boot", "masked-runtime", "another-boot"},
250 {"persistent", "masked", "same-boot"},
251 {"unmasked", "enabled", "same-boot"},
252 } {
253 t.Run(test.name, func(t *testing.T) {
254 store, resources, owners, _ := fix4JournalFixture(t)
255 owner := plan9Fix2MaskOwner(t)
256 var calls []string
257 plan9Fix2Systemctl(t, test.state, &calls)
258 if err := migrateClaimJournals(t.Context(), store, resources, owners); err != nil {
259 t.Fatal(err)
260 }
261 record := test.record
262 if record == "same-boot" {
263 record = plan9Fix2Boot(t)
264 }
265 if record != "" {
266 if err := os.WriteFile(owner, []byte(record), 0600); err != nil {
267 t.Fatal(err)
268 }
269 }
270 calls = nil
271 if err := migrateClaimJournals(t.Context(), store, resources, owners); err != nil {
272 t.Fatal(err)
273 }
274 for _, call := range calls {
275 if strings.HasPrefix(call, "unmask") || strings.HasPrefix(call, "mask") {
276 t.Fatalf("operator state changed: %v", calls)
277 }
278 }
279 })
280 }
281}
282
283func TestPlan9Fix4CompletedMigrationRetainsRecordOnUnmaskFailure(t *testing.T) {
284 store, resources, owners, _ := fix4JournalFixture(t)
285 owner := plan9Fix2MaskOwner(t)
286 var calls []string
287 plan9Fix2Systemctl(t, "masked-runtime", &calls)
288 if err := migrateClaimJournals(t.Context(), store, resources, owners); err != nil {
289 t.Fatal(err)
290 }
291 record := plan11Rev4WriteOwnedMask(t, owner)
292 systemctl := claimSystemctl
293 claimSystemctl = func(ctx context.Context, args ...string) ([]byte, error) {
294 if args[0] == "unmask" {
295 return nil, os.ErrPermission
296 }
297 return systemctl(ctx, args...)
298 }
299 if err := migrateClaimJournals(t.Context(), store, resources, owners); err == nil {
300 t.Fatal("unmask failure reported success")
301 }
302 if raw, err := os.ReadFile(owner); err != nil {
303 t.Fatalf("failed recovery discarded mask ownership: %q %v", raw, err)
304 } else if got, ok := decodeClaimEdgeMaskRecord(raw, plan9Fix2Boot(t)); !ok || got != record {
305 t.Fatalf("failed recovery changed mask ownership: %q", raw)
306 }
307 claimSystemctl = systemctl
308 if err := migrateClaimJournals(t.Context(), store, resources, owners); err != nil {
309 t.Fatal(err)
310 }
311 if _, err := os.Stat(owner); !os.IsNotExist(err) {
312 t.Fatalf("successful recovery kept ownership record: %v", err)
313 }
314}
315