The take-in watched the builder's and the indexer's buckets, started a follower per module that retried on a 30 s timer, and at start took every catalogued module in alphabetically: on 2026-10-25 a restart took in about 2 modules a minute, about 190 followers queued behind 16 slots, and every take-in composed two or three times even with nothing new, so a new build waited 30 minutes and more for its credential. Now two durable consumers named mesh-controller-take-in read the indexer's modules bucket (every module's last trunk value when the consumer is made, then each write) and the builder's builds bucket (each write), at most 16 keys unacknowledged each. The bus keeps what is not taken in: the start's look is the consumers' own catch-up, and a restart costs only what was left unacknowledged. A batch reads the catalogue once, takes each module in once, and asks the composer twice: once for its modules' nodes, once for the bus's node with the grants. A module registered at its trunk whose compositions and grant exist, or that this process took in at the same trunk, composes nothing; a key delivered again is taken in whole. A wait for the builder's or indexer's next write is acknowledged, a wait on a read or a failure refused with a 30 s delay. The backlog is in status, and a backlog standing still for 5 minutes raises take-in-stalled. The controller's account is granted the two consumers' acks. The follower tests go with the followers: twelve tests become three. Research 072: the take-ins on a bounded pull consumer (milestone 4, done keys alone) and the start's look composing only what changed (milestone 2).
643 lines
26 KiB
Go
643 lines
26 KiB
Go
package main
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"path"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
"github.com/novox/mesh-controller/internal/artifacts"
|
|
"github.com/novox/mesh-controller/internal/builderkeys"
|
|
"github.com/novox/mesh-controller/internal/buildreads"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// The take-in from the builder's done key (novox/hq ADR 0501, ADR 0498 §8, ADR 0453 §5; issue 1851).
|
|
//
|
|
// From R4e the controller asks no build but the builder's own module's: a module of a merge is taken from the builder's
|
|
// key for the fingerprint the indexer's trunk value of the module names (ADR 0501 §2). Only a done key whose every
|
|
// artifact names a reference is taken (ADR 0498 §8); a key not done yet, one waiting for a reference, a templates
|
|
// archive the store does not answer, or a list of predefined keys the forge does not answer is waited for and said,
|
|
// never failed and never delivered from. A failed key fails, in the builder's own words. The take-in then records the
|
|
// key as a build of today's record layout, so a controller of before reads it on a rollback, and judges it as every
|
|
// build is judged (registerJudged).
|
|
|
|
// errKeyWaits says a take-in from a key waits: for the builder, a reference, the store or the forge. Never a failure.
|
|
var errKeyWaits = errors.New("waits")
|
|
|
|
// errWaitsForAWrite is a take-in that waits for the builder or the indexer to write again (the key not done, its
|
|
// artifacts not referenced, no fingerprint of the new form): that write wakes it, so nothing looks again before it
|
|
// (ADR 0544 §4).
|
|
var errWaitsForAWrite = fmt.Errorf("%w for the builder's or the indexer's next write", errKeyWaits)
|
|
|
|
// errKeyFailed says the module's key failed: the builder's own words follow.
|
|
var errKeyFailed = errors.New("the builder's key failed")
|
|
|
|
// keyReads is what a take-in from a key reads of the builder and the indexer (builderkeys.Reader, or a test's).
|
|
type keyReads interface {
|
|
Build(ctx context.Context, module, fp string) (builderkeys.Key, error)
|
|
Manifest(ctx context.Context, digest string) (sdkdelivery.Manifest, error)
|
|
}
|
|
|
|
// keyBuildID is the id a key's build is recorded under: the key and its revision, so the same revision recorded twice
|
|
// is one record, and a rebuild of the same fingerprint (a new revision, ADR 0429 §18) another.
|
|
func keyBuildID(k builderkeys.Key) string {
|
|
part, err := sdkdelivery.FingerprintKeyPart(k.Fingerprint)
|
|
if err != nil {
|
|
part = strings.TrimPrefix(k.Fingerprint, sdkdelivery.SourcePrefix2)
|
|
}
|
|
return fmt.Sprintf("key-%s-%d", part, k.Revision)
|
|
}
|
|
|
|
// readArchive is the archive a key's reference names, read from the artifact store and checked against its digest. A
|
|
// variable so a test needs no store.
|
|
var readArchive = func(ctx context.Context, inv *inventory.Inventory, reference string) ([]byte, error) {
|
|
shelf, err := inv.Catalogue(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
address, err := artifactStoreAddress(ctx, inv, shelf, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if address == "" {
|
|
return nil, errors.New("the artifact store is on no network the controller reaches")
|
|
}
|
|
return artifacts.Store{Address: address}.GetBlob(ctx, reference)
|
|
}
|
|
|
|
// holdArtifact holds what a key's reference names in the artifact store: Gone when the store does not hold it, an
|
|
// archive held by its manifest besides. A variable so a test needs no store.
|
|
var holdArtifact = func(ctx context.Context, inv *inventory.Inventory, reference string) error {
|
|
shelf, err := inv.Catalogue(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
address, err := artifactStoreAddress(ctx, inv, shelf, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if address == "" {
|
|
// A mesh being raised has no store on its network yet (as the sweep, which collects nothing there): nothing
|
|
// can be held, and waiting for the store would keep the store itself from being taken in. Said, never silent.
|
|
fmt.Printf("the artifact store is on no network the controller reaches, so %s is taken in without being held\n",
|
|
reference)
|
|
return nil
|
|
}
|
|
store := artifacts.Store{Address: address}
|
|
if err := store.Holds(ctx, reference); err != nil {
|
|
return err
|
|
}
|
|
_, err = store.Hold(ctx, reference)
|
|
return err
|
|
}
|
|
|
|
// predefinedHead is the list of predefined keys at the head of the catalogue repository's trunk, and that head.
|
|
type predefinedHead struct {
|
|
List catalogue.PredefinedList
|
|
Repository string
|
|
Commit string
|
|
// Generation orders two heads of the trunk: the head's commit time in seconds. A list kept from a build's
|
|
// generation (its commit count) is always older than a head read here.
|
|
Generation int
|
|
}
|
|
|
|
// predefinedAtTrunk is the list of predefined keys at the head of the catalogue repository's trunk (ADR 0501 §8), read
|
|
// from the forge. A variable so a test needs no forge.
|
|
var predefinedAtTrunk = func(ctx context.Context, inv *inventory.Inventory) (predefinedHead, error) {
|
|
entries, err := inv.Catalogued(ctx)
|
|
if err != nil {
|
|
return predefinedHead{}, err
|
|
}
|
|
return readPredefinedHead(ctx, entries, formsOfSources(ctx, inv))
|
|
}
|
|
|
|
// formsOfSources reads how the sources are spelled on this mesh; a variable so a test names the forge's address.
|
|
var formsOfSources = newSourceForms
|
|
|
|
// errNoCatalogueRepository says no module of the mesh is built from the catalogue's repository on the forge: not a wait,
|
|
// since no read of the forge makes one appear (issue 1902); the plan fails with these words.
|
|
var errNoCatalogueRepository = errors.New("no module of the catalogue names the repository " +
|
|
catalogue.CatalogueRepository + " on the forge")
|
|
|
|
// catalogueRepositoryAmong is the catalogue's repository on the forge, as owner/name, found as catch-up finds the trunks
|
|
// of the repositories the catalogue builds from: a source in the URL form or the seat form, `.git` dropped (issue 1902).
|
|
func catalogueRepositoryAmong(entries []inventory.Entry, forms *sourceForms) (string, error) {
|
|
for _, e := range entries {
|
|
if e.Source.Repository == "" {
|
|
continue
|
|
}
|
|
owner, name, onForge := forms.onTheForge(e.Source.Repository, e.Source.Seat)
|
|
if onForge && strings.EqualFold(name, catalogue.CatalogueRepository) {
|
|
return owner + "/" + name, nil
|
|
}
|
|
}
|
|
return "", errNoCatalogueRepository
|
|
}
|
|
|
|
// readPredefinedHead is the list at the trunk head of the catalogue's repository, found among the modules' sources and
|
|
// read from the forge.
|
|
func readPredefinedHead(ctx context.Context, entries []inventory.Entry, forms *sourceForms) (predefinedHead, error) {
|
|
// A URL source is on the forge only once the forge's address is known; an address that cannot be read is a wait.
|
|
if _, err := forms.forgeAddress(ctx, forgeAddressWait); err != nil {
|
|
return predefinedHead{}, fmt.Errorf("the forge's address cannot be read: %w", err)
|
|
}
|
|
repository, err := catalogueRepositoryAmong(entries, forms)
|
|
if err != nil {
|
|
return predefinedHead{}, err
|
|
}
|
|
var heads []struct {
|
|
SHA string `json:"sha"`
|
|
Commit struct {
|
|
Committer struct {
|
|
Date time.Time `json:"date"`
|
|
} `json:"committer"`
|
|
} `json:"commit"`
|
|
}
|
|
if err := askGitea(ctx, "gitea_api", map[string]any{"path": "/repos/" + repository +
|
|
"/commits?limit=1&stat=false&verification=false&files=false"}, &heads); err != nil {
|
|
return predefinedHead{}, err
|
|
}
|
|
if len(heads) == 0 || heads[0].SHA == "" {
|
|
return predefinedHead{}, fmt.Errorf("the forge named no head of %s's trunk", repository)
|
|
}
|
|
head := predefinedHead{Repository: repository, Commit: heads[0].SHA,
|
|
Generation: int(heads[0].Commit.Committer.Date.Unix())}
|
|
var file struct {
|
|
Content string `json:"content"`
|
|
Encoding string `json:"encoding"`
|
|
}
|
|
if err := askGitea(ctx, "gitea_api", map[string]any{"path": "/repos/" + repository + "/contents/" +
|
|
catalogue.PredefinedFile + "?ref=" + head.Commit}, &file); err != nil {
|
|
return predefinedHead{}, err
|
|
}
|
|
if file.Encoding != "base64" {
|
|
return predefinedHead{}, fmt.Errorf("the forge answered %s at %.8s in the encoding %q", catalogue.PredefinedFile,
|
|
head.Commit, file.Encoding)
|
|
}
|
|
raw, err := base64.StdEncoding.DecodeString(file.Content)
|
|
if err != nil {
|
|
return predefinedHead{}, err
|
|
}
|
|
if head.List, err = catalogue.ParsePredefined(raw); err != nil {
|
|
return predefinedHead{}, fmt.Errorf("%s at %.8s is refused: %w", catalogue.PredefinedFile, head.Commit, err)
|
|
}
|
|
return head, nil
|
|
}
|
|
|
|
// askGitea asks the forge's module one tool on the bus and reads its result into into.
|
|
var askGitea = func(ctx context.Context, tool string, args map[string]any, into any) error {
|
|
js, err := aBus()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer js.Close()
|
|
raw, _ := json.Marshal(args)
|
|
answer, err := link.Ask(ctx, link.OverNATS{Conn: js.Conn()}, "gitea", tool, raw, 30*time.Second)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if answer.Error != "" {
|
|
return fmt.Errorf("gitea.%s: %s", tool, answer.Error)
|
|
}
|
|
var wrapped struct {
|
|
Result json.RawMessage `json:"result"`
|
|
}
|
|
if err := json.Unmarshal(answer.Result, &wrapped); err != nil {
|
|
return err
|
|
}
|
|
return json.Unmarshal(wrapped.Result, into)
|
|
}
|
|
|
|
// keyTake is one take-in from a key: the module, the indexer's trunk value naming the key's fingerprint, and the
|
|
// registration's warrant.
|
|
type keyTake struct {
|
|
Module string
|
|
Trunk sdkdelivery.Module
|
|
Ask registrationAsk
|
|
// Hand names, by artifact, the references the controller's terminal registers the module at instead of a key's
|
|
// (ADR 0526 clause 3, `register --reference`): the key, when the builder has one, supplies the artifacts not named
|
|
// here. Nil for every take-in but that one.
|
|
Hand map[string]string
|
|
}
|
|
|
|
// takeInKey takes a module in from the builder's key for the fingerprint its trunk value names: records the key as a
|
|
// build and registers the module, or says why not. errKeyWaits when it waits, errKeyFailed when the key failed.
|
|
func takeInKey(ctx context.Context, inv *inventory.Inventory, keys keyReads, t keyTake) (
|
|
catalogue.Manifest, inventory.Build, error) {
|
|
head, err := predefinedAtTrunk(ctx, inv)
|
|
if errors.Is(err, errNoCatalogueRepository) {
|
|
// Fails the plan, loudly, with these words (issue 1902): waiting would never end.
|
|
return catalogue.Manifest{}, inventory.Build{}, fmt.Errorf("%s is not judged: %w", t.Module, err)
|
|
}
|
|
if err != nil {
|
|
return catalogue.Manifest{}, inventory.Build{}, fmt.Errorf("%w: the list of predefined keys at the catalogue's "+
|
|
"trunk head cannot be read, so %s is not judged: %v", errKeyWaits, t.Module, err)
|
|
}
|
|
keepPredefinedHead(ctx, inv, head)
|
|
previous, err := inv.Builds(ctx, t.Module, 5)
|
|
if err != nil {
|
|
return catalogue.Manifest{}, inventory.Build{}, err
|
|
}
|
|
kept, manifest, err := buildOfKey(ctx, keys, t, keyContext{list: head.List, previous: previous,
|
|
archive: func(ctx context.Context, reference string) ([]byte, error) { return readArchive(ctx, inv, reference) },
|
|
hold: func(ctx context.Context, reference string) error { return holdArtifact(ctx, inv, reference) }})
|
|
if err != nil {
|
|
return catalogue.Manifest{}, kept, err
|
|
}
|
|
if err := inv.RecordBuild(ctx, kept); err != nil {
|
|
return catalogue.Manifest{}, kept, err
|
|
}
|
|
recorded := inventory.Source{Repository: t.Trunk.Repository, Seat: gitSeat, Path: keyPath(t.Trunk),
|
|
BuiltFrom: t.Trunk.Commit, Head: t.Trunk.Commit, Against: kept.Against}
|
|
trunk := "main"
|
|
if was, err := inv.SourceOf(ctx, manifest.Module); err == nil {
|
|
recorded.Ref = followedBranch(was.Ref)
|
|
if recorded.Ref != "" {
|
|
trunk = recorded.Ref
|
|
}
|
|
}
|
|
registered, err := registerJudged(ctx, inv, takenBuild{kept: kept, manifest: manifest, recorded: recorded,
|
|
ask: t.Ask, path: recorded.Path, trunk: trunk, by: "the builder", what: t.Trunk.Repository,
|
|
commit: t.Trunk.Commit})
|
|
return registered, kept, err
|
|
}
|
|
|
|
// keyContext is what a key's build is made with besides the key: the list of predefined keys its manifest is judged
|
|
// against, the module's recent builds (the bases its last one stood on are carried) and how an archive is read.
|
|
type keyContext struct {
|
|
list catalogue.PredefinedList
|
|
previous []inventory.Build
|
|
archive func(ctx context.Context, reference string) ([]byte, error)
|
|
// hold holds one artifact the key names in the artifact store, artifacts.Gone when the store does not hold it.
|
|
hold func(ctx context.Context, reference string) error
|
|
}
|
|
|
|
// keyPath is a module's directory as the catalogue records it: empty for a module at its repository's root.
|
|
func keyPath(m sdkdelivery.Module) string {
|
|
if m.Path == sdkdelivery.RootPath {
|
|
return ""
|
|
}
|
|
return strings.Trim(m.Path, "/")
|
|
}
|
|
|
|
// buildOfKey reads the key of the trunk value's fingerprint and makes its build record and the manifest it resolves to
|
|
// (ADR 0498 §8, ADR 0501 §1, §8): the module's manifest by the key's digest, judged against the list of predefined keys
|
|
// at the catalogue's trunk head, resolved with the key's artifacts.
|
|
func buildOfKey(ctx context.Context, keys keyReads, t keyTake, kc keyContext) (
|
|
inventory.Build, catalogue.Manifest, error) {
|
|
waits := func(format string, a ...any) (inventory.Build, catalogue.Manifest, error) {
|
|
return inventory.Build{}, catalogue.Manifest{}, fmt.Errorf("%w: "+format, append([]any{errKeyWaits}, a...)...)
|
|
}
|
|
waitsForAWrite := func(format string, a ...any) (inventory.Build, catalogue.Manifest, error) {
|
|
return inventory.Build{}, catalogue.Manifest{}, fmt.Errorf("%w: "+format, append([]any{errWaitsForAWrite}, a...)...)
|
|
}
|
|
if t.Trunk.FP2 == "" && t.Hand == nil {
|
|
why := t.Trunk.None2
|
|
if why == "" {
|
|
why = "the indexer wrote no fingerprint of the new form"
|
|
}
|
|
return waitsForAWrite("%s at %.8s has no fingerprint to take a build by: %s", t.Module, t.Trunk.Commit, why)
|
|
}
|
|
k, err := keys.Build(ctx, t.Module, t.Trunk.FP2)
|
|
if t.Hand != nil {
|
|
// **Registered at a reference, no key needed** (ADR 0526 clause 3): the key the builder holds, if any, is the
|
|
// start; the references given replace or add its artifacts, and the key counts as done.
|
|
k = handKey(t, k)
|
|
err = nil
|
|
}
|
|
if err != nil {
|
|
return waits("%s's key cannot be read: %v", t.Module, err)
|
|
}
|
|
switch {
|
|
case k.Failed():
|
|
return inventory.Build{}, catalogue.Manifest{}, fmt.Errorf("%w: %s", errKeyFailed, k.Says())
|
|
case !k.Done():
|
|
return waitsForAWrite("%s", k.Says())
|
|
case len(k.Unreferenced()) > 0:
|
|
return waitsForAWrite("%s", k.Says())
|
|
}
|
|
src, err := keys.Manifest(ctx, k.Build.Manifest)
|
|
if err != nil {
|
|
return waits("%s's manifest: %v", t.Module, err)
|
|
}
|
|
manifest, err := catalogue.ParseManifestWith([]byte(src.JSON), kc.list)
|
|
if err != nil {
|
|
// **A word the running controller does not read waits for the controller that reads it** (ADR 0501 §7): the
|
|
// key stays done, and the controller that reads the word registers it with no second build.
|
|
if word := refusedWord(err.Error()); word != "" {
|
|
return waits("%s's manifest uses the word %q, which this controller does not read: it is registered "+
|
|
"when a controller that reads it runs", t.Module, word)
|
|
}
|
|
return inventory.Build{}, catalogue.Manifest{}, fmt.Errorf("the builder built %s and its manifest is "+
|
|
"refused: %w", t.Module, err)
|
|
}
|
|
if manifest.Module != t.Module {
|
|
return inventory.Build{}, catalogue.Manifest{}, fmt.Errorf("the key of %s names the manifest of %s",
|
|
t.Module, manifest.Module)
|
|
}
|
|
if t.Hand != nil {
|
|
if err := handCoversEvery(manifest, k, t.Hand); err != nil {
|
|
return inventory.Build{}, catalogue.Manifest{}, err
|
|
}
|
|
}
|
|
// **Every artifact held in the store before anything is recorded** (novox/hq issue 1933): a definition naming an
|
|
// archive the store does not hold is sent, and every machine assigned the module fails its apply on the 404. An
|
|
// archive is held by its manifest, so the store's own collector keeps it too.
|
|
if kc.hold != nil {
|
|
// **Every gone archive named, and said** (issue 1937): the builder hears `artifacts-gone` and builds the key
|
|
// again, so the refusal is the last word on this key, not a plan that fails at every retry.
|
|
var gone, goneNames []string
|
|
for _, a := range k.Build.Artifacts {
|
|
reference := catalogue.Recorded(a.Reference)
|
|
if _, ours := catalogue.InArtifactStore(reference); !ours {
|
|
continue
|
|
}
|
|
err := kc.hold(ctx, reference)
|
|
switch {
|
|
case err == nil, errors.Is(err, artifacts.ErrNotOurs):
|
|
case errors.Is(err, artifacts.Gone):
|
|
gone = append(gone, reference)
|
|
goneNames = append(goneNames, fmt.Sprintf("%s for its artifact %q", reference, a.Name))
|
|
default:
|
|
return waits("the artifact store could not hold %s's artifact %q (%s): %v", t.Module, a.Name, reference, err)
|
|
}
|
|
}
|
|
if len(gone) > 0 {
|
|
if t.Hand == nil {
|
|
sayArtifactsGone(ctx, k, t.Module, gone)
|
|
}
|
|
return inventory.Build{}, catalogue.Manifest{}, fmt.Errorf("the builder's key of %s names %s, not in "+
|
|
"the artifact store: nothing was recorded or registered, and the key must be built "+
|
|
"again (said as %s, which the builder builds it again on)", t.Module, strings.Join(goneNames, " and "),
|
|
link.KeyArtifactsGone)
|
|
}
|
|
}
|
|
built, err := builtOfKey(ctx, kc.archive, manifest, k)
|
|
if err != nil {
|
|
return waits("%v", err)
|
|
}
|
|
resolved, err := manifest.Resolve(built)
|
|
if err != nil {
|
|
return inventory.Build{}, catalogue.Manifest{}, fmt.Errorf("the builder built %s and it does not resolve: %w",
|
|
t.Module, err)
|
|
}
|
|
raw, err := json.Marshal(resolved)
|
|
if err != nil {
|
|
return inventory.Build{}, catalogue.Manifest{}, err
|
|
}
|
|
kept := inventory.Build{ID: keyBuildID(k), Repository: t.Trunk.Repository, Module: t.Module,
|
|
Commit: t.Trunk.Commit, On: k.Build.By, Path: keyPath(t.Trunk), Manifest: raw,
|
|
SourceFingerprint: k.Fingerprint, At: time.Now().UTC()}
|
|
// **Read from the manifest, fresh at every key** (the same rule build-agent says, internal/buildreads). Sources are
|
|
// left unsaid but for a module whose every artifact names its paths: the planner then reads the module's whole
|
|
// directory, over-planning and never under-planning, and a fingerprint that did not change moves nothing (R4e A9).
|
|
// **Its build source where its every artifact names its paths** (novox/hq ADR 0499 §1, §5; issue 1758, B1): the
|
|
// planner reads a merge's changed files against those paths and the manifest alone, so a change elsewhere in the
|
|
// module's directory touches nothing it builds from.
|
|
if own := pathsBuildSource(manifest, keyPath(t.Trunk)); own != nil {
|
|
kept.Sources = []inventory.BuildSource{{Paths: own}}
|
|
}
|
|
for _, c := range buildreads.Of(manifest) {
|
|
kept.Read = append(kept.Read, inventory.ReadRepository{Repository: c.Repository, Ref: c.Ref})
|
|
}
|
|
// **The bases its last build stood on, carried** (R4e, lead decision): what the collector keeps a mirrored base by.
|
|
// A key names none; the module's newest successful build's are carried so the keep set does not change.
|
|
for _, b := range kc.previous {
|
|
if b.Worked() && b.Module == t.Module && b.ID != kept.ID {
|
|
kept.Against = append([]string(nil), b.Against...)
|
|
break
|
|
}
|
|
}
|
|
if t.Hand != nil {
|
|
// A build made by hand, said so: the id is its own, so it never reads as the builder's key.
|
|
kept.ID, kept.On = "hand-"+keyBuildID(k)+"-"+time.Now().UTC().Format("20060102T150405Z"), "hand"
|
|
}
|
|
for _, a := range k.Build.Artifacts {
|
|
kept.Made = append(kept.Made, inventory.Artifact{Name: a.Name, Kind: kindOf(manifest, a),
|
|
Reference: catalogue.Recorded(a.Reference)})
|
|
}
|
|
return kept, resolved, nil
|
|
}
|
|
|
|
// kindOf is an artifact's kind: the key's, else the manifest's.
|
|
func kindOf(m catalogue.Manifest, a sdkdelivery.BuiltArtifact) string {
|
|
if a.Kind != "" || m.Build == nil {
|
|
return a.Kind
|
|
}
|
|
for _, d := range m.Build.Artifacts {
|
|
if d.Name == a.Name {
|
|
return d.Kind
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// builtOfKey is each of a done key's artifacts as Resolve reads it: its reference and digest, a TypeScript bundle's
|
|
// launchers by the build modules' one rule (mesh-sdk buildstep.Launchers), and, for the archive a module packs its
|
|
// templates into, each template's text read back from the archive the store holds under the key's reference.
|
|
func builtOfKey(ctx context.Context, archive func(context.Context, string) ([]byte, error), m catalogue.Manifest,
|
|
k builderkeys.Key) (
|
|
[]catalogue.Built, error) {
|
|
declared := map[string]catalogue.Artifact{}
|
|
if m.Build != nil {
|
|
for _, a := range m.Build.Artifacts {
|
|
declared[a.Name] = a
|
|
}
|
|
}
|
|
templates := ""
|
|
if m.Templated() {
|
|
templates = catalogue.TemplatesArtifact(m)
|
|
}
|
|
var out []catalogue.Built
|
|
for _, a := range k.Build.Artifacts {
|
|
d := declared[a.Name]
|
|
b := catalogue.Built{Name: a.Name, Kind: kindOf(m, a), Reference: catalogue.Recorded(a.Reference),
|
|
Digest: a.Digest}
|
|
if launchers := buildstep.Launchers(d.Language, d.Entrypoints); len(launchers) > 0 {
|
|
b.Launchers = launchers
|
|
}
|
|
if a.Name == templates && templates != "" {
|
|
body, err := archive(ctx, b.Reference)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the templates archive %q of %s cannot be read from the artifact store: %v",
|
|
a.Name, m.Module, err)
|
|
}
|
|
if b.Texts, err = unpackTexts(body); err != nil {
|
|
return nil, fmt.Errorf("the templates archive %q of %s cannot be read back: %v", a.Name, m.Module, err)
|
|
}
|
|
}
|
|
out = append(out, b)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// unpackTexts is every file of a packed templates archive (gzip over tar), by its path inside it.
|
|
func unpackTexts(body []byte) (map[string]string, error) {
|
|
zipped, err := gzip.NewReader(bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer zipped.Close()
|
|
reader := tar.NewReader(zipped)
|
|
texts := map[string]string{}
|
|
for {
|
|
header, err := reader.Next()
|
|
if errors.Is(err, io.EOF) {
|
|
return texts, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if header.Typeflag != tar.TypeReg {
|
|
continue
|
|
}
|
|
text, err := io.ReadAll(io.LimitReader(reader, 16<<20))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
texts[strings.TrimPrefix(header.Name, "./")] = string(text)
|
|
}
|
|
}
|
|
|
|
// keepPredefinedHead keeps the list read at the catalogue's trunk head when it is newer than the one kept, as a build of
|
|
// the catalogue's did (ADR 0348 §7), and judges every manifest read again against it. Said, never fatal: the list read
|
|
// is the one this take-in judges against either way.
|
|
func keepPredefinedHead(ctx context.Context, inv *inventory.Inventory, head predefinedHead) {
|
|
kept, err := inv.KeepPredefined(ctx, inventory.PredefinedRecord{Keys: head.List, Repository: head.Repository,
|
|
Commit: head.Commit, Generation: head.Generation, Build: "the trunk head " + short(head.Commit)})
|
|
if err != nil {
|
|
fmt.Printf("the list of predefined keys at %s's trunk head %s is not kept: %v\n", head.Repository,
|
|
short(head.Commit), err)
|
|
return
|
|
}
|
|
if kept {
|
|
fmt.Printf("the list of predefined keys at %s's trunk head %s is published: %s\n", head.Repository,
|
|
short(head.Commit), strings.Join(head.List.Keys(), ", "))
|
|
}
|
|
if err := loadPredefined(ctx, inv); err != nil {
|
|
fmt.Printf("the list of predefined keys kept cannot be read: %v\n", err)
|
|
}
|
|
}
|
|
|
|
// openBuilderKeys is the builder's and the indexer's buckets, read on a connection for the moment.
|
|
func openBuilderKeys() (builderkeys.Reader, func(), error) {
|
|
js, err := aBus()
|
|
if err != nil {
|
|
return builderkeys.Reader{}, nil, err
|
|
}
|
|
api, err := jetstream.New(js.Conn())
|
|
if err != nil {
|
|
js.Close()
|
|
return builderkeys.Reader{}, nil, err
|
|
}
|
|
return builderkeys.Reader{JS: api}, js.Close, nil
|
|
}
|
|
|
|
// pathsBuildSource is the build source of a module whose every artifact names the paths it is built from (ADR 0499
|
|
// §1): its manifest and each path, relative to its repository's root, a path read as a file or everything below a
|
|
// directory of that name (`<path>/**` holds both). Nil when an artifact names none: the module reads its directory.
|
|
func pathsBuildSource(m catalogue.Manifest, dir string) []string {
|
|
if m.Build == nil || len(m.Build.Artifacts) == 0 {
|
|
return nil
|
|
}
|
|
in := func(p string) string {
|
|
if dir == "" {
|
|
return p
|
|
}
|
|
return dir + "/" + p
|
|
}
|
|
seen := map[string]bool{}
|
|
out := []string{in("module.json")}
|
|
for _, a := range m.Build.Artifacts {
|
|
if len(a.Paths) == 0 {
|
|
return nil
|
|
}
|
|
for _, p := range a.Paths {
|
|
entry := in(strings.Trim(path.Clean(p), "/")) + "/**"
|
|
if !seen[entry] {
|
|
seen[entry] = true
|
|
out = append(out, entry)
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// handKey is the key a registration at references is made from: the builder's key when it has one (its other artifacts
|
|
// stand), with each reference given in place of or beside the key's artifact of that name, and counted done.
|
|
func handKey(t keyTake, k builderkeys.Key) builderkeys.Key {
|
|
if !k.Found {
|
|
k = builderkeys.Key{Module: t.Module, Fingerprint: t.Trunk.FP2, Found: true}
|
|
}
|
|
build := k.Build
|
|
build.State, build.By, build.Error = sdkdelivery.BuildDone, "hand", ""
|
|
if build.Manifest == "" {
|
|
build.Manifest = t.Trunk.Manifest
|
|
}
|
|
build.Artifacts = nil
|
|
for _, a := range k.Build.Artifacts {
|
|
if _, replaced := t.Hand[a.Name]; !replaced {
|
|
build.Artifacts = append(build.Artifacts, a)
|
|
}
|
|
}
|
|
names := make([]string, 0, len(t.Hand))
|
|
for name := range t.Hand {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
for _, name := range names {
|
|
reference := t.Hand[name]
|
|
build.Artifacts = append(build.Artifacts, sdkdelivery.BuiltArtifact{Name: name,
|
|
Digest: handDigestOf.FindString(reference), Reference: reference})
|
|
}
|
|
k.Build = build
|
|
return k
|
|
}
|
|
|
|
// handCoversEvery refuses a registration at references that names an artifact the manifest does not declare, or leaves
|
|
// one it declares without a reference (the key's or a given one).
|
|
func handCoversEvery(m catalogue.Manifest, k builderkeys.Key, hand map[string]string) error {
|
|
declared := map[string]bool{}
|
|
if m.Build != nil {
|
|
for _, a := range m.Build.Artifacts {
|
|
declared[a.Name] = true
|
|
}
|
|
}
|
|
for name := range hand {
|
|
if !declared[name] {
|
|
return fmt.Errorf("--reference %s: %s declares no artifact of that name. Nothing was registered", name, m.Module)
|
|
}
|
|
}
|
|
have := map[string]bool{}
|
|
for _, a := range k.Build.Artifacts {
|
|
have[a.Name] = true
|
|
}
|
|
var missing []string
|
|
for name := range declared {
|
|
if !have[name] {
|
|
missing = append(missing, name)
|
|
}
|
|
}
|
|
sort.Strings(missing)
|
|
if len(missing) > 0 {
|
|
return fmt.Errorf("%s declares the artifact(s) %s and no --reference names them, nor does a key of the builder. "+
|
|
"Nothing was registered", m.Module, strings.Join(missing, ", "))
|
|
}
|
|
return nil
|
|
}
|