| 1 | //go:build linux && rootrunner |
| 2 | |
| 3 | package host |
| 4 | |
| 5 | import ( |
| 6 | "bytes" |
| 7 | "context" |
| 8 | "errors" |
| 9 | "os" |
| 10 | "os/exec" |
| 11 | "path/filepath" |
| 12 | "testing" |
| 13 | "time" |
| 14 | |
| 15 | "github.com/carlosframework/platform/internal/hostwrite" |
| 16 | "github.com/carlosframework/platform/internal/registry" |
| 17 | "github.com/carlosframework/platform/internal/release" |
| 18 | "github.com/carlosframework/platform/internal/rootparticipation" |
| 19 | "github.com/carlosframework/platform/internal/rootstate" |
| 20 | ) |
| 21 | |
| 22 | func TestPlan14NoCensusReleaseRootRunner(t *testing.T) { |
| 23 | if _, err := os.Lstat("/var/lib/carlos-release"); !os.IsNotExist(err) { |
| 24 | t.Fatal("existing release state", err) |
| 25 | } |
| 26 | t.Cleanup(func() { os.RemoveAll("/var/lib/carlos-release") }) |
| 27 | for _, scenario := range []string{"absent", "empty", "held", "aborted"} { |
| 28 | t.Run(scenario, func(t *testing.T) { |
| 29 | var s rootstate.Store |
| 30 | if scenario != "absent" { |
| 31 | var err error |
| 32 | s, err = rootstate.Open(rootReleasePrivate, true, false) |
| 33 | if err != nil { |
| 34 | t.Fatal(err) |
| 35 | } |
| 36 | defer s.Close() |
| 37 | } |
| 38 | if scenario == "held" || scenario == "aborted" { |
| 39 | unlock, err := s.Lock(t.Context(), "release.lock") |
| 40 | if err != nil { |
| 41 | t.Fatal(err) |
| 42 | } |
| 43 | if scenario == "held" { |
| 44 | defer unlock() |
| 45 | } else { |
| 46 | unlock() |
| 47 | } |
| 48 | } |
| 49 | before, _ := os.ReadDir(rootReleasePrivate) |
| 50 | for _, flag := range []string{"", "1"} { |
| 51 | t.Setenv("CARLOS_HOST_CLAIM", flag) |
| 52 | plan14NoCensusCommands(t) |
| 53 | ctx, cancel := context.WithTimeout(t.Context(), time.Second) |
| 54 | called := false |
| 55 | err := withClaimRootRelease(ctx, false, func() (rootstate.Store, error) { t.Fatal("opened root release"); return nil, os.ErrPermission }, func() error { called = true; return nil }, func() (rootparticipation.Ledger, bool, error) { return rootparticipation.Ledger{}, false, nil }) |
| 56 | if err != nil || !called { |
| 57 | t.Fatal("adopt", err, called) |
| 58 | } |
| 59 | for _, recover := range []bool{false, true} { |
| 60 | f := newApplyFixture(t, "canary", "v1.0.0") |
| 61 | f.a.participation = nil |
| 62 | f.a.rootMoves = rootMovesFloorIdentity |
| 63 | if err := f.a.runWithRelease(ctx, recover); err != nil { |
| 64 | t.Fatal("system", recover, err) |
| 65 | } |
| 66 | } |
| 67 | cancel() |
| 68 | } |
| 69 | after, _ := os.ReadDir(rootReleasePrivate) |
| 70 | if len(after) != len(before) { |
| 71 | t.Fatal("new release file", before, after) |
| 72 | } |
| 73 | if scenario == "absent" { |
| 74 | if _, err := os.Lstat(rootReleasePrivate); !os.IsNotExist(err) { |
| 75 | t.Fatal("created release directory", err) |
| 76 | } |
| 77 | } |
| 78 | }) |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | func plan14NoCensusCommands(t *testing.T) { |
| 83 | t.Helper() |
| 84 | for _, mode := range []string{"adopt", "apply", "recover"} { |
| 85 | dir := t.TempDir() |
| 86 | ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) |
| 87 | cmd := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestPlan14NoCensusCommandPayload$") |
| 88 | cmd.Env = isolatedAdoptOrderEnv(t, "PLAN14_R10_COMMAND="+mode, |
| 89 | "CARLOS_HOST_CLAIM="+os.Getenv("CARLOS_HOST_CLAIM"), "CARLOS_DATA_DIR="+filepath.Join(dir, "data"), |
| 90 | "CARLOS_OPT_DIR="+filepath.Join(dir, "opt"), "CARLOS_DEPLOYMENT_BUCKET=", "CARLOS_DEPLOYMENT_DIR="+filepath.Join(dir, "bucket"), "CARLOS_REGION_HA=", "CARLOS_MOVES_PHASE1=") |
| 91 | out, err := cmd.CombinedOutput() |
| 92 | cancel() |
| 93 | if mode == "adopt" { |
| 94 | var status *exec.ExitError |
| 95 | if !errors.As(err, &status) || status.ExitCode() != 2 || !bytes.Contains(out, []byte("flag provided but not defined: -plan14-no-such-option")) { |
| 96 | t.Fatal("adopt did not reach ordinary argument handling", err, string(out)) |
| 97 | } |
| 98 | } else if err != nil { |
| 99 | t.Fatal(mode, err, string(out)) |
| 100 | } |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | func TestPlan14NoCensusCommandPayload(t *testing.T) { |
| 105 | switch os.Getenv("PLAN14_R10_COMMAND") { |
| 106 | case "adopt": |
| 107 | RunAdopt([]string{"-plan14-no-such-option"}) |
| 108 | case "apply": |
| 109 | RunSystemApply(nil) |
| 110 | case "recover": |
| 111 | RunSystemApply([]string{"-recover"}) |
| 112 | default: |
| 113 | t.Fatal("fixture only") |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | func TestPlan14PendingAcquisitionStartupRootRunner(t *testing.T) { |
| 118 | if err := os.Mkdir("/var/lib/carlos-release", 0700); err != nil { |
| 119 | t.Fatal(err) |
| 120 | } |
| 121 | t.Cleanup(func() { os.RemoveAll("/var/lib/carlos-release") }) |
| 122 | for _, mode := range []string{"instance-control", "system"} { |
| 123 | t.Run(mode, func(t *testing.T) { |
| 124 | t.Setenv("PLAN14_R10_ACQUISITION", mode) |
| 125 | runServingParkPeerNamespace(t, false, false, "seed-construction") |
| 126 | }) |
| 127 | } |
| 128 | } |
| 129 | |
| 130 | func TestPlan14AcquisitionCrashPayload(t *testing.T) { |
| 131 | if os.Getenv("PLAN14_R10_CRASH") != "1" { |
| 132 | t.Fatal("fixture only") |
| 133 | } |
| 134 | s, err := rootstate.Open(rootparticipation.Namespace, false, false) |
| 135 | if err != nil { |
| 136 | t.Fatal(err) |
| 137 | } |
| 138 | l := rootparticipation.Ledger{State: s} |
| 139 | if err := l.Acquire(t.Context(), func() error { os.Exit(0); return nil }); err != nil { |
| 140 | t.Fatal(err) |
| 141 | } |
| 142 | t.Fatal("did not die at durable acquisition") |
| 143 | } |
| 144 | |
| 145 | func plan14PendingAcquisition(t *testing.T, opt, deployment string) { |
| 146 | cmd := exec.Command(os.Args[0], "-test.run=^TestPlan14AcquisitionCrashPayload$") |
| 147 | cmd.Env = append(os.Environ(), "PLAN14_R10_CRASH=1") |
| 148 | if out, err := cmd.CombinedOutput(); err != nil { |
| 149 | t.Fatal(string(out), err) |
| 150 | } |
| 151 | s, err := rootstate.Open(rootparticipation.Namespace, false, false) |
| 152 | if err != nil { |
| 153 | t.Fatal(err) |
| 154 | } |
| 155 | defer s.Close() |
| 156 | l := rootparticipation.Ledger{State: s} |
| 157 | if _, err := l.Census(); !errors.Is(err, rootparticipation.ErrUnavailable) { |
| 158 | t.Fatal("pending acquisition admitted", err) |
| 159 | } |
| 160 | c, err := l.RecoveryCensus() |
| 161 | if err != nil || c.Pending != "acquisition" { |
| 162 | t.Fatal(c, err) |
| 163 | } |
| 164 | t.Setenv("CARLOS_DEPLOYMENT_BUCKET", deployment) |
| 165 | id, required, err := rootMovesFloorIdentity() |
| 166 | if err != nil || !required { |
| 167 | t.Fatal("floor identity dead end", required, err) |
| 168 | } |
| 169 | t.Setenv("CARLOS_DEPLOYMENT_BUCKET", "") |
| 170 | if os.Getenv("PLAN14_R10_ACQUISITION") == "system" { |
| 171 | f := newApplyFixture(t, "canary", "v1.0.0") |
| 172 | f.a.optDir = opt |
| 173 | f.a.dataDir = "/data" |
| 174 | f.a.rootMoves = func() (rootFloorIdentity, bool, error) { return id, true, nil } |
| 175 | f.a.participation = nil |
| 176 | if err := f.a.runWithRelease(t.Context(), true); err != nil { |
| 177 | t.Fatal("system recovery entrypoint", err) |
| 178 | } |
| 179 | if _, err := l.Census(); err != nil { |
| 180 | t.Fatal("acquisition not settled", err) |
| 181 | } |
| 182 | } |
| 183 | } |
| 184 | |
| 185 | func TestPlan14RetiredInstallationStartupRootRunner(t *testing.T) { |
| 186 | if err := os.Mkdir("/var/lib/carlos-release", 0700); err != nil { |
| 187 | t.Fatal(err) |
| 188 | } |
| 189 | t.Cleanup(func() { os.RemoveAll("/var/lib/carlos-release") }) |
| 190 | for _, mode := range []string{"complete", "interrupted"} { |
| 191 | t.Run(mode, func(t *testing.T) { |
| 192 | t.Setenv("PLAN14_R10_RETIRED", mode) |
| 193 | runServingParkPeerNamespace(t, false, false, "seed-construction") |
| 194 | }) |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | func TestPlan14RetiredMigrationPayloadRootRunner(t *testing.T) { |
| 199 | testRootMigrationBootstrap(t, true, false) |
| 200 | } |
| 201 | |
| 202 | func TestPlan14LocalBackingStateRootRunner(t *testing.T) { |
| 203 | if err := os.Mkdir("/var/lib/private", 0700); err == nil { |
| 204 | t.Cleanup(func() { os.Remove("/var/lib/private") }) |
| 205 | } else if !os.IsExist(err) { |
| 206 | t.Fatal(err) |
| 207 | } |
| 208 | for _, dir := range []string{"/var/lib/carlos-app", "/var/lib/private/carlos-app"} { |
| 209 | t.Run(dir, func(t *testing.T) { |
| 210 | if err := os.Mkdir(dir, 0755); err != nil { |
| 211 | t.Fatal("existing backing state", err) |
| 212 | } |
| 213 | t.Cleanup(func() { os.RemoveAll(dir) }) |
| 214 | if err := os.Mkdir(filepath.Join(dir, "hidden.example"), 0700); err != nil { |
| 215 | t.Fatal(err) |
| 216 | } |
| 217 | reg := restartTestRegistry(t) |
| 218 | for _, attack := range []string{"omitted", "cleared"} { |
| 219 | if attack == "cleared" { |
| 220 | if err := reg.Upsert(registry.Route{Host: "hidden.example", Account: "bab", App: "app", Socket: "/run/hidden.sock"}); err != nil { |
| 221 | t.Fatal(err) |
| 222 | } |
| 223 | } |
| 224 | p := &activationPreflightOwner{} |
| 225 | scratch := t.TempDir() |
| 226 | r := ClaimRuntimeResources{Mode: "instance-control", Registry: reg, DataDir: scratch, OptDir: scratch, UnitsDir: scratch} |
| 227 | store := release.NewDirStore(t.TempDir()) |
| 228 | if _, err := OpenProductionClaimAssembly(t.Context(), store, r, p); !errors.Is(err, hostwrite.ErrUnavailable) || p.calls != 0 { |
| 229 | t.Fatal("local backing omitted", attack, err, p.calls) |
| 230 | } |
| 231 | if err := migrateClaimJournalsWith(t.Context(), store, r, p, true); !errors.Is(err, hostwrite.ErrUnavailable) || p.calls != 0 { |
| 232 | t.Fatal("migration backing omitted", attack, err, p.calls) |
| 233 | } |
| 234 | } |
| 235 | }) |
| 236 | } |
| 237 | } |
| 238 | |