package main import ( "context" "errors" "flag" "fmt" "strings" "github.com/novox/mesh-control/internal/inventory" "github.com/novox/mesh-control/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 [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 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) }