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. # 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"
}
] ]
} }
} }