take ends its preview with a digest and --yes names it, as the flip does; a changed preview or an account older than the flip allows is refused. A module the machine holds nothing for has nothing to compare, and --yes suffices. A published port's reach is said as the machine reported it. Every secret the module holds on the machine is listed with where it came from, and one the mesh minted for a service whose data was found refuses unless --mint names it. One judgement of a module's settings against its definition, in the catalogue: settings set refuses what cannot compose or reaches nothing, naming node, module, layer and key; Compose leaves out a module whose definition moved under a stored setting, the envelope says so (left_out), plan and push say it by name, and the machine is told everything else. A stray setting no longer refuses the whole machine where it is read (issue 096). The per-machine setting networks keeps a found network for a taken container, on an adopted machine only; the container's declaration carries it and the preview names it (rule 4).
241 lines
8.7 KiB
Go
241 lines
8.7 KiB
Go
package inventory
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
)
|
|
|
|
// What removing a module takes with it, and what re-registering one does not.
|
|
//
|
|
// Both were reported as one fault — "operator settings do not persist, because re-registering a
|
|
// module cascade-deletes them". Only half of it was true, and it was the other half.
|
|
|
|
// **Registering a module again does not touch what the mesh holds for it.** The upsert is on the
|
|
// name, so a manifest changing — a new version, a new requirement, a new resource — leaves the
|
|
// settings, the secrets and the ports exactly where they were.
|
|
//
|
|
// This is here because it was believed not to be. A wrong belief about which command destroys data
|
|
// is expensive in both directions: it sends people looking for a fault that is not there, and it
|
|
// leaves the command that really does destroy it unexamined.
|
|
func TestRegisteringAModuleAgainKeepsWhatTheMeshHoldsForIt(t *testing.T) {
|
|
inv := fresh(t)
|
|
ctx := t.Context()
|
|
node, err := inv.AddNode(ctx, "anchor")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
key, _ := aSealingKey(t)
|
|
if err := inv.RecordSealingKey(ctx, node.ID, key); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// A mergeable file, so any setting composes (novox/hq ADR 0163, rule 6: a setting is judged
|
|
// where it is stored).
|
|
m := catalogue.Manifest{Module: "step-ca", Version: "1",
|
|
Provides: catalogue.Offers("acme-ca"), OwnSecrets: catalogue.OwnSecrets{"password": {Path: "/run/password"}},
|
|
Resources: []map[string]any{{"id": "conf", "type": "file", "path": "/etc/step-ca.json", "content": "{}", "merge": "json"}}}
|
|
if err := inv.RegisterModule(ctx, m, Source{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.SetSettings(ctx, "", "step-ca", map[string]any{"issuer": "the mesh"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.SetSettings(ctx, "anchor", "step-ca", map[string]any{"port": 9000}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.AcceptSecretForModule(ctx, "anchor", "step-ca", "password", "sealed"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := inv.PortFor(ctx, "anchor", "step-ca", 9000, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Re-registered at a new version, which is exactly what somebody bumping one does.
|
|
m.Version = "2"
|
|
if err := inv.RegisterModule(ctx, m, Source{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
layers, err := inv.SettingsFor(ctx, "anchor", "step-ca")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(layers) != 2 {
|
|
t.Fatalf("re-registering lost a settings layer: %+v", layers)
|
|
}
|
|
held, err := inv.HeldFor(ctx, "step-ca")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !held.Mesh || len(held.Nodes) != 1 || len(held.Secrets) != 1 || len(held.Ports) != 1 {
|
|
t.Fatalf("re-registering lost something the mesh held: %+v", held)
|
|
}
|
|
|
|
// And the new manifest is what resolution sees, which is the point of re-registering at all.
|
|
shelf, err := inv.Catalogue(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if shelf["step-ca"].Version != "2" {
|
|
t.Fatalf("the new manifest did not replace the old: %+v", shelf["step-ca"])
|
|
}
|
|
if len(shelf["step-ca"].Provides) != 1 || shelf["step-ca"].Provides[0].Name != "acme-ca" {
|
|
// A version bump was reported as un-providing a provision. It does not.
|
|
t.Fatalf("a version bump changed what the module provides: %+v", shelf["step-ca"].Provides)
|
|
}
|
|
}
|
|
|
|
// **Forgetting a module refuses while the mesh still holds things for it, and says what they are.**
|
|
//
|
|
// The settings, the module's own secrets and the ports the mesh chose all name the module by a
|
|
// foreign key that cascades. So the removal took them, silently, and reported "forgotten" — an
|
|
// action succeeding into a state its own verify would reject (novox/hq 04-ISSUES/017). A sealed
|
|
// secret is not recoverable afterwards: the mesh discarded the plaintext when it made it.
|
|
func TestForgettingAModuleRefusesRatherThanDiscardingWhatTheMeshHolds(t *testing.T) {
|
|
inv := fresh(t)
|
|
ctx := t.Context()
|
|
node, err := inv.AddNode(ctx, "anchor")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
key, _ := aSealingKey(t)
|
|
if err := inv.RecordSealingKey(ctx, node.ID, key); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RegisterModule(ctx, withOwnSecret(manifest("step-ca", nil, nil), "password"), Source{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.SetSettings(ctx, "anchor", "step-ca", map[string]any{"port": 9000}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.AcceptSecretForModule(ctx, "anchor", "step-ca", "password", "sealed"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := inv.PortFor(ctx, "anchor", "step-ca", 9000, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = inv.ForgetModule(ctx, "step-ca")
|
|
if !errors.Is(err, ErrStillHolds) {
|
|
t.Fatalf("forgetting discarded what the mesh held, or refused for another reason: %v", err)
|
|
}
|
|
// It names them one at a time. "3 things" says there is something to lose and not whether it
|
|
// can be afforded; the sealed secret is the one that cannot be made again, and it is named.
|
|
for _, want := range []string{"settings, on anchor", "password on anchor",
|
|
"the mesh cannot make it again", "the port 9000 on anchor", "--and-what-it-holds"} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Errorf("the refusal does not say %q:\n%s", want, err)
|
|
}
|
|
}
|
|
// And nothing went. A refusal that half-happened would be worse than the cascade.
|
|
shelf, err := inv.Catalogue(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, still := shelf["step-ca"]; !still {
|
|
t.Fatal("the module was removed by a command that refused")
|
|
}
|
|
layers, err := inv.SettingsFor(ctx, "anchor", "step-ca")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(layers) != 1 {
|
|
t.Fatalf("a refusal took the settings anyway: %+v", layers)
|
|
}
|
|
}
|
|
|
|
// Having been told, it goes — and what went is reported, because this is the only record that any
|
|
// of it existed.
|
|
func TestDiscardingAModuleSaysWhatWentWithIt(t *testing.T) {
|
|
inv := fresh(t)
|
|
ctx := t.Context()
|
|
node, err := inv.AddNode(ctx, "anchor")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
key, _ := aSealingKey(t)
|
|
if err := inv.RecordSealingKey(ctx, node.ID, key); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RegisterModule(ctx, withOwnSecret(manifest("step-ca", nil, nil), "password"), Source{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.SetSettings(ctx, "", "step-ca", map[string]any{"issuer": "the mesh"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.AcceptSecretForModule(ctx, "anchor", "step-ca", "password", "sealed"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
held, err := inv.DiscardModule(ctx, "step-ca")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !held.Mesh || len(held.Secrets) != 1 {
|
|
t.Fatalf("what went was not reported: %+v", held)
|
|
}
|
|
shelf, err := inv.Catalogue(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, still := shelf["step-ca"]; still {
|
|
t.Fatal("the module is still there")
|
|
}
|
|
}
|
|
|
|
// A module the mesh holds nothing for is forgotten without ceremony. The refusal is about loss,
|
|
// not about the command.
|
|
func TestForgettingAModuleTheMeshHoldsNothingForJustWorks(t *testing.T) {
|
|
inv := fresh(t)
|
|
ctx := t.Context()
|
|
if _, err := inv.AddNode(ctx, "anchor"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RegisterModule(ctx, manifest("plain", nil, nil), Source{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.ForgetModule(ctx, "plain"); err != nil {
|
|
t.Fatalf("a module holding nothing was refused: %v", err)
|
|
}
|
|
}
|
|
|
|
// A module still on a machine refuses first, whatever it holds: that is not a loss an operator can
|
|
// consent to on the machine's behalf, and unassign is what "I do not want this" means.
|
|
func TestAModuleStillAssignedRefusesBeforeAnythingAboutWhatItHolds(t *testing.T) {
|
|
inv := fresh(t)
|
|
ctx := t.Context()
|
|
if _, err := inv.AddNode(ctx, "anchor"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RegisterModule(ctx, withOwnSecret(manifest("step-ca", nil, nil), "password"), Source{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.SetSettings(ctx, "anchor", "step-ca", map[string]any{"a": 1}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := inv.Assign(ctx, "anchor", "step-ca"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, forget := range []func() error{
|
|
func() error { return inv.ForgetModule(ctx, "step-ca") },
|
|
func() error { _, err := inv.DiscardModule(ctx, "step-ca"); return err },
|
|
} {
|
|
if err := forget(); !errors.Is(err, ErrStillAssigned) {
|
|
t.Fatalf("an assigned module was not refused for being assigned: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// withOwnSecret gives a fixture manifest an own secret, so a delivery to it is one the module
|
|
// declares (novox/hq 04-ISSUES/078).
|
|
func withOwnSecret(m catalogue.Manifest, name string) catalogue.Manifest {
|
|
m.OwnSecrets = catalogue.OwnSecrets{name: {Path: "/run/" + name}}
|
|
// And a mergeable file, so any setting these tests store composes (novox/hq ADR 0163, rule 6:
|
|
// a setting is judged where it is stored).
|
|
m.Resources = append(m.Resources, map[string]any{"id": "conf", "type": "file",
|
|
"path": "/etc/" + m.Module + ".json", "content": "{}", "merge": "json"})
|
|
return m
|
|
}
|