Publish to the registry, and a command that builds a repository
One store, and it is the registry the bootstrap already pulls from. An OCI registry is a content-addressed blob store that also understands images: PUT a blob and it is retrievable at /v2/<name>/blobs/sha256:… for ever, by digest. An archive is a content-addressed blob. A second store beside it was considered and is the right answer for objects that are mutable, need per-reader access, or are not build output — somebody's uploads, a backup, a thing with a lifecycle. None of that describes a digest-pinned archive, and running a second service to hold one kind of immutable blob is two things to run, two to back up, and two ways for an artifact to be missing. Overturnable by reading: the manifest carries a URL and a digest, and neither says what served it. `build <repository>` clones, reads module.json, builds what it declares, publishes, and records the manifest with the commit it came from. It is a command rather than something the control plane does on its own, because building runs things on a machine and what the control plane may send a machine is bounded by the declaration language. This is the shape the builder module takes when it is given work over the broker. Proven end to end on a real repository and a real registry: a shell module with a package, a user and a dotfile archive built, published, fetched back at the digest it declared, rebuilt to the same digest, and its manifest accepted by the host's own parser — including `user` and `archive`, which did not exist this morning. A tag is never accepted as a pin, and a blob already stored is not sent again — it is named by its content, so re-uploading asks the registry to store what it already has under the name it already has.
This commit is contained in:
@@ -20,6 +20,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-control/internal/broker"
|
||||
"github.com/novox/mesh-control/internal/builder"
|
||||
"github.com/novox/mesh-control/internal/catalogue"
|
||||
"github.com/novox/mesh-control/internal/identity"
|
||||
"github.com/novox/mesh-control/internal/inventory"
|
||||
@@ -63,6 +64,8 @@ func run() error {
|
||||
defer stop()
|
||||
|
||||
switch args[0] {
|
||||
case "build":
|
||||
return buildCommand(ctx, args[1:])
|
||||
case "pin":
|
||||
return pinCommand(ctx, args[1:], true)
|
||||
case "unpin":
|
||||
@@ -131,6 +134,7 @@ func usage() {
|
||||
settings set <module> <file> what a module's config should say, for the whole mesh
|
||||
settings set <module> <file> --node <n> ...or for one machine
|
||||
settings clear <module> [--node <n>] take a layer away
|
||||
build <repository> [--ref R] build a module from its source and record it
|
||||
pin <node> <provision> <from> which node this one gets a provision from
|
||||
unpin <node> <provision> put that question back
|
||||
plan <node> [--files|--json] what that node would run, and why
|
||||
@@ -1625,3 +1629,66 @@ func pinCommand(ctx context.Context, args []string, setting bool) error {
|
||||
fmt.Printf(" run `push %s` to send it\n", args[0])
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildCommand builds a module from its source and records what came out.
|
||||
//
|
||||
// **Run where there is a container runtime**, which is why it is a command rather than something
|
||||
// the control plane does on its own: building needs to run things on a machine, and what the
|
||||
// control plane may send a machine is bounded by the declaration language. This is the shape the
|
||||
// builder module will take when it is given work over the broker; today a person runs it, and the
|
||||
// mesh records the result the same way either way.
|
||||
func buildCommand(ctx context.Context, args []string) error {
|
||||
set := flag.NewFlagSet("build", flag.ContinueOnError)
|
||||
ref := set.String("ref", "", "the branch, tag or commit to build")
|
||||
registry := set.String("registry", os.Getenv("MESH_REGISTRY"),
|
||||
"host:port of the registry to publish to")
|
||||
workspace := set.String("workspace", os.TempDir(), "where to clone and build")
|
||||
dryRun := set.Bool("dry-run", false, "build and print the manifest, recording nothing")
|
||||
positionals, err := parseAround(set, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(positionals) != 1 {
|
||||
return errors.New("build <repository> [--ref R] [--registry host:port]")
|
||||
}
|
||||
if strings.TrimSpace(*registry) == "" {
|
||||
return errors.New(
|
||||
"no --registry and no MESH_REGISTRY: a built artifact nobody can fetch is not built")
|
||||
}
|
||||
|
||||
publisher := builder.Registry{Address: *registry, Run: builder.Command}
|
||||
result, err := builder.Build(ctx, builder.Command, publisher, positionals[0], *ref, *workspace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, made := range result.Built {
|
||||
fmt.Printf(" %-12s %s %s\n", made.Name, made.Kind, made.Reference)
|
||||
}
|
||||
if *dryRun {
|
||||
body, err := json.MarshalIndent(result.Manifest, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(body))
|
||||
return nil
|
||||
}
|
||||
|
||||
inv, err := openInventory(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer inv.Close()
|
||||
|
||||
// Recorded with where it came from, so "is this current?" is answerable without building it
|
||||
// again (novox/hq ADR 0009).
|
||||
if err := inv.RegisterModule(ctx, result.Manifest, inventory.Source{
|
||||
Repository: positionals[0], Ref: *ref, BuiltFrom: result.Commit, Head: result.Commit,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("\n%s %s, built from %s\n",
|
||||
result.Manifest.Module, result.Manifest.Version, short(result.Commit))
|
||||
fmt.Printf(" run `assign <node> %s` to put it somewhere\n", result.Manifest.Module)
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user