diff --git a/cmd/mesh-controller/compose_fallbacks_test.go b/cmd/mesh-controller/compose_fallbacks_test.go new file mode 100644 index 00000000..f80d8ca6 --- /dev/null +++ b/cmd/mesh-controller/compose_fallbacks_test.go @@ -0,0 +1,91 @@ +package main + +import ( + "encoding/base64" + "errors" + "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/compose" + "github.com/novox/mesh-controller/internal/broker" +) + +// aCoreInput is an Input of one machine that takes a core module whose credential the vault has not boxed: the composition +// stands in a vault box for it, and the machine holds the controller's own seal of it. +func aCoreInput(t *testing.T) (*meshInput, string) { + t.Helper() + raw := make([]byte, 32) + raw[0] = 9 + key := base64.StdEncoding.EncodeToString(raw) + client := catalogue.Manifest{Module: "mesh-provisioner-client", Version: "1", + Claims: []catalogue.Claim{{Name: broker.NodeProvisionerSeat, Scope: catalogue.ScopeNode}}, + OwnSecrets: catalogue.OwnSecrets{broker.ProvisionerSecret: {Path: "/var/lib/mesh-provisioner-client/broker"}}} + return &meshInput{Input: compose.Input{ + Catalogue: map[string]catalogue.Manifest{client.Module: client}, + Nodes: []compose.Node{{Name: "laptop", Assigned: []string{client.Module}, SealingKey: key, Stated: true, + Contract: compose.VaultBoxContract}}, + OwnSecrets: []compose.OwnSecretRow{{Node: "laptop", Module: client.Module, Name: broker.ProvisionerSecret, + Origin: "made", SealedTo: key, Sealed: "SEALED-BY-THE-CONTROLLER"}}, + }}, client.Module +} + +// 1385 (the lead's answer 1): where the vault is silent and no box is kept, the declaration carries the controller's own seal +// for the core credential, as ADR 0396 has it, and stands in for nothing. It failed first: the composition's declaration +// carried the stand-in box, and a send was refused. +func TestACoreCredentialTheVaultDidNotBoxIsSealedByTheController(t *testing.T) { + in, module := aCoreInput(t) + d, _, err := compose.ComposeIn(in.Input, "laptop", compose.Foreseeing) + if err != nil { + t.Fatal(err) + } + if len(d.CoreBoxesWanted) != 1 || len(stillForeseen(d)) == 0 { + t.Fatalf("the composition does not stand in for the vault's box: %v", d.CoreBoxesWanted) + } + sealed, _, err := sealedByTheController(in, "laptop", d, []string{module}) + if err != nil { + t.Fatal(err) + } + if left := stillForeseen(sealed); len(left) != 0 { + t.Fatalf("the declaration still stands in for %v", left) + } + var file map[string]any + for _, r := range sealed.Resources { + if r["id"] == module+"."+catalogue.NeedID(broker.ProvisionerSecret) { + file = r + } + } + if file == nil || file["sealed"] != "SEALED-BY-THE-CONTROLLER" || file[catalogue.VaultSealedField] != nil { + t.Fatalf("the core credential is carried as %v: the controller's own seal, not the vault's box", file) + } + // And a module the vault did answer for is left as it is. + other, _, err := sealedByTheController(in, "laptop", d, []string{"another-module"}) + if err != nil { + t.Fatal(err) + } + if len(stillForeseen(other)) == 0 { + t.Fatal("a module the vault answered for was sealed by the controller") + } +} + +// 1383 (the lead's answer 4): the machine that runs the bus is refused where no user has a credential, with busUserList's +// words, and a machine that does not run the bus is not. It failed first: the Input stood a list in and the bus node composed. +func TestTheMachineThatRunsTheBusIsRefusedWhereNoUserHasACredential(t *testing.T) { + bus := catalogue.Manifest{Module: "nats", Version: "1", BusUsers: "/etc/nats/users.conf", + Claims: []catalogue.Claim{{Name: "mesh-broker", Scope: catalogue.ScopeMesh}}} + other := catalogue.Manifest{Module: "files", Version: "1"} + why := errors.New("this machine runs the bus and not one user has a credential, so the composed list would refuse every connection in the mesh") + in := &meshInput{busRefused: why, Input: compose.Input{ + Catalogue: map[string]catalogue.Manifest{"nats": bus, "files": other}, + Nodes: []compose.Node{{Name: "anchor", Assigned: []string{"nats"}}, {Name: "laptop", Assigned: []string{"files"}}}, + }} + if err := refusedBus(in, "anchor"); err == nil || !strings.Contains(err.Error(), "not one user has a credential") { + t.Fatalf("the machine that runs the bus is not refused: %v", err) + } + if err := refusedBus(in, "laptop"); err != nil { + t.Fatalf("a machine that does not run the bus is refused: %v", err) + } + if err := refusedBus(&meshInput{Input: in.Input}, "anchor"); err != nil { + t.Fatalf("a bus with users is refused: %v", err) + } +} diff --git a/cmd/mesh-controller/compose_input_credentials_integration_test.go b/cmd/mesh-controller/compose_input_credentials_integration_test.go index d3905fd9..c0d43de9 100644 --- a/cmd/mesh-controller/compose_input_credentials_integration_test.go +++ b/cmd/mesh-controller/compose_input_credentials_integration_test.go @@ -328,3 +328,27 @@ func TestTheAdaptersInputCarriesTheBuildsAndTheMachinesKey(t *testing.T) { } } } + +// The lead's answer 4: where the mesh holds no credential for any bus user, the machine that runs the bus is refused with +// busUserList's words (the controller's composition refused it), the Input still builds, and another machine composes. +func TestTheBusMachineIsRefusedWhereNoUserHasACredentialAndTheOthersCompose(t *testing.T) { + open, _ := aMeshWithSecrets(t) + ctx := t.Context() + bus := catalogue.Manifest{Module: "nats", Version: "1", BusUsers: "/etc/nats/users.conf", + Claims: []catalogue.Claim{{Name: "mesh-broker", Scope: catalogue.ScopeMesh}}} + register(t, open, bus) + register(t, open, aWebModule()) + if _, err := assign(ctx, open, "anchor", "nats"); err != nil { + t.Fatal(err) + } + if _, err := assign(ctx, open, "laptop", "web"); err != nil { + t.Fatal(err) + } + if _, _, err := composeQuestion(ctx, open, "anchor", compose.Foreseeing, false); err == nil || + !strings.Contains(err.Error(), "not one user has a credential") { + t.Fatalf("the bus machine is composed: %v", err) + } + if _, _, err := composeQuestion(ctx, open, "laptop", compose.Foreseeing, false); err != nil { + t.Fatalf("a machine that does not run the bus is refused: %v", err) + } +} diff --git a/cmd/mesh-controller/compose_input_integration_test.go b/cmd/mesh-controller/compose_input_integration_test.go index 6b19fd8f..a7996877 100644 --- a/cmd/mesh-controller/compose_input_integration_test.go +++ b/cmd/mesh-controller/compose_input_integration_test.go @@ -249,3 +249,70 @@ func TestTheAdaptersInputCarriesTheNetworkAsTheInventoryHoldsIt(t *testing.T) { t.Error("the range moved with the environment") } } + +// The lead's equality finding (build 1792487777757012513): a provider on one machine serves a fact that says ${setting:domain}, +// the setting is set for it, and a consumer on another machine takes that fact. The first pass of the adapter resolved every +// node with no settings in the Input, so the served fact could not be settled and the whole Input failed. The settings are in +// the Input before any node is resolved, and the Input builds, with the same plan for the consumer as the controller's own +// composition. It failed first, with the finding's words. +func TestAServedFactThatSaysASettingIsResolvedWithTheSettingInTheInput(t *testing.T) { + open := aMesh(t) + ctx := t.Context() + register(t, open, catalogue.Manifest{Module: "step-ca", Version: "1", + Provides: []catalogue.Offer{{Name: "acme-ca", Scope: catalogue.ScopeMesh}}, + Serves: map[string]map[string]any{"acme-ca": {"domain": "${setting:domain}"}}}) + register(t, open, catalogue.Manifest{Module: "route-proxy", Version: "1", Requires: []string{"acme-ca"}}) + if _, err := assign(ctx, open, "anchor", "step-ca"); err != nil { + t.Fatal(err) + } + if _, err := assign(ctx, open, "laptop", "route-proxy"); err != nil { + t.Fatal(err) + } + if err := open.inventory.SetSettings(ctx, "", "step-ca", map[string]any{"domain": "example.test"}); err != nil { + t.Fatal(err) + } + in, err := readComposeInput(ctx, open) + if err != nil { + t.Fatalf("the Input cannot be built: %v", err) + } + if anchor, _ := nodeOf(in.Input, "anchor"); len(anchor.Settings["step-ca"]) == 0 { + t.Fatalf("the Input does not carry the setting of the provider: %v", anchor.Settings) + } + oldPlan, _, err := planFor(ctx, open, "laptop") + if err != nil { + t.Fatal(err) + } + newPlan, _, err := resolveNode(withInputCache(ctx), open, "laptop") + if err != nil { + t.Fatalf("the consumer does not resolve: %v", err) + } + if !reflect.DeepEqual(oldPlan.Needs, newPlan.Needs) { + t.Errorf("the consumer's needs are %+v, the controller's own %+v", newPlan.Needs, oldPlan.Needs) + } +} + +// A setting that nothing sets, in a fact a provider serves, is not the adapter's to hide: the controller's own composition +// refuses it as well (theRestOfTheMesh returns servedOnNode's error), and the adapter refuses with the same words rather than +// building an Input that composes a placeholder. +func TestAServedFactThatSaysAnUnsetSettingIsRefusedAsTheControllerRefusesIt(t *testing.T) { + open := aMesh(t) + ctx := t.Context() + register(t, open, catalogue.Manifest{Module: "step-ca", Version: "1", + Provides: []catalogue.Offer{{Name: "acme-ca", Scope: catalogue.ScopeMesh}}, + Serves: map[string]map[string]any{"acme-ca": {"domain": "${setting:domain}"}}}) + register(t, open, catalogue.Manifest{Module: "route-proxy", Version: "1", Requires: []string{"acme-ca"}}) + if _, err := assign(ctx, open, "anchor", "step-ca"); err != nil { + t.Fatal(err) + } + if _, err := assign(ctx, open, "laptop", "route-proxy"); err != nil { + t.Fatal(err) + } + _, _, oldErr := planFor(ctx, open, "laptop") + _, newErr := readComposeInput(ctx, open) + if (oldErr == nil) != (newErr == nil) { + t.Fatalf("the controller's composition says %v, the adapter says %v", oldErr, newErr) + } + if oldErr != nil && !strings.Contains(newErr.Error(), "nothing sets \"domain\"") { + t.Errorf("the adapter's words are %q, the controller's %q", newErr, oldErr) + } +} diff --git a/cmd/mesh-controller/compose_perform_integration_test.go b/cmd/mesh-controller/compose_perform_integration_test.go index f42c93dc..1d9ccbeb 100644 --- a/cmd/mesh-controller/compose_perform_integration_test.go +++ b/cmd/mesh-controller/compose_perform_integration_test.go @@ -146,17 +146,31 @@ func TestPerformMakesTheOwnSecretsAndThePairCredentialsTheCompositionNamed(t *te } } -// 1385/AC3: with the vault unreachable and no box of the credential kept, the send fails naming the module, and composes -// nothing to send. -func TestAVaultThatIsSilentRefusesTheSendWhereNoBoxIsKept(t *testing.T) { +// 1385/AC3 (the lead's answer 1): with the vault unreachable and no box of the credential kept, the send carries the +// controller's own seal for it (ADR 0396), says so as a condition, and keeps no box. +func TestAVaultThatIsSilentSealsTheSendByTheControllerWhereNoBoxIsKept(t *testing.T) { open, vault := aCoreMesh(t, link.VaultBoxContract) vault.silent = errors.New("no responders") declared, err := composeSend(t.Context(), open, "laptop") - if err == nil || !strings.Contains(err.Error(), "mesh-provisioner-client") { - t.Fatalf("the send was not refused, naming the module: %v", err) + if err != nil { + t.Fatalf("the send is refused: %v", err) } - if len(declared.Resources) != 0 { - t.Fatal("a declaration came with the refusal") + f := clientsBroker(declared.Resources) + if f == nil || f["sealed"] == nil || f[catalogue.VaultSealedField] != nil { + t.Fatalf("the core credential is composed as %v: the controller's own seal, until the vault holds it", f) + } + if len(declared.foreseen) != 0 { + t.Fatalf("the declaration still names %v foreseen", declared.foreseen) + } + if c, found := vaultSilence(t, "laptop"); !found || !strings.Contains(c.Summary, "sealed by the controller until the vault holds it") { + t.Fatalf("no condition says why: %+v", c) + } + kept, err := open.inventory.KeptCoreBoxes(t.Context(), "laptop") + if err != nil { + t.Fatal(err) + } + if len(kept["mesh-provisioner-client"]) != 0 { + t.Fatalf("a box was kept for a credential the vault never boxed: %v", kept) } }