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 } } got, seen := bySource[source] if !seen { got.body, got.err = fetchMinding(ctx, source, in.away) bySource[source] = 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 }