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
2
3package hostexec
4
5import (
6 "context"
7 "crypto/ed25519"
8 "encoding/base64"
9 "encoding/json"
10 "errors"
11 "github.com/carlosframework/platform/internal/release"
12 "github.com/carlosframework/platform/internal/version"
13 "os"
14 "path/filepath"
15 "runtime"
16 "sync/atomic"
17 "testing"
18
19 "github.com/carlosframework/platform/internal/hostfloor"
20)
21
22func TestPendingRevalidatesRunningImageAfterCompletedUpdate(t *testing.T) {
23 cfg, _, _, store, _ := fixture(t)
24 cfg.UpdateGuardDir = t.TempDir()
25 cfg.UpdateJournal = filepath.Join(t.TempDir(), "journal.json")
26 var oldImage atomic.Bool
27 oldImage.Store(true)
28 var checked atomic.Int32
29 cfg.verifyRunning = func(context.Context) error {
30 checked.Add(1)
31 if oldImage.Load() {
32 return hostfloor.ErrFloor
33 }
34 return nil
35 }
36 // Suspend precisely after pending persistence. Root finished its update by
37 // the time the controller resumes, so marker-only checking would acknowledge.
38 cfg.writeLocal = func(d *localDir, s State) error {
39 if err := d.write(s); err != nil {
40 return err
41 }
42 marker := filepath.Join(cfg.UpdateGuardDir, "update-begun.json")
43 if err := hostfloor.Durable(marker, []byte(`{"id":"update"}`), 0644); err != nil {
44 return err
45 }
46 if err := hostfloor.Durable(filepath.Join(cfg.UpdateGuardDir, "completed.json"), []byte("update"), 0644); err != nil {
47 return err
48 }
49 return hostfloor.RemoveDurable(marker)
50 }
51 c, err := Open(t.Context(), cfg)
52 if c != nil || !errors.Is(err, hostfloor.ErrFloor) {
53 t.Fatal("old executing image acknowledged", err)
54 }
55 if checked.Load() == 0 {
56 t.Fatal("running-image verifier skipped")
57 }
58 store.mu.Lock()
59 calls := store.calls
60 store.mu.Unlock()
61 if calls != 0 {
62 t.Fatal("acknowledged before identity check", calls)
63 }
64}
65
66func TestUpdateIdentityRecheckedBeforeLeaseAcknowledgement(t *testing.T) {
67 cfg, _, _, store, _ := fixture(t)
68 cfg.UpdateGuardDir = t.TempDir()
69 cfg.UpdateJournal = filepath.Join(t.TempDir(), "journal.json")
70 var checked atomic.Int32
71 cfg.verifyRunning = func(context.Context) error {
72 if checked.Add(1) > 1 {
73 return hostfloor.ErrFloor
74 }
75 return nil
76 }
77 c, err := Open(t.Context(), cfg)
78 if c != nil || !errors.Is(err, hostfloor.ErrFloor) {
79 t.Fatal("identity change during cleanup accepted", err)
80 }
81 store.mu.Lock()
82 calls := store.calls
83 store.mu.Unlock()
84 if calls != 0 {
85 t.Fatal("lease written", calls)
86 }
87}
88func TestUpdateMarkerBlocksBeforeIdentityCheck(t *testing.T) {
89 cfg, _, _, _, _ := fixture(t)
90 cfg.UpdateGuardDir = t.TempDir()
91 cfg.UpdateJournal = filepath.Join(t.TempDir(), "journal.json")
92 cfg.verifyRunning = func(context.Context) error { t.Fatal("verification ran despite update"); return nil }
93 for _, path := range []string{filepath.Join(cfg.UpdateGuardDir, "update-begun.json"), cfg.UpdateJournal} {
94 if err := os.WriteFile(path, []byte("present"), 0644); err != nil {
95 t.Fatal(err)
96 }
97 if err := cfg.checkUpdateIdentity(t.Context()); err == nil {
98 t.Fatal("update accepted")
99 }
100 if err := os.Remove(path); err != nil {
101 t.Fatal(err)
102 }
103 }
104}
105
106func TestRunningIdentityUsesExecutingImageAndCurrentFloor(t *testing.T) {
107 oldBuild := version.Build
108 version.Build = "v1.2.3"
109 t.Cleanup(func() { version.Build = oldBuild })
110 pub, priv, err := ed25519.GenerateKey(nil)
111 if err != nil {
112 t.Fatal(err)
113 }
114 t.Setenv("CARLOS_RELEASE_PUBKEY", base64.StdEncoding.EncodeToString(pub))
115 t.Setenv("CARLOS_DEPLOYMENT_BUCKET", "bucket")
116 cfg := Config{UpdateGuardDir: t.TempDir(), UpdateJournal: filepath.Join(t.TempDir(), "journal.json"), Store: release.NewDirStore(t.TempDir())}
117 sg := release.RootSigner(priv)
118 if err := os.Mkdir(filepath.Join(cfg.UpdateGuardDir, "identities"), 0755); err != nil {
119 t.Fatal(err)
120 }
121 if err := os.WriteFile(filepath.Join(cfg.UpdateGuardDir, "carlos"), []byte("persistent checker"), 0755); err != nil {
122 t.Fatal(err)
123 }
124 writeFloor := func(min string) {
125 floor, _ := json.Marshal(hostfloor.Floor{Min: 1, MinRelease: min})
126 auth, err := release.SignRootEnvelope(sg, hostfloor.Authorization{Schema: 1, Purpose: "moves-protocol", DeploymentBucket: "bucket", Key: hostfloor.Key, SHA256: hostfloor.Digest(floor)})
127 if err != nil {
128 t.Fatal(err)
129 }
130 policy, _ := json.Marshal(hostfloor.Policy{Schema: 1, DeploymentBucket: "bucket", Mode: "armed", Floor: floor, Authorization: auth})
131 if err := hostfloor.Durable(filepath.Join(cfg.UpdateGuardDir, "policy.json"), policy, 0644); err != nil {
132 t.Fatal(err)
133 }
134 for key, wire := range map[string][]byte{hostfloor.Key: floor, hostfloor.AuthKey: auth} {
135 _, etag, _ := cfg.Store.Get(t.Context(), key)
136 if _, err := cfg.Store.Put(t.Context(), key, wire, etag); err != nil {
137 t.Fatal(err)
138 }
139 }
140 }
141 writeIdentity := func(hash string) {
142 platform := runtime.GOOS + "-" + runtime.GOARCH
143 rel := release.SystemRelease{Version: version.Build, ReleaseTag: "v1.2.3", MovesProtocol: 1, Artifacts: map[string]release.SystemArtifact{platform: {Key: release.SystemArtifactKey(version.Build, platform), SHA256: hash}}}
144 store := release.NewDirStore(t.TempDir())
145 if _, err := release.PromoteSystem(t.Context(), store, sg, "router", "stable", rel, release.SystemPromoteOptions{}); err != nil {
146 t.Fatal(err)
147 }
148 wire, _, err := store.Get(t.Context(), release.SystemPointerKey("router", "stable"))
149 if err != nil {
150 t.Fatal(err)
151 }
152 if err := hostfloor.Durable(filepath.Join(cfg.UpdateGuardDir, "identities", "v1.2.3.json"), wire, 0644); err != nil {
153 t.Fatal(err)
154 }
155 }
156 writeFloor("v1.2.3")
157 hash, err := hostfloor.ExecutingHash()
158 if err != nil {
159 t.Fatal(err)
160 }
161 writeIdentity(hash)
162 if err := cfg.checkUpdateIdentity(t.Context()); err != nil {
163 t.Fatal("valid running image refused", err)
164 }
165 writeIdentity(hostfloor.Digest([]byte("replacement at current binary path")))
166 if err := cfg.checkUpdateIdentity(t.Context()); err == nil {
167 t.Fatal("replacement identity authorized stale executing inode")
168 }
169 writeIdentity(hash)
170 writeFloor("v1.2.4")
171 if err := cfg.checkUpdateIdentity(t.Context()); err == nil {
172 t.Fatal("resumed image below current floor authorized")
173 }
174}
175