A fingerprint written into a recipe names one particular copy of the base — the copy on whichever machine the person typing it was using. On any other mesh that copy has never existed, so the build stops on its first line with a message about an image nobody can look up. Three modules in the catalogue were in exactly that state, and the line each of them replaced was equally dead. A module now names the module and artifact instead, and the mesh answers with what it holds. The builder is still a thing that clones, builds and answers: the answer travels with the question, because only the mesh knows what it has. A base the mesh has not built is refused before anything is built, naming which module has to exist first.
166 lines
6.2 KiB
Go
166 lines
6.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"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)")
|
|
on := multiple(set, "on",
|
|
"a base this build stands on, as <module>/<artifact>=<reference>. Repeatable")
|
|
positionals, err := parseAround(set, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(positionals) != 1 {
|
|
return errors.New("mesh-builder build <repository> [--path P] [--ref COMMIT] [--registry HOST:PORT]")
|
|
}
|
|
repository := positionals[0]
|
|
|
|
where := *workspace
|
|
if where == "" {
|
|
where = os.TempDir() + "/mesh-builder-once"
|
|
}
|
|
|
|
// What this build may stand on, said on the command line because there is no mesh to ask.
|
|
//
|
|
// **This is the genesis case and it is meant to be awkward.** On a running mesh the control
|
|
// plane answers this, because only it knows what this mesh holds. Here nothing has been built
|
|
// yet, so whoever runs this says what to use — and for the control plane, which is the one
|
|
// module raised before anything else exists, the answer is ordinarily nothing at all.
|
|
bases := map[string]string{}
|
|
for _, pair := range *on {
|
|
key, reference, found := strings.Cut(pair, "=")
|
|
if !found || key == "" || reference == "" {
|
|
return fmt.Errorf(
|
|
"--on takes <module>/<artifact>=<reference>, and %q is not that", pair)
|
|
}
|
|
bases[key] = reference
|
|
}
|
|
|
|
// 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, buildErr := builder.Build(ctx, builder.Command, publisher, repository, *path, *ref, where, bases)
|
|
if buildErr != nil {
|
|
return buildErr
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// parseAround lets flags appear on either side of the repository, because a person writing this by
|
|
// hand will put them wherever reads best and the standard parser stops at the first thing that is
|
|
// not a flag. The same helper the control plane's commands use, for the same reason.
|
|
func parseAround(set *flag.FlagSet, args []string) ([]string, error) {
|
|
var positionals []string
|
|
rest := args
|
|
for {
|
|
if err := set.Parse(rest); err != nil {
|
|
return nil, err
|
|
}
|
|
rest = set.Args()
|
|
if len(rest) == 0 {
|
|
return positionals, nil
|
|
}
|
|
positionals = append(positionals, rest[0])
|
|
rest = rest[1:]
|
|
}
|
|
}
|
|
|
|
// multiple is a flag that may be given more than once.
|
|
type repeated []string
|
|
|
|
func (r *repeated) String() string { return strings.Join(*r, ", ") }
|
|
func (r *repeated) Set(v string) error { *r = append(*r, v); return nil }
|
|
|
|
func multiple(set *flag.FlagSet, name, usage string) *[]string {
|
|
var values repeated
|
|
set.Var(&values, name, usage)
|
|
return (*[]string)(&values)
|
|
}
|