355 lines
18 KiB
Go
355 lines
18 KiB
Go
package inventory
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
)
|
|
|
|
// APlan is a whole plan, as design 57 P6 composes one for a module handed to the provisioner.
|
|
func aPlan() Provisioning {
|
|
return Provisioning{
|
|
Templates: TemplatesArchive{Source: "artifacts/dunst/files.tar.gz",
|
|
Digest: "sha256:" + strings.Repeat("ab", 32)},
|
|
Files: []ProvisionedFile{{ID: "dunstrc", Path: "/home/nox/.config/dunst/dunstrc",
|
|
Template: "dunstrc", OwnerAtRuntime: "owned", Takes: "reload-on"}},
|
|
Placement: map[string]string{"machine:account-home": "/home/nox", "dir:state": "/var/lib/dunst"},
|
|
}
|
|
}
|
|
|
|
// **An assignment with no plan is the bytes it was** (design 57 P5, issue 671): the plan joins the value of a
|
|
// module handed to the provisioner alone, so no other assignment's key is written again.
|
|
func TestAnAssignmentWithoutAPlanIsMirroredAsBefore(t *testing.T) {
|
|
at := time.Date(2026, 10, 14, 20, 0, 0, 0, time.UTC)
|
|
values, err := AssignmentValues([]AssignmentMirrored{{Node: "g14", Module: "zsh", Assigned: at}}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, want := string(values["g14.zsh"]), `{"node":"g14","module":"zsh","assigned":"2026-10-14T20:00:00Z"}`; got != want {
|
|
t.Errorf("an assignment without a plan is mirrored as %s, want %s", got, want)
|
|
}
|
|
}
|
|
|
|
// **A handed module's assignment carries its plan**, whole, under `provisioning`, as the client reads it.
|
|
func TestAHandedModulesAssignmentCarriesItsPlan(t *testing.T) {
|
|
at := time.Date(2026, 10, 14, 20, 0, 0, 0, time.UTC)
|
|
values, err := AssignmentValues([]AssignmentMirrored{{Node: "g14", Module: "dunst", Assigned: at},
|
|
{Node: "g14", Module: "zsh", Assigned: at}}, map[string]Provisioning{"g14.dunst": aPlan()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var got AssignmentMirrored
|
|
if err := json.Unmarshal(values["g14.dunst"], &got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Provisioning == nil || got.Provisioning.Files[0].Path != "/home/nox/.config/dunst/dunstrc" ||
|
|
got.Provisioning.Placement["dir:state"] != "/var/lib/dunst" {
|
|
t.Fatalf("the plan is mirrored as %s", values["g14.dunst"])
|
|
}
|
|
for _, field := range []string{`"templates":{"source":`, `"digest":"sha256:`, `"owner-at-runtime":"owned"`,
|
|
`"template":"dunstrc"`, `"takes":"reload-on"`} {
|
|
if !strings.Contains(string(values["g14.dunst"]), field) {
|
|
t.Errorf("the plan's value lacks %s: %s", field, values["g14.dunst"])
|
|
}
|
|
}
|
|
if strings.Contains(string(values["g14.zsh"]), "provisioning") {
|
|
t.Errorf("an assignment with no plan carries one: %s", values["g14.zsh"])
|
|
}
|
|
}
|
|
|
|
// **A plan the client would have to guess at is refused, by name**, and so is one for a module not assigned
|
|
// there: nothing is written rather than something wrong.
|
|
func TestAPlanWithAProblemIsRefusedByName(t *testing.T) {
|
|
at := time.Now().UTC()
|
|
assigned := []AssignmentMirrored{{Node: "g14", Module: "dunst", Assigned: at}}
|
|
cases := map[string]func(*Provisioning){
|
|
"templates.digest": func(p *Provisioning) { p.Templates.Digest = "sha256:short" },
|
|
"templates.source": func(p *Provisioning) { p.Templates.Source = "" },
|
|
"files and items are empty": func(p *Provisioning) { p.Files = nil },
|
|
`collection "Shell"`: func(p *Provisioning) { p.Items = []ProvisionedItem{{Collection: "Shell", Template: "x"}} },
|
|
"collection a is contributed to twice": func(p *Provisioning) {
|
|
p.Items = []ProvisionedItem{{Collection: "a", Template: "x"}, {Collection: "a", Template: "y"}}
|
|
},
|
|
`items[0]: template "/x"`: func(p *Provisioning) { p.Items = []ProvisionedItem{{Collection: "a", Template: "/x"}} },
|
|
"collections[1]: a is named twice": func(p *Provisioning) {
|
|
p.Collections = []ProvisionedCollection{{Name: "a"}, {Name: "a"}}
|
|
},
|
|
"the comment of a holds a line break": func(p *Provisioning) {
|
|
p.Collections = []ProvisionedCollection{{Name: "a", Comment: "#\n"}}
|
|
},
|
|
"not a clean": func(p *Provisioning) { p.Files[0].Path = "relative/dunstrc" },
|
|
"inside the templates": func(p *Provisioning) { p.Files[0].Template = "../dunstrc" },
|
|
"owner-at-runtime": func(p *Provisioning) { p.Files[0].OwnerAtRuntime = "mine" },
|
|
"takes": func(p *Provisioning) { p.Files[0].Takes = "now" },
|
|
`placement "secret`: func(p *Provisioning) { p.Placement["secret:token"] = "x" },
|
|
`placement "dir:"`: func(p *Provisioning) { p.Placement["dir:"] = "x" },
|
|
"is named twice": func(p *Provisioning) { p.Files = append(p.Files, p.Files[0]) },
|
|
"not a resource id": func(p *Provisioning) { p.Files[0].ID = "Dunst RC" },
|
|
`secret "Token"`: func(p *Provisioning) { p.Secrets = []ProvisionedSecret{{Name: "Token"}} },
|
|
"secret token is named twice": func(p *Provisioning) {
|
|
p.Secrets = []ProvisionedSecret{{Name: "token"}, {Name: "token"}}
|
|
},
|
|
`secret token: path "rel/token"`: func(p *Provisioning) {
|
|
p.Secrets = []ProvisionedSecret{{Name: "token", Path: "rel/token"}}
|
|
},
|
|
"secret token: path /home/nox/.config/dunst/dunstrc is named twice": func(p *Provisioning) {
|
|
p.Secrets = []ProvisionedSecret{{Name: "token", Path: "/home/nox/.config/dunst/dunstrc"}}
|
|
},
|
|
}
|
|
for want, spoil := range cases {
|
|
p := aPlan()
|
|
spoil(&p)
|
|
_, err := AssignmentValues(assigned, map[string]Provisioning{"g14.dunst": p})
|
|
if err == nil || !strings.Contains(err.Error(), want) || !strings.Contains(err.Error(), "g14.dunst") {
|
|
t.Errorf("%s: refused with %v", want, err)
|
|
}
|
|
}
|
|
if _, err := AssignmentValues(assigned, map[string]Provisioning{"shanks.dunst": aPlan()}); err == nil ||
|
|
!strings.Contains(err.Error(), "shanks.dunst: a plan for a module not assigned there") {
|
|
t.Errorf("a plan for a module not assigned there: %v", err)
|
|
}
|
|
}
|
|
|
|
// **A plan names the module's own secrets its declaration names there** (design 57 V4, issue 647): each by name, and
|
|
// a secret kept in a file of its own with that file's path; the vault hands a node's client a secret only when the
|
|
// plan names it. Names, never a value.
|
|
func TestAPlanNamesTheModulesSecretsAndNoValue(t *testing.T) {
|
|
p := aPlan()
|
|
p.Secrets = []ProvisionedSecret{{Name: "token"}, {Name: "db-password", Path: "/var/lib/dunst/db"}}
|
|
values, err := AssignmentValues([]AssignmentMirrored{{Node: "g14", Module: "dunst", Assigned: time.Now().UTC()}},
|
|
map[string]Provisioning{"g14.dunst": p})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if want := `"secrets":[{"name":"token"},{"name":"db-password","path":"/var/lib/dunst/db"}]`; !strings.Contains(
|
|
string(values["g14.dunst"]), want) {
|
|
t.Errorf("the plan's value is %s, want it to carry %s", values["g14.dunst"], want)
|
|
}
|
|
plain := aPlan()
|
|
values, err = AssignmentValues([]AssignmentMirrored{{Node: "g14", Module: "dunst", Assigned: time.Now().UTC()}},
|
|
map[string]Provisioning{"g14.dunst": plain})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(values["g14.dunst"]), "secrets") {
|
|
t.Errorf("a plan naming no secret says secrets: %s", values["g14.dunst"])
|
|
}
|
|
}
|
|
|
|
// **A contributor's plan may fill no file** (design 57 L1, issue 684): its item is its part on the node; and a
|
|
// collector's names the collections its files read, the old namespaces' by their own names.
|
|
func TestAPlanMayContributeAnItemAndNameItsCollections(t *testing.T) {
|
|
p := aPlan()
|
|
p.Files = nil
|
|
p.Items = []ProvisionedItem{{Collection: "shell:zsh:normal", Template: "zsh"}}
|
|
p.Collections = []ProvisionedCollection{{Name: "widgets", Comment: "#"}, {Name: "contribution:node-hotkeys:trigger"}}
|
|
values, err := AssignmentValues([]AssignmentMirrored{{Node: "g14", Module: "prompt", Assigned: time.Now()}},
|
|
map[string]Provisioning{"g14.prompt": p})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var back AssignmentMirrored
|
|
if err := json.Unmarshal(values["g14.prompt"], &back); err != nil || back.Provisioning == nil ||
|
|
len(back.Provisioning.Items) != 1 || len(back.Provisioning.Collections) != 2 {
|
|
t.Fatalf("mirrored %s (%v)", values["g14.prompt"], err)
|
|
}
|
|
}
|
|
|
|
// **A plan names the credentials of what the module requires that the vault keeps for it alone** (issue 757): each
|
|
// by the name its files call it, its provision, its provider and its file; never a value. Read as strictly as the
|
|
// rest of the plan.
|
|
func TestAPlanNamesTheCredentialsIssuedForTheModuleAndNoValue(t *testing.T) {
|
|
assigned := []AssignmentMirrored{{Node: "g14", Module: "dunst", Assigned: time.Now().UTC()}}
|
|
good := ProvisionedCredential{Name: "gitea-read", Provision: "gitea-read", Provider: "novox",
|
|
ProviderModule: "gitea", Path: "/var/lib/dunst/gitea-read.secret"}
|
|
p := aPlan()
|
|
p.Provisions = []ProvisionedCredential{good}
|
|
values, err := AssignmentValues(assigned, map[string]Provisioning{"g14.dunst": p})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if want := `"provisions":[{"name":"gitea-read","provision":"gitea-read","provider":"novox",` +
|
|
`"provider-module":"gitea","path":"/var/lib/dunst/gitea-read.secret"}]`; !strings.Contains(string(values["g14.dunst"]), want) {
|
|
t.Errorf("the plan's value is %s, want it to carry %s", values["g14.dunst"], want)
|
|
}
|
|
plain, err := AssignmentValues(assigned, map[string]Provisioning{"g14.dunst": aPlan()})
|
|
if err != nil || strings.Contains(string(plain["g14.dunst"]), "provisions") {
|
|
t.Errorf("a plan naming no credential says provisions: %s (%v)", plain["g14.dunst"], err)
|
|
}
|
|
for want, spoil := range map[string]func(*ProvisionedCredential){
|
|
`provision credential "Gitea Read"`: func(c *ProvisionedCredential) { c.Name = "Gitea Read" },
|
|
`provision ""`: func(c *ProvisionedCredential) { c.Provision = "" },
|
|
`provider "../ace"`: func(c *ProvisionedCredential) { c.Provider = "../ace" },
|
|
`path "rel"`: func(c *ProvisionedCredential) { c.Path = "rel" },
|
|
"is named twice": func(c *ProvisionedCredential) { c.Path = "/home/nox/.config/dunst/dunstrc" },
|
|
} {
|
|
c := good
|
|
spoil(&c)
|
|
bad := aPlan()
|
|
bad.Provisions = []ProvisionedCredential{c}
|
|
if _, err := AssignmentValues(assigned, map[string]Provisioning{"g14.dunst": bad}); err == nil ||
|
|
!strings.Contains(err.Error(), want) {
|
|
t.Errorf("%s: refused with %v", want, err)
|
|
}
|
|
}
|
|
twice := aPlan()
|
|
twice.Secrets = []ProvisionedSecret{{Name: "gitea-read"}}
|
|
twice.Provisions = []ProvisionedCredential{good}
|
|
if _, err := AssignmentValues(assigned, map[string]Provisioning{"g14.dunst": twice}); err == nil ||
|
|
!strings.Contains(err.Error(), "secret gitea-read is named twice") {
|
|
t.Errorf("a credential named as an own secret too: %v", err)
|
|
}
|
|
}
|
|
|
|
// **A module only pulled in by another has a value of its own** (issue 765): where the machine runs a handed module
|
|
// that nothing assigned there, its plan is mirrored under `<node>.<module>` like an assigned one's, saying which
|
|
// module requires it and no time it was assigned. A plan for a module neither assigned nor required there is still
|
|
// refused, and a module assigned there is mirrored as an assignment whatever else requires it.
|
|
func TestAModulePulledInHasAValueOfItsOwnWithItsPlan(t *testing.T) {
|
|
at := time.Date(2026, 10, 14, 20, 0, 0, 0, time.UTC)
|
|
assigned := []AssignmentMirrored{{Node: "g14", Module: "zsh", Assigned: at}}
|
|
pulled := aPlan()
|
|
pulled.RequiredBy = "desk"
|
|
both := aPlan()
|
|
both.RequiredBy = "desk"
|
|
values, err := AssignmentValues(assigned, map[string]Provisioning{"g14.dunst": pulled, "g14.zsh": both})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := string(values["g14.dunst"]); !strings.HasPrefix(got, `{"node":"g14","module":"dunst","required-by":"desk",`+
|
|
`"provisioning":{"templates":{"source":"artifacts/dunst/files.tar.gz",`) || strings.Contains(got, "assigned") {
|
|
t.Errorf("a pulled-in module is mirrored as %s", got)
|
|
}
|
|
var got AssignmentMirrored
|
|
if err := json.Unmarshal(values["g14.dunst"], &got); err != nil || got.Provisioning == nil ||
|
|
got.Provisioning.Files[0].Path != "/home/nox/.config/dunst/dunstrc" {
|
|
t.Fatalf("the pulled-in module's plan is mirrored as %s (%v)", values["g14.dunst"], err)
|
|
}
|
|
if zsh := string(values["g14.zsh"]); !strings.HasPrefix(zsh, `{"node":"g14","module":"zsh","assigned":"2026-10-14T20:00:00Z",`+
|
|
`"provisioning":`) || strings.Contains(zsh, "required-by") {
|
|
t.Errorf("an assigned module also required is mirrored as %s", zsh)
|
|
}
|
|
if len(values) != 2 {
|
|
t.Errorf("the mirror holds %d values, want 2", len(values))
|
|
}
|
|
spoilt := pulled
|
|
spoilt.Templates.Digest = "sha256:short"
|
|
if _, err := AssignmentValues(assigned, map[string]Provisioning{"g14.dunst": spoilt}); err == nil ||
|
|
!strings.Contains(err.Error(), "g14.dunst: templates.digest") {
|
|
t.Errorf("a pulled-in module's spoilt plan: %v", err)
|
|
}
|
|
if _, err := AssignmentValues(assigned, map[string]Provisioning{"shanks.dunst": aPlan()}); err == nil ||
|
|
!strings.Contains(err.Error(), "shanks.dunst: a plan for a module not assigned there") {
|
|
t.Errorf("a plan for a module neither assigned nor required there: %v", err)
|
|
}
|
|
}
|
|
|
|
// **A plan names the provider serving each provision its module requires** (design 57 V5a, issue 760): machine,
|
|
// module and local name, so the vault reading the mirror sees a bind and an unbind; an entry with no credential
|
|
// to fill has no name and no file. Read as strictly as the rest of the plan.
|
|
func TestAPlanNamesTheProviderOfEachProvision(t *testing.T) {
|
|
assigned := []AssignmentMirrored{{Node: "g14", Module: "dunst", Assigned: time.Now().UTC()}}
|
|
p := aPlan()
|
|
p.Provisions = []ProvisionedCredential{
|
|
{Provision: "postgres-database", Provider: "novox", ProviderModule: "postgres"},
|
|
{Provision: "s3-bucket", Local: "hot", Provider: "ace", ProviderModule: "minio"}}
|
|
values, err := AssignmentValues(assigned, map[string]Provisioning{"g14.dunst": p})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if want := `"provisions":[{"provision":"postgres-database","provider":"novox","provider-module":"postgres"},` +
|
|
`{"provision":"s3-bucket","local":"hot","provider":"ace","provider-module":"minio"}]`; !strings.Contains(
|
|
string(values["g14.dunst"]), want) {
|
|
t.Errorf("the plan's value is %s, want it to carry %s", values["g14.dunst"], want)
|
|
}
|
|
for want, provisions := range map[string][]ProvisionedCredential{
|
|
`postgres-database: provider-module ""`: {{Provision: "postgres-database", Provider: "novox"}},
|
|
`postgres-database: provider ""`: {{Provision: "postgres-database", ProviderModule: "postgres"}},
|
|
"s3-bucket (hot): its provider is named twice": {
|
|
{Provision: "s3-bucket", Local: "hot", Provider: "ace", ProviderModule: "minio"},
|
|
{Provision: "s3-bucket", Local: "hot", Provider: "novox", ProviderModule: "minio"}},
|
|
`postgres-database: path "/var/lib/dunst/db" names a file and no credential`: {{Provision: "postgres-database",
|
|
Provider: "novox", ProviderModule: "postgres", Path: "/var/lib/dunst/db"}},
|
|
} {
|
|
bad := aPlan()
|
|
bad.Provisions = provisions
|
|
if _, err := AssignmentValues(assigned, map[string]Provisioning{"g14.dunst": bad}); err == nil ||
|
|
!strings.Contains(err.Error(), want) {
|
|
t.Errorf("%s: refused with %v", want, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The plan a composition makes (catalogue.Plan) is the plan the mirror carries, its provisions whole: kept with the
|
|
// send as the one and read back as the other, nothing is dropped and nothing is unknown.
|
|
func TestAComposedPlansProvisionsAreTheMirroredPlans(t *testing.T) {
|
|
composed := catalogue.Plan{Templates: catalogue.PlanTemplates{Source: "s", Digest: "d"},
|
|
Provisions: []catalogue.PlanProvision{{Name: "db-hot", Provision: "postgres-database", Local: "hot",
|
|
Provider: "novox", ProviderModule: "postgres", CredentialForm: catalogue.CredentialFormPair,
|
|
Path: "/var/lib/x/hot"}}}
|
|
raw, err := json.Marshal(composed)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var mirrored Provisioning
|
|
dec := json.NewDecoder(bytes.NewReader(raw))
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(&mirrored); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := []ProvisionedCredential{{Name: "db-hot", Provision: "postgres-database", Local: "hot", Provider: "novox",
|
|
ProviderModule: "postgres", CredentialForm: "pair", Path: "/var/lib/x/hot"}}
|
|
if !reflect.DeepEqual(mirrored.Provisions, want) {
|
|
t.Fatalf("the mirrored plan's provisions are %+v, want %+v", mirrored.Provisions, want)
|
|
}
|
|
}
|
|
|
|
// **A plan names the credential form of each provider's offer** (issue 768), so the vault makes a first value in
|
|
// that form for a provider it holds no row of: carried as written, and refused when it is not a form.
|
|
func TestAPlanNamesTheCredentialFormOfEachProvidersOffer(t *testing.T) {
|
|
assigned := []AssignmentMirrored{{Node: "g14", Module: "dunst", Assigned: time.Now().UTC()}}
|
|
p := aPlan()
|
|
p.Provisions = []ProvisionedCredential{
|
|
{Provision: "postgres-database", Provider: "novox", ProviderModule: "postgres", CredentialForm: "pair"},
|
|
{Provision: "sonarr-api", Provider: "ace", ProviderModule: "sonarr", CredentialForm: "shared"}}
|
|
values, err := AssignmentValues(assigned, map[string]Provisioning{"g14.dunst": p})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if want := `{"provision":"postgres-database","provider":"novox","provider-module":"postgres",` +
|
|
`"credential-form":"pair"},{"provision":"sonarr-api","provider":"ace","provider-module":"sonarr",` +
|
|
`"credential-form":"shared"}`; !strings.Contains(string(values["g14.dunst"]), want) {
|
|
t.Errorf("the plan's value is %s, want it to carry %s", values["g14.dunst"], want)
|
|
}
|
|
bad := aPlan()
|
|
bad.Provisions = []ProvisionedCredential{{Provision: "postgres-database", Provider: "novox",
|
|
ProviderModule: "postgres", CredentialForm: "one-for-all"}}
|
|
if _, err := AssignmentValues(assigned, map[string]Provisioning{"g14.dunst": bad}); err == nil ||
|
|
!strings.Contains(err.Error(), `credential-form "one-for-all"`) {
|
|
t.Errorf("a credential form that is not one was refused with %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAPlanMayHoldSeveralNamedItemsOfOneCollectionAndRefusesABadOne(t *testing.T) {
|
|
p := aPlan()
|
|
p.Items = []ProvisionedItem{
|
|
{Collection: "blocks", Name: "a", Attributes: map[string]string{"bar": "top"}, Template: "a"},
|
|
{Collection: "blocks", Name: "b", Template: "b"}}
|
|
if problems := p.Problems(); len(problems) != 0 {
|
|
t.Fatalf("a plan with two named items is refused: %v", problems)
|
|
}
|
|
p.Items = append(p.Items, ProvisionedItem{Collection: "blocks", Name: "b", Template: "c"},
|
|
ProvisionedItem{Collection: "blocks", Name: "c", Attributes: map[string]string{"Bar": "x y"}, Template: "d"})
|
|
got := strings.Join(p.Problems(), "; ")
|
|
for _, want := range []string{"item blocks/b is contributed to twice", "attribute Bar"} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("the problems %q lack %q", got, want)
|
|
}
|
|
}
|
|
}
|