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
114 lines
4.6 KiB
Go
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
|
|
}
|