Test the controller's seal under a silent vault, the bus machine's refusal and a served fact that says a setting (issues 1385, 1383, 1382)
This commit is contained in:
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -249,3 +249,70 @@ func TestTheAdaptersInputCarriesTheNetworkAsTheInventoryHoldsIt(t *testing.T) {
|
|||||||
t.Error("the range moved with the environment")
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
// 1385/AC3 (the lead's answer 1): with the vault unreachable and no box of the credential kept, the send carries the
|
||||||
// nothing to send.
|
// controller's own seal for it (ADR 0396), says so as a condition, and keeps no box.
|
||||||
func TestAVaultThatIsSilentRefusesTheSendWhereNoBoxIsKept(t *testing.T) {
|
func TestAVaultThatIsSilentSealsTheSendByTheControllerWhereNoBoxIsKept(t *testing.T) {
|
||||||
open, vault := aCoreMesh(t, link.VaultBoxContract)
|
open, vault := aCoreMesh(t, link.VaultBoxContract)
|
||||||
vault.silent = errors.New("no responders")
|
vault.silent = errors.New("no responders")
|
||||||
declared, err := composeSend(t.Context(), open, "laptop")
|
declared, err := composeSend(t.Context(), open, "laptop")
|
||||||
if err == nil || !strings.Contains(err.Error(), "mesh-provisioner-client") {
|
if err != nil {
|
||||||
t.Fatalf("the send was not refused, naming the module: %v", err)
|
t.Fatalf("the send is refused: %v", err)
|
||||||
}
|
}
|
||||||
if len(declared.Resources) != 0 {
|
f := clientsBroker(declared.Resources)
|
||||||
t.Fatal("a declaration came with the refusal")
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user