A take is a comparison: the host's facts, former targets, and strays (hq ADR 0163)
Every held thing carries what a take compares: for a found container its image and the image's date, the networks it is on and the other containers on each, its mounts and published ports, beside the declared image (and its date once pulled), ports and volumes, with the downgrade decided when both dates are known; for a found file whether the declared content differs and how, as lines lost and lines new. A resource whose target moved keeps the former target on record as an orphan, so the next apply removes the container or file the host wrote under the old name (issue 097). Every apply reports the strays: containers the mesh neither wrote nor holds.
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
package store
|
||||
|
||||
import "testing"
|
||||
|
||||
// A resource whose target moves leaves what the host wrote under the old target on record as a
|
||||
// former one, undeclared by construction, so the next apply removes it (novox/hq issue 097, ADR 0163).
|
||||
func TestARecordWhoseTargetMovedKeepsTheFormerTargetToRemove(t *testing.T) {
|
||||
s := State{}
|
||||
s.Record(Applied{ID: "gitea.server", Type: "container", Target: "mesh-gitea", Origin: OriginDeclared})
|
||||
s.Record(Applied{ID: "gitea.server", Type: "container", Target: "gitea", Origin: OriginDeclared})
|
||||
if len(s.Resources) != 2 {
|
||||
t.Fatalf("a moved target produced %d record(s): %+v", len(s.Resources), s.Resources)
|
||||
}
|
||||
orphans := s.Orphans(map[string]bool{"gitea.server": true}, OriginDeclared)
|
||||
if len(orphans) != 1 || orphans[0].Target != "mesh-gitea" || !IsFormer(orphans[0].ID) {
|
||||
t.Fatalf("the former target is not an orphan to remove: %+v", orphans)
|
||||
}
|
||||
s.Forget(orphans[0].ID)
|
||||
if len(s.Resources) != 1 || s.Resources[0].Target != "gitea" {
|
||||
t.Fatalf("forgetting the former target touched the current one: %+v", s.Resources)
|
||||
}
|
||||
// The same target again is not a move; a carried record is not the host's to remove.
|
||||
s.Record(Applied{ID: "gitea.server", Type: "container", Target: "gitea", Origin: OriginDeclared})
|
||||
s.Record(Applied{ID: "bundle", Type: "file", Target: "/a"})
|
||||
s.Record(Applied{ID: "bundle", Type: "file", Target: "/b"})
|
||||
if len(s.Resources) != 2 {
|
||||
t.Fatalf("an unmoved or carried record grew the list: %+v", s.Resources)
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -274,6 +275,41 @@ type Held struct {
|
||||
// reverted: that is how a predecessor still writing is caught.
|
||||
Changed string `json:"changed,omitempty"`
|
||||
ChangedAt time.Time `json:"changed_at,omitempty"`
|
||||
// Facts is what a take would compare: the found thing beside what the module declares
|
||||
// (novox/hq ADR 0163). Read fresh on every apply while held, so the controller's preview
|
||||
// speaks of the machine as it is.
|
||||
Facts *Facts `json:"facts,omitempty"`
|
||||
}
|
||||
|
||||
// Facts is a held thing beside what its module declares — what a take compares (ADR 0163).
|
||||
type Facts struct {
|
||||
// A found container: the image it runs and when that image was made; the networks it is on
|
||||
// and the other containers on each; what it mounts; what it publishes.
|
||||
Image string `json:"image,omitempty"`
|
||||
ImageCreated string `json:"image_created,omitempty"`
|
||||
Networks map[string][]string `json:"networks,omitempty"`
|
||||
Mounts []string `json:"mounts,omitempty"`
|
||||
Ports []string `json:"ports,omitempty"`
|
||||
// What the module declares for it, and the declared image's creation date when the image
|
||||
// is on the machine already.
|
||||
DeclaredImage string `json:"declared_image,omitempty"`
|
||||
DeclaredImageCreated string `json:"declared_image_created,omitempty"`
|
||||
DeclaredPorts []string `json:"declared_ports,omitempty"`
|
||||
DeclaredVolumes []string `json:"declared_volumes,omitempty"`
|
||||
// Downgrade is true when both creation dates are known and the declared image is the older.
|
||||
Downgrade bool `json:"downgrade,omitempty"`
|
||||
// A found file: whether the declared content differs from what was found, and how, as lines
|
||||
// only in the found file (-) and lines only in the declared one (+), bounded.
|
||||
Differs bool `json:"differs,omitempty"`
|
||||
Difference []string `json:"difference,omitempty"`
|
||||
}
|
||||
|
||||
// A Stray is something running on the machine that the mesh neither wrote nor holds
|
||||
// (novox/hq ADR 0163): the answer to "what is here that nobody asked for".
|
||||
type Stray struct {
|
||||
Kind string `json:"kind"`
|
||||
Name string `json:"name"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
}
|
||||
|
||||
// Recorded reports whether this host has a record, of any origin, of putting something of this
|
||||
@@ -462,6 +498,20 @@ func Save(path string, s State) error {
|
||||
func (s *State) Record(a Applied) {
|
||||
for i, existing := range s.Resources {
|
||||
if existing.ID == a.ID {
|
||||
// A resource whose target moved leaves what the host wrote under the old target
|
||||
// behind — a container under the old name, a file at the old path. Rewriting the
|
||||
// record would erase the only trace of it (novox/hq issue 097, ADR 0163), so the old
|
||||
// target stays on record as a former one, undeclared by construction, until the next
|
||||
// apply removes it the way it removes anything the host wrote and no longer declares.
|
||||
// What was found is held, never recorded here, and so never removed by this.
|
||||
if originOf(existing) == OriginDeclared && existing.Target != "" && a.Target != "" &&
|
||||
existing.Target != a.Target && existing.Type == a.Type {
|
||||
former := existing
|
||||
former.ID = FormerID(existing.ID, existing.Target)
|
||||
s.Resources[i] = a
|
||||
s.Resources = append(s.Resources, former)
|
||||
return
|
||||
}
|
||||
s.Resources[i] = a
|
||||
return
|
||||
}
|
||||
@@ -469,6 +519,13 @@ func (s *State) Record(a Applied) {
|
||||
s.Resources = append(s.Resources, a)
|
||||
}
|
||||
|
||||
// FormerID names the record of a resource's former target: the resource's id and the target it
|
||||
// had, so the record is distinct from the current one and is never what a declaration names.
|
||||
func FormerID(id, target string) string { return id + "@former:" + target }
|
||||
|
||||
// IsFormer says whether a record names a former target.
|
||||
func IsFormer(id string) bool { return strings.Contains(id, "@former:") }
|
||||
|
||||
// Forget drops a resource from what the node owns.
|
||||
func (s *State) Forget(id string) {
|
||||
kept := s.Resources[:0]
|
||||
|
||||
Reference in New Issue
Block a user