| 1 | //go:build linux && rootrunner |
| 2 | |
| 3 | package host |
| 4 | |
| 5 | import ( |
| 6 | "encoding/json" |
| 7 | "os" |
| 8 | "path/filepath" |
| 9 | "testing" |
| 10 | "time" |
| 11 | |
| 12 | "github.com/carlosframework/platform/internal/hostclaim" |
| 13 | "github.com/carlosframework/platform/internal/hostguard" |
| 14 | "github.com/carlosframework/platform/internal/provision" |
| 15 | "github.com/carlosframework/platform/internal/release" |
| 16 | "github.com/carlosframework/platform/internal/rollout" |
| 17 | "github.com/carlosframework/platform/internal/rootparticipation" |
| 18 | "github.com/carlosframework/platform/internal/rootstate" |
| 19 | ) |
| 20 | |
| 21 | func TestPlan14RootFailureProductionOpener(t *testing.T) { |
| 22 | if os.Geteuid() != 0 { |
| 23 | t.Fatal("root required") |
| 24 | } |
| 25 | for _, path := range []string{"/var/lib/carlos-claims", "/var/lib/carlos-release", rootRetryProjection} { |
| 26 | if err := os.Mkdir(path, 0755); err != nil { |
| 27 | t.Fatal("existing fixture", path, err) |
| 28 | } |
| 29 | path := path |
| 30 | t.Cleanup(func() { os.RemoveAll(path) }) |
| 31 | } |
| 32 | state, err := rootstate.Open(rootparticipation.Namespace, true, false) |
| 33 | if err != nil { |
| 34 | t.Fatal(err) |
| 35 | } |
| 36 | c := rootparticipation.Census{Schema: 1, Box: "box", Enrollment: "en", Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true} |
| 37 | raw, _ := json.Marshal(c) |
| 38 | if err = state.Write("census.json", raw); err != nil { |
| 39 | t.Fatal(err) |
| 40 | } |
| 41 | state.Close() |
| 42 | failures, err := rootstate.Open(rollout.RootFailuresDir, true, false) |
| 43 | if err != nil { |
| 44 | t.Fatal(err) |
| 45 | } |
| 46 | defer failures.Close() |
| 47 | book, err := rollout.InitializeRootFailures(failures, rootUpdatesMigration(c), nil) |
| 48 | if err != nil { |
| 49 | t.Fatal(err) |
| 50 | } |
| 51 | rec := provision.Record{Schema: 1, Host: "h.example.com", Account: "bab", App: "km", Channel: "canary/x", Pin: "v1", CreatedAt: time.Now().UTC()} |
| 52 | if _, err = book.Record(rollout.FailureRecord{Account: rec.Account, App: rec.App, Host: rec.Host, Build: "v1", HeldDown: []string{rec.Host}}); err != nil { |
| 53 | t.Fatal(err) |
| 54 | } |
| 55 | inc, _ := hostclaim.Incarnation(rec.Account, rec.App, rec.Host, rec.CreatedAt) |
| 56 | fences, err := rootstate.Open("/var/lib/carlos-claims/authority/fences", true, false) |
| 57 | if err != nil { |
| 58 | t.Fatal(err) |
| 59 | } |
| 60 | defer fences.Close() |
| 61 | in := hostguard.RootInstallation{Host: rec.Host, Account: rec.Account, App: rec.App, Record: provision.RecordKey(rec.Account, rec.App, rec.Host), Incarnation: inc, Stamp: 1, Box: c.Box, Enrollment: c.Enrollment, Migration: c.Migration, InventoryRevision: 1} |
| 62 | if err = hostguard.NewRootAuthority(fences).RetainMigrationDeny(t.Context(), in, 1); err != nil { |
| 63 | t.Fatal(err) |
| 64 | } |
| 65 | store := release.NewDirStore(t.TempDir()) |
| 66 | plan14PutRecord(t, store, rec) |
| 67 | claim := hostclaim.Claim{V: 2, Host: rec.Host, Gen: 1, Holder: &hostclaim.Holder{Family: "instance", Key: in.Record, Account: rec.Account, App: rec.App, Incarnation: inc, CreatedAt: rec.CreatedAt, Location: "instances/" + rec.Host, Stamp: 1, Session: 1, WriterBox: c.Box}, Op: hostclaim.Op{Kind: hostclaim.KindServing, Exec: c.Box}} |
| 68 | if err = claim.Validate(); err != nil { |
| 69 | t.Fatal(err) |
| 70 | } |
| 71 | key, _ := hostclaim.Key(rec.Host) |
| 72 | raw, _ = json.Marshal(claim) |
| 73 | if _, err = store.Put(t.Context(), key, raw, ""); err != nil { |
| 74 | t.Fatal(err) |
| 75 | } |
| 76 | seed, public, _ := release.GenerateKey() |
| 77 | priv, _ := release.ParseSigningKey(seed, "test") |
| 78 | pub, _ := release.ParsePublicKey(public, "test") |
| 79 | m := release.Manifest{Schema: 1, App: rec.App, Version: "v1", Kind: "binary", Artifacts: map[string]release.Artifact{runtimePlatform(): {}}} |
| 80 | if err = release.Ship(t.Context(), store, rec.Account, m, map[string][]byte{runtimePlatform(): []byte("v1")}); err != nil { |
| 81 | t.Fatal(err) |
| 82 | } |
| 83 | if err = release.Promote(t.Context(), store, priv, rec.Account, rec.App, rec.Channel, "v1"); err != nil { |
| 84 | t.Fatal(err) |
| 85 | } |
| 86 | data, opt := t.TempDir(), t.TempDir() |
| 87 | if err = os.Mkdir(filepath.Join(data, rollout.FailureFile), 0700); err != nil { |
| 88 | t.Fatal(err) |
| 89 | } |
| 90 | rt := provision.RouteFor(rec, data, "", false, nil) |
| 91 | rt.Account, rt.App, rt.Channel, rt.GoodVersion = "bad", "foreign", "forged", "forged" |
| 92 | reg := openReg(t, rt) |
| 93 | if err = release.Pass(t.Context(), reg, store, pub, opt, nil, updatesPassOption(data, true)); err != nil { |
| 94 | t.Fatal(err) |
| 95 | } |
| 96 | if v, _ := release.LiveVersion(opt, rec.Host); v != "v1" { |
| 97 | t.Fatal("sticky or legacy state held production adoption", v) |
| 98 | } |
| 99 | opened, err := productionRootUpdates(data)(t.Context(), store, pub) |
| 100 | if err != nil || opened == nil { |
| 101 | t.Fatal(err) |
| 102 | } |
| 103 | resolved, err := opened.Resolve(t.Context(), rec.Host) |
| 104 | if err != nil || resolved.Route.Account != rec.Account || resolved.Route.App != rec.App || resolved.Failed("v1") { |
| 105 | t.Fatal("canonical activation policy", resolved, err) |
| 106 | } |
| 107 | opened.Close() |
| 108 | wire, err := release.SignHostRetry(release.RootSigner(priv), rec.Account, rec.App, rec.Host, "v1", book.ID(), book.PublishedEpoch(rec.Account, rec.App, "v1", rec.Host), time.Now()) |
| 109 | if err != nil { |
| 110 | t.Fatal(err) |
| 111 | } |
| 112 | retries := release.ScopedRestartGeneration{Schema: 1, Requests: []release.RestartRequest{{ID: "retry", RequestedAt: time.Now().UTC().Format(time.RFC3339), Scope: release.RestartScope{Kind: release.RestartScopeRetry, Value: rec.Host, Signed: wire}}}} |
| 113 | raw, _ = json.Marshal(retries) |
| 114 | if _, err = store.Put(t.Context(), release.HostRetryKey(rec.Account, rec.App), raw, ""); err != nil { |
| 115 | t.Fatal(err) |
| 116 | } |
| 117 | opened, err = productionRootUpdates(data)(t.Context(), store, pub) |
| 118 | if err != nil { |
| 119 | t.Fatal(err) |
| 120 | } |
| 121 | if _, err = opened.Resolve(t.Context(), rec.Host); err != nil { |
| 122 | t.Fatal(err) |
| 123 | } |
| 124 | opened.Close() |
| 125 | book, err = rollout.OpenRootFailures(failures, rootUpdatesMigration(c)) |
| 126 | if err != nil || book.HeldDown(rec.Account, rec.App, "v1", rec.Host) { |
| 127 | t.Fatal("production retry not settled", err) |
| 128 | } |
| 129 | _, etag, _ := store.Get(t.Context(), key) |
| 130 | claim.Op.Exec = "foreign" |
| 131 | raw, _ = json.Marshal(claim) |
| 132 | if _, err = store.Put(t.Context(), key, raw, etag); err != nil { |
| 133 | t.Fatal(err) |
| 134 | } |
| 135 | opened, err = productionRootUpdates(data)(t.Context(), store, pub) |
| 136 | if err != nil { |
| 137 | t.Fatal(err) |
| 138 | } |
| 139 | defer opened.Close() |
| 140 | if _, err = opened.Resolve(t.Context(), rec.Host); err == nil { |
| 141 | t.Fatal("foreign controller authority accepted") |
| 142 | } |
| 143 | } |
| 144 | |