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 && rootrunner
2
3package host
4
5import (
6 "bytes"
7 "context"
8 "crypto/ed25519"
9 "crypto/sha256"
10 "encoding/base64"
11 "encoding/hex"
12 "encoding/json"
13 "errors"
14 "os"
15 "path/filepath"
16 "runtime"
17 "testing"
18
19 "github.com/carlosframework/platform/internal/moves"
20 "github.com/carlosframework/platform/internal/release"
21 "github.com/carlosframework/platform/internal/rootstate"
22)
23
24func TestPlan14RootFloorRecoveryRollbackRootRunner(t *testing.T) {
25 scratch := generationRootRunnerDir(t)
26 t.Setenv("TMPDIR", scratch)
27 for _, entry := range []string{"recover", "rollback"} {
28 for _, target := range []string{"v1.0.0", "v2.0.0"} {
29 t.Run(entry+"/"+target, func(t *testing.T) {
30 f, _ := applyHappyVersion(t, "v2.0.0")
31 for path := f.a.optDir; path != scratch; path = filepath.Dir(path) {
32 if err := os.Chmod(path, 0755); err != nil {
33 t.Fatal(err)
34 }
35 }
36 // A compatible previous image can have the same release as the failed
37 // current image. This tests the real restore without inventing idle evidence.
38 j := f.brokenBox("v2.0.0", target, target)
39 delete(f.answer, "version:"+systemPrevPath(f.a.optDir))
40 t.Setenv("CARLOS_RELEASE_PUBKEY", base64.StdEncoding.EncodeToString(f.pub))
41 t.Setenv("CARLOS_DEPLOYMENT_BUCKET", "deploy")
42 id := rootFloorIdentity{Deployment: "deploy", Box: "box", Enrollment: "en"}
43 f.a.rootMoves = func() (rootFloorIdentity, bool, error) { return id, true, nil }
44 floors, err := openRootMovesFloors(f.a.optDir, id, true)
45 if err != nil {
46 t.Fatal(err)
47 }
48 defer floors.close()
49 a := moves.Floor{Min: 1, MinRelease: "v1.0.0"}
50 b := moves.Floor{Min: 1, MinRelease: "v2.0.0"}
51 if err := floors.reconstruct(t.Context(), rootFloorReconstruction{Identity: id, Floor: &a, Source: "history", Digest: floorTestDigest, Authorization: floorTestAuthorization(t, floors)}); err != nil {
52 t.Fatal(err)
53 }
54 raw, _ := json.Marshal(b)
55 if _, err := f.store.Put(t.Context(), moves.FloorKey, raw, ""); err != nil {
56 t.Fatal(err)
57 }
58 for _, path := range []string{systemBinaryPath(f.a.optDir), systemPrevPath(f.a.optDir)} {
59 body, err := os.ReadFile(path)
60 if err != nil {
61 t.Fatal(err)
62 }
63 sum := sha256.Sum256(body)
64 version := "v2.0.0"
65 if path == systemPrevPath(f.a.optDir) {
66 version = target
67 }
68 platform := runtime.GOOS + "-" + runtime.GOARCH
69 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[:])}}}}
70 wire, err := release.SignRootEnvelope(release.RootSigner(f.priv), p)
71 if err != nil {
72 t.Fatal(err)
73 }
74 identities, err := rootstate.Open(filepath.Join(f.a.optDir, "system", "guard", "identities"), true, true)
75 if err != nil {
76 t.Fatal(err)
77 }
78 if err := identities.Write(version+".json", wire); err != nil {
79 t.Fatal(err)
80 }
81 identities.Close()
82 }
83 if err := refreshRootMovesFloor(t.Context(), f.store, f.a.optDir, f.a.dataDir, id); err != nil {
84 t.Fatal(err)
85 }
86 f.a.openStore = func(context.Context) (release.Store, ed25519.PublicKey, error) {
87 t.Error("recovery read network")
88 return nil, nil, errors.New("offline")
89 }
90 if err := os.WriteFile(prevVersionPath(f.a.optDir), []byte("v0.0.1\n"), 0644); err != nil {
91 t.Fatal(err)
92 }
93 before, _ := os.ReadFile(systemBinaryPath(f.a.optDir))
94 journal, _ := os.ReadFile(f.a.file("journal.json"))
95 calls := len(f.calls)
96 if entry == "recover" {
97 _, err = f.a.recoverIfNeeded(t.Context())
98 } else {
99 err = f.a.rollback(t.Context(), systemRequest{Version: j.Target}, "fixture", &j)
100 }
101 if target == "v1.0.0" {
102 after, _ := os.ReadFile(systemBinaryPath(f.a.optDir))
103 retained, _ := os.ReadFile(f.a.file("journal.json"))
104 if err == nil || !bytes.Equal(before, after) || !bytes.Equal(journal, retained) {
105 t.Fatal("below-floor restore changed state", err)
106 }
107 for _, call := range f.calls[calls:] {
108 if len(call) >= 10 && call[:10] == "systemctl " {
109 t.Fatal("refused restore touched systemd", call)
110 }
111 }
112 } else {
113 if entry == "recover" && err != nil {
114 t.Fatal(err)
115 }
116 if f.result() == nil || f.result().Outcome != systemOutcomeRolledBack {
117 t.Fatal("restore incomplete", err, f.result())
118 }
119 if _, err := os.Stat(f.a.file("journal.json")); !os.IsNotExist(err) {
120 t.Fatal("successful restore retained journal", err)
121 }
122 }
123 p, e := readRootFloorProjection(f.a.optDir)
124 if e != nil || p.Floor == nil || *p.Floor != b {
125 t.Fatal("recovery lost retained B", p, e)
126 }
127 })
128 }
129 }
130}
131