A take is a comparison: the preview, its refusals, the strays, and the policy said at build (hq ADR 0163)
The host now reports, for every held thing, the facts a take compares; the controller keeps them, and take puts them beside what the module declares — the found image and its age against the declared one, the found networks and who else is on them, ports and mounts, a found file's difference from the declared content — and refuses a downgrade without --downgrade and a differing file without --replace <path>. Without --yes the comparison is printed and nothing is taken. node show lists the facts and the strays the machine reports. build and the daemon's take-in say when a module's policy rolls the result out at once. The own-secret refusal points at the provider form for a required secret.
This commit is contained in:
+194
-16
@@ -62,6 +62,15 @@ func showMode(ctx context.Context, inv *inventory.Inventory, node inventory.Node
|
||||
if h.Kept != "" {
|
||||
fmt.Printf(" %-17s original kept at %s\n", "", h.Kept)
|
||||
}
|
||||
for _, f := range comparisonLines(h) {
|
||||
fmt.Printf(" %-17s %s\n", "", f)
|
||||
}
|
||||
}
|
||||
if len(said.Strays) > 0 {
|
||||
fmt.Printf(" strays %d container(s) the mesh neither wrote nor holds:\n", len(said.Strays))
|
||||
for _, s := range said.Strays {
|
||||
fmt.Printf(" %-17s %s (%s)\n", "", s.Name, s.Detail)
|
||||
}
|
||||
}
|
||||
fmt.Printf(" as of %s\n", said.At.Local().Format(time.DateTime))
|
||||
return nil
|
||||
@@ -136,7 +145,14 @@ const DefaultFilter = "nftables"
|
||||
// take is a module's cutover on an adopted node: the operator's act, done when that module's data
|
||||
// has moved. From the next push its resources converge there like any other, replacing what the
|
||||
// node found and holds for it.
|
||||
func take(ctx context.Context, open *stores, node, module string) (string, error) {
|
||||
// takeOptions is what a take was told about the differences it may pass (novox/hq ADR 0163).
|
||||
type takeOptions struct {
|
||||
Yes bool
|
||||
Downgrade bool
|
||||
Replace map[string]bool
|
||||
}
|
||||
|
||||
func take(ctx context.Context, open *stores, node, module string, opts takeOptions) (string, error) {
|
||||
inv := open.inventory
|
||||
assigned, err := inv.Assigned(ctx, node)
|
||||
if err != nil {
|
||||
@@ -150,27 +166,170 @@ func take(ctx context.Context, open *stores, node, module string) (string, error
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := inv.Take(ctx, node, module); err != nil {
|
||||
return "", err
|
||||
}
|
||||
said := fmt.Sprintf("%s is taken on %s", module, node)
|
||||
// The comparison first (novox/hq ADR 0163): every held thing the module would replace, beside
|
||||
// what the module declares, and the differences that refuse unless named.
|
||||
reported, err := inv.AdoptionOf(ctx, node)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var replaces []string
|
||||
for _, h := range reported.Held {
|
||||
if h.Module == module {
|
||||
replaces = append(replaces, " "+heldLine(h))
|
||||
}
|
||||
preview, refusals := comparisonOf(reported.Held, module, opts)
|
||||
if len(refusals) > 0 {
|
||||
return "", fmt.Errorf("taking %s on %s is refused:\n %s\n%s", module, node,
|
||||
strings.Join(refusals, "\n "), preview)
|
||||
}
|
||||
if len(replaces) > 0 {
|
||||
said += "; the next push replaces what the node found and holds for it:\n" +
|
||||
strings.Join(replaces, "\n")
|
||||
if !opts.Yes {
|
||||
return preview + fmt.Sprintf("\nnothing taken; `take %s %s --yes` cuts it over as previewed", node, module), nil
|
||||
}
|
||||
if err := inv.Take(ctx, node, module); err != nil {
|
||||
return "", err
|
||||
}
|
||||
said := fmt.Sprintf("%s is taken on %s", module, node)
|
||||
if preview != "" {
|
||||
said += "; the next push replaces what the node found and holds for it:\n" + preview
|
||||
}
|
||||
return said + fmt.Sprintf("\n run `push %s` to cut it over", node), nil
|
||||
}
|
||||
|
||||
// comparisonOf is a take's preview: for every held thing of the module, what runs beside what the
|
||||
// module declares, and the refusals the differences earn unless the take named them
|
||||
// (novox/hq ADR 0163): an image older than the one running, a declared file that differs from the
|
||||
// found one. A narrowed port and a shared network are said and not refused.
|
||||
func comparisonOf(held []inventory.Held, module string, opts takeOptions) (string, []string) {
|
||||
var b strings.Builder
|
||||
var refusals []string
|
||||
for _, h := range held {
|
||||
if h.Module != module {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(&b, " %s", heldLine(h))
|
||||
if h.Kept != "" {
|
||||
fmt.Fprintf(&b, ", original kept at %s", h.Kept)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
for _, line := range comparisonLines(h) {
|
||||
fmt.Fprintf(&b, " %s\n", line)
|
||||
}
|
||||
f := factsOf(h)
|
||||
if f.downgrade && !opts.Downgrade {
|
||||
refusals = append(refusals, fmt.Sprintf("%s: the module's image (%s, made %s) is older than the one running (%s, made %s) — "+
|
||||
"a service that migrated its data forward may not start on it; `--downgrade` to take it anyway",
|
||||
h.Target, f.declaredImage, day(f.declaredCreated), f.image, day(f.imageCreated)))
|
||||
}
|
||||
if f.differs && !opts.Replace[h.Target] && !opts.Replace["*"] {
|
||||
refusals = append(refusals, fmt.Sprintf("%s: the module's content differs from the file found; the lines above "+
|
||||
"marked - are lost by taking it; `--replace %s` to replace it anyway, or declare the file partially",
|
||||
h.Target, h.Target))
|
||||
}
|
||||
}
|
||||
return b.String(), refusals
|
||||
}
|
||||
|
||||
// facts is a held thing's facts as the preview reads them.
|
||||
type facts struct {
|
||||
image, imageCreated, declaredImage, declaredCreated string
|
||||
downgrade, differs bool
|
||||
networks map[string][]string
|
||||
mounts, ports, declaredPorts, declaredVolumes []string
|
||||
difference []string
|
||||
}
|
||||
|
||||
func factsOf(h inventory.Held) facts {
|
||||
var f facts
|
||||
if h.Facts == nil {
|
||||
return f
|
||||
}
|
||||
str := func(k string) string { s, _ := h.Facts[k].(string); return s }
|
||||
list := func(k string) []string {
|
||||
var out []string
|
||||
if raw, ok := h.Facts[k].([]any); ok {
|
||||
for _, x := range raw {
|
||||
if s, ok := x.(string); ok {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
f.image, f.imageCreated = str("image"), str("image_created")
|
||||
f.declaredImage, f.declaredCreated = str("declared_image"), str("declared_image_created")
|
||||
f.downgrade, _ = h.Facts["downgrade"].(bool)
|
||||
f.differs, _ = h.Facts["differs"].(bool)
|
||||
f.mounts, f.ports = list("mounts"), list("ports")
|
||||
f.declaredPorts, f.declaredVolumes, f.difference = list("declared_ports"), list("declared_volumes"), list("difference")
|
||||
if raw, ok := h.Facts["networks"].(map[string]any); ok {
|
||||
f.networks = map[string][]string{}
|
||||
for name, members := range raw {
|
||||
var out []string
|
||||
if ms, ok := members.([]any); ok {
|
||||
for _, m := range ms {
|
||||
if s, ok := m.(string); ok {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
f.networks[name] = out
|
||||
}
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// comparisonLines says a held thing's facts the way a person weighs them.
|
||||
func comparisonLines(h inventory.Held) []string {
|
||||
f := factsOf(h)
|
||||
var out []string
|
||||
if f.image != "" || f.declaredImage != "" {
|
||||
line := fmt.Sprintf("runs %s", orNone(f.image))
|
||||
if f.imageCreated != "" {
|
||||
line += " (made " + day(f.imageCreated) + ")"
|
||||
}
|
||||
line += "; the module declares " + orNone(f.declaredImage)
|
||||
switch {
|
||||
case f.declaredCreated != "":
|
||||
line += " (made " + day(f.declaredCreated) + ")"
|
||||
case f.declaredImage != "":
|
||||
line += " (not on the machine yet, so its age is unknown)"
|
||||
}
|
||||
if f.downgrade {
|
||||
line += " — DOWNGRADE"
|
||||
}
|
||||
out = append(out, line)
|
||||
}
|
||||
names := make([]string, 0, len(f.networks))
|
||||
for n := range f.networks {
|
||||
names = append(names, n)
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, n := range names {
|
||||
if members := f.networks[n]; len(members) > 0 {
|
||||
out = append(out, fmt.Sprintf("on the network %s with %s, which may reach it by name and will not once it moves to the module's own network",
|
||||
n, strings.Join(members, ", ")))
|
||||
}
|
||||
}
|
||||
if len(f.ports) > 0 || len(f.declaredPorts) > 0 {
|
||||
out = append(out, fmt.Sprintf("publishes %s; the module declares %s",
|
||||
orNone(strings.Join(f.ports, " ")), orNone(strings.Join(f.declaredPorts, " "))))
|
||||
}
|
||||
if len(f.mounts) > 0 || len(f.declaredVolumes) > 0 {
|
||||
out = append(out, fmt.Sprintf("mounts %s; the module declares %s",
|
||||
orNone(strings.Join(f.mounts, " ")), orNone(strings.Join(f.declaredVolumes, " "))))
|
||||
}
|
||||
if f.differs {
|
||||
out = append(out, "the declared content differs from the file found (- lost, + new):")
|
||||
for _, d := range f.difference {
|
||||
out = append(out, " "+d)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// day is a timestamp as a person reads it in a preview: its date.
|
||||
func day(stamp string) string {
|
||||
if len(stamp) >= 10 {
|
||||
return stamp[:10]
|
||||
}
|
||||
return stamp
|
||||
}
|
||||
|
||||
// reportFreshFor is how old a node's account of itself may be for the flip to act on it. A
|
||||
// variable so a test can age a report without waiting.
|
||||
var reportFreshFor = 15 * time.Minute
|
||||
@@ -596,12 +755,31 @@ func adopt(ctx context.Context, open *stores, node string) (string, error) {
|
||||
|
||||
// takeCommand, convergeCommand and adoptCommand are the command line's adapters to the acts above.
|
||||
func takeCommand(ctx context.Context, args []string) error {
|
||||
if len(args) != 2 {
|
||||
return errors.New("take <node> <module>")
|
||||
set := flag.NewFlagSet("take", flag.ContinueOnError)
|
||||
yes := set.Bool("yes", false, "cut over as previewed; without it the comparison is printed and nothing is taken")
|
||||
downgrade := set.Bool("downgrade", false, "take it although the module's image is older than the one running")
|
||||
var replace stringList
|
||||
set.Var(&replace, "replace", "a found file's path whose content the module may replace although it differs (repeatable; * for every one)")
|
||||
positionals, err := parseAround(set, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return runAct(ctx, func(open *stores) (string, error) { return take(ctx, open, args[0], args[1]) })
|
||||
if len(positionals) != 2 {
|
||||
return errors.New("take <node> <module> [--yes] [--downgrade] [--replace <path>]...")
|
||||
}
|
||||
opts := takeOptions{Yes: *yes, Downgrade: *downgrade, Replace: map[string]bool{}}
|
||||
for _, r := range replace {
|
||||
opts.Replace[r] = true
|
||||
}
|
||||
return runAct(ctx, func(open *stores) (string, error) { return take(ctx, open, positionals[0], positionals[1], opts) })
|
||||
}
|
||||
|
||||
// stringList is a repeatable flag.
|
||||
type stringList []string
|
||||
|
||||
func (l *stringList) String() string { return strings.Join(*l, ",") }
|
||||
func (l *stringList) Set(v string) error { *l = append(*l, v); return nil }
|
||||
|
||||
func convergeCommand(ctx context.Context, args []string) error {
|
||||
set := flag.NewFlagSet("converge", flag.ContinueOnError)
|
||||
yes := set.String("yes", "", "do it, naming the digest the preview printed; without it, only "+
|
||||
|
||||
Reference in New Issue
Block a user