Rename mesh-control -> mesh-controller, substrate -> foundation
One name per thing, per the HQ glossary: the module/container/image/binary/repo becomes mesh-controller, the seat the-controller, and the store+broker pair the foundation (embedded base bundles, default template and example lock renamed with their go:embed directives). No behaviour change — a pure vocabulary rename. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
)
|
||||
|
||||
// following acts on what the catalogue announces.
|
||||
//
|
||||
// **It holds the stores, not a copy of the decision.** What to do about an upgrade is read when
|
||||
// one arrives, so changing it takes effect on the next upgrade rather than on the next restart of
|
||||
// the control plane.
|
||||
type following struct{ open *stores }
|
||||
|
||||
// Upgraded sends the machines running a module the version the catalogue now considers current —
|
||||
// or records that they are behind, which is the default and needs no record.
|
||||
//
|
||||
// **Recording is not a second code path.** A machine that is not running what the mesh would send
|
||||
// it is already something the mesh notices and reports; that is what `status` and `push --behind`
|
||||
// are built on. So "record it" is the absence of an action, and the only thing this has to decide
|
||||
// is whether to act.
|
||||
func (f following) Upgraded(ctx context.Context, u link.Upgraded) error {
|
||||
inv := f.open.inventory
|
||||
|
||||
decision, err := inv.UpgradeOf(ctx, u.Module)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
on, err := inv.Running(ctx, u.Module)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(on) == 0 {
|
||||
fmt.Printf("%s moved to %s; no machine runs it\n", u.Module, shortCommit(u.Commit))
|
||||
return nil
|
||||
}
|
||||
if !decision.RollOut {
|
||||
// Named rather than counted, and said even though nothing happens: an upgrade that was
|
||||
// deliberately not rolled out and an upgrade that was never noticed look identical in a
|
||||
// log that only speaks when it acts.
|
||||
fmt.Printf("%s moved to %s; %s %s behind it, and this mesh records upgrades rather than "+
|
||||
"rolling them out — `push --behind` when you want them\n",
|
||||
u.Module, shortCommit(u.Commit), readableList(on), isAre(len(on)))
|
||||
return nil
|
||||
}
|
||||
|
||||
if decision.Together {
|
||||
fmt.Printf("%s moved to %s; sending %s together\n",
|
||||
u.Module, shortCommit(u.Commit), readableList(on))
|
||||
return sendTo(ctx, f.open, on)
|
||||
}
|
||||
// One at a time, and stopping at the first that fails.
|
||||
//
|
||||
// **Stopping is the point.** The machines are done one after another precisely so that a
|
||||
// version that breaks the first one does not reach the rest; carrying on past a failure would
|
||||
// make this the same as sending them together, only slower.
|
||||
fmt.Printf("%s moved to %s; sending %s one at a time\n",
|
||||
u.Module, shortCommit(u.Commit), readableList(on))
|
||||
for _, node := range on {
|
||||
if err := sendTo(ctx, f.open, []string{node}); err != nil {
|
||||
return fmt.Errorf("%s did not take %s, so the machines after it were left alone: %w",
|
||||
node, u.Module, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// readableList names machines the way a sentence does, because this is read by a person deciding
|
||||
// whether an upgrade went where they expected.
|
||||
func readableList(names []string) string {
|
||||
switch len(names) {
|
||||
case 0:
|
||||
return "nothing"
|
||||
case 1:
|
||||
return names[0]
|
||||
case 2:
|
||||
return names[0] + " and " + names[1]
|
||||
}
|
||||
return strings.Join(names[:len(names)-1], ", ") + " and " + names[len(names)-1]
|
||||
}
|
||||
|
||||
func shortCommit(commit string) string {
|
||||
if len(commit) > 8 {
|
||||
return commit[:8]
|
||||
}
|
||||
return commit
|
||||
}
|
||||
|
||||
func isAre(n int) string {
|
||||
if n == 1 {
|
||||
return "is"
|
||||
}
|
||||
return "are"
|
||||
}
|
||||
|
||||
// upgradeCommand says what should happen when a module's current version moves.
|
||||
func upgradeCommand(ctx context.Context, args []string) error {
|
||||
set := flag.NewFlagSet("upgrade", flag.ContinueOnError)
|
||||
together := set.Bool("together", false,
|
||||
"send every machine running it at once, instead of one after another")
|
||||
positionals, err := parseAround(set, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(positionals) == 0 {
|
||||
return errors.New("upgrade <module> [roll-out|record] [--together]")
|
||||
}
|
||||
module := positionals[0]
|
||||
|
||||
open, err := openStores(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer open.Close()
|
||||
inv := open.inventory
|
||||
|
||||
if len(positionals) == 1 {
|
||||
decision, err := inv.UpgradeOf(ctx, module)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(sayUpgrade(module, decision))
|
||||
return nil
|
||||
}
|
||||
|
||||
var decision inventory.Upgrade
|
||||
switch positionals[1] {
|
||||
case "roll-out":
|
||||
decision = inventory.Upgrade{RollOut: true, Together: *together}
|
||||
case "record":
|
||||
if *together {
|
||||
// Refused rather than ignored: --together only means anything for a roll-out, and
|
||||
// accepting it here would store a preference that never applies and looks like it does.
|
||||
return errors.New("`--together` says how to roll out, so it cannot be given with " +
|
||||
"`record`, which is the choice not to")
|
||||
}
|
||||
decision = inventory.Upgrade{}
|
||||
default:
|
||||
return fmt.Errorf("upgrade <module> roll-out|record — not %q", positionals[1])
|
||||
}
|
||||
if err := inv.SetUpgradeOf(ctx, module, decision); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(sayUpgrade(module, decision))
|
||||
return nil
|
||||
}
|
||||
|
||||
func sayUpgrade(module string, u inventory.Upgrade) string {
|
||||
if !u.RollOut {
|
||||
return fmt.Sprintf("when %s moves, the mesh records it and the machines running it are "+
|
||||
"reported as behind", module)
|
||||
}
|
||||
if u.Together {
|
||||
return fmt.Sprintf("when %s moves, every machine running it is sent the new version "+
|
||||
"together", module)
|
||||
}
|
||||
return fmt.Sprintf("when %s moves, the machines running it are sent the new version one at "+
|
||||
"a time, stopping at the first that fails", module)
|
||||
}
|
||||
|
||||
// Announceable is every build this mesh recorded, in the shape the builder announces one.
|
||||
//
|
||||
// **The catalogue asks for this when it starts, and the answer is the graph's foundation**
|
||||
// (novox/hq 04-ISSUES/050). A durable queue keeps what arrived after it existed, so a running
|
||||
// catalogue misses nothing — but the modules built before it first ran were announced to a queue
|
||||
// that did not exist, and on a fresh mesh those are always the same three: the shared base, the
|
||||
// store the catalogue runs on, and the catalogue itself.
|
||||
func (f following) Announceable(ctx context.Context) ([]link.Announcement, error) {
|
||||
builds, err := f.open.inventory.Announceable(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]link.Announcement, 0, len(builds))
|
||||
for _, b := range builds {
|
||||
a := link.Announcement{
|
||||
Module: b.Module, Commit: b.Commit, Repository: b.Repository,
|
||||
Path: b.Path, Ref: b.Ref, Against: b.Against,
|
||||
}
|
||||
if len(b.Manifest) > 0 {
|
||||
a.Manifest = b.Manifest
|
||||
}
|
||||
for _, made := range b.Made {
|
||||
a.Made = append(a.Made, link.MadeArtifact{
|
||||
Name: made.Name, Kind: made.Kind, Reference: made.Reference,
|
||||
})
|
||||
}
|
||||
out = append(out, a)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
Reference in New Issue
Block a user