Files
mesh-sdk/go/takein/takein.go
T
jschoubben 7a8e9f9706
mesh/delivery merged: source.main moved to novox/mesh-sdk@d48365aed010
mesh/delivery-group group fix/1126-node-creds merged: every member is merged
mesh/check checking 7a8e9f970648 on d48365aed010 | novox/mesh-sdk#109 in group fix/1126-node-creds
Wait for a done key whose artifact names no reference, as the controller's take-in does, and hold the fold guard with a row (issue 1126, review round 1)
2026-10-27 21:12:13 +01:00

135 lines
5.2 KiB
Go

// Package takein is the one rule that turns a builder's done key into the manifest the mesh composes a module from
// (issue 1126): the module's manifest resolved with what its build made. It is mesh-controller's take-in of a done key
// (cmd/mesh-controller/key_takein.go builtOfKey, unpackTexts and Manifest.Resolve) moved into the SDK, so a node agent
// composing its own modules (novox/hq ADR 0547 §2) composes from the same built manifest the controller composes from:
// every artifact a resource names resolved to its reference and digest, a TypeScript bundle's launchers, and, for a
// moved module, each template's text read back from its templates archive and what its build keeps for the
// provisioner (catalogue.Handed). Composing from the manifest the indexer read instead hands no moved module and
// leaves every artifact unbuilt.
//
// It reads nothing itself: the archive is read through the caller's Archive, from wherever the caller reaches the
// artifact store.
package takein
import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"errors"
"fmt"
"io"
"strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// Archive reads the archive a recorded reference names (catalogue.Recorded: the artifact store's address left out),
// checked against its digest.
type Archive func(ctx context.Context, reference, digest string) ([]byte, error)
// Manifest is m, as its manifest at b's fingerprint reads, resolved with what b made: the built manifest. b must be done
// and every artifact must name its reference.
// A templated module's templates archive is read through archive; a module without templates reads none, and archive
// may then be nil.
func Manifest(ctx context.Context, m catalogue.Manifest, b delivery.BuilderBuild, archive Archive) (catalogue.Manifest, error) {
if b.State != delivery.BuildDone {
return catalogue.Manifest{}, fmt.Errorf("takein: the build of %s is %s, not done: nothing to resolve it with", m.Module, b.State)
}
// **A done key whose artifact names no reference is waited for, never composed from** (novox/hq ADR 0498 §8), in the
// controller's words (mesh-controller internal/builderkeys Key.Says): a key done before R4e, or a candidate's image or
// package not yet built again for the trunk.
var missing []string
for _, a := range b.Artifacts {
if strings.TrimSpace(a.Reference) == "" {
missing = append(missing, a.Name)
}
}
if len(missing) > 0 {
return catalogue.Manifest{}, fmt.Errorf("takein: the build of %s is done and waits for the reference of %s",
m.Module, strings.Join(missing, ", "))
}
built, err := Built(ctx, m, b.Artifacts, archive)
if err != nil {
return catalogue.Manifest{}, err
}
resolved, err := m.Resolve(built)
if err != nil {
return catalogue.Manifest{}, fmt.Errorf("takein: the build of %s does not resolve: %w", m.Module, err)
}
return resolved, nil
}
// Built is each of a done key's artifacts as catalogue.Manifest.Resolve reads it: its recorded reference and digest, a
// TypeScript bundle's launchers by the build modules' one rule (catalogue.Launchers), and, for the archive a module packs
// its templates into, each template's text read back from that archive.
func Built(ctx context.Context, m catalogue.Manifest, artifacts []delivery.BuiltArtifact, archive Archive) ([]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 artifacts {
d := declared[a.Name]
kind := a.Kind
if kind == "" {
kind = d.Kind
}
b := catalogue.Built{Name: a.Name, Kind: kind, Reference: catalogue.Recorded(a.Reference), Digest: a.Digest}
if launchers := catalogue.Launchers(d.Language, d.Entrypoints); len(launchers) > 0 {
b.Launchers = launchers
}
if a.Name == templates && templates != "" {
if archive == nil {
return nil, fmt.Errorf("takein: %s packs its templates into %q, and nothing here reads the artifact store",
m.Module, a.Name)
}
body, err := archive(ctx, b.Reference, b.Digest)
if err != nil {
return nil, fmt.Errorf("takein: the templates archive %q of %s cannot be read from the artifact store: %w",
a.Name, m.Module, err)
}
if b.Texts, err = UnpackTexts(body); err != nil {
return nil, fmt.Errorf("takein: the templates archive %q of %s cannot be read back: %w", a.Name, m.Module, err)
}
}
out = append(out, b)
}
return out, nil
}
// UnpackTexts is every regular 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)
}
}