| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "context" |
| 6 | "crypto/ed25519" |
| 7 | "crypto/sha256" |
| 8 | "encoding/hex" |
| 9 | "encoding/json" |
| 10 | "errors" |
| 11 | "os" |
| 12 | "path/filepath" |
| 13 | "reflect" |
| 14 | "strings" |
| 15 | "testing" |
| 16 | |
| 17 | "github.com/carlosframework/platform/internal/hostfloor" |
| 18 | "github.com/carlosframework/platform/internal/release" |
| 19 | ) |
| 20 | |
| 21 | // channelFixture is a fresh box (applyFixture with no binary on it) whose |
| 22 | // stable channel names v1.4.0, whose identity is published, and whose |
| 23 | // deployment has a host floor. |
| 24 | type channelFixture struct { |
| 25 | *applyFixture |
| 26 | in channelInstall |
| 27 | body []byte |
| 28 | fetches int |
| 29 | owned []string |
| 30 | } |
| 31 | |
| 32 | func newChannelFixture(t *testing.T, floor string) *channelFixture { |
| 33 | t.Helper() |
| 34 | f := &channelFixture{applyFixture: newApplyFixture(t, "stable", "bootstrap")} |
| 35 | if err := os.Remove(systemBinaryPath(f.a.optDir)); err != nil { |
| 36 | t.Fatal(err) |
| 37 | } |
| 38 | f.body = []byte("#!/bin/sh\necho carlos v1.4.0\n") |
| 39 | f.publish(t, "v1.4.0", f.body) |
| 40 | if floor != "" { |
| 41 | if err := hostfloor.Publish(context.Background(), f.store, release.RootSigner(f.priv), "bucket", hostfloor.Floor{Min: 1, MinRelease: floor}); err != nil { |
| 42 | t.Fatal(err) |
| 43 | } |
| 44 | } |
| 45 | f.in = channelInstall{a: f.a, bucket: "bucket", fetchTo: func(ctx context.Context, s release.Store, key, dst, sha string) error { |
| 46 | f.fetches++ |
| 47 | return release.FetchToFile(ctx, s, key, dst, systemArtifactLimit, sha) |
| 48 | }, own: func(path string) error { |
| 49 | f.owned = append(f.owned, path) |
| 50 | return nil |
| 51 | }} |
| 52 | return f |
| 53 | } |
| 54 | |
| 55 | // publish promotes version on stable with body as its linux-arm64 |
| 56 | // artifact and publishes its release identity. |
| 57 | func (f *channelFixture) publish(t *testing.T, version string, body []byte) { |
| 58 | t.Helper() |
| 59 | ctx := context.Background() |
| 60 | sum := sha256.Sum256(body) |
| 61 | rel := release.SystemRelease{Version: version, ReleaseTag: version, MovesProtocol: 1, Artifacts: map[string]release.SystemArtifact{ |
| 62 | "linux-arm64": {Key: release.SystemArtifactKey(version, "linux-arm64"), SHA256: hex.EncodeToString(sum[:])}}} |
| 63 | if _, err := f.store.Put(ctx, rel.Artifacts["linux-arm64"].Key, body, ""); err != nil { |
| 64 | t.Fatal(err) |
| 65 | } |
| 66 | if _, err := release.PromoteSystem(ctx, f.store, release.RootSigner(f.priv), "router", "stable", rel, release.SystemPromoteOptions{}); err != nil { |
| 67 | t.Fatal(err) |
| 68 | } |
| 69 | wire, _, err := f.store.Get(ctx, release.SystemPointerKey("router", "stable")) |
| 70 | if err != nil { |
| 71 | t.Fatal(err) |
| 72 | } |
| 73 | if err := release.PublishSystemIdentity(ctx, f.store, f.pub, wire); err != nil { |
| 74 | t.Fatal(err) |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | func (f *channelFixture) slots(t *testing.T) (cur, prev []byte) { |
| 79 | t.Helper() |
| 80 | cur, _ = os.ReadFile(systemBinaryPath(f.a.optDir)) |
| 81 | prev, _ = os.ReadFile(systemPrevPath(f.a.optDir)) |
| 82 | return cur, prev |
| 83 | } |
| 84 | |
| 85 | func TestInstallChannelPutsTheChannelReleaseInBothSlots(t *testing.T) { |
| 86 | f := newChannelFixture(t, "v1.2.0") |
| 87 | got, err := f.in.run(context.Background()) |
| 88 | if err != nil { |
| 89 | t.Fatal(err) |
| 90 | } |
| 91 | if got.Version != "v1.4.0" || got.Tag != "v1.4.0" || got.Already { |
| 92 | t.Fatalf("installed %+v", got) |
| 93 | } |
| 94 | cur, prev := f.slots(t) |
| 95 | if !bytes.Equal(cur, f.body) || !bytes.Equal(prev, f.body) { |
| 96 | t.Fatalf("slots hold %q and %q, not the channel's artifact", cur, prev) |
| 97 | } |
| 98 | // The identity guard-arm will be given is the channel release's. |
| 99 | held, err := os.ReadFile(channelIdentityPath(f.a.optDir)) |
| 100 | if err != nil { |
| 101 | t.Fatal(err) |
| 102 | } |
| 103 | p, err := release.VerifySystemIdentity(f.pub, held) |
| 104 | if err != nil || p.Current.Version != "v1.4.0" { |
| 105 | t.Fatalf("identity %v %v", p, err) |
| 106 | } |
| 107 | // An update later finds the binary on disk known good, and has seen |
| 108 | // this promote. |
| 109 | applied, err := readSystemApplied(f.a.optDir) |
| 110 | if err != nil || applied == nil || applied.Version != "v1.4.0" || applied.Channel != "stable" { |
| 111 | t.Fatalf("applied %+v %v", applied, err) |
| 112 | } |
| 113 | seen, err := readSystemSeen(f.a.optDir) |
| 114 | if err != nil || seen["stable"].IsZero() { |
| 115 | t.Fatalf("seen %v %v", seen, err) |
| 116 | } |
| 117 | // Nothing is left in staging or the agent's cache. |
| 118 | if _, err := os.Stat(f.a.stagingDir()); !os.IsNotExist(err) { |
| 119 | t.Fatalf("staging left: %v", err) |
| 120 | } |
| 121 | if _, err := os.Stat(filepath.Dir(systemCachePath(f.a.dataDir, "v1.4.0"))); !os.IsNotExist(err) { |
| 122 | t.Fatalf("cache left: %v", err) |
| 123 | } |
| 124 | // The candidate was asked its version from staging before it was put |
| 125 | // in place. |
| 126 | if !f.called("run carlos version") { |
| 127 | t.Fatalf("calls %v", f.calls) |
| 128 | } |
| 129 | // R2, S2: every directory on the cache's path is handed to the |
| 130 | // service user, and nothing else, so the agent's own update can |
| 131 | // download the next release beside them and ask for it. |
| 132 | if want := cachePathDirs(f.a.dataDir, "v1.4.0"); !reflect.DeepEqual(f.owned, want) { |
| 133 | t.Fatalf("handed to the service user: %v, want %v", f.owned, want) |
| 134 | } |
| 135 | // Run again: nothing to do, nothing fetched. |
| 136 | again, err := f.in.run(context.Background()) |
| 137 | if err != nil || !again.Already || f.fetches != 1 { |
| 138 | t.Fatalf("second run %+v %v, %d fetches", again, err, f.fetches) |
| 139 | } |
| 140 | if line := installedLine(again); line != "v1.4.0 (v1.4.0) from the stable channel is already installed" { |
| 141 | t.Fatalf("line %q", line) |
| 142 | } |
| 143 | // The agent, its next release promoted, downloads it and asks for it |
| 144 | // in the directories install-channel left. |
| 145 | leg := &systemLeg{dataDir: f.a.dataDir, fetchTo: func(ctx context.Context, key, dst, sha string) error { |
| 146 | return release.FetchToFile(ctx, f.store, key, dst, systemArtifactLimit, sha) |
| 147 | }} |
| 148 | next := []byte("#!/bin/sh\necho carlos v1.5.0\n") |
| 149 | f.publish(t, "v1.5.0", next) |
| 150 | sum := sha256.Sum256(next) |
| 151 | pending := &systemPending{version: "v1.5.0", artifact: release.SystemArtifact{Key: release.SystemArtifactKey("v1.5.0", "linux-arm64"), SHA256: hex.EncodeToString(sum[:])}} |
| 152 | if err := leg.fetch(context.Background(), pending); err != nil { |
| 153 | t.Fatalf("the agent's next download: %v", err) |
| 154 | } |
| 155 | if err := leg.writeRequest(pending, f.now); err != nil { |
| 156 | t.Fatalf("the agent's next request: %v", err) |
| 157 | } |
| 158 | } |
| 159 | |
| 160 | // cachePathDirs are the directories install-channel hands the service |
| 161 | // user for version: system, system/router and the version's own. |
| 162 | func cachePathDirs(dataDir, version string) []string { |
| 163 | return []string{filepath.Join(dataDir, "system"), filepath.Join(dataDir, "system", "router"), filepath.Join(dataDir, "system", "router", version)} |
| 164 | } |
| 165 | |
| 166 | // S2: a run whose ownership handoff fails installs nothing; the next run |
| 167 | // reconciles every directory on the path, including the ones the failed |
| 168 | // run created, not only the ones it creates itself. |
| 169 | func TestInstallChannelReconcilesOwnershipOnARetry(t *testing.T) { |
| 170 | f := newChannelFixture(t, "v1.2.0") |
| 171 | own := f.in.own |
| 172 | f.in.own = func(string) error { return errors.New("chown: operation not permitted") } |
| 173 | if _, err := f.in.run(context.Background()); err == nil { |
| 174 | t.Fatal("a failed handoff installed") |
| 175 | } |
| 176 | if cur, prev := f.slots(t); cur != nil || prev != nil || f.fetches != 0 { |
| 177 | t.Fatalf("a failed handoff wrote %q %q after %d fetches", cur, prev, f.fetches) |
| 178 | } |
| 179 | // The failed run created system, as root, and never handed it over. |
| 180 | if _, err := os.Stat(filepath.Join(f.a.dataDir, "system")); err != nil { |
| 181 | t.Fatalf("the failed run's parent: %v", err) |
| 182 | } |
| 183 | f.in.own = own |
| 184 | if _, err := f.in.run(context.Background()); err != nil { |
| 185 | t.Fatal(err) |
| 186 | } |
| 187 | if want := cachePathDirs(f.a.dataDir, "v1.4.0"); !reflect.DeepEqual(f.owned, want) { |
| 188 | t.Fatalf("the retry handed %v, want %v", f.owned, want) |
| 189 | } |
| 190 | } |
| 191 | |
| 192 | // S2: a symlinked parent is refused before anything is fetched, and never |
| 193 | // followed; the real handoff opens each directory without following one. |
| 194 | func TestInstallChannelRefusesASymlinkedParent(t *testing.T) { |
| 195 | f := newChannelFixture(t, "v1.2.0") |
| 196 | elsewhere := t.TempDir() |
| 197 | if err := os.Symlink(elsewhere, filepath.Join(f.a.dataDir, "system")); err != nil { |
| 198 | t.Fatal(err) |
| 199 | } |
| 200 | if _, err := f.in.run(context.Background()); err == nil || f.fetches != 0 || len(f.owned) != 0 { |
| 201 | t.Fatalf("symlinked parent: %v, %d fetches, owned %v", err, f.fetches, f.owned) |
| 202 | } |
| 203 | if ents, _ := os.ReadDir(elsewhere); len(ents) != 0 { |
| 204 | t.Fatalf("followed the symlink: %v", ents) |
| 205 | } |
| 206 | if err := ownDir(filepath.Join(f.a.dataDir, "system"), os.Getuid(), os.Getgid()); err == nil { |
| 207 | t.Fatal("the handoff followed a symlink") |
| 208 | } |
| 209 | if err := ownDir(elsewhere, os.Getuid(), os.Getgid()); err != nil { |
| 210 | t.Fatalf("the handoff of a real directory: %v", err) |
| 211 | } |
| 212 | } |
| 213 | |
| 214 | func TestInstallChannelRefusesBelowTheFloorAndWithoutOne(t *testing.T) { |
| 215 | for _, c := range []struct{ name, floor, want string }{ |
| 216 | {"below the floor", "v2.0.0", "below the host floor v2.0.0"}, |
| 217 | {"no floor", "", "no host floor is published"}, |
| 218 | } { |
| 219 | t.Run(c.name, func(t *testing.T) { |
| 220 | f := newChannelFixture(t, c.floor) |
| 221 | _, err := f.in.run(context.Background()) |
| 222 | if err == nil || !strings.Contains(err.Error(), c.want) { |
| 223 | t.Fatalf("err %v", err) |
| 224 | } |
| 225 | cur, prev := f.slots(t) |
| 226 | if cur != nil || prev != nil || f.fetches != 0 { |
| 227 | t.Fatalf("refused install wrote %q %q after %d fetches", cur, prev, f.fetches) |
| 228 | } |
| 229 | }) |
| 230 | } |
| 231 | } |
| 232 | |
| 233 | func TestInstallChannelRefusesAnUnreadableStoreAndABadArtifact(t *testing.T) { |
| 234 | f := newChannelFixture(t, "v1.2.0") |
| 235 | f.a.openStore = func(context.Context) (release.Store, ed25519.PublicKey, error) { |
| 236 | return nil, nil, errors.New("no route to bucket") |
| 237 | } |
| 238 | if _, err := f.in.run(context.Background()); err == nil || !strings.Contains(err.Error(), "could not be read") { |
| 239 | t.Fatalf("unreachable store: %v", err) |
| 240 | } |
| 241 | g := newChannelFixture(t, "v1.2.0") |
| 242 | // The object no longer matches the signed sha256. |
| 243 | _, etag, _ := g.store.Get(context.Background(), release.SystemArtifactKey("v1.4.0", "linux-arm64")) |
| 244 | if _, err := g.store.Put(context.Background(), release.SystemArtifactKey("v1.4.0", "linux-arm64"), []byte("#!/bin/sh\necho carlos v0.0.1\n"), etag); err != nil { |
| 245 | t.Fatal(err) |
| 246 | } |
| 247 | if _, err := g.in.run(context.Background()); err == nil || !strings.Contains(err.Error(), "could not be fetched") { |
| 248 | t.Fatalf("swapped artifact: %v", err) |
| 249 | } |
| 250 | if cur, prev := g.slots(t); cur != nil || prev != nil { |
| 251 | t.Fatalf("swapped artifact installed %q %q", cur, prev) |
| 252 | } |
| 253 | } |
| 254 | |
| 255 | func TestInstallChannelLeavesAnInstalledBoxToUpdates(t *testing.T) { |
| 256 | // An armed guard. |
| 257 | f := newChannelFixture(t, "v1.2.0") |
| 258 | ctx := context.Background() |
| 259 | raw, _, _ := f.store.Get(ctx, hostfloor.Key) |
| 260 | auth, _, _ := f.store.Get(ctx, hostfloor.AuthKey) |
| 261 | policy, _ := json.Marshal(hostfloor.Policy{Schema: 1, DeploymentBucket: "bucket", Mode: "armed", Floor: raw, Authorization: auth}) |
| 262 | if err := os.MkdirAll(systemGuardDir(f.a.optDir), 0o755); err != nil { |
| 263 | t.Fatal(err) |
| 264 | } |
| 265 | if err := os.WriteFile(filepath.Join(systemGuardDir(f.a.optDir), "policy.json"), policy, 0o644); err != nil { |
| 266 | t.Fatal(err) |
| 267 | } |
| 268 | if _, err := f.in.run(ctx); err == nil || !strings.Contains(err.Error(), "guard is armed") { |
| 269 | t.Fatalf("armed guard: %v", err) |
| 270 | } |
| 271 | // A box an earlier install or update put another release on. |
| 272 | g := newChannelFixture(t, "v1.2.0") |
| 273 | if _, err := g.in.run(ctx); err != nil { |
| 274 | t.Fatal(err) |
| 275 | } |
| 276 | g.publish(t, "v1.5.0", []byte("#!/bin/sh\necho carlos v1.5.0\n")) |
| 277 | if _, err := g.in.run(ctx); err == nil || !strings.Contains(err.Error(), "already runs v1.4.0") { |
| 278 | t.Fatalf("installed box: %v", err) |
| 279 | } |
| 280 | if cur, _ := g.slots(t); !bytes.Equal(cur, g.body) { |
| 281 | t.Fatalf("installed box changed to %q", cur) |
| 282 | } |
| 283 | } |
| 284 | |
| 285 | // A refused run removes nothing through a symlinked parent: the cleanup of |
| 286 | // the version directory is made only after the directories are made, and |
| 287 | // goes by descriptor, so root cannot be walked out of /data. |
| 288 | func TestInstallChannelRemovesNothingThroughASymlink(t *testing.T) { |
| 289 | f := newChannelFixture(t, "v1.2.0") |
| 290 | elsewhere := t.TempDir() |
| 291 | keep := filepath.Join(elsewhere, "router", "v1.4.0", "keep") |
| 292 | if err := os.MkdirAll(filepath.Dir(keep), 0o755); err != nil { |
| 293 | t.Fatal(err) |
| 294 | } |
| 295 | if err := os.WriteFile(keep, []byte("mine"), 0o644); err != nil { |
| 296 | t.Fatal(err) |
| 297 | } |
| 298 | if err := os.Symlink(elsewhere, filepath.Join(f.a.dataDir, "system")); err != nil { |
| 299 | t.Fatal(err) |
| 300 | } |
| 301 | if _, err := f.in.run(context.Background()); err == nil { |
| 302 | t.Fatal("a symlinked parent installed") |
| 303 | } |
| 304 | if b, err := os.ReadFile(keep); err != nil || string(b) != "mine" { |
| 305 | t.Fatalf("a refused run removed what the symlink led to: %q %v", b, err) |
| 306 | } |
| 307 | // The removal itself, called directly: a symlinked parent, and a |
| 308 | // version directory that is itself a symlink, are neither entered nor |
| 309 | // followed. |
| 310 | if err := removeCacheEntry(f.a.dataDir, "v1.4.0"); err == nil { |
| 311 | t.Fatal("the removal went through a symlinked parent") |
| 312 | } |
| 313 | if b, err := os.ReadFile(keep); err != nil || string(b) != "mine" { |
| 314 | t.Fatalf("removal followed a symlinked parent: %q %v", b, err) |
| 315 | } |
| 316 | g := newChannelFixture(t, "v1.2.0") |
| 317 | router := filepath.Join(g.a.dataDir, "system", "router") |
| 318 | if err := os.MkdirAll(router, 0o755); err != nil { |
| 319 | t.Fatal(err) |
| 320 | } |
| 321 | other := t.TempDir() |
| 322 | if err := os.WriteFile(filepath.Join(other, "keep"), []byte("mine"), 0o644); err != nil { |
| 323 | t.Fatal(err) |
| 324 | } |
| 325 | if err := os.Symlink(other, filepath.Join(router, "v1.4.0")); err != nil { |
| 326 | t.Fatal(err) |
| 327 | } |
| 328 | if err := removeCacheEntry(g.a.dataDir, "v1.4.0"); err == nil { |
| 329 | t.Fatal("the removal entered a symlinked version directory") |
| 330 | } |
| 331 | if b, err := os.ReadFile(filepath.Join(other, "keep")); err != nil || string(b) != "mine" { |
| 332 | t.Fatalf("removal followed the version directory's symlink: %q %v", b, err) |
| 333 | } |
| 334 | // And a real entry, with a nested directory and a symlink inside it, is |
| 335 | // removed whole, its parents left. |
| 336 | h := newChannelFixture(t, "v1.2.0") |
| 337 | entry := filepath.Join(h.a.dataDir, "system", "router", "v1.4.0") |
| 338 | if err := os.MkdirAll(filepath.Join(entry, "sub"), 0o755); err != nil { |
| 339 | t.Fatal(err) |
| 340 | } |
| 341 | if err := os.WriteFile(filepath.Join(entry, "sub", "carlos"), []byte("x"), 0o755); err != nil { |
| 342 | t.Fatal(err) |
| 343 | } |
| 344 | if err := os.Symlink(other, filepath.Join(entry, "link")); err != nil { |
| 345 | t.Fatal(err) |
| 346 | } |
| 347 | if err := removeCacheEntry(h.a.dataDir, "v1.4.0"); err != nil { |
| 348 | t.Fatal(err) |
| 349 | } |
| 350 | if _, err := os.Stat(entry); !os.IsNotExist(err) { |
| 351 | t.Fatalf("entry left: %v", err) |
| 352 | } |
| 353 | if _, err := os.Stat(filepath.Join(h.a.dataDir, "system", "router")); err != nil { |
| 354 | t.Fatalf("a parent was removed: %v", err) |
| 355 | } |
| 356 | if b, err := os.ReadFile(filepath.Join(other, "keep")); err != nil || string(b) != "mine" { |
| 357 | t.Fatalf("removal followed a symlink inside the entry: %q %v", b, err) |
| 358 | } |
| 359 | if err := removeCacheEntry(h.a.dataDir, "v1.4.0"); err != nil { |
| 360 | t.Fatalf("an entry already gone: %v", err) |
| 361 | } |
| 362 | } |
| 363 | |
| 364 | // A box an update has managed, at the channel's head, has applied.json and no |
| 365 | // channel identity: its releases come through carlos system update, and its |
| 366 | // carlos.prev is not overwritten. |
| 367 | func TestInstallChannelRefusesAnUpdatedBoxAtTheChannelHead(t *testing.T) { |
| 368 | f := newChannelFixture(t, "v1.2.0") |
| 369 | if err := os.WriteFile(systemBinaryPath(f.a.optDir), f.body, 0o755); err != nil { |
| 370 | t.Fatal(err) |
| 371 | } |
| 372 | prev := []byte("#!/bin/sh\necho carlos v1.3.0\n") |
| 373 | if err := os.WriteFile(systemPrevPath(f.a.optDir), prev, 0o755); err != nil { |
| 374 | t.Fatal(err) |
| 375 | } |
| 376 | if err := writeJSONDurable(f.a.file("applied.json"), systemApplied{Version: "v1.4.0", Channel: "stable", At: f.now}, 0o644); err != nil { |
| 377 | t.Fatal(err) |
| 378 | } |
| 379 | _, err := f.in.run(context.Background()) |
| 380 | if err == nil || err.Error() != "this box already runs v1.4.0, so carlos system update installs its releases" { |
| 381 | t.Fatalf("updated box: %v", err) |
| 382 | } |
| 383 | if cur, p := f.slots(t); !bytes.Equal(cur, f.body) || !bytes.Equal(p, prev) || f.fetches != 0 { |
| 384 | t.Fatalf("an updated box was changed: %q %q after %d fetches", cur, p, f.fetches) |
| 385 | } |
| 386 | } |
| 387 | |
| 388 | // A binary this command did not put in a slot, with no applied.json, is |
| 389 | // refused and left where it is; a run cut off part way, which leaves only the |
| 390 | // artifact behind, is completed. |
| 391 | func TestInstallChannelRefusesAHandRolledBoxAndCompletesACutOffRun(t *testing.T) { |
| 392 | for _, slot := range []func(string) string{systemBinaryPath, systemPrevPath} { |
| 393 | f := newChannelFixture(t, "v1.2.0") |
| 394 | path := slot(f.a.optDir) |
| 395 | if err := os.WriteFile(path, []byte("#!/bin/sh\necho hand rolled\n"), 0o755); err != nil { |
| 396 | t.Fatal(err) |
| 397 | } |
| 398 | _, err := f.in.run(context.Background()) |
| 399 | if err == nil || err.Error() != path+" holds a build this command did not install, so nothing was installed" { |
| 400 | t.Fatalf("hand-rolled %s: %v", path, err) |
| 401 | } |
| 402 | if b, _ := os.ReadFile(path); string(b) != "#!/bin/sh\necho hand rolled\n" || f.fetches != 0 { |
| 403 | t.Fatalf("a hand-rolled binary was replaced: %q after %d fetches", b, f.fetches) |
| 404 | } |
| 405 | if _, err := os.Stat(systemCachePath(f.a.dataDir, "v1.4.0")); !os.IsNotExist(err) { |
| 406 | t.Fatalf("a refused run left a cache entry: %v", err) |
| 407 | } |
| 408 | } |
| 409 | // Cut off after carlos.prev, or after both slots: no applied.json, and |
| 410 | // only the artifact on disk. |
| 411 | for name, slots := range map[string][]func(string) string{ |
| 412 | "prev only": {systemPrevPath}, |
| 413 | "both slots": {systemPrevPath, systemBinaryPath}, |
| 414 | } { |
| 415 | t.Run(name, func(t *testing.T) { |
| 416 | f := newChannelFixture(t, "v1.2.0") |
| 417 | for _, s := range slots { |
| 418 | if err := os.WriteFile(s(f.a.optDir), f.body, 0o755); err != nil { |
| 419 | t.Fatal(err) |
| 420 | } |
| 421 | } |
| 422 | got, err := f.in.run(context.Background()) |
| 423 | if err != nil || got.Already { |
| 424 | t.Fatalf("retry %+v %v", got, err) |
| 425 | } |
| 426 | if cur, prev := f.slots(t); !bytes.Equal(cur, f.body) || !bytes.Equal(prev, f.body) { |
| 427 | t.Fatalf("slots hold %q %q", cur, prev) |
| 428 | } |
| 429 | if applied, err := readSystemApplied(f.a.optDir); err != nil || applied == nil || applied.Version != "v1.4.0" { |
| 430 | t.Fatalf("applied %+v %v", applied, err) |
| 431 | } |
| 432 | if _, err := os.Stat(channelIdentityPath(f.a.optDir)); err != nil { |
| 433 | t.Fatal(err) |
| 434 | } |
| 435 | }) |
| 436 | } |
| 437 | } |
| 438 | |
| 439 | // A slot that is a symlink is refused, even one to a file that holds the |
| 440 | // artifact, and neither the link nor its target is touched. |
| 441 | func TestInstallChannelRefusesASymlinkedSlot(t *testing.T) { |
| 442 | for _, slot := range []func(string) string{systemBinaryPath, systemPrevPath} { |
| 443 | f := newChannelFixture(t, "v1.2.0") |
| 444 | target := filepath.Join(t.TempDir(), "elsewhere") |
| 445 | if err := os.WriteFile(target, f.body, 0o755); err != nil { |
| 446 | t.Fatal(err) |
| 447 | } |
| 448 | path := slot(f.a.optDir) |
| 449 | if err := os.Symlink(target, path); err != nil { |
| 450 | t.Fatal(err) |
| 451 | } |
| 452 | _, err := f.in.run(context.Background()) |
| 453 | if err == nil || err.Error() != path+" holds a build this command did not install, so nothing was installed" || f.fetches != 0 { |
| 454 | t.Fatalf("symlinked %s: %v after %d fetches", path, err, f.fetches) |
| 455 | } |
| 456 | if l, err := os.Readlink(path); err != nil || l != target { |
| 457 | t.Fatalf("the link was changed: %q %v", l, err) |
| 458 | } |
| 459 | if b, _ := os.ReadFile(target); !bytes.Equal(b, f.body) { |
| 460 | t.Fatalf("the link's target was changed: %q", b) |
| 461 | } |
| 462 | // A slot that cannot be read at all is an error, not a pass. |
| 463 | if err := os.Remove(path); err != nil { |
| 464 | t.Fatal(err) |
| 465 | } |
| 466 | if err := os.Mkdir(path, 0o755); err != nil { |
| 467 | t.Fatal(err) |
| 468 | } |
| 469 | if _, err := f.in.run(context.Background()); err == nil || !strings.Contains(err.Error(), "holds a build this command did not install") { |
| 470 | t.Fatalf("a directory in a slot: %v", err) |
| 471 | } |
| 472 | } |
| 473 | } |
| 474 | |