The control plane's recipe declares its base, and an undeclared FROM is refused (ADR 0097 live) #39

Merged
jschoubben merged 1 commits from multiple-fixes into main 2026-09-21 20:58:22 +00:00
5 changed files with 20 additions and 12 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
ARG GO_BASE=golang:1.25-alpine
# The control plane's image.
#
# novox/hq ADR 0006: this image is pinned by digest in the bundle the host carries, fetched on a
@@ -11,7 +12,7 @@
# (novox/hq ADR 0004). Adding them "just in case" would put a trust store in the one image whose
# whole argument is that it contains nothing to reason about.
FROM golang:1.25-alpine AS build
FROM ${GO_BASE} AS build
WORKDIR /src
# Dependencies first, so a change to the source does not refetch them.
+4 -2
View File
@@ -1,17 +1,19 @@
ARG ALPINE_BASE=alpine:3
ARG GO_BASE=golang:1.25-alpine
# The builder, as a module ships one.
#
# Not FROM scratch, unlike the control plane: this one runs git and a container client, so it
# needs a filesystem with them on it. That is the honest cost of a machine whose job is to build —
# and it is why building is a MODULE on a machine that has a runtime rather than something the
# control plane does (novox/hq ADR 0005).
FROM golang:1.25-alpine AS build
FROM ${GO_BASE} AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags '-s -w' -o /mesh-builder ./cmd/mesh-builder
FROM alpine:3
FROM ${ALPINE_BASE}
# git to clone what it is asked to build, and the docker client to build and push it. The daemon
# is the machine's, reached through its socket — a build machine shares the runtime it was given
# rather than running one inside itself.
+7 -7
View File
@@ -397,13 +397,13 @@ func one(ctx context.Context, run Runner, publish Publisher,
module, a.From, strings.Join(copies, ", "))
}
if len(bases) > 0 {
// Said, not yet refused: the mesh's own images start FROM a public base — the control
// plane's, the builder's, the tool runtime's — and refusing those refuses genesis.
// They declare their bases next; until then a base fetched on its own is named here,
// with the remedy, every build.
say("recipe", "UNDECLARED base(s) %s in %s — declare each under build.on as "+
"{arg, image@sha256:…} and read it from that argument (novox/hq ADR 0097)",
strings.Join(bases, ", "), a.From)
// Refused, since the mesh's own images declare theirs (ADR 0097): a base fetched on
// its own is a build that works when a public registry answers, which is sometimes.
return catalogue.Built{}, fmt.Errorf(
"%s: the recipe %s starts FROM %s, which the manifest does not declare. Declare "+
"each under build.on as {\"arg\": \"<NAME>\", \"image\": \"<image>@sha256:…\"} "+
"and start FROM ${<NAME>} (novox/hq ADR 0097)",
module, a.From, strings.Join(bases, ", "))
}
invocation := append([]string{"build", "-f", a.From, "-t", local}, args...)
if a.Target != "" {
+1 -2
View File
@@ -124,8 +124,7 @@ FROM golang:1.25-alpine AS go
if strings.Join(copies, "|") != "${MC_BASE} (a build argument the manifest does not declare)|vendor/tool:latest" {
t.Fatalf("copies out of undeclared images: %v", copies)
}
// A base fetched on its own is named apart: the mesh's own images still start FROM one, so
// it is said rather than refused until they declare theirs.
// A base fetched on its own is named apart, and refused like a copy (ADR 0097).
if strings.Join(bases, "|") != "golang:1.25-alpine" {
t.Fatalf("undeclared bases: %v", bases)
}
+6
View File
@@ -66,6 +66,12 @@
"kind": "image",
"from": "Dockerfile"
}
],
"on": [
{
"arg": "GO_BASE",
"image": "golang@sha256:1ae0735f00daffa3aaf1363a5184c0d2dc55c78e3db4ec70241cdac97bf84b59"
}
]
}
}