From d47c1ad6a9205e92454e8f2b4e96153f4f1abf14 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 14 Oct 2026 09:26:29 +0200 Subject: [PATCH 1/5] feat: exec --override-backend writes into the directory of tofu -chdir exec now writes the backend override where tofu runs the module: the directory that tofu's or terraform's -chdir=DIR names, or else the working directory. So several execs can run at once on separate copies of a module. Co-Authored-By: Claude Opus 5.5 --- cmd/buildingblock/tfstate/exec.go | 14 ++++++++------ cmd/buildingblock/tfstate/tfstate.go | 2 +- internal/tfstate/override.go | 20 ++++++++++++++++++++ internal/tfstate/override_test.go | 18 ++++++++++++++++++ 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/cmd/buildingblock/tfstate/exec.go b/cmd/buildingblock/tfstate/exec.go index a9d05d77..0a141d7e 100644 --- a/cmd/buildingblock/tfstate/exec.go +++ b/cmd/buildingblock/tfstate/exec.go @@ -69,9 +69,10 @@ metadata.ownedByWorkspace, unless --workspace names another one. The module needs a backend "http" block, which the runner adds only while it runs. --override-backend adds one while the command runs: it writes the file -` + tfstate.BackendOverrideFile + ` into the working directory, where it also replaces a backend -of the module's own, and removes the file when the command ends. So run exec in the module's -directory rather than with tofu -chdir, and run tofu init through exec with --override-backend once. +` + tfstate.BackendOverrideFile + ` into the directory that tofu -chdir names, or else into the +working directory, where it also replaces a backend of the module's own, and removes the file when +the command ends. Run a command that wraps tofu in the module's directory, and run tofu init through +exec with --override-backend once. tofu's meshstack provider talks to meshStack through exec as well, with the login of the CLI. This works with any version of the provider, as long as its provider "meshstack" block sets no endpoint @@ -151,7 +152,7 @@ With --backup-dir, it copies the stored state into that directory before it repl } if overrideBackend { - remove, overrideErr := tfstate.WriteBackendOverride(".") + remove, overrideErr := tfstate.WriteBackendOverride(tfstate.ModuleDir(command)) if overrideErr != nil { return fmt.Errorf("%w: remove it, or run without --override-backend", overrideErr) } @@ -170,7 +171,8 @@ With --backup-dir, it copies the stored state into that directory before it repl switch { case requests > 0: case overrideBackend: - slog.WarnContext(ctx, command[0]+" asked for no state. --override-backend adds the backend in the working directory, so run exec in the module's directory rather than with tofu -chdir") + slog.WarnContext(ctx, fmt.Sprintf("%s asked for no state, although --override-backend added the backend in %s. Run a command that wraps tofu in the module's directory", + command[0], tfstate.ModuleDir(command))) default: slog.WarnContext(ctx, fmt.Sprintf("%s asked for no state, so the module likely has no backend \"http\" block, which the runner adds only while it runs. "+ "Run again with --override-backend, and run `%s %s --override-backend -- tofu init` once", @@ -183,7 +185,7 @@ With --backup-dir, it copies the stored state into that directory before it repl flags := cmd.Flags() flags.Var(&access, "mode", "read serves the state, readwrite stores what the command writes as well") flags.BoolVar(&force, "force", false, "store the state even while a run of the building block is pending or in progress, or where it does not follow the stored state") - flags.BoolVar(&overrideBackend, "override-backend", false, "add the backend \"http\" block to the module in the working directory while the command runs") + flags.BoolVar(&overrideBackend, "override-backend", false, "add the backend \"http\" block to the module in tofu's -chdir or the working directory while the command runs") flags.StringVar(&backupDir, "backup-dir", "", "with --mode readwrite, copy the stored state into this directory before each write, in files only you can read, since a state can hold secrets") return cmd diff --git a/cmd/buildingblock/tfstate/tfstate.go b/cmd/buildingblock/tfstate/tfstate.go index 49ed4391..cc380b74 100644 --- a/cmd/buildingblock/tfstate/tfstate.go +++ b/cmd/buildingblock/tfstate/tfstate.go @@ -39,7 +39,7 @@ reads and writes through tofu's http backend. "meshstack buildingblock tfstate exec" serves the state to tofu's http backend, so the module needs a backend "http" block. The runner adds one only while it runs, and exec with --override-backend -adds one while its command runs. Run it in the module's directory, with tofu init once: +adds one while its command runs. Run tofu init through it once: meshstack buildingblock tfstate exec --override-backend -- tofu init`, Example: ` meshstack bb tfstate show 0b5c1d3e-5f1a-4c2b-9d7e-2a6f8e4b1c90 | jq .resources diff --git a/internal/tfstate/override.go b/internal/tfstate/override.go index 8d17f13f..0a4b5b0c 100644 --- a/internal/tfstate/override.go +++ b/internal/tfstate/override.go @@ -6,6 +6,7 @@ import ( "io/fs" "os" "path/filepath" + "strings" ) // BackendOverrideFile ends in _override.tf, so tofu adds its backend to a module without one, and @@ -34,3 +35,22 @@ func WriteBackendOverride(dir string) (remove func() error, err error) { } return remove, nil } + +// ModuleDir is where command runs its module: the directory of tofu's or terraform's -chdir, or else +// the working directory. It must stay in step with extractChdirOption of OpenTofu's +// internal/command/workdir/chdir.go, which takes -chdir only as -chdir=DIR before the subcommand. +func ModuleDir(command []string) string { + if name := strings.TrimSuffix(filepath.Base(command[0]), ".exe"); name != "tofu" && name != "terraform" { + return "." + } + dir := "." + for _, arg := range command[1:] { + if !strings.HasPrefix(arg, "-") { + break + } + if value, ok := strings.CutPrefix(arg, "-chdir="); ok && value != "" { + dir = value + } + } + return dir +} diff --git a/internal/tfstate/override_test.go b/internal/tfstate/override_test.go index 49e55903..9cf825aa 100644 --- a/internal/tfstate/override_test.go +++ b/internal/tfstate/override_test.go @@ -34,3 +34,21 @@ func TestBackendOverride(t *testing.T) { assert.NoFileExists(t, path) }) } + +func TestModuleDir(t *testing.T) { + for _, command := range []struct { + args []string + dir string + }{ + {[]string{"tofu", "-chdir=module", "plan"}, "module"}, + {[]string{"/usr/local/bin/terraform", "-chdir=module", "plan"}, "module"}, + {[]string{"tofu.exe", "-chdir=module", "plan"}, "module"}, + {[]string{"tofu", "-chdir=first", "-chdir=last", "plan"}, "last"}, + {[]string{"tofu", "plan", "-chdir=module"}, "."}, + {[]string{"tofu", "-chdir", "module", "plan"}, "."}, + {[]string{"tofu"}, "."}, + {[]string{"terragrunt", "-chdir=module", "plan"}, "."}, + } { + assert.Equalf(t, command.dir, ModuleDir(command.args), "%v", command.args) + } +} From 7f8ae426271f637874bed112b21f6259e71dc8fc Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 14 Oct 2026 08:46:55 +0200 Subject: [PATCH 2/5] test: run the tfstate tests on a building block of our own The bootstrapped building block ran the noop module of meshstack-hub, so a change there broke TestAccTfstate, as the last commit fixed. Its definition now runs testdata/buildingblock, a single terraform_data, at the commit under test, which the exec tests apply as well. The test checks that GitHub has that commit, because the tf-block-runner clones it from there. Co-Authored-By: Claude Opus 5.5 --- .../testacc/testdata/bootstrap/main.tf | 65 ++++++++++++++----- .../testacc/testdata/buildingblock/main.tf | 11 ++++ cmd/internal/testacc/testdata/module/main.tf | 16 ----- cmd/internal/testacc/tfstate_test.go | 33 +++++++++- 4 files changed, 92 insertions(+), 33 deletions(-) create mode 100644 cmd/internal/testacc/testdata/buildingblock/main.tf delete mode 100644 cmd/internal/testacc/testdata/module/main.tf diff --git a/cmd/internal/testacc/testdata/bootstrap/main.tf b/cmd/internal/testacc/testdata/bootstrap/main.tf index e5304501..5ada3eb1 100644 --- a/cmd/internal/testacc/testdata/bootstrap/main.tf +++ b/cmd/internal/testacc/testdata/bootstrap/main.tf @@ -1,6 +1,6 @@ -# The building block that the tfstate tests work on: a block of the noop definition of -# meshstack-hub, in a workspace of its own, which the tf-block-runner applies with meshStack's state -# backend. +# The building block that the tfstate tests work on, in a workspace of its own. Its definition runs +# testdata/buildingblock at the commit under test, which the tf-block-runner applies with meshStack's +# state backend. terraform { required_providers { @@ -24,6 +24,14 @@ variable "workspace_manager" { type = string } +variable "repository_url" { + type = string +} + +variable "git_ref" { + type = string +} + provider "meshstack" { profile = var.profile } @@ -62,20 +70,47 @@ resource "meshstack_workspace_user_binding" "manager" { } } -module "noop" { - source = "git::https://github.com/meshcloud/meshstack-hub.git//modules/meshstack/noop/e2e?ref=main" - # test_context reaches a const variable of the noop module, which tofu evaluates at init, so it - # takes the names from var.run_id rather than from the resources above. +resource "meshstack_building_block_definition" "noop" { + metadata = { + owned_by_workspace = meshstack_workspace.this.metadata.name + } + spec = { + display_name = "${var.run_id} meshStack CLI noop" + description = "Runs testdata/buildingblock of the meshStack CLI acceptance tests." + target_type = "WORKSPACE_LEVEL" + } + version_spec = { + draft = true + deletion_mode = "DELETE" + implementation = { + terraform = { + repository_url = var.repository_url + repository_path = "cmd/internal/testacc/testdata/buildingblock" + ref_name = var.git_ref + terraform_version = "1.12.5" + } + } + } +} + +resource "meshstack_building_block" "noop" { depends_on = [meshstack_project.this, meshstack_workspace_user_binding.manager] - test_context = { - hub_git_ref = "main" - workspace = var.run_id - project = var.run_id - run_id = var.run_id - # The acceptance backend defines no mandatory tags. - meshstack = { - tag_schema = { mandatory = { payment_method = {} } } + wait_for_completion = true + spec = { + building_block_definition_version_ref = meshstack_building_block_definition.noop.version_latest + display_name = "${var.run_id}-noop" + inputs = {} + target_ref = { + kind = "meshWorkspace" + name = meshstack_workspace.this.metadata.name + } + } + + lifecycle { + postcondition { + condition = self.status.status == "SUCCEEDED" + error_message = "The run of building block ${self.metadata.uuid} is ${self.status.status}." } } } diff --git a/cmd/internal/testacc/testdata/buildingblock/main.tf b/cmd/internal/testacc/testdata/buildingblock/main.tf new file mode 100644 index 00000000..070bfb3c --- /dev/null +++ b/cmd/internal/testacc/testdata/buildingblock/main.tf @@ -0,0 +1,11 @@ +# The code that the tf-block-runner runs for the bootstrapped building block. It declares no backend, +# so the runner uses meshStack's state backend; the tfstate tests add one of their own. + +variable "input" { + type = string + default = null +} + +resource "terraform_data" "noop" { + input = var.input +} diff --git a/cmd/internal/testacc/testdata/module/main.tf b/cmd/internal/testacc/testdata/module/main.tf deleted file mode 100644 index 1a41578b..00000000 --- a/cmd/internal/testacc/testdata/module/main.tf +++ /dev/null @@ -1,16 +0,0 @@ -# Stands in for the noop module of meshstack-hub, which the bootstrapped building block runs: it -# keeps the one resource of that module, so that a plan against the block's state changes no resource. -# An apply with another input changes the state, as a test of concurrent writes needs. - -terraform { - backend "http" {} -} - -variable "input" { - type = string - default = null -} - -resource "terraform_data" "noop" { - input = var.input -} diff --git a/cmd/internal/testacc/tfstate_test.go b/cmd/internal/testacc/tfstate_test.go index 57fdbf36..cbd52a6e 100644 --- a/cmd/internal/testacc/tfstate_test.go +++ b/cmd/internal/testacc/tfstate_test.go @@ -33,7 +33,7 @@ func TestAccTfstate(t *testing.T) { require.NoErrorf(t, err, "the API key login did not finish:\n%s", output) block := bootstrapBuildingBlock(t, c, manager.Username) c.setEnv(envWorkspace, block.workspace) - module := copyTestdata(t, "module") + module := stateModule(t) t.Run("show writes the state that the run stored", func(t *testing.T) { output, err := c.run("", "buildingblock", "tfstate", "show", block.uuid) @@ -190,7 +190,10 @@ func bootstrapBuildingBlock(t *testing.T, c *cli, workspaceManager string) boots t.Helper() runId := "cli-" + strings.ToLower(rand.Text()[:8]) dir := copyTestdata(t, "bootstrap") - vars := []string{"-var", "profile=" + defaultProfile, "-var", "run_id=" + runId, "-var", "workspace_manager=" + workspaceManager} + vars := []string{ + "-var", "profile=" + defaultProfile, "-var", "run_id=" + runId, "-var", "workspace_manager=" + workspaceManager, + "-var", "repository_url=" + repositoryUrl, "-var", "git_ref=" + pushedHead(t), + } c.tofu(t, dir, append([]string{"init", "-input=false"}, vars...)...) t.Cleanup(func() { @@ -221,6 +224,32 @@ func bootstrapBuildingBlock(t *testing.T, c *cli, workspaceManager string) boots return bootstrappedBlock{} } +const repositoryUrl = "https://github.com/meshcloud/meshstack-cli.git" + +// pushedHead names the commit whose testdata/buildingblock the tf-block-runner runs, so that a branch +// tests its own module. The runner clones it from GitHub. +func pushedHead(t *testing.T) string { + t.Helper() + head, err := exec.CommandContext(t.Context(), "git", "rev-parse", "HEAD").Output() + require.NoError(t, err, "the tfstate tests run in a git checkout") + sha := strings.TrimSpace(string(head)) + output, err := exec.CommandContext(t.Context(), "git", "fetch", "--dry-run", "--depth=1", repositoryUrl, sha).CombinedOutput() + require.NoErrorf(t, err, "push HEAD %s first, as the tf-block-runner checks it out from %s:\n%s", sha, repositoryUrl, output) + return sha +} + +// stateModule is the module that the runner applies, with the http backend that tofu reads the +// TF_HTTP_ variables of exec into. +func stateModule(t *testing.T) string { + t.Helper() + dir := copyTestdata(t, "buildingblock") + require.NoError(t, os.WriteFile(filepath.Join(dir, "backend.tf"), []byte(`terraform { + backend "http" {} +} +`), 0o600)) + return dir +} + // copyTestdata keeps what tofu writes, .terraform and the local state, out of the source tree. func copyTestdata(t *testing.T, name string) string { t.Helper() From db62a8254baabf2be506b122fe1274ab42dd356e Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 14 Oct 2026 08:56:51 +0200 Subject: [PATCH 3/5] test: run the pull request head when the lane tests its merge commit meshfed-release tests a pull request at refs/pull/N/merge, which the tf-block-runner cannot check out: it clones only branches and tags. Co-Authored-By: Claude Opus 5.5 --- cmd/internal/testacc/tfstate_test.go | 36 ++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/cmd/internal/testacc/tfstate_test.go b/cmd/internal/testacc/tfstate_test.go index cbd52a6e..5def15e1 100644 --- a/cmd/internal/testacc/tfstate_test.go +++ b/cmd/internal/testacc/tfstate_test.go @@ -192,7 +192,7 @@ func bootstrapBuildingBlock(t *testing.T, c *cli, workspaceManager string) boots dir := copyTestdata(t, "bootstrap") vars := []string{ "-var", "profile=" + defaultProfile, "-var", "run_id=" + runId, "-var", "workspace_manager=" + workspaceManager, - "-var", "repository_url=" + repositoryUrl, "-var", "git_ref=" + pushedHead(t), + "-var", "repository_url=" + repositoryUrl, "-var", "git_ref=" + runnerRef(t), } c.tofu(t, dir, append([]string{"init", "-input=false"}, vars...)...) @@ -226,16 +226,32 @@ func bootstrapBuildingBlock(t *testing.T, c *cli, workspaceManager string) boots const repositoryUrl = "https://github.com/meshcloud/meshstack-cli.git" -// pushedHead names the commit whose testdata/buildingblock the tf-block-runner runs, so that a branch -// tests its own module. The runner clones it from GitHub. -func pushedHead(t *testing.T) string { +// runnerRef names the commit whose testdata/buildingblock the tf-block-runner runs, so that a branch +// tests its own module. The runner clones only the branches and tags of GitHub, so for the merge +// commit of a pull request, which meshfed-release's satellite lane tests, it takes the pull request's head. +func runnerRef(t *testing.T) string { t.Helper() - head, err := exec.CommandContext(t.Context(), "git", "rev-parse", "HEAD").Output() - require.NoError(t, err, "the tfstate tests run in a git checkout") - sha := strings.TrimSpace(string(head)) - output, err := exec.CommandContext(t.Context(), "git", "fetch", "--dry-run", "--depth=1", repositoryUrl, sha).CombinedOutput() - require.NoErrorf(t, err, "push HEAD %s first, as the tf-block-runner checks it out from %s:\n%s", sha, repositoryUrl, output) - return sha + head := git(t, "rev-parse", "HEAD") + remote := map[string]string{} + for line := range strings.Lines(git(t, "ls-remote", repositoryUrl)) { + sha, ref, _ := strings.Cut(strings.TrimSpace(line), "\t") + remote[ref] = sha + } + for ref, sha := range remote { + if pull, isMerge := strings.CutSuffix(ref, "/merge"); isMerge && strings.HasPrefix(ref, "refs/pull/") && sha == head { + return remote[pull+"/head"] + } + } + output, err := exec.CommandContext(t.Context(), "git", "fetch", "--dry-run", "--depth=1", repositoryUrl, head).CombinedOutput() + require.NoErrorf(t, err, "push HEAD %s first, as the tf-block-runner checks it out from %s:\n%s", head, repositoryUrl, output) + return head +} + +func git(t *testing.T, args ...string) string { + t.Helper() + output, err := exec.CommandContext(t.Context(), "git", args...).CombinedOutput() + require.NoErrorf(t, err, "git %s failed:\n%s", strings.Join(args, " "), output) + return strings.TrimSpace(string(output)) } // stateModule is the module that the runner applies, with the http backend that tofu reads the From f15863496d7ec667c938c79a654ccdc7c84d7f64 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 14 Oct 2026 09:16:50 +0200 Subject: [PATCH 4/5] test: run the runner's module with exec --override-backend The acceptance tests now write and remove the override, so its unit test keeps only the refusal to replace a file that is there already. Co-Authored-By: Claude Opus 5.5 --- .../testacc/testdata/buildingblock/main.tf | 2 +- cmd/internal/testacc/tfstate_test.go | 46 ++++++++++--------- internal/tfstate/override_test.go | 23 ++-------- 3 files changed, 30 insertions(+), 41 deletions(-) diff --git a/cmd/internal/testacc/testdata/buildingblock/main.tf b/cmd/internal/testacc/testdata/buildingblock/main.tf index 070bfb3c..a9ff3c20 100644 --- a/cmd/internal/testacc/testdata/buildingblock/main.tf +++ b/cmd/internal/testacc/testdata/buildingblock/main.tf @@ -1,5 +1,5 @@ # The code that the tf-block-runner runs for the bootstrapped building block. It declares no backend, -# so the runner uses meshStack's state backend; the tfstate tests add one of their own. +# so the runner uses meshStack's state backend, and the tfstate tests run it with exec --override-backend. variable "input" { type = string diff --git a/cmd/internal/testacc/tfstate_test.go b/cmd/internal/testacc/tfstate_test.go index 5def15e1..a56c8b63 100644 --- a/cmd/internal/testacc/tfstate_test.go +++ b/cmd/internal/testacc/tfstate_test.go @@ -16,6 +16,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/meshcloud/meshstack-cli/internal/tfstate" ) const noState = "has no state in workspace" @@ -33,7 +35,7 @@ func TestAccTfstate(t *testing.T) { require.NoErrorf(t, err, "the API key login did not finish:\n%s", output) block := bootstrapBuildingBlock(t, c, manager.Username) c.setEnv(envWorkspace, block.workspace) - module := stateModule(t) + module := copyTestdata(t, "buildingblock") t.Run("show writes the state that the run stored", func(t *testing.T) { output, err := c.run("", "buildingblock", "tfstate", "show", block.uuid) @@ -42,12 +44,13 @@ func TestAccTfstate(t *testing.T) { }) t.Run("exec --mode read runs tofu against the state", func(t *testing.T) { - output := c.exec(t, block, "read", "tofu", "-chdir="+module, "init", "-input=false") + output := c.tofuExec(t, block, "read", module, "init", "-input=false") assert.Contains(t, output, "Successfully configured the backend \"http\"") - output = c.exec(t, block, "read", "tofu", "-chdir="+module, "state", "list") + output = c.tofuExec(t, block, "read", module, "state", "list") assert.Contains(t, output, "terraform_data.noop") - c.exec(t, block, "read", "tofu", "-chdir="+module, "plan", "-input=false") + c.tofuExec(t, block, "read", module, "plan", "-input=false") + assert.NoFileExists(t, filepath.Join(module, tfstate.BackendOverrideFile)) }) t.Run("exec --mode read lets the meshstack provider read meshStack, and nothing else", func(t *testing.T) { @@ -63,7 +66,7 @@ func TestAccTfstate(t *testing.T) { }) t.Run("exec --mode readwrite applies with tofu, and leaves no lock behind", func(t *testing.T) { - output := c.exec(t, block, "readwrite", "tofu", "-chdir="+module, "apply", "-auto-approve", "-input=false") + output := c.tofuExec(t, block, "readwrite", module, "apply", "-auto-approve", "-input=false") assert.Contains(t, output, "Apply complete!") assert.Contains(t, c.forceUnlock(t, block), "has no lock") }) @@ -71,13 +74,13 @@ func TestAccTfstate(t *testing.T) { t.Run("a second exec fails on the lock that the first one holds, and names it", func(t *testing.T) { release := c.holdLock(t, block, "held-by-the-first-exec") - output, err := c.run("", "buildingblock", "tfstate", "exec", block.uuid, "--mode", "readwrite", "--", + output, err := c.run("", "buildingblock", "tfstate", "exec", block.uuid, "--mode", "readwrite", "--override-backend", "--", "tofu", "-chdir="+module, "apply", "-auto-approve", "-input=false", "-lock-timeout=1s") require.Errorf(t, err, "tofu applied while another exec held the lock:\n%s", output) assert.Contains(t, output, "Error acquiring the state lock") assert.Contains(t, output, "held-by-the-first-exec") - c.exec(t, block, "read", "tofu", "-chdir="+module, "plan", "-input=false") + c.tofuExec(t, block, "read", module, "plan", "-input=false") release(t) }) @@ -102,8 +105,11 @@ func TestAccTfstate(t *testing.T) { before := c.storedVersion(t, block) runs := make([]*cliRun, execs) for i := range runs { - runs[i] = c.start("", "buildingblock", "tfstate", "exec", block.uuid, "--mode", "readwrite", "--", - "tofu", "-chdir="+module, "apply", "-auto-approve", "-input=false", "-lock-timeout=2m", "-var", fmt.Sprintf("input=exec-%d", i)) + // --override-backend refuses to replace an override file, so each exec runs a copy. + dir := filepath.Join(t.TempDir(), fmt.Sprintf("exec-%d", i)) + require.NoError(t, os.CopyFS(dir, os.DirFS(module))) + runs[i] = c.start("", "buildingblock", "tfstate", "exec", block.uuid, "--mode", "readwrite", "--override-backend", "--", + "tofu", "-chdir="+dir, "apply", "-auto-approve", "-input=false", "-lock-timeout=2m", "-var", fmt.Sprintf("input=exec-%d", i)) } for i, run := range runs { require.NoErrorf(t, run.wait(), "exec %d failed:\n%s", i, run.output.String()) @@ -138,7 +144,7 @@ func TestAccTfstate(t *testing.T) { output, err := browser.run("", "buildingblock", "tfstate", "show", block.uuid) require.NoErrorf(t, err, "the Workspace Manager could not read the state:\n%s", output) - output = browser.exec(t, block, "readwrite", "tofu", "-chdir="+module, "apply", "-auto-approve", "-input=false") + output = browser.tofuExec(t, block, "readwrite", module, "apply", "-auto-approve", "-input=false") assert.Contains(t, output, "Apply complete!") }) @@ -254,18 +260,6 @@ func git(t *testing.T, args ...string) string { return strings.TrimSpace(string(output)) } -// stateModule is the module that the runner applies, with the http backend that tofu reads the -// TF_HTTP_ variables of exec into. -func stateModule(t *testing.T) string { - t.Helper() - dir := copyTestdata(t, "buildingblock") - require.NoError(t, os.WriteFile(filepath.Join(dir, "backend.tf"), []byte(`terraform { - backend "http" {} -} -`), 0o600)) - return dir -} - // copyTestdata keeps what tofu writes, .terraform and the local state, out of the source tree. func copyTestdata(t *testing.T, name string) string { t.Helper() @@ -289,6 +283,14 @@ func (c *cli) tofu(t *testing.T, dir string, args ...string) { require.NoErrorf(t, err, "tofu %s failed:\n%s", args[0], output) } +// tofuExec runs tofu with the backend of --override-backend, as the module of the runner has none. +func (c *cli) tofuExec(t *testing.T, block bootstrappedBlock, mode, module string, args ...string) string { + t.Helper() + output, err := c.run("", append([]string{"buildingblock", "tfstate", "exec", block.uuid, "--mode", mode, "--override-backend", "--", "tofu", "-chdir=" + module}, args...)...) + require.NoErrorf(t, err, "the exec of tofu %s failed:\n%s", strings.Join(args, " "), output) + return output +} + func (c *cli) exec(t *testing.T, block bootstrappedBlock, mode string, command ...string) string { t.Helper() output, err := c.run("", append([]string{"buildingblock", "tfstate", "exec", block.uuid, "--mode", mode, "--"}, command...)...) diff --git a/internal/tfstate/override_test.go b/internal/tfstate/override_test.go index 9cf825aa..9288200d 100644 --- a/internal/tfstate/override_test.go +++ b/internal/tfstate/override_test.go @@ -13,26 +13,13 @@ import ( func TestBackendOverride(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, BackendOverrideFile) + require.NoError(t, os.WriteFile(path, []byte("# the user's own\n"), 0o600)) - remove, err := WriteBackendOverride(dir) + _, err := WriteBackendOverride(dir) + require.ErrorContains(t, err, "exists already") + kept, err := fs.ReadFile(os.DirFS(dir), BackendOverrideFile) require.NoError(t, err) - - t.Run("adds the http backend", func(t *testing.T) { - written, err := fs.ReadFile(os.DirFS(dir), BackendOverrideFile) - require.NoError(t, err) - assert.Equal(t, backendOverride, string(written)) - }) - - t.Run("refuses to replace a file of the same name", func(t *testing.T) { - _, err := WriteBackendOverride(dir) - require.ErrorContains(t, err, "exists already") - assert.FileExists(t, path, "the file that was there stays") - }) - - t.Run("is gone once removed", func(t *testing.T) { - require.NoError(t, remove()) - assert.NoFileExists(t, path) - }) + assert.Equal(t, "# the user's own\n", string(kept)) } func TestModuleDir(t *testing.T) { From dd90a0df6b0b0187ee124f5bff3eb5719065ce20 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 14 Oct 2026 09:39:59 +0200 Subject: [PATCH 5/5] test: count the acceptance testdata into the cached suite result meshfed-release keys the cached result on the Go sources only, so a change to a .tf file under testdata alone reused a stale result. Co-Authored-By: Claude Opus 5.5 --- meshstack-satellite.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/meshstack-satellite.gradle b/meshstack-satellite.gradle index 6a9f7497..132d4238 100644 --- a/meshstack-satellite.gradle +++ b/meshstack-satellite.gradle @@ -6,6 +6,7 @@ satellite { acceptance { run = 'TestAcc' services 'meshfed-api', 'block-coordinator-api', 'building-block-runner:runners' + files 'cmd/internal/testacc/testdata/**' environment MESHSTACK_CLI_TEST_ACC: '1', MESHSTACK_CLI_NO_BROWSER: '1', MESHSTACK_API_KEY: fromBackendConfig('auth.openid.apiKeys.terraform-provider-acceptance.clientId'),