Put back by hand, it was found afresh and its copy became the hold's original, so a machine that kept restoring it kept growing copies and lost which one was first. The first original now stays the record's, content that differs is kept once beside it, and the note says a rollback means unassigning the private network. Nothing is removed unless it is <wireguard dir>/<iface>.conf, not a path the mesh writes, and not a link, which would leave the key-bearing target behind.
580 lines
25 KiB
Go
580 lines
25 KiB
Go
package apply
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-host/internal/declaration"
|
|
"github.com/novox/mesh-host/internal/store"
|
|
"github.com/novox/mesh-host/internal/system"
|
|
"github.com/novox/mesh-host/internal/tunnel"
|
|
)
|
|
|
|
// The private network takes over the tunnel it found (novox/hq ADR 0105).
|
|
//
|
|
// The controller says so on the interface's service: `takes-over` names the found interface, the
|
|
// unit that raised it and its configuration file. Before the mesh's unit is started, the host keeps
|
|
// that file like any held file — the original recorded before anything else happens to it — and
|
|
// stops and disables the found unit. Never a flush: `wg set … peer … remove` is never run, the
|
|
// file is never written, and the found interface goes down the way its own unit takes it down.
|
|
// Then the mesh's interface comes up, with the found key the node took at enrolment, on the found
|
|
// port, with the found peers in its list — and a peer of the tunnel cannot tell it changed hands.
|
|
//
|
|
// Every apply, not once: a found unit somebody starts again would take the port back from the
|
|
// mesh's interface, so it is stopped again and said so. That is the one place an adopted node
|
|
// undoes something done by hand, and it is because the tunnel is the mesh's now.
|
|
//
|
|
// **Until the take is proven, and then the found configuration is retired** (novox/hq ADR 0119).
|
|
// Keeping it on disk was the caution the take needed: if the mesh's interface does not come up,
|
|
// the found unit is started again and the peers never notice. That caution is spent once the
|
|
// tunnel is taken — the found unit down and disabled, the mesh's interface up — and a peer has
|
|
// handshaken with the mesh's interface. From then on a configuration nothing maintains, one
|
|
// command away from raising a second way onto the network, is not a rollback path but a door
|
|
// nobody watches. So it is removed from where its unit reads it; its original, kept before
|
|
// anything happened to it (ADR 0100), stays kept; and the hold on it ends. A take never proven
|
|
// keeps it, and says so — a broken take is visible, not silently retired.
|
|
|
|
// TakenTunnel is what an apply says about a tunnel it took over, for the node's report.
|
|
type TakenTunnel struct {
|
|
Interface string
|
|
Port int
|
|
Range string
|
|
Peers int
|
|
// State is "not-taken" (the found interface still up, the mesh's not), "taken" (the found one
|
|
// down and disabled, the mesh's up with its key) or "down" (the found one down and the mesh's
|
|
// not up: the peers reach nothing). Note is what this apply did about it — and, for a taken
|
|
// tunnel, whether the take is proven and its found configuration retired (novox/hq ADR 0119).
|
|
// Kept is where the found configuration's original is, retired or not.
|
|
State string
|
|
Note string
|
|
Kept string
|
|
}
|
|
|
|
// The states, as the link says them.
|
|
const (
|
|
NotTaken = "not-taken"
|
|
Taken = "taken"
|
|
TunnelDown = "down"
|
|
)
|
|
|
|
// takeoverRecheck is how often, and takeoverRechecks how many times, a found interface still up
|
|
// after its unit stopped is looked at again before the takeover is refused: `wg-quick down` by a
|
|
// person takes a moment. Variables so a test need not wait.
|
|
var (
|
|
takeoverRecheck = 2 * time.Second
|
|
takeoverRechecks = 3
|
|
)
|
|
|
|
// takeOverID is the held record's id for the found configuration: the service's own with a suffix,
|
|
// so it is declared for as long as the service is and never mistaken for the service itself.
|
|
func takeOverID(svc *declaration.Service) string { return svc.ID + ".takes-over" }
|
|
|
|
// takeOver keeps the found tunnel's configuration and stops its unit, ahead of the service that
|
|
// replaces it. Returned is the hold's outcome, and what was found for the report.
|
|
//
|
|
// **Nothing is stopped until the mesh's interface is known to be able to replace it** (the record's
|
|
// option 2 is exactly this going wrong): the declared configuration must listen on the found port
|
|
// at the found address, and the key file it points at must hold the found key. Only then is the
|
|
// found unit stopped — and `stopped` says whether this apply did, so a mesh interface that then
|
|
// fails to start can have the found unit started again.
|
|
func takeOver(ctx context.Context, sys system.System, svc *declaration.Service, d *declaration.Declaration,
|
|
known *store.State, run Runner, keep Keep, now time.Time) (out Outcome, facts TakenTunnel, stopped bool, err error) {
|
|
t := svc.TakesOver
|
|
id := takeOverID(svc)
|
|
module, _ := d.Adoption.UntakenModuleOf(svc.ID)
|
|
if module == "" {
|
|
module = "the private network"
|
|
}
|
|
facts = TakenTunnel{Interface: t.Interface, State: NotTaken}
|
|
|
|
// 0. What the found configuration says, before anything: the checks below are against it.
|
|
found, ferr := readFoundTunnel(t.Config)
|
|
|
|
// 1. The configuration, kept like any held file. A synthetic file resource stands for it, so
|
|
// the same code keeps its original, digests it and notices it changing.
|
|
//
|
|
// **Unless it was retired** (novox/hq ADR 0119): the take was proven and the mesh removed
|
|
// it, so there is nothing to hold and nothing missing — only where its original is, which
|
|
// the retirement recorded. A hold still standing is let go: that is what retiring it meant.
|
|
//
|
|
// **One that comes back is held again on its FIRST original** — the one kept before anything
|
|
// happened to it (ADR 0100), never whatever was put back — and retired again by the first
|
|
// apply that finds the take still proven, keeping what came back only if it differs from
|
|
// what is already kept. Put back by hand while the private network is assigned, it is not a
|
|
// rollback: that means unassigning the private network first, and the note says so.
|
|
file := &declaration.File{ID: id, Type: declaration.TypeFile, Path: t.Config}
|
|
var held store.Held
|
|
retired, wasRetired := known.RetiredAt(t.Config)
|
|
cameBack := wasRetired && present(t.Config)
|
|
switch {
|
|
case wasRetired && !cameBack:
|
|
known.Release(id)
|
|
held = store.Held{Kept: retired.Kept}
|
|
out = begin(file)
|
|
out.Action = "unchanged"
|
|
facts.Note = "the found configuration " + t.Config + " was retired once the take was proven; " +
|
|
"its original is kept at " + retired.Kept + " and the mesh never brings it back"
|
|
default:
|
|
was, already := known.HeldAt(id)
|
|
why := "the configuration of the tunnel " + t.Interface + ", taken over by " + svc.Unit
|
|
if cameBack && !already {
|
|
digest := retired.Digest
|
|
if digest == "" {
|
|
if raw, err := os.ReadFile(retired.Kept); err == nil {
|
|
digest = digestOf(string(raw))
|
|
}
|
|
}
|
|
was = store.Held{ID: id, Module: module, Kind: string(declaration.TypeFile), Target: t.Config,
|
|
Since: now, Why: why, Kept: retired.Kept, Digest: digest}
|
|
already = true
|
|
}
|
|
out, held, err = hold(ctx, sys, file, module, was, already, why, run, keep, now)
|
|
if err != nil {
|
|
return begin(file), facts, false, fmt.Errorf("keeping the found tunnel's configuration: %w", err)
|
|
}
|
|
known.RecordHeld(held)
|
|
if cameBack {
|
|
facts.Note = "the found configuration " + t.Config + " came back after it was retired; while the " +
|
|
"private network is assigned the mesh retires it again, so rolling back to the found tunnel " +
|
|
"means unassigning the private network first"
|
|
}
|
|
}
|
|
facts.Kept = held.Kept
|
|
// What the file says, for the report: from the machine, or from the kept original when the
|
|
// machine's copy is gone. The private key stays in the file; nothing here keeps it.
|
|
unread := ""
|
|
if ferr != nil && held.Kept != "" {
|
|
found, ferr = readFoundTunnel(held.Kept)
|
|
}
|
|
if ferr == nil {
|
|
facts.Port, facts.Range, facts.Peers = found.Port, found.Range, len(found.Peers)
|
|
} else {
|
|
unread = ferr.Error()
|
|
}
|
|
|
|
// 2. Where things stand: the found unit, and the mesh's.
|
|
foundState, unitErr := sys.ServiceState(ctx, run, t.Unit)
|
|
meshState, _ := sys.ServiceState(ctx, run, svc.Unit)
|
|
if foundState == "running" && meshState == "running" {
|
|
// Both up. On the hub this cannot last — the found unit cannot bind the port the mesh's
|
|
// holds — and on a spoke two interfaces with one key flap between them. Not stopped again
|
|
// by the mesh: what is found on an adopted node is reported, and the first takeover was
|
|
// the one act (the PR note says why). Said, so a person sees it.
|
|
facts.Note = t.Unit + " is running again beside the mesh's interface; not stopped by the mesh — " +
|
|
"`systemctl stop " + t.Unit + "` on the machine"
|
|
}
|
|
|
|
// 3. Before the found unit is stopped: can the mesh's interface replace it? Its declared
|
|
// configuration must listen on the found port at the found address, and the key file it
|
|
// points at must hold the found key, or the peers would be dropped the moment it came up.
|
|
if foundState == "running" && meshState != "running" {
|
|
if ferr != nil {
|
|
return out, facts, false, fmt.Errorf("the found tunnel's configuration at %s cannot be read as a "+
|
|
"tunnel's (%v), so nothing says what the mesh's interface must match; %s is left running",
|
|
t.Config, ferr, t.Unit)
|
|
}
|
|
if err := replaces(d, svc, found); err != nil {
|
|
return out, facts, false, fmt.Errorf("%w; %s is left running", err, t.Unit)
|
|
}
|
|
}
|
|
|
|
// 4. The found unit: stopped if it runs and the mesh's does not, disabled if it starts at
|
|
// boot. A unit that is not there is not an error — the interface may have been raised
|
|
// another way, which the check below catches — and neither is one already down.
|
|
var did []string
|
|
switch {
|
|
case unitErr != nil:
|
|
did = append(did, t.Unit+" is not a unit here")
|
|
case foundState == "running" && meshState != "running":
|
|
if err := sys.SetServiceState(ctx, run, t.Unit, "stopped"); err != nil {
|
|
return out, facts, false, fmt.Errorf("stopping the found %s: %w", t.Unit, err)
|
|
}
|
|
after, err := sys.ServiceState(ctx, run, t.Unit)
|
|
if err != nil {
|
|
return out, facts, true, err
|
|
}
|
|
if after != "stopped" {
|
|
return out, facts, true, fmt.Errorf("%s was asked to stop and is %s", t.Unit, after)
|
|
}
|
|
stopped = true
|
|
did = append(did, "stopped "+t.Unit)
|
|
}
|
|
if unitErr == nil {
|
|
if boot, err := sys.ServiceBoot(ctx, run, t.Unit); err == nil && boot == "enabled" {
|
|
if err := sys.SetServiceBoot(ctx, run, t.Unit, "disabled"); err != nil {
|
|
return out, facts, stopped, fmt.Errorf("disabling the found %s at boot: %w", t.Unit, err)
|
|
}
|
|
did = append(did, "disabled it at boot")
|
|
}
|
|
}
|
|
|
|
// 5. The interface is gone. If it is still up, something other than its unit raised it —
|
|
// the predecessor brings its up by hand — and the mesh's interface cannot take its port
|
|
// and address while it is. Looked at again for a moment, since a person taking it down
|
|
// takes a moment; then refused, naming what to do.
|
|
if meshState != "running" {
|
|
for try := 0; ; try++ {
|
|
if !interfaceUp(ctx, run, t.Interface) {
|
|
break
|
|
}
|
|
if try >= takeoverRechecks {
|
|
return out, facts, stopped, fmt.Errorf("%s is still up although its unit %s is not running: it was "+
|
|
"raised by hand, not by its unit, and the mesh's interface cannot take its port and "+
|
|
"address while it is. On the machine: `wg-quick down %s` — the next reconcile takes it "+
|
|
"over. Nothing was flushed", t.Interface, t.Unit, t.Interface)
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return out, facts, stopped, ctx.Err()
|
|
case <-time.After(takeoverRecheck):
|
|
}
|
|
}
|
|
}
|
|
|
|
out.Detail = "the tunnel " + t.Interface + "'s configuration, kept as found"
|
|
switch {
|
|
case cameBack:
|
|
out.Detail = "the tunnel " + t.Interface + "'s configuration, back after it was retired; held until " +
|
|
"it is retired again"
|
|
case wasRetired:
|
|
out.Detail = "the tunnel " + t.Interface + "'s configuration, retired once the take was proven"
|
|
}
|
|
if held.Kept != "" {
|
|
out.Detail += " (original at " + held.Kept + ")"
|
|
}
|
|
if len(did) > 0 {
|
|
out.Detail += "; " + strings.Join(did, ", ") + " — never flushed"
|
|
}
|
|
if held.Changed != "" {
|
|
out.Detail += "; " + held.Changed + " by something other than the mesh since it was found"
|
|
}
|
|
if unread != "" {
|
|
// Said, not swallowed: the report would otherwise say a tunnel with no port and no
|
|
// peers was carried, which reads as a tunnel that was not one.
|
|
out.Detail += "; what it says could not be read as a tunnel's: " + unread
|
|
}
|
|
return out, facts, stopped, nil
|
|
}
|
|
|
|
// readFoundTunnel is the found configuration as a tunnel.
|
|
func readFoundTunnel(path string) (tunnel.Found, error) {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return tunnel.Found{}, err
|
|
}
|
|
return tunnel.Parse(raw)
|
|
}
|
|
|
|
// interfaceUp is whether a WireGuard interface is up on the machine.
|
|
func interfaceUp(ctx context.Context, run Runner, iface string) bool {
|
|
up, err := run(ctx, "wg", "show", "interfaces")
|
|
if err != nil {
|
|
return false
|
|
}
|
|
for _, name := range strings.Fields(up) {
|
|
if name == iface {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// replaces holds the mesh's declared interface configuration against the found tunnel it is to
|
|
// replace: same port, same address, and a key file holding the found key. The configuration is
|
|
// the file the service restarts on; its `PostUp = wg set %i private-key <path>` names the key.
|
|
func replaces(d *declaration.Declaration, svc *declaration.Service, found tunnel.Found) error {
|
|
var conf *declaration.File
|
|
for _, r := range d.Resources {
|
|
f, ok := r.(*declaration.File)
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, id := range svc.RestartOn {
|
|
if f.ID == id {
|
|
conf = f
|
|
}
|
|
}
|
|
}
|
|
if conf == nil {
|
|
return fmt.Errorf("%s takes over %s and restarts on no declared file, so the interface it would "+
|
|
"raise cannot be checked against the found one", svc.Unit, found.Interface)
|
|
}
|
|
port, address, keyPath := "", "", ""
|
|
for _, line := range strings.Split(conf.Content, "\n") {
|
|
key, value, ok := strings.Cut(strings.TrimSpace(line), "=")
|
|
if !ok {
|
|
continue
|
|
}
|
|
key, value = strings.ToLower(strings.TrimSpace(key)), strings.TrimSpace(value)
|
|
switch key {
|
|
case "listenport":
|
|
port = value
|
|
case "address":
|
|
address = strings.TrimSpace(strings.Split(value, ",")[0])
|
|
case "postup":
|
|
if _, after, ok := strings.Cut(value, "private-key "); ok {
|
|
keyPath = strings.Fields(after)[0]
|
|
}
|
|
}
|
|
}
|
|
var wrong []string
|
|
if port != fmt.Sprint(found.Port) {
|
|
wrong = append(wrong, fmt.Sprintf("it listens on port %q and the tunnel on %d", port, found.Port))
|
|
}
|
|
if host(address) != host(found.Address) {
|
|
wrong = append(wrong, fmt.Sprintf("its address is %q and the tunnel's %s", address, found.Address))
|
|
}
|
|
switch raw, err := os.ReadFile(keyPath); {
|
|
case keyPath == "":
|
|
wrong = append(wrong, "it names no key file")
|
|
case err != nil:
|
|
wrong = append(wrong, fmt.Sprintf("its key file %s cannot be read (%v)", keyPath, err))
|
|
default:
|
|
public, perr := tunnel.PublicKeyOf(strings.TrimSpace(string(raw)))
|
|
if perr != nil || public != found.PublicKey {
|
|
wrong = append(wrong, fmt.Sprintf("the key at %s is not the tunnel's — `mesh-host overlay take "+
|
|
"--tunnel %s` on this machine takes it, then push again", keyPath, found.Interface))
|
|
}
|
|
}
|
|
if len(wrong) > 0 {
|
|
return fmt.Errorf("the mesh's interface would not replace the tunnel on %s: %s — the peers would be "+
|
|
"dropped the moment it came up. Re-place the hub on the tunnel's address and port and push again",
|
|
found.Interface, strings.Join(wrong, "; "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// host is an address without its prefix length.
|
|
func host(address string) string {
|
|
if i := strings.Index(address, "/"); i >= 0 {
|
|
return address[:i]
|
|
}
|
|
return address
|
|
}
|
|
|
|
// tunnelState is where the tunnel stands, read from the machine: the found unit or interface up
|
|
// and the mesh's not is not taken; the mesh's up and the found one down is taken; neither up is
|
|
// down — the peers reach nothing.
|
|
func tunnelState(ctx context.Context, sys system.System, foundUnit, meshUnit string, run Runner) string {
|
|
foundState, _ := sys.ServiceState(ctx, run, foundUnit)
|
|
meshState, _ := sys.ServiceState(ctx, run, meshUnit)
|
|
foundUp := foundState == "running" || interfaceUp(ctx, run, strings.TrimPrefix(foundUnit, "wg-quick@"))
|
|
switch {
|
|
case meshState == "running" && !foundUp:
|
|
return Taken
|
|
case meshState == "running":
|
|
// Both up: not a takeover that holds, and said as not taken so nobody reads it as one.
|
|
return NotTaken
|
|
case foundUp:
|
|
return NotTaken
|
|
default:
|
|
return TunnelDown
|
|
}
|
|
}
|
|
|
|
// restoreFound starts the found unit again after the mesh's interface failed to replace it, so the
|
|
// machine has the tunnel it had rather than none, and says so in the account.
|
|
func restoreFound(ctx context.Context, sys system.System, unit string, run Runner, facts *TakenTunnel) {
|
|
if err := sys.SetServiceState(ctx, run, unit, "running"); err != nil {
|
|
facts.State = TunnelDown
|
|
facts.Note += "; " + unit + " could not be started again (" + err.Error() + ") — on the machine: systemctl start " + unit
|
|
return
|
|
}
|
|
if state, err := sys.ServiceState(ctx, run, unit); err != nil || state != "running" {
|
|
facts.State = TunnelDown
|
|
facts.Note += "; " + unit + " was started again and is not running — on the machine: systemctl start " + unit
|
|
return
|
|
}
|
|
facts.State = NotTaken
|
|
facts.Note += "; " + unit + " was started again, so the machine has the tunnel it had"
|
|
}
|
|
|
|
// wireguardDir is where a found tunnel's configuration may be retired from: wg-quick's own, and
|
|
// nowhere else. A variable so a test can hand in a directory.
|
|
var wireguardDir = tunnel.ConfigDir
|
|
|
|
// retireFound removes the found tunnel's configuration from where its unit reads it, once the take
|
|
// is proven, and ends the hold on it (novox/hq ADR 0119). Asked after the mesh's service applied
|
|
// and the tunnel reads as taken; retired says whether this apply retired it, and out is then what
|
|
// replaces the take's outcome for the configuration.
|
|
//
|
|
// **Proven is taken and a handshake.** Taken alone — the found unit down and disabled, the mesh's
|
|
// interface up — says the mesh's interface exists, not that any peer reaches it: an interface up
|
|
// with the wrong key is taken and carries nothing. A peer that has completed a handshake with it
|
|
// has checked its key, so that is the proof, asked of the kernel through `wg`. Any handshake counts,
|
|
// however old: a change to the mesh's configuration restarts its unit, which recreates the
|
|
// interface and resets its counters, so a time that is there at all was made by this interface.
|
|
// Anything short of one — no peer yet, every time zero, `wg` missing or failing — keeps the file,
|
|
// and the account says which: a take that never proves itself is visible rather than silently
|
|
// retired.
|
|
//
|
|
// **Only what the take names, and only wg-quick's own file.** Nothing is removed unless the path
|
|
// is exactly `<wireguard dir>/<found interface>.conf`, is not a path the mesh itself writes, and is
|
|
// a file rather than a link: removing a link would leave the key-bearing file it points at where it
|
|
// is, a retirement in name only, so that one is said and left to a person.
|
|
//
|
|
// **The original must still be kept.** It is the record of what the predecessor was and a
|
|
// person's only way back (ADR 0100); a kept copy that has gone missing is said, and the file is
|
|
// not removed, since removing it then would lose the only copy. What is on disk now, if it differs
|
|
// from the first original and from what was kept at the last retirement, is kept too before it
|
|
// goes — by content, so the first original is never overwritten and a file that keeps coming back
|
|
// the same keeps nothing more.
|
|
//
|
|
// The found unit is left disabled; without its configuration it cannot raise the interface, so
|
|
// every later apply's check of it finds nothing to do. Nothing here ever writes the file back.
|
|
func retireFound(ctx context.Context, svc *declaration.Service, d *declaration.Declaration, known *store.State,
|
|
run Runner, keep Keep, facts *TakenTunnel, now time.Time) (out Outcome, retired bool) {
|
|
t := svc.TakesOver
|
|
id := takeOverID(svc)
|
|
if facts.State != Taken {
|
|
return out, false
|
|
}
|
|
held, isHeld := known.HeldAt(id)
|
|
if !isHeld {
|
|
// Retired already (takeOver let any hold go and said so), or never held: nothing to do.
|
|
return out, false
|
|
}
|
|
say := func(note string) {
|
|
if facts.Note != "" {
|
|
facts.Note += "; "
|
|
}
|
|
facts.Note += note
|
|
}
|
|
notRetired := func(why string) (Outcome, bool) {
|
|
say("the found configuration " + t.Config + " is not retired: " + why)
|
|
return Outcome{}, false
|
|
}
|
|
mesh := strings.TrimPrefix(svc.Unit, "wg-quick@")
|
|
peers, err := tunnel.Handshaken(ctx, tunnel.Runner(run), mesh)
|
|
if err != nil {
|
|
say("taken, not yet proven: " + err.Error() + "; the found configuration " + t.Config + " is kept")
|
|
return out, false
|
|
}
|
|
if peers == 0 {
|
|
say("taken, not yet proven: no peer has handshaken on " + mesh + "; the found configuration " +
|
|
t.Config + " is kept")
|
|
return out, false
|
|
}
|
|
proven := fmt.Sprintf("proven: %d peer(s) handshaken on %s", peers, mesh)
|
|
say(proven)
|
|
|
|
// What may be removed at all.
|
|
if want := filepath.Join(wireguardDir, t.Interface+".conf"); t.Config != want {
|
|
return notRetired("only " + want + ", the found interface's own wg-quick configuration, is ever " +
|
|
"retired by the mesh, and the take names " + t.Config)
|
|
}
|
|
if known.Recorded(string(declaration.TypeFile), t.Config) || declaresFile(d, t.Config) {
|
|
return notRetired("it is a path the mesh itself writes")
|
|
}
|
|
if info, err := os.Lstat(t.Config); err == nil && info.Mode()&os.ModeSymlink != 0 {
|
|
target, _ := os.Readlink(t.Config)
|
|
return notRetired("it is a link to " + target + "; removing the link would leave the key-bearing file " +
|
|
"it points at, so it must be retired by hand — both are kept")
|
|
}
|
|
|
|
// The kept original, read back — not just named in a record.
|
|
if held.Kept == "" {
|
|
return notRetired("no original of it was kept, so removing it would leave no record of what the " +
|
|
"predecessor was")
|
|
}
|
|
original, err := os.ReadFile(held.Kept)
|
|
if err != nil || (held.Digest != "" && digestOf(string(original)) != held.Digest) {
|
|
why := "is missing"
|
|
if err == nil {
|
|
why = "no longer holds what was found"
|
|
} else if !errors.Is(err, os.ErrNotExist) {
|
|
why = "cannot be read (" + err.Error() + ")"
|
|
}
|
|
return notRetired("its kept original " + held.Kept + " " + why + ", so removing it would lose the only copy")
|
|
}
|
|
|
|
before, cameBack := known.RetiredAt(t.Config)
|
|
record := store.Retired{ID: id, Path: t.Config, Kept: held.Kept, Digest: digestOf(string(original)), At: now}
|
|
if cameBack {
|
|
// The first original stays the record's, and so does what the last retirement kept.
|
|
record.Extra, record.ExtraDigest, record.Again = before.Extra, before.ExtraDigest, before.Again+1
|
|
}
|
|
newCopy := ""
|
|
gone := !present(t.Config)
|
|
if !gone {
|
|
current, err := os.ReadFile(t.Config)
|
|
if err != nil {
|
|
return notRetired("it cannot be read (" + err.Error() + ")")
|
|
}
|
|
if sum := digestOf(string(current)); sum != record.Digest && sum != record.ExtraDigest {
|
|
if keep == nil {
|
|
return notRetired("it holds something other than its kept original and this host has nowhere " +
|
|
"to keep it")
|
|
}
|
|
where, err := keep(t.Config, current, 0o600)
|
|
if err != nil {
|
|
return notRetired("keeping what it holds now failed (" + err.Error() + ")")
|
|
}
|
|
record.Extra, record.ExtraDigest, newCopy = where, sum, where
|
|
}
|
|
if err := os.Remove(t.Config); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return notRetired("removing it failed (" + err.Error() + ")")
|
|
}
|
|
if present(t.Config) {
|
|
return notRetired("it is still there after it was removed")
|
|
}
|
|
}
|
|
|
|
known.RecordRetired(record)
|
|
known.Release(id)
|
|
facts.Kept = held.Kept
|
|
copied := ""
|
|
if newCopy != "" {
|
|
copied = "; what it held, which differed from the original, is kept at " + newCopy
|
|
}
|
|
out = Outcome{ID: id, Type: string(declaration.TypeFile), Target: t.Config, Action: "removed"}
|
|
switch {
|
|
case cameBack:
|
|
say("the found configuration came back and was retired again — its original still kept at " +
|
|
held.Kept + copied + "; rolling back to the found tunnel means unassigning the private network first")
|
|
out.Detail = "the found configuration came back and was retired again; original kept at " + held.Kept + copied
|
|
default:
|
|
say("the found configuration " + t.Config + " is retired — its original kept at " + held.Kept + copied +
|
|
", " + t.Unit + " left disabled, and the mesh never brings it back")
|
|
out.Detail = "retired: the take of " + t.Interface + " is " + proven + "; original kept at " + held.Kept + copied
|
|
}
|
|
if gone {
|
|
// Already gone — removed by something other than the mesh, or by an apply whose record was
|
|
// never saved. Nothing removed here; the hold ends all the same.
|
|
out.Action = "unchanged"
|
|
out.Detail = "retired: the take of " + t.Interface + " is " + proven + " and " + t.Config +
|
|
" was already gone; original kept at " + held.Kept
|
|
}
|
|
return out, true
|
|
}
|
|
|
|
// declaresFile is whether a declaration writes a file at a path.
|
|
func declaresFile(d *declaration.Declaration, path string) bool {
|
|
for _, r := range d.Resources {
|
|
if f, ok := r.(*declaration.File); ok && filepath.Clean(f.Path) == filepath.Clean(path) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// takesOver is the service in a declaration that takes over a tunnel, if any: one per node, since
|
|
// a machine has one private network.
|
|
func takesOver(d *declaration.Declaration) *declaration.Service {
|
|
for _, r := range d.Resources {
|
|
if svc, ok := r.(*declaration.Service); ok && svc.TakesOver != nil {
|
|
return svc
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// errNotAdopted is a takeover on a declaration that does not say the node is adopted, which the
|
|
// parser refuses already; kept as a second line of defence at the point of acting.
|
|
var errNotAdopted = errors.New("a tunnel is taken over on an adopted node only")
|