diff --git a/internal/bootstrap/control.go b/internal/bootstrap/control.go index bcade8b..a0c509b 100644 --- a/internal/bootstrap/control.go +++ b/internal/bootstrap/control.go @@ -311,6 +311,17 @@ func secretsByVariableIn(manifest []byte) (map[string]string, error) { for name, path := range m.OwnSecrets { secretAt[path] = name } + // **A file the module itself declares is its own, not a secret** (novox/hq issue 597). The + // controller reads its merge window's settings from a file its manifest writes from its settings + // (MESH_MERGE_WINDOW_FILE, ADR 0276): the mesh writes it with the module's declaration, and there + // is nothing for genesis to carry in. Only a `…_FILE` naming neither an own-secret nor a file of + // the module is refused. + declared := map[string]bool{} + for _, r := range m.Resources { + if r.Type == "file" && r.Path != "" { + declared[r.Path] = true + } + } wanted := map[string]string{} for _, r := range m.Resources { @@ -333,10 +344,13 @@ func secretsByVariableIn(manifest []byte) (map[string]string, error) { on = from } secret, named := secretAt[on] + if !named && declared[on] { + continue + } if !named { return nil, fmt.Errorf( - "the %s module's container reads %s from %s, and no own-secret of that "+ - "module writes that file.\n"+ + "the %s module's container reads %s from %s, and neither an own-secret nor "+ + "a file of that module writes that file.\n"+ "So the mesh would seal nothing there and the control plane would find "+ "an empty file where a connection string has to be. The manifest has to "+ "name the two ends the same, directly or through a mount", diff --git a/internal/bootstrap/genesis_form_test.go b/internal/bootstrap/genesis_form_test.go index e009568..b22da3a 100644 --- a/internal/bootstrap/genesis_form_test.go +++ b/internal/bootstrap/genesis_form_test.go @@ -180,3 +180,85 @@ func TestAProcessFormNamingNoOneReplacementIsRefused(t *testing.T) { } } } + +// theStepNineSecretsOf is what step 9 delivers for a process-form controller manifest: its genesis +// container made, pinned, and its secrets read from that container's environment. +func theStepNineSecretsOf(t *testing.T, manifest []byte) (map[string]string, error) { + t.Helper() + form, err := processFormOf(manifest) + if err != nil || !form.Found { + t.Fatalf("the manifest has no process form: %v", err) + } + genesis, err := genesisForm(manifest, form, builtImage) + if err != nil { + t.Fatal(err) + } + pinned, _, err := pinImage(genesis, builtImage, "registry.internal:5000/mesh-controller@sha256:"+strings.Repeat("e", 64), ControlPlaneModule) + if err != nil { + t.Fatal(err) + } + return secretsByVariableIn(pinned) +} + +// **A file the controller's manifest writes itself is not a secret** (novox/hq issue 597). The merge +// window's settings reach the controller in a file its manifest declares from its settings +// (MESH_MERGE_WINDOW_FILE, ADR 0276); genesis refused it as a secret nobody writes, and stopped at +// step 11 of 24. +func TestAFileTheControllersManifestWritesIsNotASecret(t *testing.T) { + manifest := strings.Replace(processFormManifest, + `{"id": "controller", "type": "process",`, + `{"id": "merge-window", "type": "file", "path": "${dir:mesh-state}/merge-window.json", "mode": "0600", + "owner": "mesh-controller", "content": "{\"merge-window\": \"${setting:merge-window}\"}\n"}, + {"id": "controller", "type": "process",`, 1) + manifest = strings.Replace(manifest, `"MESH_BUS_NATS_FILE": "${dir:mesh-state}/bus"}`, + `"MESH_BUS_NATS_FILE": "${dir:mesh-state}/bus", + "MESH_MERGE_WINDOW_FILE": "${dir:mesh-state}/merge-window.json"}`, 1) + if !strings.Contains(manifest, "MESH_MERGE_WINDOW_FILE") || !strings.Contains(manifest, `"id": "merge-window"`) { + t.Fatal("the fixture was not changed") + } + wanted, err := theStepNineSecretsOf(t, []byte(manifest)) + if err != nil { + t.Fatalf("step 9 refuses a file the manifest writes itself: %v", err) + } + if _, asked := wanted["MESH_MERGE_WINDOW"]; asked { + t.Errorf("the merge window's settings are delivered as a secret: %v", wanted) + } + if wanted["MESH_STORE_INVENTORY"] != "inventory" || wanted["MESH_BUS_NATS"] != "bus" { + t.Errorf("step 9 delivers %v; the store's and the bus's secrets are still wanted", wanted) + } +} + +// And a `…_FILE` that neither an own-secret nor a file of the module writes is still refused. +func TestAFileNothingOfTheControllerWritesIsStillRefused(t *testing.T) { + manifest := strings.Replace(processFormManifest, `"MESH_BUS_NATS_FILE": "${dir:mesh-state}/bus"}`, + `"MESH_BUS_NATS_FILE": "${dir:mesh-state}/bus", + "MESH_MERGE_WINDOW_FILE": "${dir:mesh-state}/merge-window.json"}`, 1) + if _, err := theStepNineSecretsOf(t, []byte(manifest)); err == nil || !strings.Contains(err.Error(), "MESH_MERGE_WINDOW_FILE") { + t.Fatalf("a file nothing writes was accepted, or refused without its name: %v", err) + } +} + +// The controller's own manifest, as the mesh runs it, passes step 9 (issue 597): in a merge check the +// node-engine's check has the controller cloned beside it (MESH_CHECK_BESIDE), where it must be; else a +// checkout beside this one; else it skips aloud. +func TestTheControllersOwnManifestPassesStepNine(t *testing.T) { + var manifest []byte + if root := os.Getenv("MESH_CHECK_BESIDE"); root != "" { + raw, err := os.ReadFile(filepath.Join(root, "mesh-controller", "module.json")) + if err != nil { + t.Fatalf("the controller is not beside this check in MESH_CHECK_BESIDE=%s: %v", root, err) + } + manifest = raw + } else if raw, err := os.ReadFile(filepath.Join("..", "..", "..", "mesh-controller", "module.json")); err == nil { + manifest = raw + } else { + t.Skip("NOT JUDGED: no controller beside this checkout and no MESH_CHECK_BESIDE; the merge check judges it") + } + wanted, err := theStepNineSecretsOf(t, manifest) + if err != nil { + t.Fatalf("genesis's step 9 refuses the controller the mesh runs: %v", err) + } + if wanted["MESH_STORE_INVENTORY"] != "inventory" { + t.Errorf("step 9 delivers %v", wanted) + } +}