diff --git a/cmd/mesh-controller/compose_fallbacks_test.go b/cmd/mesh-controller/compose_fallbacks_test.go index f80d8ca6..04537cc4 100644 --- a/cmd/mesh-controller/compose_fallbacks_test.go +++ b/cmd/mesh-controller/compose_fallbacks_test.go @@ -89,3 +89,26 @@ func TestTheMachineThatRunsTheBusIsRefusedWhereNoUserHasACredential(t *testing.T t.Fatalf("a bus with users is refused: %v", err) } } + +// The controller's seal replaces the stand-in box whatever path the resource carries: the path is the manifest's with the +// machine's words filled in, not the manifest's own. It failed first, with the path compared (the live try: "composed twice and +// still names ... asked of the vault by the next send"). +func TestTheControllersSealReplacesTheBoxWhateverPathTheFileCarries(t *testing.T) { + in, module := aCoreInput(t) + d, _, err := compose.ComposeIn(in.Input, "laptop", compose.Foreseeing) + if err != nil { + t.Fatal(err) + } + for _, r := range d.Resources { + if _, box := r[catalogue.VaultSealedField]; box { + r["path"] = "/home/someone/.var/lib/mesh-provisioner-client/broker" + } + } + 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) + } +} diff --git a/cmd/mesh-controller/compose_input_integration_test.go b/cmd/mesh-controller/compose_input_integration_test.go index a7996877..daf1ea5f 100644 --- a/cmd/mesh-controller/compose_input_integration_test.go +++ b/cmd/mesh-controller/compose_input_integration_test.go @@ -262,15 +262,17 @@ func TestAServedFactThatSaysASettingIsResolvedWithTheSettingInTheInput(t *testin 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"}}) + // The setting is there before the machines are assigned: an assignment resolves the mesh, and a served fact that says + // ${setting:domain} resolves only with it. + if err := open.inventory.KeepSettings(ctx, "", "step-ca", map[string]any{"domain": "example.test"}); err != nil { + t.Fatal(err) + } 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) @@ -301,12 +303,19 @@ func TestAServedFactThatSaysAnUnsetSettingIsRefusedAsTheControllerRefusesIt(t *t 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"}}) + // Assigned with the setting there, then the setting is cleared: the mesh the controller holds when someone clears it. + if err := open.inventory.KeepSettings(ctx, "", "step-ca", map[string]any{"domain": "example.test"}); err != nil { + t.Fatal(err) + } 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.ClearSettings(ctx, "", "step-ca"); err != nil { + t.Fatal(err) + } _, _, oldErr := planFor(ctx, open, "laptop") _, newErr := readComposeInput(ctx, open) if (oldErr == nil) != (newErr == nil) { diff --git a/cmd/mesh-controller/compose_sdk.go b/cmd/mesh-controller/compose_sdk.go index 2b08a559..28896e58 100644 --- a/cmd/mesh-controller/compose_sdk.go +++ b/cmd/mesh-controller/compose_sdk.go @@ -426,10 +426,11 @@ func sealedByTheController(in *meshInput, node string, d compose.Declared, modul if sealed == "" { return d, nil, fmt.Errorf("%s/%s on %s has no seal of the controller's to carry until the vault holds it", w.Module, w.Name, node) } - path := in.Catalogue[w.Module].OwnSecrets[w.Name].Path for _, r := range d.Resources { box, isBox := r[catalogue.VaultSealedField] - if !isBox || r["id"] != w.Module+"."+catalogue.NeedID(w.Name) || r["path"] != path { + // By the resource's id alone: its path is the manifest's with the machine's words filled in, and so is not the + // manifest's own. + if !isBox || r["id"] != w.Module+"."+catalogue.NeedID(w.Name) { continue } if had, err := compactBox(box); err != nil || had != stood { @@ -439,6 +440,17 @@ func sealedByTheController(in *meshInput, node string, d compose.Declared, modul r["sealed"] = sealed } } + // A stand-in box that is left is said by its resource, never sent and never guessed at. + for _, r := range d.Resources { + if box, has := r[catalogue.VaultSealedField]; has { + if had, err := compactBox(box); err == nil && had == stood { + if id, _ := r["id"].(string); slices.ContainsFunc(modules, func(m string) bool { return strings.HasPrefix(id, m+".") }) { + return d, nil, fmt.Errorf("%s on %s still carries a stand-in for the vault's box, and the controller has "+ + "no seal of its own for it", id, node) + } + } + } + } d.CoreBoxesWanted = wanted for _, f := range d.Foreseen { module, _, _ := strings.Cut(f, "/")