package host import ( "crypto/ed25519" "os" "path/filepath" "testing" "github.com/carlosframework/platform/internal/hostfloor" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rootparticipation" "github.com/carlosframework/platform/internal/rootstate" ) func TestPlan14Fix2LiveSafety(t *testing.T) { if !plan14RootFixture(t) { return } for _, scenario := range []string{"console update with empty history", "two consecutive updates", "incident recipe", "failed forward fix", "crash rollback", "successful restart crash before settle"} { t.Run(scenario, func(t *testing.T) { e, rt, base, c, _ := plan14AdoptionFixture(t) u := &r2Units{plan14RestartUnits: base} c.legacy, c.legacyReady = legacyRootRestart, c.ready if err := e.store.Delete(t.Context(), provision.RecordKey(rt.Account, rt.App, rt.Host)); err != nil { t.Fatal(err) } rt.Account = "" if err := e.root.reg.Upsert(rt); err != nil { t.Fatal(err) } source := productionProofBindingSource(e.store, u, u.opt) source.namedPath = filepath.Join(t.TempDir(), "named-units.json") source.unitDirs = []string{filepath.Dir(u.props.FragmentPath)} source.healthy = nil makeProofAncestorsPublic(t, source.namedPath) put := func(path string, raw []byte) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(path, raw, 0644); err != nil { t.Fatal(err) } makeProofAncestorsPublic(t, path) } put(filepath.Join(u.opt, "system/guard/console-ready.json"), []byte(release.LiveLink(u.opt, rt.Host))) drop := filepath.Join(source.unitDirs[0], "console.service.d/90-host-floor.conf") put(drop, []byte(hostfloor.ConsoleGuardDropIn(release.LiveLink(u.opt, rt.Host)))) u.props.DropInPaths = []string{drop} u.props.ExecPath = "/opt/carlos/system/guard/carlos" put(u.props.ExecPath, []byte("guard")) makeProofAncestorsPublic(t, u.exe) makeProofAncestorsPublic(t, u.props.FragmentPath) c.resolve, c.host, c.rollback, c.prepareRollback, c.dispatch, c.targetFile = source.resolve, source.forHost, source.rollback, source.prepareRollback, source.needsRestart, rootstate.OpenTrustedFile pub, key, err := ed25519.GenerateKey(nil) if err != nil { t.Fatal(err) } if err := e.store.Delete(t.Context(), release.ChannelKey("bab", "console", rt.Channel)); err != nil { t.Fatal(err) } promote := func(v string) { t.Helper() if err := release.Promote(t.Context(), e.store, key, "bab", "console", rt.Channel, v); err != nil { t.Fatal(err) } } promote("v1") promote("v2") if scenario == "crash rollback" { u.crashNew = true } if scenario == "failed forward fix" { u.props.ActiveState, u.props.SubState, u.props.MainPID = "failed", "failed", 0 } if scenario == "incident recipe" { if err := release.StageArtifact(u.opt, "", "v2", "console", []byte("new build")); err != nil { t.Fatal(err) } if err := release.PointLive(u.opt, rt.Host, "", "v2", "console"); err != nil { t.Fatal(err) } if err := e.root.reg.SetVersions(rt.Host, "v2", "v1"); err != nil { t.Fatal(err) } u.exe = release.VersionPath(u.opt, "", "v2", "console") u.props.ActiveState, u.props.SubState, u.props.MainPID = "failed", "failed", 0 promote("v1") } restart := rootRestartRouteWith(e.root.reg, u, u.opt, func(string) string { return "5s" }, nil, c) rollback := rollbackUnitRestart(e.root.reg, u, u.opt, func(string) string { return "5s" }, c) if scenario == "successful restart crash before settle" { u.afterEffect = func() { panic("crash") } func() { defer func() { if recover() != "crash" { t.Fatal("missing crash") } }() _ = release.Pass(t.Context(), e.root.reg, e.store, pub, u.opt, restart, release.WithRestartFailurePropagation()) }() u.afterEffect = nil before := len(u.effects) if err := recoverRootRestarts(t.Context(), u, u.opt, func(string) string { return "5s" }, c, hostwrite.Config{}); err != nil { t.Fatal(err) } if len(u.effects) != before || u.exe != release.VersionPath(u.opt, "", "v2", "console") { t.Fatal("recovery repeated restart", u.effects) } return } run := func() error { return release.Pass(t.Context(), e.root.reg, e.store, pub, u.opt, restart, release.WithRestartFailurePropagation(), release.WithRollbackRestart(rollback)) } err = run() if err != nil { t.Fatal(err) } want := "v2" if scenario == "incident recipe" { want = "v1" } if scenario == "crash rollback" || scenario == "incident recipe" || scenario == "failed forward fix" { // Main cannot settle a guard ExecStart against the live-link // stamp. A stopped unit needs the operator's reset and start. if u.props.MainPID != 0 { t.Fatal("main path woke a stopped guard unit", u.effects) } if live, err := release.LiveVersion(u.opt, rt.Host); err != nil || live != want { t.Fatal("main pointer result", live, err) } if scenario == "crash rollback" { return } if err := u.ResetFailed(rt.Unit); err != nil { t.Fatal(err) } if err := u.Start(rt.Unit); err != nil { t.Fatal(err) } } if u.props.MainPID <= 0 || u.exe != release.VersionPath(u.opt, "", want, "console") { t.Fatalf("not serving %s: %+v", want, u) } if scenario == "two consecutive updates" { m := release.Manifest{App: "console", Version: "v3", Kind: "binary", Artifacts: map[string]release.Artifact{release.AdoptPlatform(): {}}} if err := release.Ship(t.Context(), e.store, "bab", m, map[string][]byte{release.AdoptPlatform(): []byte("third build")}); err != nil { t.Fatal(err) } promote("v3") if err := run(); err != nil { t.Fatal(err) } if u.exe != release.VersionPath(u.opt, "", "v3", "console") || len(u.effects) != 2 { t.Fatal("second update", u.effects, u.exe) } } }) } } func TestPlan14Fix2PlatformUpdateClaimsOffNoCensus(t *testing.T) { if !plan14RootFixture(t) { return } for _, flag := range []string{"", "1"} { t.Run("flag="+flag, func(t *testing.T) { t.Setenv("CARLOS_HOST_CLAIM", flag) l, required, err := rootparticipation.OpenForUpdate() if l.State != nil { l.State.Close() } if err != nil || required { t.Fatalf("unactivated census blocked update: %v %v", required, err) } if legacyActivationReady(t.Context()) == nil { t.Fatal("claims activated without census and verifier") } guard := hostfloor.Disk{Dir: t.TempDir()} data := t.TempDir() if err := os.Mkdir(filepath.Join(data, ".hostexec"), 0700); err != nil { t.Fatal(err) } if err := guard.Begin(t.Context(), "update", data); err != nil { t.Fatal("real update path", err) } if _, err := os.Stat(filepath.Join(guard.Dir, "update-begun.json")); err != nil { t.Fatal(err) } if _, err := os.Stat(rootparticipation.Namespace); !os.IsNotExist(err) { t.Fatal("update created census", err) } }) } }