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:
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user