The mesh knows where a module came from, and whether it is behind
Delivery is a comparison, not a pipeline: the control plane holds what source
exists and what has been built from it, and the difference is the work. Both
halves are written down now, so "is this current" is a question about two
columns rather than something you find out by building.
`status` answers "did my change go out?", which ADR 0010 names as the real risk
of replacing a pipeline with a comparison -- it is answerable today by opening
a pipeline, and something had to replace that.
zsh holds 4f2a9c1e, source has 9e3b7d2a
running on laptop
The machines are the point. A module being out of date is a fact about the
catalogue; which machines are running last week's version is the thing with
consequences.
Three things this had to get right.
A module with no source is never behind -- it was handed over directly, which
is how a one-off arrives, and saying "out of date" about it would be inventing
a comparison against nothing.
A source nobody has checked is not behind either. Reporting it as behind would
put every module on the list the moment provenance was recorded, which makes
the list say nothing. Fault injection found this: my first test passed with the
guard removed, because both halves were empty strings and compared equal. The
case that actually needed it -- a known commit and an unknown head -- was
untested.
And handing over a manifest by hand does not erase where the module normally
comes from. Fixing something in a hurry is legitimate; silently forgetting its
origin is not, because that record is the only thing that would say afterwards
that a machine is running something nobody can rebuild.
Also fixed the flag parsing, which stopped at the first positional argument and
silently ignored every flag after it -- so `module add thing.json --source x`
recorded no source at all and said it had succeeded. The host's own parser
documents this exact footgun and I wrote it again anyway.
This commit is contained in:
+128
-5
@@ -87,6 +87,8 @@ func run() error {
|
||||
return planCommand(ctx, args[1:])
|
||||
case "push":
|
||||
return pushCommand(ctx, args[1:])
|
||||
case "status":
|
||||
return statusCommand(ctx)
|
||||
case "version":
|
||||
fmt.Println(version)
|
||||
return nil
|
||||
@@ -115,7 +117,9 @@ func usage() {
|
||||
overlay show the private network, as the mesh computes it
|
||||
module add <file> register a module from its manifest
|
||||
module list what modules this mesh knows about
|
||||
module moved <name> <commit> the source has a newer commit than the mesh built
|
||||
module forget <name> remove one, unless a node is running it
|
||||
status what the mesh is behind on, and which nodes
|
||||
assign <node> <module> put a module on a node
|
||||
unassign <node> <module> take it off
|
||||
plan <node> what that node would run, and why
|
||||
@@ -663,10 +667,18 @@ func moduleCommand(ctx context.Context, args []string) error {
|
||||
|
||||
switch args[0] {
|
||||
case "add":
|
||||
if len(args) != 2 {
|
||||
return errors.New("module add <manifest.json>")
|
||||
set := flag.NewFlagSet("module add", flag.ContinueOnError)
|
||||
repo := set.String("source", "", "where this module comes from")
|
||||
ref := set.String("ref", "", "the branch followed there")
|
||||
commit := set.String("commit", "", "the commit this manifest was read at")
|
||||
positionals, err := parseAround(set, args[1:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
raw, err := os.ReadFile(args[1])
|
||||
if len(positionals) != 1 {
|
||||
return errors.New("module add <manifest.json> [--source <repo> --ref <branch> --commit <sha>]")
|
||||
}
|
||||
raw, err := os.ReadFile(positionals[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -674,10 +686,23 @@ func moduleCommand(ctx context.Context, args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := inv.RegisterModule(ctx, m); err != nil {
|
||||
// Provenance together or not at all. A source with no commit cannot be compared against
|
||||
// anything, so it would record where the module came from and still never be able to say
|
||||
// the mesh is behind it — which is the one thing recording it is for.
|
||||
if (*repo == "") != (*commit == "") {
|
||||
return errors.New("--source and --commit go together: a source with no commit " +
|
||||
"cannot be compared against anything, and a commit with no source has nothing " +
|
||||
"to be compared with")
|
||||
}
|
||||
if err := inv.RegisterModule(ctx, m, inventory.Source{
|
||||
Repository: *repo, Ref: *ref, BuiltFrom: *commit,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("%s registered", m.Module)
|
||||
if *commit != "" {
|
||||
fmt.Printf(" from %s", short(*commit))
|
||||
}
|
||||
if len(m.Provides) > 0 {
|
||||
fmt.Printf(", providing %s", strings.Join(m.Provides, ", "))
|
||||
}
|
||||
@@ -714,6 +739,26 @@ func moduleCommand(ctx context.Context, args []string) error {
|
||||
}
|
||||
return nil
|
||||
|
||||
case "moved":
|
||||
if len(args) != 3 {
|
||||
return errors.New("module moved <name> <commit> — the source has a newer commit")
|
||||
}
|
||||
if err := inv.SourceMoved(ctx, args[1], args[2]); err != nil {
|
||||
return err
|
||||
}
|
||||
from, err := inv.SourceOf(ctx, args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if from.Current() {
|
||||
fmt.Printf("%s is current at %s\n", args[1], short(from.Head))
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("%s is behind: the mesh holds %s and the source has %s\n",
|
||||
args[1], short(from.BuiltFrom), short(from.Head))
|
||||
fmt.Println(" build it and `module add` the result to catch up")
|
||||
return nil
|
||||
|
||||
case "forget":
|
||||
if len(args) != 2 {
|
||||
return errors.New("module forget <name>")
|
||||
@@ -725,7 +770,7 @@ func moduleCommand(ctx context.Context, args []string) error {
|
||||
return nil
|
||||
|
||||
default:
|
||||
return fmt.Errorf("module has no %q; it has add, list and forget", args[0])
|
||||
return fmt.Errorf("module has no %q; it has add, list, moved and forget", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -934,3 +979,81 @@ func pushCommand(ctx context.Context, args []string) error {
|
||||
fmt.Printf("\n%d node(s) told\n", len(sending))
|
||||
return nil
|
||||
}
|
||||
|
||||
// short is a commit as a person refers to it.
|
||||
func short(commit string) string {
|
||||
if len(commit) > 8 {
|
||||
return commit[:8]
|
||||
}
|
||||
return commit
|
||||
}
|
||||
|
||||
// statusCommand answers "did my change go out?".
|
||||
//
|
||||
// novox/hq ADR 0010 names losing that question as the real risk of replacing a pipeline with a
|
||||
// comparison: it is answerable today by opening a pipeline, and something has to replace that or
|
||||
// this is worse to live with whatever its other properties.
|
||||
//
|
||||
// The answer is not "a job succeeded". It is which modules the mesh has not built from what their
|
||||
// source now has, and which machines are running the old one.
|
||||
func statusCommand(ctx context.Context) error {
|
||||
inv, err := openInventory(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer inv.Close()
|
||||
|
||||
behind, err := inv.Behind(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(behind) == 0 {
|
||||
fmt.Println("every module with a source is built from what that source has")
|
||||
return nil
|
||||
}
|
||||
|
||||
var names []string
|
||||
for m := range behind {
|
||||
names = append(names, m)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
fmt.Printf("%d module(s) behind their source:\n\n", len(behind))
|
||||
for _, m := range names {
|
||||
from, err := inv.SourceOf(ctx, m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf(" %-20s holds %s, source has %s\n", m, short(from.BuiltFrom), short(from.Head))
|
||||
if nodes := behind[m]; len(nodes) > 0 {
|
||||
// The part somebody actually wants. A module being out of date is a fact about the
|
||||
// catalogue; machines running the old one is the thing with consequences.
|
||||
fmt.Printf(" %-20s running on %s\n", "", strings.Join(nodes, ", "))
|
||||
} else {
|
||||
fmt.Printf(" %-20s assigned to nothing\n", "")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseAround reads flags that may sit before, after or between positional arguments.
|
||||
//
|
||||
// The standard library stops at the first non-flag argument, so `module add thing.json --source x`
|
||||
// parses no flags at all and silently ignores every one of them. The host learned this the same
|
||||
// way and says so in its own parser: a flag that is quietly dropped is the fault this project
|
||||
// keeps naming, and it looks exactly like success.
|
||||
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:]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user