//go:build linux && rootrunner package host import ( "bytes" "context" "crypto/ed25519" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "errors" "os" "path/filepath" "runtime" "testing" "github.com/carlosframework/platform/internal/moves" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rootstate" ) func TestPlan14RootFloorRecoveryRollbackRootRunner(t *testing.T) { scratch := generationRootRunnerDir(t) t.Setenv("TMPDIR", scratch) for _, entry := range []string{"recover", "rollback"} { for _, target := range []string{"v1.0.0", "v2.0.0"} { t.Run(entry+"/"+target, func(t *testing.T) { f, _ := applyHappyVersion(t, "v2.0.0") for path := f.a.optDir; path != scratch; path = filepath.Dir(path) { if err := os.Chmod(path, 0755); err != nil { t.Fatal(err) } } // A compatible previous image can have the same release as the failed // current image. This tests the real restore without inventing idle evidence. j := f.brokenBox("v2.0.0", target, target) delete(f.answer, "version:"+systemPrevPath(f.a.optDir)) t.Setenv("CARLOS_RELEASE_PUBKEY", base64.StdEncoding.EncodeToString(f.pub)) t.Setenv("CARLOS_DEPLOYMENT_BUCKET", "deploy") id := rootFloorIdentity{Deployment: "deploy", Box: "box", Enrollment: "en"} f.a.rootMoves = func() (rootFloorIdentity, bool, error) { return id, true, nil } floors, err := openRootMovesFloors(f.a.optDir, id, true) if err != nil { t.Fatal(err) } defer floors.close() a := moves.Floor{Min: 1, MinRelease: "v1.0.0"} b := moves.Floor{Min: 1, MinRelease: "v2.0.0"} if err := floors.reconstruct(t.Context(), rootFloorReconstruction{Identity: id, Floor: &a, Source: "history", Digest: floorTestDigest, Authorization: floorTestAuthorization(t, floors)}); err != nil { t.Fatal(err) } raw, _ := json.Marshal(b) if _, err := f.store.Put(t.Context(), moves.FloorKey, raw, ""); err != nil { t.Fatal(err) } for _, path := range []string{systemBinaryPath(f.a.optDir), systemPrevPath(f.a.optDir)} { body, err := os.ReadFile(path) if err != nil { t.Fatal(err) } sum := sha256.Sum256(body) version := "v2.0.0" if path == systemPrevPath(f.a.optDir) { version = target } platform := runtime.GOOS + "-" + runtime.GOARCH p := release.SystemPointer{Schema: 1, Component: "router", Channel: "stable", Spread: "0s", PromotedAt: f.now, Current: release.SystemRelease{Version: version, ReleaseTag: version, MovesProtocol: 1, Artifacts: map[string]release.SystemArtifact{platform: {Key: release.SystemArtifactKey(version, platform), SHA256: hex.EncodeToString(sum[:])}}}} wire, err := release.SignRootEnvelope(release.RootSigner(f.priv), p) if err != nil { t.Fatal(err) } identities, err := rootstate.Open(filepath.Join(f.a.optDir, "system", "guard", "identities"), true, true) if err != nil { t.Fatal(err) } if err := identities.Write(version+".json", wire); err != nil { t.Fatal(err) } identities.Close() } if err := refreshRootMovesFloor(t.Context(), f.store, f.a.optDir, f.a.dataDir, id); err != nil { t.Fatal(err) } f.a.openStore = func(context.Context) (release.Store, ed25519.PublicKey, error) { t.Error("recovery read network") return nil, nil, errors.New("offline") } if err := os.WriteFile(prevVersionPath(f.a.optDir), []byte("v0.0.1\n"), 0644); err != nil { t.Fatal(err) } before, _ := os.ReadFile(systemBinaryPath(f.a.optDir)) journal, _ := os.ReadFile(f.a.file("journal.json")) calls := len(f.calls) if entry == "recover" { _, err = f.a.recoverIfNeeded(t.Context()) } else { err = f.a.rollback(t.Context(), systemRequest{Version: j.Target}, "fixture", &j) } if target == "v1.0.0" { after, _ := os.ReadFile(systemBinaryPath(f.a.optDir)) retained, _ := os.ReadFile(f.a.file("journal.json")) if err == nil || !bytes.Equal(before, after) || !bytes.Equal(journal, retained) { t.Fatal("below-floor restore changed state", err) } for _, call := range f.calls[calls:] { if len(call) >= 10 && call[:10] == "systemctl " { t.Fatal("refused restore touched systemd", call) } } } else { if entry == "recover" && err != nil { t.Fatal(err) } if f.result() == nil || f.result().Outcome != systemOutcomeRolledBack { t.Fatal("restore incomplete", err, f.result()) } if _, err := os.Stat(f.a.file("journal.json")); !os.IsNotExist(err) { t.Fatal("successful restore retained journal", err) } } p, e := readRootFloorProjection(f.a.optDir) if e != nil || p.Floor == nil || *p.Floor != b { t.Fatal("recovery lost retained B", p, e) } }) } } }