Files
mesh-host/internal/apply/fetch_first.go
T
jschoubben 5173f2a31c W13.8: the node-engine fetches a kept build through the build node's kept-artifact, the node taken from the want's kept-on
A want that names a node in kept-on, for an artifact its parts name, is fetched through kept-artifact of that node's build seat and
read as an object on the bus, checked against the digest; with no kept-on it is fetched from the store, and the builder's builds
are read no more (the apply hook, the fetch and the object read from bc8fbc7, its node and fingerprint from the want).

Builds: design 47 G §271, §526, §421 (A-50)
Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-11 23:39:36 +01:00

114 lines
4.6 KiB
Go

package apply
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"git.novox.be/novox/mesh-sdk/go/declaration"
)
// **Every artifact an apply fetches is in hand before anything is replaced** (novox/hq issue 1966, 2026-11-08 23:25 UTC).
//
// An archive and a process are fetched from the artifact store on their own turn, and a container is replaced on its
// own: so a module whose container serves the store (distribution) had that container replaced, cut off from where it
// was reached, and then failed to fetch its program from the store it had just cut off, left half applied with the
// store unreachable from every other machine. pullFirst does this for a module's images (ENG-7); fetchFirst does it for
// its archives and processes, before any of the apply's resources is applied or any orphan removed.
//
// A fetch that fails (the store does not answer for the apply's patience, or what arrived is not what was declared)
// gates the resource's module, as a pull that fails does: nothing of the module is replaced or removed, the failure names
// the artifact, and the rest of the machine is applied. A resource of no module keeps its failure for its own turn.
// fetchedArtifact is one archive's or process's bytes, fetched and checked before the apply began, or why it was not.
type fetchedArtifact struct {
body []byte
err error
}
// artifactOf is the archive's or process's source and digest, and whether the resource is one the apply fetches.
func artifactOf(r declaration.Resource) (source, digest string, ok bool) {
switch res := r.(type) {
case *declaration.Archive:
return res.Source, res.Digest, res.Source != ""
case *declaration.Process:
return res.Source, res.Digest, res.Source != ""
}
return "", "", false
}
// fetchFirst fetches each archive and process the declaration applies, checks each against its declared digest, and
// keeps the bytes for its turn (in.fetched). It is called before the loop that applies, after pullFirst.
func fetchFirst(ctx context.Context, ordered []declaration.Resource, d *declaration.Declaration, in *inputs,
failures *[]*Error, gated map[string]bool, gatedWhy map[string]string, log func(string)) {
in.fetched = map[string]fetchedArtifact{}
bySource := map[string]fetchedArtifact{}
for _, resource := range ordered {
source, digest, isArtifact := artifactOf(resource)
if !isArtifact {
continue
}
module, ours := d.ModuleOfID(resource.Identity())
if ours && gated[module] {
continue
}
// On an adopted node, what a module not yet taken declares may be held as found and never fetched (hold.go): its
// own turn fetches it, as before.
if d.Adoption != nil {
if _, untaken := d.Adoption.UntakenModuleOf(resource.Identity()); untaken || runsIn(resource) != "" {
continue
}
}
// What the module's done build kept on a build node is fetched from there, not from the store (design 47).
cacheKey, kept := source, false
if kf := keptFetchFrom(ctx); kf != nil && ours {
cacheKey = "kept " + module + " " + digest
if _, seen := bySource[cacheKey]; seen {
kept = true
} else if body, isKept, err := kf(ctx, module, digest); isKept {
kept = true
bySource[cacheKey] = fetchedArtifact{body: body, err: err}
}
}
got, seen := bySource[cacheKey]
if !seen && !kept {
got.body, got.err = fetchMinding(ctx, source, in.away)
bySource[cacheKey] = got
}
if got.err == nil {
sum := sha256.Sum256(got.body)
if have := "sha256:" + hex.EncodeToString(sum[:]); have != digest {
got = fetchedArtifact{err: fmt.Errorf("%s was declared as %s and what arrived is %s; nothing was unpacked",
source, digest, have)}
}
}
if got.err == nil {
in.fetched[resource.Identity()] = got
continue
}
if !ours {
// Nothing to gate: the resource fails on its own turn with this error.
in.fetched[resource.Identity()] = got
continue
}
err := fmt.Errorf("not replacing anything of its module, which keeps running what it ran: %w", got.err)
*failures = append(*failures, &Error{Resource: resource.Identity(), Err: err})
log(fmt.Sprintf(" failed %s (%s): %v", resource.Identity(), resource.Target(), err))
gated[module] = true
gatedWhy[module] = "its artifact " + source + " could not be fetched, so nothing of it was replaced " +
"(novox/hq issue 1966)"
}
}
// artifact is the bytes fetchFirst holds for a resource, taken once so they are not kept past their turn; nil and no
// error when it holds none (an apply that did not fetch first: its applier fetches itself).
func (in inputs) artifact(id string) ([]byte, error) {
got, ok := in.fetched[id]
if !ok {
return nil, nil
}
delete(in.fetched, id)
return got.body, got.err
}