The builder builds one module and stops, with no broker and no registry
This is how a mesh is raised: the installer carries this program and runs it once, before anything exists, to produce the control plane from the same repository and path every later rebuild will use. What raises the mesh is then the same thing that maintains it, rather than a second mechanism exercised once per new mesh — which is how often enough to rot. With nowhere to publish, an image stays in the machine's own runtime and is named by the digest of its own configuration: the same identity the installer has always used for the image it carried.
This commit is contained in:
@@ -56,6 +56,14 @@ is dialled except the broker.
|
||||
MESH_REGISTRY host:port to publish artifacts to, when the mesh has not said
|
||||
MESH_BINDING a file the mesh wrote saying where the artifact store is
|
||||
MESH_WORKSPACE where to clone and build (default: a temporary directory)
|
||||
|
||||
It also builds one module and stops, which is how a mesh is raised — before there is a
|
||||
broker to take work from or a registry to publish into:
|
||||
|
||||
mesh-builder build <repository> [--path P] [--ref COMMIT] [--registry HOST:PORT]
|
||||
|
||||
Without --registry the artifacts stay in this machine's container runtime, named by the
|
||||
digest of their own configuration. The result is printed as JSON.
|
||||
`
|
||||
|
||||
func run() error {
|
||||
@@ -64,6 +72,8 @@ func run() error {
|
||||
case "version":
|
||||
fmt.Println(version)
|
||||
return nil
|
||||
case "build":
|
||||
return buildOnce(context.Background(), os.Args[2:])
|
||||
default:
|
||||
fmt.Print(usage)
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/novox/mesh-control/internal/builder"
|
||||
)
|
||||
|
||||
// buildOnce is the builder doing one build and stopping, with no broker and no mesh.
|
||||
//
|
||||
// **This is how a mesh is raised** (novox/hq ADR 0073). The installer carries this program and runs
|
||||
// it once, before anything else exists, to produce the control plane from the same repository and
|
||||
// path that every later rebuild of it will use. What raises the mesh is therefore the same thing
|
||||
// that maintains it — not a second mechanism that has to be kept in step with the first and is
|
||||
// exercised once per new mesh, which is how often enough to rot.
|
||||
//
|
||||
// It takes no work from a queue and answers nobody: there is no broker yet, and the only thing
|
||||
// waiting for the answer is the installer that started it. So the result goes to standard output as
|
||||
// JSON, which is what the installer reads.
|
||||
//
|
||||
// With no --registry it publishes nowhere and the image stays in this machine's container runtime,
|
||||
// named by the digest of its own configuration (see builder.Local). That is the genesis case. With
|
||||
// one, it publishes as it always does — the same command is how an operator builds a module by hand
|
||||
// on a mesh that already exists.
|
||||
func buildOnce(ctx context.Context, args []string) error {
|
||||
set := flag.NewFlagSet("build", flag.ContinueOnError)
|
||||
path := set.String("path", "", "the module's directory inside the repository (default: its root)")
|
||||
ref := set.String("ref", "", "the commit to build; a branch is a moving target somebody else controls")
|
||||
registry := set.String("registry", "",
|
||||
"host:port to publish to. Without it the artifacts stay in this machine's container runtime, which is the genesis case")
|
||||
workspace := set.String("workspace", "", "where to clone and build (default: a temporary directory)")
|
||||
if err := set.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if set.NArg() != 1 {
|
||||
return errors.New("mesh-builder build <repository> [--path P] [--ref COMMIT] [--registry HOST:PORT]")
|
||||
}
|
||||
repository := set.Arg(0)
|
||||
|
||||
where := *workspace
|
||||
if where == "" {
|
||||
where = os.TempDir() + "/mesh-builder-once"
|
||||
}
|
||||
|
||||
// Local unless told otherwise, because the moment this exists for has nowhere to publish. A
|
||||
// default pointing at a registry would mean genesis failing at a push to something that is not
|
||||
// there yet, one step away from the thing that could explain it.
|
||||
var publisher builder.Publisher = builder.Local{Run: builder.Command}
|
||||
if *registry != "" {
|
||||
publisher = builder.Registry{Address: *registry, Run: builder.Command}
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "building %s", repository)
|
||||
if *path != "" {
|
||||
fmt.Fprintf(os.Stderr, " at %s", *path)
|
||||
}
|
||||
if *ref != "" {
|
||||
fmt.Fprintf(os.Stderr, " at %s", *ref)
|
||||
}
|
||||
fmt.Fprintln(os.Stderr)
|
||||
|
||||
built, err := builder.Build(ctx, builder.Command, publisher, repository, *path, *ref, where)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// To standard output, and everything else to standard error, so the caller can read this
|
||||
// without having to separate it from progress.
|
||||
out := onceResult{
|
||||
Module: built.Manifest.Module,
|
||||
Commit: built.Commit,
|
||||
Repository: repository,
|
||||
Path: *path,
|
||||
Ref: *ref,
|
||||
Manifest: built.Manifest,
|
||||
Against: built.Against,
|
||||
}
|
||||
for _, made := range built.Built {
|
||||
out.Made = append(out.Made, madeArtifact{Name: made.Name, Kind: made.Kind, Reference: made.Reference})
|
||||
}
|
||||
body, err := json.MarshalIndent(out, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(body))
|
||||
fmt.Fprintf(os.Stderr, " built %s from %s\n", built.Manifest.Module, short(built.Commit))
|
||||
return nil
|
||||
}
|
||||
|
||||
// onceResult is what one build reports to whoever started it.
|
||||
//
|
||||
// The same fields the mesh records for a build, so a reader comparing a genesis build against an
|
||||
// ordinary one is comparing the same thing said the same way.
|
||||
type onceResult struct {
|
||||
Module string `json:"module"`
|
||||
Commit string `json:"commit"`
|
||||
Repository string `json:"repository"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Ref string `json:"ref,omitempty"`
|
||||
Manifest any `json:"manifest"`
|
||||
Made []madeArtifact `json:"made"`
|
||||
Against []string `json:"against,omitempty"`
|
||||
}
|
||||
|
||||
type madeArtifact struct {
|
||||
Name string `json:"name"`
|
||||
Kind string `json:"kind"`
|
||||
Reference string `json:"reference"`
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Local is what a build publishes into when there is nowhere to publish yet.
|
||||
//
|
||||
// **This exists for exactly one moment: raising a mesh** (novox/hq ADR 0073). The installer builds
|
||||
// the control plane on the machine that is about to run it, and at that moment there is no registry
|
||||
// — the registry is installed afterwards, by the control plane this build produces. So the artifact
|
||||
// stays where the build left it: in the machine's own container runtime.
|
||||
//
|
||||
// That is not a weaker kind of pinning. An image held locally is named by the digest of its own
|
||||
// configuration, which is content-addressed and unforgeable and requires nothing to have served it
|
||||
// — the same identity the installer has always used for the image it carried. What changes when a
|
||||
// registry exists is not that the artifact becomes exact, but that something other than this
|
||||
// machine can fetch it.
|
||||
//
|
||||
// It is deliberately unable to publish an archive. An archive has no local identity to fall back
|
||||
// on: it is bytes that only mean something once something serves them at a URL. A build that
|
||||
// produces one before there is anywhere to put it has produced nothing usable, and saying so is
|
||||
// better than returning a path on a disk that no other machine can read.
|
||||
type Local struct {
|
||||
// Run is how docker is invoked, so a test does not need one.
|
||||
Run Runner
|
||||
}
|
||||
|
||||
// PublishImage leaves the image where the build put it, and names it by its own configuration.
|
||||
//
|
||||
// The local tag is not returned: a tag is a name somebody can move, and every other reference in a
|
||||
// resolved manifest is exact. The digest is read back from the runtime rather than computed, for
|
||||
// the same reason the registry publisher reads it back from the registry — what matters is what
|
||||
// will be served for that reference, and only the thing serving it can say.
|
||||
func (l Local) PublishImage(ctx context.Context, localTag, repository string) (string, error) {
|
||||
out, err := l.Run(ctx, "", "docker", "image", "inspect", "--format", "{{.Id}}", localTag)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot read back the image just built as %s: %w", localTag, err)
|
||||
}
|
||||
id := strings.TrimSpace(out)
|
||||
if !strings.HasPrefix(id, "sha256:") || len(id) != len("sha256:")+64 {
|
||||
// Refused rather than passed on. A bundle naming an image by anything a person could move
|
||||
// is refused by the machine applying it, and a value that is not an identity would fail
|
||||
// there instead — one step further from the thing that could explain it.
|
||||
return "", fmt.Errorf(
|
||||
"the container runtime named the image just built %q, which is not an image id: "+
|
||||
"sha256 and sixty-four hex characters", id)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// PublishArchive refuses, and says why rather than inventing somewhere to put it.
|
||||
func (l Local) PublishArchive(ctx context.Context, repository string, body []byte, digest string) (string, error) {
|
||||
return "", fmt.Errorf(
|
||||
"%s declares an archive, and this build has nowhere to publish one. An image can stay in "+
|
||||
"the machine's own runtime and still be named exactly; an archive is bytes that mean "+
|
||||
"nothing until something serves them. Build this once the mesh has a registry",
|
||||
repository)
|
||||
}
|
||||
Reference in New Issue
Block a user