package host import ( "bytes" "context" "crypto/ed25519" "encoding/json" "errors" "flag" "fmt" "os" "path/filepath" "runtime" "strings" "github.com/carlosframework/platform/internal/cliflag" "github.com/carlosframework/platform/internal/hostfloor" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/version" ) // carlos system install-channel (region HA spec 2 §5 step 3, A1-6, A2-2). // // A group box boots from a bootstrap object, which is whatever build the // deployment last uploaded there, not its neighbours' release. This puts // the release the box's signed channel pointer names in place instead, // through the same path `carlos system update` takes: the pointer verified // with CARLOS_RELEASE_PUBKEY (verifyPointer), the artifact fetched against // the signed sha256 into the agent's cache, then copied into root's // staging, checked again and asked its version (stageCandidate). // // Both protected slots, carlos and carlos.prev, become that artifact // (A2-2): guard-arm checks both against the release identity and refuses // a box with any other build in either. The release identity is kept // beside them (channelIdentityPath) for the join step's guard-arm, and the // release must pass the published host floor (hostfloor.CheckImage, the // check guard-arm makes) before anything is fetched. // // It is for a fresh box. A box whose guard is armed, or that already runs // another release this way or through an update, is refused: its releases // come through `carlos system update`. Run again on a box it has already // installed, it changes nothing. // // --policy FILE (claim trial, prev-rehearsal ruling R1) is for arming a box // before the host floor is published. FILE is exactly what `carlos system // claims run` sign-policy writes. Its floor is verified as guard-arm // verifies it (the root-signed moves-protocol authorization, for this // deployment's bucket, under CARLOS_RELEASE_PUBKEY) and stands in for the // published floor while there is none; a published floor, or either half // of one, must be the policy's own bytes. Every other refusal stands. It // also admits the one updated box A2-2 would otherwise leave unarmable: // one whose carlos already is the channel release's signed artifact. Its // carlos.prev, the release before, is replaced by that artifact // (refreshPrev), because guard-arm requires both slots to be the // identity's release. Without --policy nothing here changes. // // --identity FILE (claim trial, untagged-pointer ruling R1) goes with // --policy, and only with it. FILE is exactly what `carlos system claims run` // publish-identity writes. The channel pointer `carlos system update` writes // before a floor is published carries no release tag (the console tags a // release only once a floor is published), so with --policy the release's // identity is never found through the pointer: --identity must verify under // CARLOS_RELEASE_PUBKEY, be byte for byte the object published under its own // tag, and name the version and this platform's artifact sha256 the verified // pointer names (and its tag, if the pointer carries one). That identity is // the one the floor is checked against and the one kept for guard-arm. const installChannelUsage = "usage: carlos system install-channel [--policy FILE --identity FILE]\n\nInstall the release this box's channel names, as root, on a box that has none yet.\n" const installChannelPolicyFlag = "the armed policy carlos system claims run sign-policy wrote, used as the host floor until one is published" const installChannelIdentityFlag = "the release identity carlos system claims run publish-identity wrote, used with --policy in place of the channel's release tag" const installChannelNotRoot = "carlos system install-channel runs as root: it installs the platform binary" // channelIdentityPath is where the installed release's signed identity is // kept for guard-arm (§5 step 5.3). func channelIdentityPath(optDir string) string { return filepath.Join(systemRootDir(optDir), "channel-identity.json") } // channelInstall is one run. fetchTo is release.FetchToFile over the // pointer's store, and own hands a directory tree to the service user // (ownByService); both are seams for tests. type channelInstall struct { a *systemApplier bucket string fetchTo func(ctx context.Context, s release.Store, key, dst, sha string) error own func(path string) error // policy is --policy's file, byte for byte; nil without the flag. policy []byte // identity is --identity's file, byte for byte; set with policy and // only with it. identity []byte } // slotKind is what one protected slot holds (slotState). type slotKind int const ( slotAbsent slotKind = iota // nothing there slotHolds // the signed artifact slotForeign // a symlink, a non-regular file or any other build ) // channelInstalled is what a run did. type channelInstalled struct { Version, Tag, Channel string Already bool // Replaced: --policy replaced an updated box's carlos.prev (refreshPrev). Replaced bool } // RunSystemInstallChannel is `carlos system install-channel`. func RunSystemInstallChannel(args []string) { fs := flag.NewFlagSet("system install-channel", flag.ExitOnError) policyPath := fs.String("policy", "", installChannelPolicyFlag) identityPath := fs.String("identity", "", installChannelIdentityFlag) fs.Usage = cliflag.Usage(fs, installChannelUsage) _ = fs.Parse(args) if fs.NArg() != 0 { fs.Usage() os.Exit(2) } if (*policyPath == "") != (*identityPath == "") { fmt.Fprintln(os.Stderr, errChannelPolicyIdentity) os.Exit(2) } if os.Geteuid() != 0 { fmt.Fprintln(os.Stderr, installChannelNotRoot) os.Exit(1) } channel, invalid := systemChannel(os.Getenv) if invalid != "" { fmt.Fprintln(os.Stderr, fmt.Sprintf("%s=%q is not a channel, so nothing was installed", systemChannelEnv, invalid)) os.Exit(1) } policy, identity, err := readChannelInputs(*policyPath, *identityPath) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } in := newChannelInstall(channel, policy, identity) got, err := in.run(context.Background()) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } fmt.Println(installedLine(got)) } // channelPolicyLimit caps --policy's file: sign-policy writes about 500 // bytes. const channelPolicyLimit = 64 << 10 // errChannelPolicy is --policy's one refusal of the policy itself, said // alike before and after its signature is checked. var errChannelPolicy = errors.New("the --policy file is not an armed policy signed for this deployment, so nothing was installed") // errChannelPolicyIdentity refuses --policy without --identity, or // --identity without --policy. var errChannelPolicyIdentity = errors.New("--policy and --identity go together, so nothing was installed") // errChannelIdentity is --identity's refusal of a file that is not a release // identity signed under the box's release key. var errChannelIdentity = errors.New("the --identity file is not a release identity signed for this deployment, so nothing was installed") // newChannelInstall is the production run. The --policy path refreshes // carlos.prev as an update does, so its applier reads the root // participation census as the update's does (systemapply.go): on a census // box it takes the root moves floor branch, where a floor step that fails // fails (fix round 1, I2). Without --policy the applier is as it was. func newChannelInstall(channel string, policy, identity []byte) channelInstall { in := channelInstall{ a: &systemApplier{ops: productionSysOps(), openStore: openSystemStore, channel: channel, optDir: release.OptDir(), dataDir: dataDir(), etcDir: "/etc", build: version.Build, platform: runtime.GOOS + "-" + runtime.GOARCH}, bucket: os.Getenv("CARLOS_DEPLOYMENT_BUCKET"), fetchTo: func(ctx context.Context, s release.Store, key, dst, sha string) error { return release.FetchToFile(ctx, s, key, dst, systemArtifactLimit, sha) }, own: ownByService, policy: policy, identity: identity, } if policy != nil { in.a.rootMoves = rootMovesFloorIdentity } return in } // readChannelInputs reads --policy's and --identity's files, before the lock // or any store read: both or neither (errChannelPolicyIdentity), the policy // first (readChannelPolicy), then the identity through the same hardened // open (openChannelPolicy). The identity is verified once the release key is // known (channelIdentity). With neither flag nothing is read. func readChannelInputs(policyPath, identityPath string) (policy, identity []byte, err error) { if (policyPath == "") != (identityPath == "") { return nil, nil, errChannelPolicyIdentity } if policyPath == "" { return nil, nil, nil } if policy, err = readChannelPolicy(policyPath); err != nil { return nil, nil, err } if identity, err = openChannelPolicy(identityPath, channelPolicyLimit); err != nil { return nil, nil, fmt.Errorf("the identity %s could not be read, so nothing was installed: %w", identityPath, err) } return policy, identity, nil } // readChannelPolicy reads --policy's file and refuses, before anything else // is read, a file that is not an armed policy at all. It is opened without // following a symlink or waiting on a FIFO, and must be a regular file of // at most channelPolicyLimit bytes (openChannelPolicy). Its signature is // checked once the release key is known (channelPolicy). func readChannelPolicy(path string) ([]byte, error) { raw, err := openChannelPolicy(path, channelPolicyLimit) if err != nil { return nil, fmt.Errorf("the policy %s could not be read, so nothing was installed: %w", path, err) } var p hostfloor.Policy if json.Unmarshal(raw, &p) != nil || p.Mode != "armed" { return nil, errChannelPolicy } return raw, nil } // channelPolicy is guard-arm's check of --policy (guardArmInputs, first // half): an armed policy whose floor carries the root-signed // moves-protocol authorization for this deployment's bucket. func channelPolicy(raw []byte, pub ed25519.PublicKey, bucket string) (hostfloor.Policy, hostfloor.Verified, error) { var p hostfloor.Policy if json.Unmarshal(raw, &p) != nil || p.Mode != "armed" { return hostfloor.Policy{}, hostfloor.Verified{}, hostfloor.ErrFloor } f, err := p.Verify(pub, bucket) if err != nil { return hostfloor.Policy{}, hostfloor.Verified{}, err } return p, f, nil } func installedLine(got channelInstalled) string { if got.Replaced { return fmt.Sprintf("replaced the rollback copy carlos.prev with %s (%s) from the %s channel, the release carlos already runs. the release carlos.prev held before is no longer on this box", got.Version, got.Tag, got.Channel) } if got.Already { return fmt.Sprintf("%s (%s) from the %s channel is already installed", got.Version, got.Tag, got.Channel) } return fmt.Sprintf("installed %s (%s) from the %s channel as carlos and carlos.prev", got.Version, got.Tag, got.Channel) } // run takes the apply lock, so no update runs beside it, and installs. func (in channelInstall) run(ctx context.Context) (channelInstalled, error) { a := in.a if err := os.MkdirAll(a.rootDir(), 0o755); err != nil { return channelInstalled{}, err } lock, err := os.OpenFile(a.lockPath(), os.O_CREATE|os.O_RDWR, 0o600) if err != nil { return channelInstalled{}, err } defer lock.Close() if err := flockExclusiveNonblock(lock); err != nil { return channelInstalled{}, fmt.Errorf("an update is running on this box, so nothing was installed: %w", err) } return in.install(ctx) } func (in channelInstall) install(ctx context.Context) (channelInstalled, error) { a := in.a var pub ed25519.PublicKey open := a.openStore a.openStore = func(ctx context.Context) (release.Store, ed25519.PublicKey, error) { s, k, err := open(ctx) pub = k return s, k, err } p, err := a.verifyPointer(ctx) if err != nil { return channelInstalled{}, fmt.Errorf("the %s channel could not be read, so nothing was installed: %w", a.channel, err) } got := channelInstalled{Version: p.Current.Version, Tag: p.Current.ReleaseTag, Channel: a.channel} var policy *hostfloor.Policy var policyFloor hostfloor.Verified if in.policy != nil { if in.identity == nil { return got, errChannelPolicyIdentity } pol, f, err := channelPolicy(in.policy, pub, in.bucket) if err != nil { return got, errChannelPolicy } policy, policyFloor = &pol, f } art, ok := p.Current.Artifacts[a.platform] if !ok { return got, fmt.Errorf("the %s channel names %s with no artifact for %s, so nothing was installed", a.channel, p.Current.Version, a.platform) } // A box whose guard is armed takes releases only through an update. guard := hostfloor.Disk{Dir: systemGuardDir(a.optDir), Bucket: in.bucket, PublicKey: pub} if policy, _, err := guard.Load(); err == nil && policy.Mode != "unpublished" { return got, errors.New("this box's host lifecycle guard is armed, so carlos system update installs its releases") } else if err != nil && !errors.Is(err, os.ErrNotExist) { return got, fmt.Errorf("this box's host lifecycle guard could not be read, so nothing was installed: %w", err) } applied, err := readSystemApplied(a.optDir) if err != nil { return got, fmt.Errorf("this box's install record could not be read, so nothing was installed: %w", err) } if applied != nil && applied.Version != p.Current.Version { return got, fmt.Errorf("this box already runs %s, so carlos system update installs its releases", applied.Version) } wire, tag, err := in.checkFloor(ctx, p, art, pub, policy, policyFloor) if err != nil { return got, err } // The tag is the identity's: with --policy it is --identity's, which an // untagged pointer cannot name. got.Tag = tag // With --policy, a box whose update has not finished (an open journal, // or an update-begun marker beside the guard) is refused, as guard-arm // refuses it: recovery owns carlos.prev until then (fix round 1, I1). if policy != nil { if err := guardArmIdle(systemGuardDir(a.optDir), systemRootDir(a.optDir)); err != nil { // The reason is the journal or marker guard-arm names, without // guard-arm's floor prefix: it is an unfinished update. if errors.Is(err, hostfloor.ErrFloor) { err = errors.New(strings.TrimPrefix(err.Error(), hostfloor.ErrFloor.Error()+": ")) } return got, fmt.Errorf("an update on this box has not finished, so nothing was installed: %w", err) } } already := systemCacheHolds(systemBinaryPath(a.optDir), art.SHA256) && systemCacheHolds(systemPrevPath(a.optDir), art.SHA256) && systemCacheHolds(channelIdentityPath(a.optDir), hostfloor.Digest(wire)) if applied != nil { // applied.json is the last thing a run writes, so a box with one is // either this install, complete, or a box an update has since // managed: its releases come through carlos system update. if already { // With --policy, "already installed" also needs carlos.prev's // version record to name this release, and its judgement to have // run. A record a cut-off run left missing or stale is completed // first (which judges); otherwise the judgement runs again, as it // is idempotent, so one a run left unfinished is finished. Either // failing fails the run (fix round 2). if policy != nil { var err error if !prevRecorded(a.optDir, p.Current.Version) { err = in.completePrev(ctx, p.Current.Version, false) } else { err = channelPrevJudge(a, ctx) } if err != nil { return got, err } } got.Already = true return got, nil } if policy != nil { return in.refreshPrev(ctx, p, art, wire, got) } return got, fmt.Errorf("this box already runs %s, so carlos system update installs its releases", applied.Version) } // With no applied.json, a slot that holds anything but the signed // artifact is a build this command did not install, and is refused and // left as it is. A run cut off part way leaves only the artifact behind // (carlos.prev is written first), so a retry passes. Each slot is opened // without following a symlink and hashed from the descriptor, against the // verified pointer's sha256; nothing is executed to learn its version. for _, slot := range []string{systemBinaryPath(a.optDir), systemPrevPath(a.optDir)} { state, err := slotState(slot, art.SHA256) if err != nil { return got, fmt.Errorf("this box's install record could not be read, so nothing was installed: %w", err) } if state == slotForeign { return got, fmt.Errorf("%s holds a build this command did not install, so nothing was installed", slot) } } cache := systemCachePath(a.dataDir, p.Current.Version) defer os.RemoveAll(a.stagingDir()) if err := in.cacheDir(p.Current.Version); err != nil { return got, err } // Only now is the version directory ours to remove, and it is removed // through the held router directory, never by a path that could pass a // symlink (the service user owns the parents). defer func() { _ = removeCacheEntry(a.dataDir, p.Current.Version) }() if err := in.fetchTo(ctx, a.store, art.Key, cache, art.SHA256); err != nil { return got, fmt.Errorf("%s could not be fetched, so nothing was installed: %w", p.Current.Version, err) } staged, err := a.stageCandidate(ctx, p, systemRequest{Version: p.Current.Version, PromotedAt: p.PromotedAt}) if err != nil { return got, fmt.Errorf("%s did not verify, so nothing was installed: %w", p.Current.Version, err) } // carlos.prev first: a run cut off between the two copies leaves the // new build in .prev and none in the current slot, which the next run // replaces, never the other way round. for _, slot := range []string{systemPrevPath(a.optDir), systemBinaryPath(a.optDir)} { if err := copyFileDurable(staged, slot, 0o755); err != nil { return got, err } if !systemCacheHolds(slot, art.SHA256) { return got, fmt.Errorf("%s does not hold %s after the copy", slot, p.Current.Version) } } // With --policy, carlos.prev's version record is part of the install // (completePrev); without it the install is exactly as it was. if policy != nil { if err := in.completePrev(ctx, p.Current.Version, false); err != nil { return got, err } } if err := hostfloor.Durable(channelIdentityPath(a.optDir), wire, 0o644); err != nil { return got, err } seen, err := readSystemSeen(a.optDir) if err != nil { return got, err } seen[a.channel] = p.PromotedAt if err := writeJSONDurable(a.file("seen.json"), seen, 0o644); err != nil { return got, err } if err := writeJSONDurable(a.file("applied.json"), systemApplied{Version: p.Current.Version, Channel: a.channel, At: a.ops.now().UTC()}, 0o644); err != nil { return got, err } return got, nil } // checkFloor reads the published host floor and the release's signed // identity, and returns the identity once the release passes the floor // exactly as guard-arm will check it. No floor, or one that cannot be read, // refuses: a joining box could not arm its guard without one, and a read // that failed says nothing about where the floor is. With --policy // (policy non-nil) the policy's verified floor is the floor instead // (policyFloorPublished), and the identity is --identity's, never one found // through the pointer's tag (channelIdentity). The tag returned is the // identity's. func (in channelInstall) checkFloor(ctx context.Context, p *release.SystemPointer, art release.SystemArtifact, pub ed25519.PublicKey, policy *hostfloor.Policy, policyFloor hostfloor.Verified) ([]byte, string, error) { s := in.a.store var floor hostfloor.Verified var wire []byte tag := p.Current.ReleaseTag if policy != nil { if err := policyFloorPublished(ctx, s, *policy); err != nil { return nil, "", err } floor = policyFloor var err error if wire, tag, err = in.channelIdentity(ctx, s, p, art, pub); err != nil { return nil, "", err } } else { raw, _, err := s.Get(ctx, hostfloor.Key) if errors.Is(err, os.ErrNotExist) { return nil, "", errors.New("no host floor is published, so this box could not arm its guard and nothing was installed") } if err != nil { return nil, "", fmt.Errorf("the host floor could not be read, so nothing was installed: %w", err) } auth, _, err := s.Get(ctx, hostfloor.AuthKey) if err != nil { return nil, "", fmt.Errorf("the host floor could not be read, so nothing was installed: %w", err) } floor, err = hostfloor.Verify(pub, in.bucket, raw, auth) if err != nil { return nil, "", fmt.Errorf("the host floor does not verify, so nothing was installed: %w", err) } key, err := release.IdentityKey(p.Current.ReleaseTag) if err != nil { return nil, "", fmt.Errorf("the %s channel names %s with no release tag, so nothing was installed", in.a.channel, p.Current.Version) } wire, _, err = s.Get(ctx, key) if err != nil { return nil, "", fmt.Errorf("the release identity for %s could not be read, so nothing was installed: %w", p.Current.ReleaseTag, err) } } if err := hostfloor.CheckImage(pub, floor, wire, p.Current.Version, in.a.platform, art.SHA256); err != nil { return nil, "", fmt.Errorf("%s (%s) is below the host floor %s or is not the release its identity names, so nothing was installed", p.Current.Version, tag, floor.Floor.MinRelease) } return wire, tag, nil } // channelIdentity is --identity's check (untagged-pointer ruling R1), once // the release key is known: the file verifies as a release identity under // it, is byte for byte the object published at release.IdentityKey of its // own tag (none published under it is its own refusal), and names what the // verified pointer names: the same version and // the same sha256 for this platform's artifact, and, when the pointer // carries a release tag, that tag. It returns the identity's bytes and tag. // Nothing is read through the pointer's tag. func (in channelInstall) channelIdentity(ctx context.Context, s release.Store, p *release.SystemPointer, art release.SystemArtifact, pub ed25519.PublicKey) ([]byte, string, error) { id, err := release.VerifySystemIdentity(pub, in.identity) if err != nil { return nil, "", errChannelIdentity } tag := id.Current.ReleaseTag key, err := release.IdentityKey(tag) if err != nil { return nil, "", errChannelIdentity } published, _, err := s.Get(ctx, key) if err != nil && !errors.Is(err, os.ErrNotExist) { return nil, "", fmt.Errorf("the release identity for %s could not be read, so nothing was installed: %w", tag, err) } if err != nil { return nil, "", fmt.Errorf("no release identity is published for %s, so nothing was installed", tag) } if !bytes.Equal(published, in.identity) { return nil, "", fmt.Errorf("the --identity file is not the identity published for %s, so nothing was installed", tag) } router, ok := id.Current.Artifacts[in.a.platform] if !ok || router.SHA256 != art.SHA256 || id.Current.Version != p.Current.Version || p.Current.ReleaseTag != "" && p.Current.ReleaseTag != tag { return nil, "", fmt.Errorf("the --identity file names %s (%s), not the release the %s channel names, so nothing was installed", id.Current.Version, tag, in.a.channel) } return in.identity, tag, nil } // policyFloorPublished checks --policy against what the bucket holds: no // floor published, or a published pair (or the half of one that a torn // publication leaves; hostfloor.Publish writes the authorization first) // that is the policy's floor and authorization byte for byte. Anything // else, a read that fails included, refuses. func policyFloorPublished(ctx context.Context, s release.Store, policy hostfloor.Policy) error { for _, half := range []struct { key string want []byte }{{hostfloor.AuthKey, policy.Authorization}, {hostfloor.Key, policy.Floor}} { got, _, err := s.Get(ctx, half.key) if errors.Is(err, os.ErrNotExist) { continue } if err != nil { return fmt.Errorf("the host floor could not be read, so nothing was installed: %w", err) } if !bytes.Equal(got, half.want) { return errors.New("the published host floor is not the floor in the --policy file, so nothing was installed") } } return nil } // channelPrevVersionWrite is writePrevVersion, a seam for the tests that // the record's write failing fails the --policy install. var channelPrevVersionWrite = writePrevVersion // prevRecorded reports whether carlos.prev's version record names version. func prevRecorded(optDir, version string) bool { v, ok := readLine(prevVersionPath(optDir)) return ok && v == version } // completePrev is --policy's refresh of carlos.prev's records, the steps // an update's notePrev takes but with every one mandatory, armed floor or // not (prev-rehearsal fix round 1): under the prev lock, the verdicts and // the version record of what carlos.prev held are removed, carlos is // copied over it when copyCurrent is set, and the version record is // written and read back. Any failure fails the run, which then keeps no // identity and reports nothing done; a retry completes it. The floor's // judgement follows as an update makes it (notePrev without a replace): // a judgement that cannot be made leaves no verdict, which refuses. func (in channelInstall) completePrev(ctx context.Context, version string, copyCurrent bool) error { a := in.a err := withPrevLock(a.optDir, func() error { for _, path := range []string{prevAllowedPath(a.optDir), seedsPrevAllowedPath(a.optDir), prevVersionPath(a.optDir)} { if err := removeDurable(path); err != nil { return err } } if copyCurrent { if err := copyFileDurable(systemBinaryPath(a.optDir), systemPrevPath(a.optDir), 0o755); err != nil { return err } } return channelPrevVersionWrite(a.optDir, version) }) if err == nil && !prevRecorded(a.optDir, version) { err = fmt.Errorf("%s does not name %s", prevVersionPath(a.optDir), version) } if err != nil { return fmt.Errorf("the record of the release in carlos.prev could not be written, so the install is not complete. run it again: %w", err) } return channelPrevJudge(a, ctx) } // channelPrevJudge is the floor's judgement of carlos.prev, as an update // makes it (notePrev without a replace); a seam for the tests of a // judgement that fails or is cut off. var channelPrevJudge = func(a *systemApplier, ctx context.Context) error { return a.notePrev(ctx, false, "") } // refreshPrev is --policy's one admission of an updated box (applied.json // names the channel release, and the slots and the identity are not yet // install-channel's): carlos must already hold the channel release's // signed artifact, hashed from a descriptor opened with O_NOFOLLOW // (slotState). Its carlos.prev, the release the update replaced, is then // refreshed from carlos the way an update refreshes it (notePrev: the // sidecar and verdicts of the old carlos.prev removed first, the copy, the // version sidecar, the floor's judgement), every step mandatory here // (completePrev), and only then is the identity kept beside // them. Nothing is fetched, and applied.json and seen.json are the // update's and stay as they are. The caller has already refused an armed // guard, another release and a release below the policy's floor. // // Why A2-2 allows it: A2-2 puts the identity's artifact in both slots so // guard-arm's two-slot rule can pass, and on a fresh box carlos.prev is // then a copy of carlos. Here carlos already is that artifact, so this // reaches A2-2's end state exactly. What is lost is the rollback to the // release before, which is below the floor whenever the floor is the // release being armed, and which the armed guard would refuse to run. func (in channelInstall) refreshPrev(ctx context.Context, p *release.SystemPointer, art release.SystemArtifact, wire []byte, got channelInstalled) (channelInstalled, error) { a := in.a current, prev := systemBinaryPath(a.optDir), systemPrevPath(a.optDir) state, err := slotState(current, art.SHA256) if err != nil { return got, fmt.Errorf("this box's install record could not be read, so nothing was installed: %w", err) } if state != slotHolds { return got, fmt.Errorf("%s holds a build this command did not install, so nothing was installed", current) } held, err := slotState(prev, art.SHA256) if err != nil { return got, fmt.Errorf("this box's install record could not be read, so nothing was installed: %w", err) } if held == slotForeign { // The release before is replaced; a symlink or anything but a // regular file is not what an update leaves, and is refused. if fi, err := os.Lstat(prev); err != nil { return got, fmt.Errorf("this box's install record could not be read, so nothing was installed: %w", err) } else if !fi.Mode().IsRegular() { return got, fmt.Errorf("%s holds a build this command did not install, so nothing was installed", prev) } } // Always, even over a carlos.prev a cut-off run already copied, so the // version record is this release's. The record is mandatory here. if err := in.completePrev(ctx, p.Current.Version, true); err != nil { return got, err } // The replaced line names a release that was there; a retry that found // carlos.prev already this release, or none, says only what is installed. replaced := held == slotForeign for _, slot := range []string{prev, current} { if state, err := slotState(slot, art.SHA256); err != nil || state != slotHolds { return got, fmt.Errorf("%s does not hold %s after the copy", slot, p.Current.Version) } } if err := hostfloor.Durable(channelIdentityPath(a.optDir), wire, 0o644); err != nil { return got, err } got.Replaced = replaced return got, nil } // cacheDir makes the agent's cache entry for version and reconciles the // ownership of every directory on its path below the data dir, on every // run: system, system/router and system/router/ are created as // bootstrap creates directories (bootstrapMakeDir: each component opened // with no symlink followed, so a symlinked parent is refused) and each is // handed to the service user, as bootstrap's chown of /data does // (privilegedPlan), whether this run created it or an earlier run that // failed before its handoff did. Nothing else is chowned. The agent // (User=carlos) later downloads releases beside the entry and writes its // update requests there. Only the entry itself is removed after the // install; its parents stay, owned by the agent. func (in channelInstall) cacheDir(version string) error { parts := systemCacheParts(version) dir := in.a.dataDir for _, part := range parts[:len(parts)-1] { dir = filepath.Join(dir, part) if err := bootstrapMakeDir(dir, 0o755); err != nil { return err } if err := in.own(dir); err != nil { return err } } return nil }