| 1 | // The release's own swap demand (zero-downtime deploys spec §4.2): how an |
| 2 | // awake instance may move onto a build, as the build's shipper said. |
| 3 | package release |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "errors" |
| 8 | "fmt" |
| 9 | "strings" |
| 10 | ) |
| 11 | |
| 12 | // DemandHold is the one swap demand a release can make (`carlos deploy |
| 13 | // --swap hold`, spec §9.1). |
| 14 | const DemandHold = "hold" |
| 15 | |
| 16 | // NormalizeDemand is the demand a box acts on for a manifest's swap field. |
| 17 | // "" is no demand. Anything else is DemandHold: "hold" itself, and any value |
| 18 | // a newer shipper wrote that this binary does not know, because missing or |
| 19 | // unverifiable policy data never permits an overlap (spec §4.2). |
| 20 | func NormalizeDemand(swap string) string { |
| 21 | if swap == "" { |
| 22 | return "" |
| 23 | } |
| 24 | return DemandHold |
| 25 | } |
| 26 | |
| 27 | // ErrStagedDigestMismatch is an adoption attempt's staging error (spec §4.2): |
| 28 | // the staged executable is not the artifact the canonical manifest names for |
| 29 | // this platform, so the manifest's demand does not describe the bytes that |
| 30 | // would run. The attempt is refused, with no failure record against the |
| 31 | // build. |
| 32 | var ErrStagedDigestMismatch = errors.New("staged artifact is not the release's canonical artifact") |
| 33 | |
| 34 | // CanonicalDemand reads build version's own swap demand from its CANONICAL |
| 35 | // manifest in the release store, and checks the staged executable at |
| 36 | // stagedPath against that manifest's digest for platform (spec §4.2). |
| 37 | // |
| 38 | // - Match: the manifest's demand (NormalizeDemand), the staged digest, nil. |
| 39 | // - Mismatch: "", the staged digest, an error wrapping |
| 40 | // ErrStagedDigestMismatch. |
| 41 | // - Cannot compare (the manifest is unreadable, it has no artifact for |
| 42 | // platform, or the staged file cannot be read): DemandHold and a nil |
| 43 | // error, because missing or unverifiable policy data never permits an |
| 44 | // overlap. digest is the staged file's when it could be hashed, else "". |
| 45 | // |
| 46 | // It is for ADOPTION attempts only (causes release, install, retry). Recovery |
| 47 | // checks a retained digest with VerifyStaged and never asks the store. |
| 48 | func CanonicalDemand(ctx context.Context, s Store, acct, app, version, platform, stagedPath string) (demand string, digest string, err error) { |
| 49 | digest, herr := FileSHA256(stagedPath) |
| 50 | if herr != nil { |
| 51 | return DemandHold, "", nil |
| 52 | } |
| 53 | m, _, merr := GetManifestRaw(ctx, s, acct, app, version) |
| 54 | if merr != nil { |
| 55 | return DemandHold, digest, nil |
| 56 | } |
| 57 | art, ok := m.Artifacts[platform] |
| 58 | if !ok { |
| 59 | return DemandHold, digest, nil |
| 60 | } |
| 61 | if !strings.EqualFold(art.SHA256, digest) { |
| 62 | return "", digest, fmt.Errorf("%w: %s/%s staged at %s hashes to %s; the release names %s", |
| 63 | ErrStagedDigestMismatch, app, version, stagedPath, digest, art.SHA256) |
| 64 | } |
| 65 | return NormalizeDemand(m.Swap), digest, nil |
| 66 | } |
| 67 | |