Merge pull request 'Genesis registers the control plane with the manifest its build produced (hq issue 072)' (#17) from feat/one-controller-manifest into main

This commit was merged in pull request #17.
This commit is contained in:
2026-09-21 19:23:17 +02:00
8 changed files with 270 additions and 99 deletions
+1 -1
View File
@@ -221,7 +221,7 @@ func newFlagSet(opts *bootstrap.Options, jsonOut *bool) *flag.FlagSet {
set.StringVar(&opts.Out, "out", opts.Out, "where the produced bundle is written") set.StringVar(&opts.Out, "out", opts.Out, "where the produced bundle is written")
set.StringVar(&opts.State, "state", opts.State, "where this node records what it has applied") set.StringVar(&opts.State, "state", opts.State, "where this node records what it has applied")
set.StringVar(&opts.Catalogue, "catalog", opts.Catalogue, set.StringVar(&opts.Catalogue, "catalog", opts.Catalogue,
"a checkout of the mesh's catalogue; without it this stops after the foundation") "a checkout of the mesh's catalogue, for the registry's and the builder's manifests and what phase two installs; without it this stops after the foundation")
set.StringVar(&opts.Source.Repository, "source", opts.Source.Repository, set.StringVar(&opts.Source.Repository, "source", opts.Source.Repository,
"the repository the control plane is built from, on a mesh that already exists") "the repository the control plane is built from, on a mesh that already exists")
set.StringVar(&opts.Source.Ref, "source-ref", opts.Source.Ref, set.StringVar(&opts.Source.Ref, "source-ref", opts.Source.Ref,
+6 -4
View File
@@ -137,9 +137,11 @@ type Options struct {
Node string Node string
// Catalogue is a checkout of the mesh's catalogue repository, which is where the registry's and // Catalogue is a checkout of the mesh's catalogue repository, which is where the registry's and
// the control plane's manifests are read from. Empty stops the installer after the foundation: // the builder's manifests are read from, and everything phase two installs. Not the control
// there is no pivot without manifests, and pretending otherwise would leave a machine that // plane's: that one comes out of the build at step 3, from the root of its own repository
// looks installed and cannot upgrade itself. // (novox/hq ADR 0069). Empty stops the installer after the foundation: there is no pivot
// without manifests, and pretending otherwise would leave a machine that looks installed and
// cannot upgrade itself.
Catalogue string Catalogue string
// Source is where the control plane is built from — a repository on a mesh that already // Source is where the control plane is built from — a repository on a mesh that already
@@ -585,7 +587,7 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro
// ---- 9. control plane ----------------------------------------------------------------- // ---- 9. control plane -----------------------------------------------------------------
say("control plane — installed as an ordinary module, pinned to that digest") say("control plane — installed as an ordinary module, pinned to that digest")
permanent, err := InstallControlPlane(ctx, o, d, temporary, rewritten.Declaration, permanent, err := InstallControlPlane(ctx, o, d, temporary, rewritten.Declaration,
published.Reference, say) built, published.Reference, say)
result.Permanent, result.PermanentAnswered = permanent.Container, permanent.Answered result.Permanent, result.PermanentAnswered = permanent.Container, permanent.Answered
result.StoresDelivered = permanent.Delivered result.StoresDelivered = permanent.Delivered
if err != nil { if err != nil {
+28 -4
View File
@@ -1,6 +1,7 @@
package bootstrap package bootstrap
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
@@ -57,13 +58,19 @@ type Built struct {
// Image is the artifact, named by the digest of its own configuration — the identity a machine // Image is the artifact, named by the digest of its own configuration — the identity a machine
// can use with nothing serving it, and the same one the installer used for a carried image. // can use with nothing serving it, and the same one the installer used for a carried image.
Image string Image string
// Manifest is the module as the mesh should hold it: the manifest at the root of the repository
// that was built, its artifact resolved to Image. It is the control plane's ONE manifest
// (novox/hq ADR 0069) — the installer used to read a second copy out of the catalogue, and the
// two drifted apart the first time somebody edited one (novox/hq 04-ISSUES/072).
Manifest []byte
} }
// builderOutput is the part of the builder's one-shot result this needs. // builderOutput is the part of the builder's one-shot result this needs.
type builderOutput struct { type builderOutput struct {
Module string `json:"module"` Module string `json:"module"`
Commit string `json:"commit"` Commit string `json:"commit"`
Made []struct { Manifest json.RawMessage `json:"manifest"`
Made []struct {
Name string `json:"name"` Name string `json:"name"`
Kind string `json:"kind"` Kind string `json:"kind"`
Reference string `json:"reference"` Reference string `json:"reference"`
@@ -142,8 +149,25 @@ func BuildControlPlane(ctx context.Context, run Runner, builderTag string, sourc
result.Module, len(images)) result.Module, len(images))
} }
// The manifest is what the mesh will hold the control plane as, so a result without one is
// a build the installer cannot finish — refused here, beside the builder that said it, rather
// than at step 9 with a message about a missing file.
manifest := bytes.TrimSpace(result.Manifest)
if len(manifest) == 0 || bytes.Equal(manifest, []byte("null")) {
return Built{}, fmt.Errorf(
"%s built, and the builder reported no manifest for it. The installer registers the "+
"control plane with the manifest the build produced — the one at the root of its "+
"repository, its artifact resolved — and has no other copy to use", result.Module)
}
if !bytes.Contains(manifest, []byte(`"`+images[0]+`"`)) {
return Built{}, fmt.Errorf(
"%s built %s, and the manifest the builder reported does not name that image, so "+
"the installer cannot tell which of its resources runs the control plane",
result.Module, images[0])
}
say(fmt.Sprintf(" built %s from %s", result.Module, shortRef(result.Commit))) say(fmt.Sprintf(" built %s from %s", result.Module, shortRef(result.Commit)))
return Built{Module: result.Module, Commit: result.Commit, Image: images[0]}, nil return Built{Module: result.Module, Commit: result.Commit, Image: images[0], Manifest: manifest}, nil
} }
func shortRef(ref string) string { func shortRef(ref string) string {
+54
View File
@@ -1,6 +1,7 @@
package bootstrap package bootstrap
import ( import (
"context"
"strings" "strings"
"testing" "testing"
) )
@@ -39,3 +40,56 @@ func TestARepositoryAndACommitIsEnough(t *testing.T) {
t.Fatalf("a repository and a commit were refused: %v", err) t.Fatalf("a repository and a commit were refused: %v", err)
} }
} }
// **The build hands over the manifest, and the installer registers that one.** The control plane
// used to have two manifests — one at the root of its repository, which the mesh reads whenever
// it rebuilds the control plane from source, and a copy in the catalogue, which genesis read — and
// nothing kept them equal (novox/hq 04-ISSUES/072). The builder already reports the manifest it
// built, artifact resolved to the image; genesis takes it from there and reads no second copy.
func TestTheBuildHandsOverTheManifestTheMeshWillHold(t *testing.T) {
manifest := `{"module":"mesh-controller","version":"1","resources":[` +
`{"id":"server","type":"container","name":"mesh-controller","image":"` + builtImage + `"}]}`
runtime := &asked{answer: func(string, []string) (string, error) {
return `{"module":"mesh-controller","commit":"a1b2c3d4","manifest":` + manifest +
`,"made":[{"name":"server","kind":"image","reference":"` + builtImage + `"}]}` + "\n", nil
}}
built, err := BuildControlPlane(context.Background(), runtime.run, "mesh-builder:test",
Source{Repository: "https://example.invalid/mesh-controller.git", Ref: "a1b2c3d4"}, false, func(string) {})
if err != nil {
t.Fatal(err)
}
if built.Image != builtImage {
t.Errorf("the built image is %q", built.Image)
}
if string(built.Manifest) != manifest {
t.Errorf("the manifest handed over is not the one the builder reported:\n%s", built.Manifest)
}
}
// A result without a manifest is a build the installer cannot finish, and it is refused beside the
// builder that said it rather than at step 9 with a message about a missing file.
func TestABuildReportingNoManifestIsRefused(t *testing.T) {
runtime := &asked{answer: func(string, []string) (string, error) {
return `{"module":"mesh-controller","commit":"a1b2c3d4",` +
`"made":[{"name":"server","kind":"image","reference":"` + builtImage + `"}]}`, nil
}}
_, err := BuildControlPlane(context.Background(), runtime.run, "mesh-builder:test",
Source{Repository: "https://example.invalid/mesh-controller.git", Ref: "a1b2c3d4"}, false, func(string) {})
if err == nil || !strings.Contains(err.Error(), "no manifest") {
t.Fatalf("a build reporting no manifest was accepted, or refused for another reason: %v", err)
}
}
// And a manifest that does not name the image the build produced describes some other build.
func TestABuildWhoseManifestNamesAnotherImageIsRefused(t *testing.T) {
runtime := &asked{answer: func(string, []string) (string, error) {
return `{"module":"mesh-controller","commit":"a1b2c3d4","manifest":{"module":"mesh-controller",` +
`"resources":[{"id":"server","type":"container","image":"sha256:` + strings.Repeat("9", 64) + `"}]},` +
`"made":[{"name":"server","kind":"image","reference":"` + builtImage + `"}]}`, nil
}}
_, err := BuildControlPlane(context.Background(), runtime.run, "mesh-builder:test",
Source{Repository: "https://example.invalid/mesh-controller.git", Ref: "a1b2c3d4"}, false, func(string) {})
if err == nil || !strings.Contains(err.Error(), "does not name that image") {
t.Fatalf("a manifest naming another image was accepted, or refused for another reason: %v", err)
}
}
+1 -1
View File
@@ -53,7 +53,7 @@ func InstallBuilder(ctx context.Context, o Options, d Deps, control controlPlane
"This is the manifest that makes the builder an ordinary module. Without it the mesh "+ "This is the manifest that makes the builder an ordinary module. Without it the mesh "+
"has the image and no way to run it, so nothing can be built here", err) "has the image and no way to run it, so nothing can be built here", err)
} }
pinned, places, err := pinImage(manifest, published.Reference, BuilderModule) pinned, places, err := pinPlaceholder(manifest, published.Reference, BuilderModule)
if err != nil { if err != nil {
return out, err return out, err
} }
+52 -54
View File
@@ -53,26 +53,33 @@ type Permanent struct {
// exactly the path for a value the mesh must carry and could not have invented — and they are read // exactly the path for a value the mesh must carry and could not have invented — and they are read
// out of the bundle this installer produced rather than reconstructed, because the bundle is what // out of the bundle this installer produced rather than reconstructed, because the bundle is what
// created them and a second opinion about what a DSN should say is a second chance to be wrong. // created them and a second opinion about what a DSN should say is a second chance to be wrong.
//
// **The manifest is the one the build produced** — the manifest at the root of the control plane's
// own repository, its artifact resolved to the image built at step 3 (novox/hq ADR 0069). The
// installer used to read a second copy out of the catalogue and pin its placeholder; the copies
// drifted, and the first rebuild from source replaced the mesh's record with the repository's shape
// while every later push was refused on its behalf (novox/hq 04-ISSUES/072). There is one manifest
// now, and the only thing this step changes in it is the image's name: the id the machine built it
// under becomes the reference the registry assigned at step 8.
func InstallControlPlane(ctx context.Context, o Options, d Deps, control controlPlane, func InstallControlPlane(ctx context.Context, o Options, d Deps, control controlPlane,
foundation *declaration.Declaration, image string, say func(string)) (Permanent, error) { foundation *declaration.Declaration, built Built, image string, say func(string)) (Permanent, error) {
out := Permanent{Image: image} out := Permanent{Image: image}
manifest, err := readManifest(o.Catalogue, ControlPlaneModule) if len(built.Manifest) == 0 {
if err != nil {
return out, fmt.Errorf( return out, fmt.Errorf(
"%w\n"+ "the control plane was not built, so there is no manifest to register it with. " +
"This is the manifest that makes the control plane an ordinary module. Without it "+ "This is the manifest that makes the control plane an ordinary module. Without it " +
"the machine keeps the temporary control plane the foundation raised, which works "+ "the machine keeps the temporary control plane the foundation raised, which works " +
"and cannot be upgraded — so the install stops here rather than pretending to "+ "and cannot be upgraded — so the install stops here rather than pretending to " +
"have pivoted", err) "have pivoted")
} }
pinned, places, err := pinImage(manifest, image, ControlPlaneModule) pinned, places, err := pinImage(built.Manifest, built.Image, image, ControlPlaneModule)
if err != nil { if err != nil {
return out, err return out, err
} }
say(fmt.Sprintf(" pinned %s, in %d place(s)", image, places)) say(fmt.Sprintf(" pinned %s → %s, in %d place(s)", shortImage(built.Image), image, places))
container, _, err := containerIn(pinned, controlPlaneResourceIn(pinned)) container, _, err := containerIn(pinned, controlPlaneResourceIn(pinned))
if err != nil { if err != nil {
@@ -118,52 +125,34 @@ func InstallControlPlane(ctx context.Context, o Options, d Deps, control control
return out, nil return out, nil
} }
// pinImage replaces the catalogue's placeholder digest with what the registry assigned. // pinImage replaces the image the build named with the reference the registry assigned.
// //
// **Textual, and every place it appears.** A manifest may name its image in more than one resource // **Textual, and every place it appears.** A manifest may name its image in more than one resource
// — the catalogue's converted modules routinely carry a runtime container beside the application's // — a migrate step beside the server, a runtime beside the application — and the same reasoning
// — and the same reasoning as the bundle rewrite applies: replacing one and not the others leaves // as the bundle rewrite applies: replacing one and not the others leaves something pointing at an
// something pointing at an image nothing serves, and it fails half way through an apply rather // image nothing serves, and it fails half way through an apply rather than here.
// than here.
// //
// It refuses a manifest with no placeholder in it. That is not pedantry: a manifest already // The built image is named by the digest of its own configuration, `sha256:…` with no registry in
// carrying a real digest is one somebody pinned by hand, and quietly registering it would install a // front, which only the machine that built it can resolve. The whole JSON string moves to the
// control plane that is not the image this machine just published — which is the one thing this // registry's `<registry>/<repository>@sha256:…`, so every machine the module is later pushed to
// step exists to guarantee. // pulls it from the mesh's own store.
func pinImage(manifest []byte, reference, module string) ([]byte, int, error) { //
places := bytes.Count(manifest, []byte(placeholderDigest)) // It refuses a manifest that does not name the built image. That is not pedantry: a manifest
// naming some other image is one that describes some other build, and quietly registering it would
// install a control plane that is not the image this machine just published — which is the one
// thing this step exists to guarantee.
func pinImage(manifest []byte, built, reference, module string) ([]byte, int, error) {
from := []byte(`"` + built + `"`)
places := bytes.Count(manifest, from)
if places == 0 { if places == 0 {
return nil, 0, fmt.Errorf( return nil, 0, fmt.Errorf(
"the %s module's manifest carries no placeholder digest (%s), so there is nothing to "+ "the %s module's manifest does not name the image this machine built (%s), so there "+
"pin to the image this machine just published.\n"+ "is nothing to pin to the image it just published.\n"+
"A manifest already naming a digest was pinned by somebody else, to some other "+ "A manifest naming some other image describes some other build. Registering it "+
"build. Registering it would install a module that is not the one this "+ "would install a module that is not the one this installer built and pushed",
"installer carried and pushed", module, placeholderDigest) module, built)
} }
// The reference the registry gave back is `<registry>/<repository>@sha256:…`, and what the pinned := bytes.ReplaceAll(manifest, from, []byte(`"`+reference+`"`))
// manifest holds is `<something>@sha256:0…0`. Replacing only the digest would leave the
// manifest's own repository name in front of it — which may be `mesh-controller` with no
// registry, and a runtime would then pull it from the internet. The whole reference moves.
var out bytes.Buffer
rest := manifest
for {
at := bytes.Index(rest, []byte(placeholderDigest))
if at < 0 {
out.Write(rest)
break
}
// Back up over the repository this digest belongs to, which runs to the opening quote.
start := bytes.LastIndexByte(rest[:at], '"')
if start < 0 {
return nil, 0, fmt.Errorf(
"the %s module's manifest has a placeholder digest that is not inside a JSON "+
"string, so the installer cannot tell what image it belongs to", module)
}
out.Write(rest[:start+1])
out.WriteString(reference)
rest = rest[at+len(placeholderDigest):]
}
pinned := out.Bytes()
// Read back. A substitution on text can catch more than it was aimed at, and the manifest is // Read back. A substitution on text can catch more than it was aimed at, and the manifest is
// about to be handed to the mesh as the description of what it runs. // about to be handed to the mesh as the description of what it runs.
@@ -172,17 +161,16 @@ func pinImage(manifest []byte, reference, module string) ([]byte, int, error) {
return nil, 0, fmt.Errorf( return nil, 0, fmt.Errorf(
"pinning the %s module's image broke its manifest: %w", module, err) "pinning the %s module's image broke its manifest: %w", module, err)
} }
if bytes.Contains(pinned, []byte(placeholderDigest)) { if bytes.Contains(pinned, from) {
return nil, 0, fmt.Errorf( return nil, 0, fmt.Errorf(
"the %s module's manifest still carries a placeholder digest after pinning", "the %s module's manifest still names the built image after pinning", module)
module)
} }
return pinned, places, nil return pinned, places, nil
} }
// controlPlaneResourceIn is the id of the resource that runs the control plane. // controlPlaneResourceIn is the id of the resource that runs the control plane.
// //
// The manifest is written by the catalogue and the installer does not get to name its resources. // The manifest is the control plane's own and the installer does not get to name its resources.
// What it can do is find the one container whose image is the one just pinned — and when a manifest // What it can do is find the one container whose image is the one just pinned — and when a manifest
// declares exactly one container, that is the answer without any searching at all. // declares exactly one container, that is the answer without any searching at all.
func controlPlaneResourceIn(manifest []byte) string { func controlPlaneResourceIn(manifest []byte) string {
@@ -410,3 +398,13 @@ func sortedKeys(m map[string]string) []string {
sort.Strings(keys) sort.Strings(keys)
return keys return keys
} }
// shortImage is an image id as a person reads one: the first twelve hex digits, without the
// algorithm in front — `shortRef` would keep the prefix and show one digit of the digest.
func shortImage(id string) string {
digest := strings.TrimPrefix(id, "sha256:")
if len(digest) > 12 {
return digest[:12]
}
return digest
}
+62 -35
View File
@@ -11,13 +11,15 @@ import (
// that could go wrong quietly: pinning it to the wrong image, and delivering it store connections // that could go wrong quietly: pinning it to the wrong image, and delivering it store connections
// the mesh invented rather than the ones the foundation actually made. // the mesh invented rather than the ones the foundation actually made.
// theControlPlaneModule is the catalogue's manifest, trimmed to what this installer reads. // theControlPlaneModule is the manifest the build produces at step 3: the control plane's own,
// from the root of its repository, its artifact resolved to the image the machine built — named by
// the digest of its own configuration, with no registry in front (novox/hq ADR 0069).
// //
// A fixture rather than the file itself, unlike the foundation example the rewrite tests use: the // A fixture rather than the file itself, unlike the foundation example the rewrite tests use: the
// catalogue is a different repository on a different branch, and a test that read it would pass or // control plane is a different repository on a different branch, and a test that read it would
// fail according to what somebody else had checked out. What it must stay faithful to is the // pass or fail according to what somebody else had checked out. What it must stay faithful to is
// SHAPE — the placeholder digest, the own-secret per context, the mount from the machine's path to // the SHAPE — the built image's bare id, the own-secret per context, the mount from the machine's
// the container's, and the environment file that fills what is not a path. // path to the container's, and the environment file that fills what is not a path.
const theControlPlaneModule = `{ const theControlPlaneModule = `{
"module": "mesh-controller", "module": "mesh-controller",
"version": "1", "version": "1",
@@ -37,7 +39,7 @@ const theControlPlaneModule = `{
"mode": "0600", "mode": "0600",
"content": "MESH_BROKER_AMQP=${secret:broker}\nMESH_BROKER_MANAGEMENT=${secret:broker-management}\nMESH_BROKER_ADDRESS=${machine:at}:5671\n"}, "content": "MESH_BROKER_AMQP=${secret:broker}\nMESH_BROKER_MANAGEMENT=${secret:broker-management}\nMESH_BROKER_ADDRESS=${machine:at}:5671\n"},
{"id": "server", "type": "container", "name": "mesh-controller", {"id": "server", "type": "container", "name": "mesh-controller",
"image": "mesh-controller@` + placeholderDigest + `", "image": "` + builtImage + `",
"network": "host", "args": ["serve"], "network": "host", "args": ["serve"],
"env-file": ["/var/lib/mesh/mesh-controller/broker.env"], "env-file": ["/var/lib/mesh/mesh-controller/broker.env"],
"env": { "env": {
@@ -58,57 +60,62 @@ const theControlPlaneModule = `{
const pushedReference = "127.0.0.1:5000/mesh-controller@sha256:" + const pushedReference = "127.0.0.1:5000/mesh-controller@sha256:" +
"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
// **The whole reference moves, not only the digest.** The manifest's placeholder names a // builtImage is what step 3 built, as the machine that built it names it.
// repository too, and replacing sixty-four zeros inside it would leave `mesh-controller@sha256:…` const builtImage = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
// with no registry in front — which a runtime would go to the internet for, and this mesh's
// control plane exists in no public registry by design. // theBuild is what step 3 hands step 9.
func theBuild(manifest string) Built {
return Built{Module: "mesh-controller", Commit: "a1b2c3d4", Image: builtImage, Manifest: []byte(manifest)}
}
// **The whole reference moves.** The build names its image by the bare digest of its configuration,
// which only the machine that built it can resolve; the mesh's record must name what the registry
// assigned, `<registry>/<repository>@sha256:…`, or every other machine the module is pushed to
// would go looking for an image nothing serves.
func TestTheControlPlaneIsPinnedToWhatThisMeshsRegistryAssigned(t *testing.T) { func TestTheControlPlaneIsPinnedToWhatThisMeshsRegistryAssigned(t *testing.T) {
pinned, places, err := pinImage([]byte(theControlPlaneModule), pushedReference, "mesh-controller") pinned, places, err := pinImage([]byte(theControlPlaneModule), builtImage, pushedReference, "mesh-controller")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if places != 1 { if places != 1 {
t.Errorf("the placeholder was found in %d place(s)", places) t.Errorf("the built image was found in %d place(s)", places)
} }
if !strings.Contains(string(pinned), `"image": "`+pushedReference+`"`) { if !strings.Contains(string(pinned), `"image": "`+pushedReference+`"`) {
t.Errorf("the manifest does not name the pushed image:\n%s", pinned) t.Errorf("the manifest does not name the pushed image:\n%s", pinned)
} }
if strings.Contains(string(pinned), `"mesh-controller@sha256:`) { if strings.Contains(string(pinned), builtImage) {
t.Errorf("the digest was replaced and the manifest's own repository name was left in "+ t.Errorf("the built image's bare id survived, which no other machine can resolve:\n%s", pinned)
"front of it, so nothing says which registry serves it:\n%s", pinned)
} }
} }
// A manifest already naming a real digest was pinned by somebody else, to some other build. // A manifest naming some other image describes some other build. Registering it would install a
// Registering it would install a control plane that is not the image this machine just published, // control plane that is not the image this machine just published, which is the one thing this
// which is the one thing this step exists to guarantee. // step exists to guarantee.
func TestAManifestAlreadyPinnedByHandIsRefused(t *testing.T) { func TestAManifestNamingAnotherBuildIsRefused(t *testing.T) {
already := strings.Replace(theControlPlaneModule, placeholderDigest, other := strings.Replace(theControlPlaneModule, builtImage, "sha256:"+strings.Repeat("9", 64), 1)
"sha256:"+strings.Repeat("9", 64), 1) if _, _, err := pinImage([]byte(other), builtImage, pushedReference, "mesh-controller"); err == nil {
if _, _, err := pinImage([]byte(already), pushedReference, "mesh-controller"); err == nil { t.Fatal("a manifest naming some other image was accepted")
t.Fatal("a manifest already pinned to some other image was accepted")
} }
} }
// Every placeholder moves. A manifest naming its image in a second resource — a runtime container // Every place moves. A manifest naming its image in a second resource — a migrate step beside the
// beside the application's, which the catalogue's converted modules routinely carry — would // server — would otherwise be left half pinned, and fail inside an apply rather than here.
// otherwise be left half pinned, and fail inside an apply rather than here.
func TestEveryPlaceTheManifestNamesTheImageIsPinned(t *testing.T) { func TestEveryPlaceTheManifestNamesTheImageIsPinned(t *testing.T) {
twice := strings.Replace(theControlPlaneModule, twice := strings.Replace(theControlPlaneModule,
`{"id": "mesh-state", "type": "directory", "path": "/var/lib/mesh/mesh-controller", "mode": "0700"},`, `{"id": "mesh-state", "type": "directory", "path": "/var/lib/mesh/mesh-controller", "mode": "0700"},`,
`{"id": "mesh-state", "type": "directory", "path": "/var/lib/mesh/mesh-controller", "mode": "0700"}, `{"id": "mesh-state", "type": "directory", "path": "/var/lib/mesh/mesh-controller", "mode": "0700"},
{"id": "migrate", "type": "container", "name": "mesh-controller-migrate", "run-once": true, {"id": "migrate", "type": "container", "name": "mesh-controller-migrate", "run-once": true,
"image": "mesh-controller@`+placeholderDigest+`", "args": ["migrate"]},`, 1) "image": "`+builtImage+`", "args": ["migrate"]},`, 1)
pinned, places, err := pinImage([]byte(twice), pushedReference, "mesh-controller") pinned, places, err := pinImage([]byte(twice), builtImage, pushedReference, "mesh-controller")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if places != 2 { if places != 2 {
t.Errorf("the placeholder was found in %d place(s), and the manifest names it twice", places) t.Errorf("the built image was found in %d place(s), and the manifest names it twice", places)
} }
if strings.Contains(string(pinned), placeholderDigest) { if strings.Contains(string(pinned), builtImage) {
t.Error("a placeholder survived the pinning") t.Error("the built image's id survived the pinning")
} }
} }
@@ -230,7 +237,7 @@ func TestAManifestWantingNoStoresIsRefusedWithTheShapeItShouldHave(t *testing.T)
bare := `{"module":"mesh-controller","version":"1","resources":[ bare := `{"module":"mesh-controller","version":"1","resources":[
{"id":"container","type":"container","name":"mesh-controller", {"id":"container","type":"container","name":"mesh-controller",
"image":"mesh-controller@` + placeholderDigest + `"}]}` "image":"` + builtImage + `"}]}`
_, err = deliverStores(context.Background(), Options{Node: "anchor"}, control, _, err = deliverStores(context.Background(), Options{Node: "anchor"}, control,
[]byte(bare), rewritten.Declaration, func(string) {}) []byte(bare), rewritten.Declaration, func(string) {})
@@ -256,9 +263,11 @@ func TestThePermanentControlPlaneIsAskedTheSameQuestion(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
out, err := InstallControlPlane(context.Background(), // No catalogue: the control plane's manifest is the one the build produced (novox/hq
installing(t, catalogueWith(t, ControlPlaneModule, theControlPlaneModule)), // 04-ISSUES/072), and step 9 reads nothing from the catalogue any more.
Deps{Run: runtime.run}, control, rewritten.Declaration, pushedReference, func(string) {}) out, err := InstallControlPlane(context.Background(), installing(t, t.TempDir()),
Deps{Run: runtime.run}, control, rewritten.Declaration, theBuild(theControlPlaneModule),
pushedReference, func(string) {})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -298,3 +307,21 @@ func TestABrokerSettingReadFromAFileIsDeliveredToo(t *testing.T) {
t.Fatal("a plain path variable was taken for a secret") t.Fatal("a plain path variable was taken for a secret")
} }
} }
// Step 9 with nothing built is a caller's fault, and it stops rather than pretending to pivot.
func TestTheControlPlaneCannotBeInstalledWithoutABuild(t *testing.T) {
runtime := &asked{answer: aMeshThatAgrees(map[string]string{})}
control := controlPlane{container: "temp-mesh-controller", run: runtime.run, timeout: time.Second}
rewritten, err := Rewrite(theRealBundle(t), held)
if err != nil {
t.Fatal(err)
}
_, err = InstallControlPlane(context.Background(), installing(t, t.TempDir()),
Deps{Run: runtime.run}, control, rewritten.Declaration, Built{}, pushedReference, func(string) {})
if err == nil || !strings.Contains(err.Error(), "was not built") {
t.Fatalf("a missing build was not refused: %v", err)
}
if runtime.ran("module add") {
t.Error("something was registered without a manifest")
}
}
+66
View File
@@ -1,7 +1,9 @@
package bootstrap package bootstrap
import ( import (
"bytes"
"context" "context"
"encoding/json"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@@ -143,3 +145,67 @@ func pushNode(ctx context.Context, o Options, control controlPlane, say func(str
say(" pushed " + o.Node) say(" pushed " + o.Node)
return strings.TrimSpace(said), nil return strings.TrimSpace(said), nil
} }
// pinPlaceholder replaces the catalogue's placeholder digest with what the registry assigned — the
// builder's manifest, which the catalogue still holds (the control plane's comes out of its own
// build, see pinImage).
//
// **Textual, and every place it appears.** A manifest may name its image in more than one resource
// — the catalogue's converted modules routinely carry a runtime container beside the application's
// — and the same reasoning as the bundle rewrite applies: replacing one and not the others leaves
// something pointing at an image nothing serves, and it fails half way through an apply rather
// than here.
//
// It refuses a manifest with no placeholder in it. That is not pedantry: a manifest already
// carrying a real digest is one somebody pinned by hand, and quietly registering it would install a
// control plane that is not the image this machine just published — which is the one thing this
// step exists to guarantee.
func pinPlaceholder(manifest []byte, reference, module string) ([]byte, int, error) {
places := bytes.Count(manifest, []byte(placeholderDigest))
if places == 0 {
return nil, 0, fmt.Errorf(
"the %s module's manifest carries no placeholder digest (%s), so there is nothing to "+
"pin to the image this machine just published.\n"+
"A manifest already naming a digest was pinned by somebody else, to some other "+
"build. Registering it would install a module that is not the one this "+
"installer carried and pushed", module, placeholderDigest)
}
// The reference the registry gave back is `<registry>/<repository>@sha256:…`, and what the
// manifest holds is `<something>@sha256:0…0`. Replacing only the digest would leave the
// manifest's own repository name in front of it — which may be `mesh-controller` with no
// registry, and a runtime would then pull it from the internet. The whole reference moves.
var out bytes.Buffer
rest := manifest
for {
at := bytes.Index(rest, []byte(placeholderDigest))
if at < 0 {
out.Write(rest)
break
}
// Back up over the repository this digest belongs to, which runs to the opening quote.
start := bytes.LastIndexByte(rest[:at], '"')
if start < 0 {
return nil, 0, fmt.Errorf(
"the %s module's manifest has a placeholder digest that is not inside a JSON "+
"string, so the installer cannot tell what image it belongs to", module)
}
out.Write(rest[:start+1])
out.WriteString(reference)
rest = rest[at+len(placeholderDigest):]
}
pinned := out.Bytes()
// Read back. A substitution on text can catch more than it was aimed at, and the manifest is
// about to be handed to the mesh as the description of what it runs.
var checked map[string]any
if err := json.Unmarshal(pinned, &checked); err != nil {
return nil, 0, fmt.Errorf(
"pinning the %s module's image broke its manifest: %w", module, err)
}
if bytes.Contains(pinned, []byte(placeholderDigest)) {
return nil, 0, fmt.Errorf(
"the %s module's manifest still carries a placeholder digest after pinning",
module)
}
return pinned, places, nil
}