Documentation
¶
Overview ¶
Package imagecheck records what a workflow's derived image was built from, and decides whether it needs a rebuild (design §7.2).
The build's record step writes a Record to the runs bucket, at RecordKey(slug, workflow). The daily check compares it with the base branch's head: the key files (KeyFiles) and the image config (ImageConfigHash) are computed the same way on both sides, from a Tree, which is a checkout directory in the build and a git tree in the check.
Index ¶
- Constants
- Variables
- func CheckKey(slug, workflow string) string
- func FailedBuild(status string) bool
- func FinalBuild(status string) bool
- func ImageConfigHash(cfg *config.Config, workflow string, tree Tree) (string, error)
- func KeyFiles(cfg *config.Config, workflow string, tree Tree) (map[string]string, error)
- func MatchPaths(globs, files []string) []string
- func RecordKey(slug, workflow string) string
- func RunningBuild(status string) bool
- type CheckState
- type CloneOptions
- type Decision
- type GitError
- type GitTree
- func (g *GitTree) BlobID(path string) (string, error)
- func (g *GitTree) Changed(from, to string, globs []string) (bool, []string, error)
- func (g *GitTree) Dir() string
- func (g *GitTree) Glob(pattern string) ([]string, error)
- func (g *GitTree) Head() string
- func (g *GitTree) IsAncestor(commit string) (bool, error)
- func (g *GitTree) ReadFile(path string) ([]byte, error)
- type ImageStatus
- type Inputs
- type InputsFingerprint
- type Record
- type Registry
- type Status
- type Tree
Constants ¶
const ( Rebuild = "rebuild" // a trigger fired: a build is submitted Skip = "skip" // nothing changed that a build would pick up // RebuildFailedLast: a trigger fired, but the last build of these very // inputs failed, or ended without clearing the trigger, so it isn't // paid for again until something changes. RebuildFailedLast = "rebuild-failed-last" // CheckFailed: the check itself could not decide. CheckFailed = "check-failed" // NotInstalled: head's fugaro.yaml and the installed check job // disagree about the workflow (fugaro update fixes it). NotInstalled = "not-installed" )
The decisions of the daily check.
const ( ReasonForce = "force" ReasonNoRecord = "no-record" ReasonImageConfig = "image-config" ReasonLockfiles = "lockfiles" ReasonBase = "base" ReasonPaths = "paths" ReasonMaxAge = "max-age" ReasonBuiltCommitGone = "built-commit-gone" ReasonLatestDrift = "latest-drift" // ReasonBaseUnknown: the base image's digest couldn't be read, so the // base trigger was skipped. ReasonBaseUnknown = "base-unknown" // ReasonLatestUnknown: the image's latest digest couldn't be read, so // the latest-drift trigger was skipped. ReasonLatestUnknown = "latest-unknown" // ReasonBuildRunning: the last build of these inputs hasn't finished. ReasonBuildRunning = "build-running" // ReasonLastBuildIneffective: the last build of these inputs ended // without failing, but didn't clear the trigger. ReasonLastBuildIneffective = "last-build-ineffective" )
The reasons a decision records. The triggers come first, in the order they are evaluated; the rest are notes that never rebuild alone.
const CheckVersion = 1
CheckVersion is the CheckState schema version this package writes.
const RecordVersion = 1
RecordVersion is the Record schema version this package writes.
Variables ¶
var ErrManifestUnknown = errors.New("the registry has no such manifest")
ErrManifestUnknown means the registry has no such tag or digest.
Functions ¶
func FailedBuild ¶
FailedBuild reports whether a Cloud Build status is a failure the check backs off from: the build ran and did not succeed. A cancelled or expired build is not one: somebody stopped it, it didn't fail.
func FinalBuild ¶
FinalBuild reports whether a Cloud Build status can no longer change.
func ImageConfigHash ¶
ImageConfigHash is the hex SHA-256 of the canonical JSON of the workflow's base, image: settings, dockerfile: and, when that is set, the repository Dockerfile's blob ID in tree.
func KeyFiles ¶
KeyFiles are the workflow's cache key files in tree, with their blob IDs: every file its cache key globs match, or, when fugaro.yaml declares no cache, the key of the per-base default (config.DefaultCache).
func MatchPaths ¶
MatchPaths returns the files matching one of globs, in files' order.
func RunningBuild ¶
RunningBuild reports whether a Cloud Build status is not final yet.
Types ¶
type CheckState ¶
type CheckState struct {
Version int `json:"version"`
CheckedAt time.Time `json:"checked_at"`
Decision string `json:"decision"`
Reasons []string `json:"reasons"`
Error string `json:"error,omitempty"`
// BuildID is the last build a check submitted, and BuildInputs what
// it was built from; both carry over until the next submission.
BuildID string `json:"build_id,omitempty"`
BuildInputs *InputsFingerprint `json:"build_inputs,omitempty"`
// LastBuildStatus is BuildID's status as this check read it (the
// status at submission when this check submitted it), and LastBuildAt
// when it was submitted.
LastBuildStatus string `json:"last_build_status,omitempty"`
LastBuildAt *time.Time `json:"last_build_at,omitempty"`
}
CheckState is the last check of one workflow, as the check job writes it to builds/<slug>/<workflow>/check.json.
func ParseCheckState ¶
func ParseCheckState(data []byte) (*CheckState, error)
ParseCheckState decodes a check.json. Unknown fields are ignored.
func (*CheckState) Marshal ¶
func (s *CheckState) Marshal() ([]byte, error)
Marshal encodes s as check.json.
type CloneOptions ¶
type CloneOptions struct {
URL, Branch string
// Dir is the directory to clone into; it must not exist yet.
Dir string
// Env is git's whole environment, credential variables included
// (gitops.CredentialVars), so no token reaches an argv.
Env []string
}
CloneOptions say what to clone and how.
type Decision ¶
type Decision struct {
Workflow string `json:"workflow"`
Decision string `json:"decision"`
Reasons []string `json:"reasons"`
// Error is why the check failed (CheckFailed only).
Error string `json:"error,omitempty"`
// Inputs are what a build submitted now would be built from.
Inputs InputsFingerprint `json:"inputs"`
}
Decision is one workflow's decision.
func Decide ¶
Decide evaluates the workflow's rebuild triggers, in order, recording every one that fires, and then the back-off, unless forced: a rebuild whose inputs are those the last build ran with is RebuildFailedLast when that build ended (failed, or finished without clearing the trigger), and Skip while it is still running. It makes no calls: the caller reads everything first.
type GitTree ¶
type GitTree struct {
// contains filtered or unexported fields
}
GitTree is a Tree over one commit of a git repository: the check's blobless, no-checkout clone (Clone), which holds every commit and tree of the branch and fetches a blob from the remote only when ReadFile asks for it, or the build's checkout (Open). Either way its blob IDs and contents are git's own, never a working tree's.
func Clone ¶
func Clone(ctx context.Context, o CloneOptions) (*GitTree, error)
Clone makes a blobless, no-checkout, single-branch clone of o.Branch and reads its head's tree. ctx bounds every later git call of the tree.
func Open ¶
Open is a Tree over commit rev of the repository at dir, with git's environment env (nil inherits fugaro's). It reads what git stores, so its blob IDs equal the check's even where the working tree's bytes differ (an eol attribute, a smudge filter), and files git doesn't track are not in it.
func (*GitTree) Changed ¶
Changed reports whether a file matching one of globs differs between the commits from and to, and which files do. It compares trees only: rename detection, which would fetch blobs, is off.
func (*GitTree) IsAncestor ¶
IsAncestor reports whether commit is an ancestor of the head (or the head itself). A commit the clone doesn't have is not: it is gone from the branch, after a force-push. The clone doesn't fetch it.
type ImageStatus ¶
type ImageStatus struct {
BuiltAt time.Time `json:"built_at"`
AgeS int64 `json:"age_s"`
SourceCommit string `json:"source_commit"`
BaseRef string `json:"base_ref"`
BaseDigest string `json:"base_digest"`
Image string `json:"image"`
ImageDigest string `json:"image_digest"`
BuildID string `json:"build_id"`
}
ImageStatus is the part of a record fugaro image status shows.
type Inputs ¶
type Inputs struct {
// Config is head's fugaro.yaml, and Workflow the workflow decided.
Config *config.Config
Workflow string
// Record is the workflow's image.json; nil when there is none.
Record *Record
// LastCheck is the previous check.json; nil when there is none.
LastCheck *CheckState
// LastBuildStatus is the Cloud Build status of LastCheck's build.
LastBuildStatus string
// Head is the base branch's head commit, and Tree its tree.
Head string
Tree Tree
// BuiltCommitReachable: the record's commit is an ancestor of Head.
BuiltCommitReachable bool
// Changed reports whether a file matching one of globs changed
// between the commits from and to, and which.
Changed func(from, to string, globs []string) (bool, []string, error)
// BaseRef is the base image the build would be given, and BaseDigest
// its current digest ("" when it couldn't be read).
BaseRef, BaseDigest string
// LatestDigest is the digest the image's latest tag points at (""
// when unknown); LatestMissing means the tag doesn't exist.
LatestDigest string
LatestMissing bool
// TemplateSalt is gcp.TemplateSalt of the build a rebuild would run;
// "" leaves the salt out of the image-config trigger.
TemplateSalt string
Now time.Time
// Rebuild is the workflow's rebuild: block, with defaults filled in.
Rebuild config.Rebuild
Force bool
}
Inputs are what one workflow's decision is made from.
type InputsFingerprint ¶
type InputsFingerprint struct {
SourceCommit string `json:"source_commit"`
ImageConfigHash string `json:"image_config_hash"`
KeyFiles map[string]string `json:"key_files"`
BaseDigest string `json:"base_digest"`
}
InputsFingerprint is what a build would be built from: the back-off compares it with the one the last failed build ran with.
func (InputsFingerprint) Equal ¶
func (f InputsFingerprint) Equal(o InputsFingerprint) bool
Equal reports whether f and o are the same inputs.
type Record ¶
type Record struct {
Version int `json:"version"`
Repo string `json:"repo"` // owner/name
Workflow string `json:"workflow"`
BuiltAt time.Time `json:"built_at"`
BuildID string `json:"build_id"`
SourceCommit string `json:"source_commit"`
// SourceCommitTime is the source commit's committer time. The gate
// compares these, since the build's shallow clone has no history.
SourceCommitTime time.Time `json:"source_commit_time"`
BaseBranch string `json:"base_branch"`
KeyFiles map[string]string `json:"key_files"` // path → git blob ID
ImageConfigHash string `json:"image_config_hash"`
BaseRef string `json:"base_ref"` // the base image as the build was given it
BaseDigest string `json:"base_digest"` // sha256:… of the base the build ran FROM
Image string `json:"image"` // the image, without a tag
ImageDigest string `json:"image_digest"` // sha256:… that latest was promoted to
FugaroVersion string `json:"fugaro_version"`
// SealedBudgetToken is true when the fugaro that rendered this record
// reads the sealed budget token (docs/design/budget-token-channel.md
// §7.1). That fugaro is the BASE image's own binary, which runs the image
// build's render step (images/derived/cloudbuild.yaml): the field says
// only that the base image's own build step recorded it. It does not say
// which fugaro the launched image runs: a dockerfile: workflow's image is
// the repository's to build, its fugaro included, and a job runs the
// repository's :latest, which no launch compares with ImageDigest (nor
// does base_ref's release gate). The launcher's seal gate trusts it as
// far as that goes; a record without it says nothing either way.
SealedBudgetToken bool `json:"sealed_budget_token,omitempty"`
TemplateSalt string `json:"template_salt"`
// Adopted is reserved for a record written for an image the build
// did not make; builds always write false.
Adopted bool `json:"adopted"`
}
Record is what one derived image was built from, as the build's record step writes it to builds/<slug>/<workflow>/image.json.
func ParseRecord ¶
ParseRecord decodes a record. Unknown fields are ignored, so a record a newer fugaro wrote still reads.
func (*Record) NewerThan ¶
NewerThan reports whether r describes a newer build than o: its source commit's committer time is later, or it is the same commit built later. Committer times need not follow history (clock skew, a rebase that keeps an old time); the check corrects a wrong answer, since a record whose commit isn't the branch head counts as changed.
type Registry ¶
type Registry struct {
// HTTP reads the anonymous registries; nil is a plain client.
HTTP *http.Client
// Google reads Artifact Registry; nil is an ADC client, made on first
// use.
Google *http.Client
// Endpoint is a registry host's base URL; nil is https://<host>.
Endpoint func(host string) string
// contains filtered or unexported fields
}
Registry reads image digests with a HEAD of the manifest (the OCI distribution API). Artifact Registry (*-docker.pkg.dev) is read with Google's Application Default Credentials: the job account's metadata server token in the check job, the operator's ADC locally. Any other registry, ghcr.io included, is read anonymously, with a pull token from the registry's own token endpoint when it asks for one.
type Status ¶
type Status struct {
Image *ImageStatus `json:"image"`
Check *CheckState `json:"check"`
Errors []string `json:"errors,omitempty"`
}
Status is what the runs bucket says about one workflow's image: its record and its last check, each nil when there is none.
func ReadStatus ¶
func ReadStatus(ctx context.Context, b *blobx.Bucket, slug, workflow string, now time.Time) (Status, error)
ReadStatus reads the workflow's image.json and check.json. An object that is missing leaves its part nil; one that can't be read or parsed does too, with the problem in Errors. Only the bucket failing to answer is an error.
type Tree ¶
type Tree interface {
// BlobID is git's object ID of the file at path.
BlobID(path string) (string, error)
// Glob returns the files matching a doublestar pattern, as cache key
// globs match them.
Glob(pattern string) ([]string, error)
ReadFile(path string) ([]byte, error)
}
Tree is a repository tree at one commit. Production uses GitTree for both sides, the build's checkout (Open) and the check's clone (Clone), so both read git's own blob IDs. Paths are slash-separated and relative to the tree's root. A missing path is fs.ErrNotExist.