Genesis registers the control plane with the manifest its build produced (hq issue 072) #17
@@ -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.State, "state", opts.State, "where this node records what it has applied")
|
||||
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,
|
||||
"the repository the control plane is built from, on a mesh that already exists")
|
||||
set.StringVar(&opts.Source.Ref, "source-ref", opts.Source.Ref,
|
||||
|
||||
@@ -137,9 +137,11 @@ type Options struct {
|
||||
Node string
|
||||
|
||||
// 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:
|
||||
// there is no pivot without manifests, and pretending otherwise would leave a machine that
|
||||
// looks installed and cannot upgrade itself.
|
||||
// the builder's manifests are read from, and everything phase two installs. Not the control
|
||||
// plane's: that one comes out of the build at step 3, from the root of its own repository
|
||||
// (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
|
||||
|
||||
// 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 -----------------------------------------------------------------
|
||||
say("control plane — installed as an ordinary module, pinned to that digest")
|
||||
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.StoresDelivered = permanent.Delivered
|
||||
if err != nil {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -57,13 +58,19 @@ type Built struct {
|
||||
// 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.
|
||||
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.
|
||||
type builderOutput struct {
|
||||
Module string `json:"module"`
|
||||
Commit string `json:"commit"`
|
||||
Made []struct {
|
||||
Module string `json:"module"`
|
||||
Commit string `json:"commit"`
|
||||
Manifest json.RawMessage `json:"manifest"`
|
||||
Made []struct {
|
||||
Name string `json:"name"`
|
||||
Kind string `json:"kind"`
|
||||
Reference string `json:"reference"`
|
||||
@@ -142,8 +149,25 @@ func BuildControlPlane(ctx context.Context, run Runner, builderTag string, sourc
|
||||
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)))
|
||||
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 {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -39,3 +40,56 @@ func TestARepositoryAndACommitIsEnough(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 "+
|
||||
"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 {
|
||||
return out, err
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// 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.
|
||||
//
|
||||
// **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,
|
||||
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}
|
||||
|
||||
manifest, err := readManifest(o.Catalogue, ControlPlaneModule)
|
||||
if err != nil {
|
||||
if len(built.Manifest) == 0 {
|
||||
return out, fmt.Errorf(
|
||||
"%w\n"+
|
||||
"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 "+
|
||||
"and cannot be upgraded — so the install stops here rather than pretending to "+
|
||||
"have pivoted", err)
|
||||
"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 " +
|
||||
"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 " +
|
||||
"have pivoted")
|
||||
}
|
||||
|
||||
pinned, places, err := pinImage(manifest, image, ControlPlaneModule)
|
||||
pinned, places, err := pinImage(built.Manifest, built.Image, image, ControlPlaneModule)
|
||||
if err != nil {
|
||||
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))
|
||||
if err != nil {
|
||||
@@ -118,52 +125,34 @@ func InstallControlPlane(ctx context.Context, o Options, d Deps, control control
|
||||
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
|
||||
// — 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.
|
||||
// — a migrate step beside the server, a runtime beside the application — 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 pinImage(manifest []byte, reference, module string) ([]byte, int, error) {
|
||||
places := bytes.Count(manifest, []byte(placeholderDigest))
|
||||
// The built image is named by the digest of its own configuration, `sha256:…` with no registry in
|
||||
// front, which only the machine that built it can resolve. The whole JSON string moves to the
|
||||
// registry's `<registry>/<repository>@sha256:…`, so every machine the module is later pushed to
|
||||
// pulls it from the mesh's own store.
|
||||
//
|
||||
// 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 {
|
||||
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 %s module's manifest does not name the image this machine built (%s), so there "+
|
||||
"is nothing to pin to the image it just published.\n"+
|
||||
"A manifest naming some other image describes some other build. Registering it "+
|
||||
"would install a module that is not the one this installer built and pushed",
|
||||
module, built)
|
||||
}
|
||||
// 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()
|
||||
pinned := bytes.ReplaceAll(manifest, from, []byte(`"`+reference+`"`))
|
||||
|
||||
// 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.
|
||||
@@ -172,17 +161,16 @@ func pinImage(manifest []byte, reference, module string) ([]byte, int, error) {
|
||||
return nil, 0, fmt.Errorf(
|
||||
"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(
|
||||
"the %s module's manifest still carries a placeholder digest after pinning",
|
||||
module)
|
||||
"the %s module's manifest still names the built image after pinning", module)
|
||||
}
|
||||
return pinned, places, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
// declares exactly one container, that is the answer without any searching at all.
|
||||
func controlPlaneResourceIn(manifest []byte) string {
|
||||
@@ -410,3 +398,13 @@ func sortedKeys(m map[string]string) []string {
|
||||
sort.Strings(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
|
||||
}
|
||||
|
||||
@@ -11,13 +11,15 @@ import (
|
||||
// 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.
|
||||
|
||||
// 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
|
||||
// catalogue is a different repository on a different branch, and a test that read it would pass or
|
||||
// fail according to what somebody else had checked out. What it must stay faithful to is the
|
||||
// SHAPE — the placeholder digest, the own-secret per context, the mount from the machine's path to
|
||||
// the container's, and the environment file that fills what is not a path.
|
||||
// control plane is a different repository on a different branch, and a test that read it would
|
||||
// pass or fail according to what somebody else had checked out. What it must stay faithful to is
|
||||
// the SHAPE — the built image's bare id, the own-secret per context, the mount from the machine's
|
||||
// path to the container's, and the environment file that fills what is not a path.
|
||||
const theControlPlaneModule = `{
|
||||
"module": "mesh-controller",
|
||||
"version": "1",
|
||||
@@ -37,7 +39,7 @@ const theControlPlaneModule = `{
|
||||
"mode": "0600",
|
||||
"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",
|
||||
"image": "mesh-controller@` + placeholderDigest + `",
|
||||
"image": "` + builtImage + `",
|
||||
"network": "host", "args": ["serve"],
|
||||
"env-file": ["/var/lib/mesh/mesh-controller/broker.env"],
|
||||
"env": {
|
||||
@@ -58,57 +60,62 @@ const theControlPlaneModule = `{
|
||||
const pushedReference = "127.0.0.1:5000/mesh-controller@sha256:" +
|
||||
"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
|
||||
|
||||
// **The whole reference moves, not only the digest.** The manifest's placeholder names a
|
||||
// repository too, and replacing sixty-four zeros inside it would leave `mesh-controller@sha256:…`
|
||||
// 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.
|
||||
// builtImage is what step 3 built, as the machine that built it names it.
|
||||
const builtImage = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
|
||||
// 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) {
|
||||
pinned, places, err := pinImage([]byte(theControlPlaneModule), pushedReference, "mesh-controller")
|
||||
pinned, places, err := pinImage([]byte(theControlPlaneModule), builtImage, pushedReference, "mesh-controller")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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+`"`) {
|
||||
t.Errorf("the manifest does not name the pushed image:\n%s", pinned)
|
||||
}
|
||||
if strings.Contains(string(pinned), `"mesh-controller@sha256:`) {
|
||||
t.Errorf("the digest was replaced and the manifest's own repository name was left in "+
|
||||
"front of it, so nothing says which registry serves it:\n%s", pinned)
|
||||
if strings.Contains(string(pinned), builtImage) {
|
||||
t.Errorf("the built image's bare id survived, which no other machine can resolve:\n%s", pinned)
|
||||
}
|
||||
}
|
||||
|
||||
// A manifest already naming a real digest was pinned by somebody else, to some other build.
|
||||
// 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 TestAManifestAlreadyPinnedByHandIsRefused(t *testing.T) {
|
||||
already := strings.Replace(theControlPlaneModule, placeholderDigest,
|
||||
"sha256:"+strings.Repeat("9", 64), 1)
|
||||
if _, _, err := pinImage([]byte(already), pushedReference, "mesh-controller"); err == nil {
|
||||
t.Fatal("a manifest already pinned to some other image was accepted")
|
||||
// A manifest naming some other image describes some other build. 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 TestAManifestNamingAnotherBuildIsRefused(t *testing.T) {
|
||||
other := strings.Replace(theControlPlaneModule, builtImage, "sha256:"+strings.Repeat("9", 64), 1)
|
||||
if _, _, err := pinImage([]byte(other), builtImage, pushedReference, "mesh-controller"); err == nil {
|
||||
t.Fatal("a manifest naming some other image was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
// Every placeholder moves. A manifest naming its image in a second resource — a runtime container
|
||||
// beside the application's, which the catalogue's converted modules routinely carry — would
|
||||
// otherwise be left half pinned, and fail inside an apply rather than here.
|
||||
// Every place moves. A manifest naming its image in a second resource — a migrate step beside the
|
||||
// server — would otherwise be left half pinned, and fail inside an apply rather than here.
|
||||
func TestEveryPlaceTheManifestNamesTheImageIsPinned(t *testing.T) {
|
||||
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": "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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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) {
|
||||
t.Error("a placeholder survived the pinning")
|
||||
if strings.Contains(string(pinned), builtImage) {
|
||||
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":[
|
||||
{"id":"container","type":"container","name":"mesh-controller",
|
||||
"image":"mesh-controller@` + placeholderDigest + `"}]}`
|
||||
"image":"` + builtImage + `"}]}`
|
||||
|
||||
_, err = deliverStores(context.Background(), Options{Node: "anchor"}, control,
|
||||
[]byte(bare), rewritten.Declaration, func(string) {})
|
||||
@@ -256,9 +263,11 @@ func TestThePermanentControlPlaneIsAskedTheSameQuestion(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
out, err := InstallControlPlane(context.Background(),
|
||||
installing(t, catalogueWith(t, ControlPlaneModule, theControlPlaneModule)),
|
||||
Deps{Run: runtime.run}, control, rewritten.Declaration, pushedReference, func(string) {})
|
||||
// No catalogue: the control plane's manifest is the one the build produced (novox/hq
|
||||
// 04-ISSUES/072), and step 9 reads nothing from the catalogue any more.
|
||||
out, err := InstallControlPlane(context.Background(), installing(t, t.TempDir()),
|
||||
Deps{Run: runtime.run}, control, rewritten.Declaration, theBuild(theControlPlaneModule),
|
||||
pushedReference, func(string) {})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -298,3 +307,21 @@ func TestABrokerSettingReadFromAFileIsDeliveredToo(t *testing.T) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -143,3 +145,67 @@ func pushNode(ctx context.Context, o Options, control controlPlane, say func(str
|
||||
say(" pushed " + o.Node)
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user