Files
jschoubben 4b9bc50aad The builder resolves the SDK from the mesh registry, and can publish packages
A new 'package' artifact kind builds a module's own code on a public base image
and publishes it to the mesh's package registry by version (hq ADR 0076) — the
SDK above all, which the toolchain is built from and so cannot be built in the
toolchain. The credential a build needs to resolve or publish packages is
rendered as an .npmrc (basic auth, hq ADR 0048) and given to an image build as a
buildkit secret, never a layer, so a token is not baked into the toolchain image.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-16 10:27:26 +02:00

196 lines
7.3 KiB
Go

package catalogue
import (
"fmt"
"sort"
"strings"
)
// Turning a manifest that names artifacts into one that names digests.
//
// **Two documents, deliberately.** The manifest in a repository says *this resource uses the
// archive called `config`*; the manifest the mesh holds says *this resource is sha256:…*. A digest
// is not knowable until something is built, so a repository carrying one would be a repository
// whose file is wrong the moment anybody edits anything — and the mesh would be pinning a value
// nobody could have checked.
//
// So the built manifest is **derived**, and the record of which commit it was derived from is what
// makes "is this current?" answerable without building (novox/hq ADR 0009).
// Built is one artifact after it exists: where it is and what it hashes to.
type Built struct {
// Name is what the manifest called it.
Name string
// Kind is "image" or "archive".
Kind string
// Reference is what a machine uses to get it — an image reference for an image, a URL for an
// archive. Both already carry the digest for an image; an archive carries it separately.
Reference string
// Digest is "sha256:<hex>", for an archive. An image reference already ends in one.
Digest string
}
// Resolve fills a manifest's resources in from what was built.
//
// Every resource naming an artifact is rewritten to name the thing itself, and the `artifact` key
// is removed — because it is a build-time word and the host has never heard of it. A resource
// naming an artifact nothing produced is refused: it would otherwise reach a machine with an
// empty image or an unpinned archive, which is the shape of failure that looks like success.
func (m Manifest) Resolve(built []Built) (Manifest, error) {
if m.Build == nil && len(built) == 0 {
return m, nil
}
by := map[string]Built{}
for _, b := range built {
by[b.Name] = b
}
// Declared and not produced is a build that did not do what the manifest asked, and saying so
// here beats a machine reporting it later.
var missing []string
if m.Build != nil {
for _, a := range m.Build.Artifacts {
if _, ok := by[a.Name]; !ok {
missing = append(missing, a.Name)
}
}
}
if len(missing) > 0 {
sort.Strings(missing)
return Manifest{}, fmt.Errorf(
"%s says it builds %s and the build did not produce %s — the build did not do what "+
"the manifest asked, which is a different fault from a resource asking for the "+
"wrong thing",
m.Module, strings.Join(missing, " and "), oneOrOther(len(missing)))
}
out := m
out.Build = nil
out.Resources = nil
for _, r := range m.Resources {
named, _ := r["artifact"].(string)
if named == "" {
out.Resources = append(out.Resources, r)
continue
}
artifact, ok := by[named]
if !ok {
return Manifest{}, fmt.Errorf(
"%s: %v uses the artifact %q, and this module builds no such thing",
m.Module, r["id"], named)
}
filled := map[string]any{}
for k, v := range r {
filled[k] = v
}
delete(filled, "artifact")
switch artifact.Kind {
case ArtifactPackage:
// A package is not a resource on any machine; it is consumed by other builds. A
// resource that names one is a manifest error, named here rather than shipped.
return Manifest{}, fmt.Errorf(
"%s: %v uses %q, which is a package — a build input, not a resource a machine runs",
m.Module, r["id"], named)
case ArtifactImage, ArtifactUpstream:
filled["image"] = artifact.Reference
case ArtifactArchive, ArtifactBundle:
// The same on the wire: both are bytes fetched by digest and unpacked. They differ in
// how they were made — one packed as it stood, the other compiled first — and a
// machine has no reason to care which.
filled["source"] = artifact.Reference
filled["digest"] = artifact.Digest
default:
return Manifest{}, fmt.Errorf("%s: %q is a %q, and an artifact is %q, %q, %q or %q",
m.Module, named, artifact.Kind, ArtifactImage, ArtifactArchive, ArtifactUpstream,
ArtifactBundle)
}
out.Resources = append(out.Resources, filled)
}
return out, nil
}
// checkBuild is the manifest's own account of what it builds.
func (b *Build) problems(module string) []string {
if b == nil {
return nil
}
var problems []string
seen := map[string]bool{}
for _, a := range b.Artifacts {
if a.Name == "" {
problems = append(problems, module+" builds an artifact with no name")
continue
}
if seen[a.Name] {
problems = append(problems, fmt.Sprintf(
"%s builds two artifacts called %q, and a resource naming it could mean either",
module, a.Name))
}
seen[a.Name] = true
switch a.Kind {
case ArtifactImage, ArtifactArchive, ArtifactUpstream, ArtifactBundle, ArtifactPackage:
default:
problems = append(problems, fmt.Sprintf(
"%s: %q is a %q, and an artifact is %q, %q, %q or %q",
module, a.Name, a.Kind, ArtifactImage, ArtifactArchive, ArtifactUpstream,
ArtifactBundle+", "+ArtifactPackage))
}
// **A bundle is built from the module itself, so it says a language instead.** Everything
// else names what it is built from: a Dockerfile, a directory, somebody else's reference.
// A bundle's source is the module's own directory by definition, and what it needs to say
// is which compiler — because the mesh chooses that, and cannot choose for a module that
// has not said.
if a.Kind == ArtifactBundle || a.Kind == ArtifactPackage {
if a.From != "" {
problems = append(problems, fmt.Sprintf(
"%s: %q is a bundle and names what it is built from (%q). A bundle is built "+
"from the module's own directory; what it says is the language",
module, a.Name, a.From))
}
if strings.TrimSpace(a.Language) == "" {
problems = append(problems, fmt.Sprintf(
"%s: %q is a bundle and says no language, so nothing can choose a compiler "+
"for it", module, a.Name))
}
} else {
if a.From == "" {
problems = append(problems, fmt.Sprintf(
"%s: %q says nothing about what it is built from", module, a.Name))
}
if a.Language != "" {
problems = append(problems, fmt.Sprintf(
"%s: %q is a %q and names a language. Only a bundle is compiled by the mesh; "+
"everything else brings its own recipe", module, a.Name, a.Kind))
}
}
// An upstream image is named, not read from the repository, so the path rule does not
// apply to it — and applying it anyway would refuse every reference with a registry host
// in it.
if a.Kind != ArtifactUpstream &&
(strings.HasPrefix(a.From, "/") || strings.Contains(a.From, "..")) {
// A build reads its own repository and nothing else. A path leaving it would make
// what gets built depend on whatever happens to be on the machine building it.
problems = append(problems, fmt.Sprintf(
"%s: %q is built from %q, which is outside its own repository",
module, a.Name, a.From))
}
if a.Kind == ArtifactUpstream && !strings.Contains(a.From, ":") {
// Without a tag or digest, what gets mirrored is whatever `latest` means today, and
// a module pinned to that is not pinned.
problems = append(problems, fmt.Sprintf(
"%s: %q mirrors %q, which names no tag or digest", module, a.Name, a.From))
}
}
return problems
}
// oneOrOther keeps the message readable for one artifact and for several, because a message that
// says "neither" about one thing reads as a bug in the message.
func oneOrOther(n int) string {
if n == 1 {
return "it"
}
return "them"
}