package build import ( "context" "errors" "fmt" "slices" "sort" "strings" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/delivery/buildrun" ) // ErrConflict is what a Store answers when a Create finds the key or an Update finds another revision than the one given. var ErrConflict = errors.New("the key exists or changed") // Store is the builder's bucket (builds): create and compare-and-set, each answering the key's new revision. Only the // builder's credential writes it (ADR 0429 clause 5). type Store interface { Get(ctx context.Context, key string) (value []byte, revision uint64, found bool, err error) Create(ctx context.Context, key string, value []byte) (uint64, error) Update(ctx context.Context, key string, value []byte, revision uint64) (uint64, error) // Keys are every key of the bucket. Keys(ctx context.Context) ([]string, error) } // Claimer claims fingerprints for the builder instance By (ADR 0429 clause 5: a builder claims a fingerprint by creating // its key and moves it by compare-and-set). type Claimer struct { Store Store By string } // Claim is a build the builder holds: its key, the revision the next write must be made at and the value last written. type Claim struct { Key string Rev uint64 Build delivery.BuilderBuild // Gone are the references of a done key the artifact store no longer holds, for which it was claimed again // (ClaimOptions.Stored; issue 1937): said in the log and the key's step. Gone []string // Unasked is why the store could not be asked about a done key taken as it stands to publish: said in the log. Unasked error } // ClaimOptions say what the builder knows about the moment of a claim. type ClaimOptions struct { // TakeOver takes a key found building whoever holds it: the builder has just started and looks at the key for the first // time, and a build left building by a builder that is gone, on this node or another, is nobody's (ADR 0478 clause 6). TakeOver bool // ImageBuilder says whether a node runs a healthy image build module now: a key failed for NoImageBuilderPrefix is taken // again only then. Nil means none does. ImageBuilder func() bool // Trunk is whether the trunk's record names this fingerprint. A done key whose image or package has no reference was // built as a candidate's, which never pushes or publishes (ADR 0487 clause 12, ADR 0498 clauses 5 and 6); on the trunk it // is taken again to publish them, an image only when an image builder runs (ImageBuilder). Trunk bool // Published says whether a package version `@` is published: one of the builder's done keys names it as // a package's reference. A key queued waiting for a version (BuilderBuild.Waits) is taken only once it is (ADR 0500 // clause 6). Nil means none is. Published func(ref string) bool // Stored answers which of the references given the artifact store no longer holds (the controller's verb held, // mesh-sdk delivery.HeldVerb; novox/hq issue 1937). A done key of the trunk's fingerprint is asked before it is left // done: the store's nightly collector deletes a blob nothing holds, and a key the controller never took in holds // nothing, so a key whose blob is gone is claimed building as a new revision and built again. Nil asks nothing. Stored func(ctx context.Context, references []string) (gone []string, err error) } // StoredReferences are the references of a done build's bundles and archives that name a blob in the artifact store // (`artifact-store:////blobs/`): what the store's collector can delete. An image or an upstream // copy is a manifest, which the collector keeps, and a package lives in the package registry. func StoredReferences(b delivery.BuilderBuild) []string { var out []string for _, a := range b.Artifacts { if strings.HasPrefix(a.Reference, "artifact-store://") && strings.Contains(a.Reference, "/blobs/") && !slices.Contains(out, a.Reference) { out = append(out, a.Reference) } } return out } // GoneStep is the step a key claimed again for gone blobs says (issue 1937). func GoneStep(gone []string) string { return "claimed again: the artifact store no longer holds " + strings.Join(gone, ", ") } // Unpublished is whether a done build holds an image built without a push or a package built without a publish: it has // no reference (a candidate's, ADR 0487 clause 12, ADR 0498 clauses 5 and 6). func Unpublished(b delivery.BuilderBuild) bool { return len(UnpublishedModules(b)) > 0 } // UnpublishedModules are the build modules that publish what a done build holds unpublished: the image build module for // an image, the Node build module for a package; sorted, none when it holds nothing unpublished. func UnpublishedModules(b delivery.BuilderBuild) []string { if b.State != delivery.BuildDone { return nil } var image, pkg bool for _, a := range b.Artifacts { image = image || (a.Kind == catalogue.ArtifactImage && a.Reference == "") pkg = pkg || (a.Kind == catalogue.ArtifactPackage && a.Reference == "") } var out []string if image { out = append(out, ImageBuildModule) } if pkg { out = append(out, buildrun.NodeBuildModule) } sort.Strings(out) return out } // publishable are the build modules of UnpublishedModules that can publish now: the image build module only while a node // runs it (imageBuilder). func publishable(b delivery.BuilderBuild, imageBuilder func() bool) []string { var out []string for _, bm := range UnpublishedModules(b) { if bm != ImageBuildModule || (imageBuilder != nil && imageBuilder()) { out = append(out, bm) } } return out } // Unreferenced is whether a done build holds a bundle, an archive or an upstream copy that names no reference: a key done // before R4e, whose artifacts stayed on their build nodes (ADR 0498 clause 8). An artifact of no kind (a key written before // kinds) is one too. Claim takes such a key again, once: its new revision names them. func Unreferenced(b delivery.BuilderBuild) bool { if b.State != delivery.BuildDone { return false } for _, a := range b.Artifacts { if a.Reference == "" && (a.Kind == "" || stored(a.Kind)) { return true } } return false } // Claim takes the build of module at fingerprint, or answers nil when it is not this builder's to build now: // - no key: it is created building, by this builder; a create that finds the key made meanwhile gets nothing; // - a done key is not taken again (the same source is never built twice), except one whose bundle, archive or upstream // copy names no reference (Unreferenced, a key done before R4e: ADR 0498 clause 8), which is claimed building as a new // revision and built whole, and one whose image or package was built unpublished as a candidate's once the trunk // names it (ClaimOptions.Trunk): that is taken as it stands, with the done value, and its builder writes the new // revision when the publishing build ends (ADR 0498 clause 6). From R4e one node builds each fingerprint (ADR 0478 // clause 4), so a key built on one node is never built again for a second; // - a done key of the trunk's fingerprint whose bundle or archive blob the artifact store no longer holds // (ClaimOptions.Stored, issue 1937) is claimed building as a new revision and built whole, its step saying which; // - a failed key is taken again as a new revision of the same key (ADR 0429 clause 18), except one failed for a kind no // builder builds, for an upstream pinned by no digest or a package version taken (delivery.FailedUntilChanged: ADR 0478 // clause 2, ADR 0498 clauses 4 and 5) or for digests that differ (buildrun.DigestsDifferPrefix, clause 4), // which wait for another fingerprint, and one failed for want of an image builder (NoImageBuilderPrefix), which waits for // another fingerprint or a node that runs the image build module (ClaimOptions.ImageBuilder); // - a key queued waiting for a package version (Waits) is claimed building as a new revision once that version is // published (ClaimOptions.Published, ADR 0500 clause 6), and not before; // - a key left queued or building by this builder is taken as it stands (ADR 0478 clause 5: a stopped builder takes its // own again when it starts), and so is one left by another builder when ClaimOptions.TakeOver (clause 6). func (c Claimer) Claim(ctx context.Context, module, fingerprint, manifest string, opt ClaimOptions) (*Claim, error) { key, err := delivery.BuilderBuildKey(module, fingerprint) if err != nil { return nil, err } building := delivery.BuilderBuild{State: delivery.BuildBuilding, By: c.By, Step: "claimed", Manifest: manifest} value, rev, found, err := c.Store.Get(ctx, key) if err != nil { return nil, fmt.Errorf("%s: %w", key, err) } if !found { return c.write(ctx, key, building, func(data []byte) (uint64, error) { return c.Store.Create(ctx, key, data) }) } cur, err := delivery.DecodeBuilderBuild(value) if err != nil { return nil, fmt.Errorf("%s holds what is not a build: %w", key, err) } switch { case cur.State == delivery.BuildDone: if Unreferenced(cur) { return c.write(ctx, key, building, func(data []byte) (uint64, error) { return c.Store.Update(ctx, key, data, rev) }) } if opt.Trunk && opt.Stored != nil { if refs := StoredReferences(cur); len(refs) > 0 { gone, err := opt.Stored(ctx, refs) if err != nil { err = fmt.Errorf("%s: whether the artifact store still holds %s cannot be asked, so the key is "+ "left done (the controller's take-in refuses it if one is gone): %w", key, strings.Join(refs, ", "), err) if len(publishable(cur, opt.ImageBuilder)) > 0 { return &Claim{Key: key, Rev: rev, Build: cur, Unasked: err}, nil } return nil, err } if len(gone) > 0 { again := building again.Step = GoneStep(gone) cl, err := c.write(ctx, key, again, func(data []byte) (uint64, error) { return c.Store.Update(ctx, key, data, rev) }) if cl != nil { cl.Gone = gone } return cl, err } } } if opt.Trunk && len(publishable(cur, opt.ImageBuilder)) > 0 { return &Claim{Key: key, Rev: rev, Build: cur}, nil } return nil, nil case cur.State == delivery.BuildFailed && (delivery.FailedUntilChanged(cur.Error) || strings.HasPrefix(cur.Error, buildrun.DigestsDifferPrefix)): return nil, nil case cur.State == delivery.BuildFailed && strings.HasPrefix(cur.Error, NoImageBuilderPrefix) && (opt.ImageBuilder == nil || !opt.ImageBuilder()): return nil, nil case cur.State == delivery.BuildFailed: return c.write(ctx, key, building, func(data []byte) (uint64, error) { return c.Store.Update(ctx, key, data, rev) }) case cur.State == delivery.BuildQueued && cur.Waits != "": if opt.Published == nil || !opt.Published(cur.Waits) { return nil, nil // the package it waits for is not published yet } return c.write(ctx, key, building, func(data []byte) (uint64, error) { return c.Store.Update(ctx, key, data, rev) }) case cur.By == c.By || opt.TakeOver: return &Claim{Key: key, Rev: rev, Build: cur}, nil } return nil, nil } func (c Claimer) write(_ context.Context, key string, b delivery.BuilderBuild, put func([]byte) (uint64, error)) (*Claim, error) { data, err := delivery.EncodeBuilderBuild(b) if err != nil { return nil, err } rev, err := put(data) if errors.Is(err, ErrConflict) { return nil, nil // another builder made it first } if err != nil { return nil, fmt.Errorf("%s: %w", key, err) } b.V = delivery.Version return &Claim{Key: key, Rev: rev, Build: b}, nil } // Finish moves the claimed key to b by compare-and-set at the claim's revision, and writes nothing when b is the value // the key holds. A key that changed meanwhile is ErrConflict: the claim is no longer this builder's. func (c Claimer) Finish(ctx context.Context, cl *Claim, b delivery.BuilderBuild) error { b.V = delivery.Version if delivery.SameBuilderBuild(cl.Build, b) { return nil } data, err := delivery.EncodeBuilderBuild(b) if err != nil { return err } rev, err := c.Store.Update(ctx, cl.Key, data, cl.Rev) if err != nil { return fmt.Errorf("%s: %w", cl.Key, err) } cl.Rev, cl.Build = rev, b return nil }