One assignment of a module per node is now the rule, not a limitation — the operator dropped the multi-assignment requirement, and the schema's (node, module) key has been the decision since migration 0005. What changed: Assign reports whether the assignment was new, and the command says 'already runs — one node runs one of each (ADR 0115); nothing changed' instead of printing 'is assigned' for a no-op, which read as an action that happened. Idempotence stays: a repeat is exit 0, because a script stating what is already true is not wrong.
234 lines
8.2 KiB
Go
234 lines
8.2 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)
|
|
}
|
|
m := catalogue.Manifest{Module: "step-ca", Version: "1",
|
|
Provides: catalogue.Offers("acme-ca"), OwnSecrets: map[string]string{"password": "/run/password"}}
|
|
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 = map[string]string{name: "/run/" + name}
|
|
return m
|
|
}
|