The control plane's recipe starts FROM the base its manifest declares, and an undeclared base is refused

The mesh's own images declare theirs now, so the refusal ADR 0097 deferred is live.
The builder's own image and the examples take arguments with defaults; make builds
them, not the mesh.
This commit is contained in:
2026-09-21 22:16:10 +02:00
parent d7dab9c09f
commit b3486270d1
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. # The control plane's image.
# #
# novox/hq ADR 0006: this image is pinned by digest in the bundle the host carries, fetched on a # 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 # (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. # whole argument is that it contains nothing to reason about.
FROM golang:1.25-alpine AS build FROM ${GO_BASE} AS build
WORKDIR /src WORKDIR /src
# Dependencies first, so a change to the source does not refetch them. # 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. # The builder, as a module ships one.
# #
# Not FROM scratch, unlike the control plane: this one runs git and a container client, so it # 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 — # 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 # 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). # control plane does (novox/hq ADR 0005).
FROM golang:1.25-alpine AS build FROM ${GO_BASE} AS build
WORKDIR /src WORKDIR /src
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
COPY . . COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags '-s -w' -o /mesh-builder ./cmd/mesh-builder 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 # 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 # is the machine's, reached through its socket — a build machine shares the runtime it was given
# rather than running one inside itself. # 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, ", ")) module, a.From, strings.Join(copies, ", "))
} }
if len(bases) > 0 { if len(bases) > 0 {
// Said, not yet refused: the mesh's own images start FROM a public base — the control // Refused, since the mesh's own images declare theirs (ADR 0097): a base fetched on
// plane's, the builder's, the tool runtime's — and refusing those refuses genesis. // its own is a build that works when a public registry answers, which is sometimes.
// They declare their bases next; until then a base fetched on its own is named here, return catalogue.Built{}, fmt.Errorf(
// with the remedy, every build. "%s: the recipe %s starts FROM %s, which the manifest does not declare. Declare "+
say("recipe", "UNDECLARED base(s) %s in %s — declare each under build.on as "+ "each under build.on as {\"arg\": \"<NAME>\", \"image\": \"<image>@sha256:…\"} "+
"{arg, image@sha256:…} and read it from that argument (novox/hq ADR 0097)", "and start FROM ${<NAME>} (novox/hq ADR 0097)",
strings.Join(bases, ", "), a.From) module, a.From, strings.Join(bases, ", "))
} }
invocation := append([]string{"build", "-f", a.From, "-t", local}, args...) invocation := append([]string{"build", "-f", a.From, "-t", local}, args...)
if a.Target != "" { 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" { 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) 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 // A base fetched on its own is named apart, and refused like a copy (ADR 0097).
// it is said rather than refused until they declare theirs.
if strings.Join(bases, "|") != "golang:1.25-alpine" { if strings.Join(bases, "|") != "golang:1.25-alpine" {
t.Fatalf("undeclared bases: %v", bases) t.Fatalf("undeclared bases: %v", bases)
} }
+6
View File
@@ -66,6 +66,12 @@
"kind": "image", "kind": "image",
"from": "Dockerfile" "from": "Dockerfile"
} }
],
"on": [
{
"arg": "GO_BASE",
"image": "golang@sha256:1ae0735f00daffa3aaf1363a5184c0d2dc55c78e3db4ec70241cdac97bf84b59"
}
] ]
} }
} }