Repository: opentofu/opentofu
Pull request #2651: Add remote sources to module on tofu test
Author: login: diofeher
Created: 2025-03-31T13:22:02Z | Merged: 2025-04-24T17:02:23Z

Resolves #2419 
Resolves #2418

By removing the restriction on remote module repositories, we're able to use everything that module sources support, like generic git URLs, bitbucket and mercurial URLs.

The original [PR (pre-fork and MPL)](https://github.com/hashicorp/terraform/pull/33456) doesn't mention why it doesn't support remote test sources, it just says that [it's not supported](https://github.com/hashicorp/terraform/pull/33456/files#diff-eb61f09ed17a251bb70a77789c6af4199be25d722c20789d245f149f573f1bbfR377). I assumed they didn't fully test and didn't want to add support and maybe add for a future iteration, but while doing the tests, all of them worked fine without adding any additional code.

Tested with:

- [x]  Git generic URL (git@github.com:diofeher/terraform-module-test.git)
- [x] Github URL (github.com/diofeher/terraform-module-test)
- [x] Bitbucket URL (bitbucket.org/PSF/terraform-module-test)
- [x] Mercurial URL (hg::ssh://diofeher@hg.code.sf.net/p/terraform-module-test/code1)
- [x] HTTP URL (local URL)
- [x] S3 Bucket (private URL)
- [x] GCS Bucket (private URL)

## Target Release

1.10.0

## Checklist

<!-- Please check of ALL items in this list for all PRs: -->

- [x] I have read the [contribution guide](https://github.com/opentofu/opentofu/blob/main/CONTRIBUTING.md).
- [x] I have not used an AI coding assistant to create this PR.
- [x] I have written all code in this PR myself OR I have marked all code I have not written myself (including modified code, e.g. copied from other places and then modified) with a comment indicating where it came from.
- [x] I (and other contributors to this PR) have not looked at the Terraform source code while implementing this PR.

### Go checklist

<!-- If your PR contains Go code, please make sure you check off all items on this list: --> 

- [x] I have run golangci-lint on my change and receive no errors relevant to my code.
- [x] I have run existing tests to ensure my code doesn't break anything.
- [x] I have added tests for all relevant use cases of my code, and those tests are passing.
- [x] I have only exported functions, variables and structs that should be used from other packages.
- [x] I have added meaningful comments to all exported functions, variables, and structs.

### Website/documentation checklist

<!-- If you have changed the website, please follow this checklist: -->

- [x] I have locally started the website as [described here](https://github.com/opentofu/opentofu/blob/main/website/README.md) and checked my changes.


Linked issue #2419: Tofu test: support Git module sources
### OpenTofu Version

```shell
1.9.0
```

### The problem in your OpenTofu project

`tofu test` `module.source` attribute only supports "local" and "registry" (despite [documentation saying otherwise](https://opentofu.org/docs/cli/commands/test/#testing-modules), see [bug report](https://github.com/opentofu/opentofu/issues/2418)).

Add the ability to use "git" module sources in `tofu test` `module.source`.

See equivalent Terraform feature request: https://github.com/hashicorp/terraform/issues/36271

### Attempted Solutions

The only solution, for both Tofu and Terraform, is wrapping the modules local to the directory to be tested. This does not scale and doubles maintenance. 

### Proposal

_No response_

### References

_No response_
Linked issue comments: user: login: apparentlymart
created_at: 2025-01-24T18:23:09Z
body: Hi @Mjb141! Thanks for opening this issue.

I think what you've captured here is essentially just a different framing for what you also recorded in https://github.com/opentofu/opentofu/issues/2418, so let's close this one and use the other issue to decide how we'll respond to this inconsistency.

As you noted over there, we could potentially resolve this either by changing the functionality to match the docs or by changing the docs to match the functionality. While that decision still needs to be made, I'm closing this just to make sure we can discuss that in only one place and not end up having two separate discussions about the same topic.

Thanks again!

author_association: CONTRIBUTOR
user: login: ghost
created_at: 2025-01-29T11:17:36Z
body: Reopening this as #2418 will now track the documentation fix whereas this is the actual missing implementation.
author_association: NONE
user: login: KarstenSiemer
created_at: 2025-03-14T14:17:12Z
body: this fix would be greatly appreciated and would certainly push the useability of the tests forward by a lot
author_association: NONE
user: login: diofeher
created_at: 2025-04-07T14:14:49Z
body: The original [PR (pre-fork and MPL)](https://github.com/hashicorp/terraform/pull/33456) says [remote sources aren't supported](https://github.com/hashicorp/terraform/pull/33456/files#diff-eb61f09ed17a251bb70a77789c6af4199be25d722c20789d245f149f573f1bbfR377), but don't give a reason why. Assuming they didn't fully test and didn't want to add support and maybe add for a future iteration, but while removing this restriction while testing the feature, all of them worked fine without adding any additional code.

The pull request above is for removing that restriction https://github.com/opentofu/opentofu/pull/2651.
author_association: MEMBER
user: login: cam72cam
created_at: 2025-04-23T12:52:22Z
body: Accepting based on the conversation in #2651 
author_association: MEMBER

Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f2e2e2c6193..cc441e25b81 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -34,6 +34,7 @@ ENHANCEMENTS:
 * Remove progress messages from commands using -concise argument ([#2549](https://github.com/opentofu/opentofu/issues/2549))
 * Upgrade aws-sdk version to include `mx-central-1` region. ([#2596](https://github.com/opentofu/opentofu/pull/2596))
 * When installing a provider from a source that offers a `.zip` archive of a provider package but that cannot also offer a signed set of official checksums for the provider, OpenTofu will now include its locally-verified zip archive checksum (`zh:` scheme) in the dependency lock file in addition to the package contents checksum (`h1:` checksum) previously recorded. This makes it more likely that a future reinstall of the same package from a different source will be verified successfully. ([#2656](https://github.com/opentofu/opentofu/pull/2656))
+* Remove restriction on test module sources now allowing all source types for modules during tests ([#2651]https://github.com/opentofu/opentofu/pull/2651)
 
 BUG FIXES:
 
diff --git a/internal/configs/test_file.go b/internal/configs/test_file.go
index 3087a0d0b67..1f813d78949 100644
--- a/internal/configs/test_file.go
+++ b/internal/configs/test_file.go
@@ -157,8 +157,8 @@ type TestRun struct {
 	// Module defines an address of another module that should be loaded and
 	// executed as part of this run block instead of the module under test.
 	//
-	// In the initial version of the testing framework we will only support
-	// loading alternate modules from local directories or the registry.
+	// We support loading from all the module sources, like local directories, registry,
+	// generic git repos, github, bitbucket, s3 and gcs repositories.
 	Module *TestRunModuleCall
 
 	// ConfigUnderTest describes the configuration this run block should execute
@@ -716,19 +716,6 @@ func decodeTestRunModuleBlock(block *hcl.Block) (*TestRunModuleCall, hcl.Diagnos
 					}
 				}
 			}
-
-			switch module.Source.(type) {
-			case addrs.ModuleSourceRemote:
-				// We only support local or registry modules when loading
-				// modules directly from alternate sources during a test
-				// execution.
-				diags = append(diags, &hcl.Diagnostic{
-					Severity: hcl.DiagError,
-					Summary:  "Invalid module source address",
-					Detail:   "Only local or registry module sources are currently supported from within test run blocks.",
-					Subject:  module.SourceDeclRange.Ptr(),
-				})
-			}
 		}
 	} else {
 		// Must have a source attribute.
diff --git a/internal/configs/test_file_test.go b/internal/configs/test_file_test.go
index 3105c12e88d..3f2a05b9db1 100644
--- a/internal/configs/test_file_test.go
+++ b/internal/configs/test_file_test.go
@@ -6,11 +6,13 @@
 package configs
 
 import (
+	"fmt"
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
 	"github.com/hashicorp/hcl/v2"
 	"github.com/hashicorp/hcl/v2/hclsyntax"
+	"github.com/hashicorp/hcl/v2/hcltest"
 )
 
 func TestTestRun_Validate(t *testing.T) {
@@ -97,3 +99,82 @@ func parseTraversal(t *testing.T, addr string) hcl.Traversal {
 	}
 	return traversal
 }
+
+func assertDiagsSummaryMatch(t *testing.T, want hcl.Diagnostics, got hcl.Diagnostics) {
+	t.Helper()
+
+	for i := range want {
+		if want[i].Summary != got[i].Summary {
+			t.Errorf("wanted %s as summary, got %s instead", want[i].Summary, got[i].Summary)
+		}
+	}
+}
+
+func TestDecodeTestRunModuleBlock(t *testing.T) {
+	tcs := map[string]struct {
+		inputModuleSource string
+		wantModuleSource string
+		expectedDiags hcl.Diagnostics
+	}{
+		"invalid": {
+			inputModuleSource: "hg",
+			wantModuleSource: "",
+			expectedDiags: hcl.Diagnostics{
+				{
+					Summary: "Invalid module source address",
+				},
+			},
+		},
+		"generic_git_url": {
+			inputModuleSource: "git@github.com:opentofu/terraform-module-test.git",
+			wantModuleSource: "git::ssh://git@github.com/opentofu/terraform-module-test.git",
+			expectedDiags: nil,
+		},
+		"github_url": {
+			inputModuleSource: "github.com/opentofu/terraform-module-test",
+			wantModuleSource: "git::https://github.com/opentofu/terraform-module-test.git",
+			expectedDiags: nil,
+		},
+	}
+
+	for name, tc := range tcs {
+		t.Run(name, func(t *testing.T) {
+			pos := hcl.Pos{Line: 1, Column: 1}
+			exprName := fmt.Sprintf("\"%s\"", tc.inputModuleSource)
+			expr, _ :=  hclsyntax.ParseExpression([]byte(exprName), "", pos)
+
+			block := &hcl.Block{
+				Type: "module",
+				Body: hcltest.MockBody(&hcl.BodyContent{
+					Attributes: hcl.Attributes{
+						"source": {
+							Name: "source",
+							Expr: expr,
+						},
+					},
+				}),
+				DefRange: blockRange,
+			}
+
+			trcm, diags := decodeTestRunModuleBlock(block)
+
+			if tc.expectedDiags != nil || diags != nil {
+				assertDiagsSummaryMatch(t, tc.expectedDiags, diags)
+				return
+			}
+
+			if len(diags) > 1 {
+				t.Fatalf("not expecting errors, but got: %d", len(diags))
+			}
+
+			if trcm.Source == nil {
+				t.Fatalf("was expecting to have a source, but did not: %d", trcm.Source)
+			}
+
+
+			if trcm.Source.String() != tc.wantModuleSource  {
+				t.Fatalf("got %#v; want %#v", trcm.Source.String(), tc.wantModuleSource)
+			}
+		})
+	}
+}
diff --git a/website/docs/cli/commands/test/index.mdx b/website/docs/cli/commands/test/index.mdx
index cf7cd5bf8c1..00c116efded 100644
--- a/website/docs/cli/commands/test/index.mdx
+++ b/website/docs/cli/commands/test/index.mdx
@@ -237,7 +237,8 @@ run "test" {
 
 The `module` block has the following two attributes:
 
-* The `source` attribute points to the [directory of the module](../../../language/modules/sources/#local-paths) or to the [module registry](../../../language/modules/sources/#module-registry). Remote sources are not allowed at this point.
+* The `source` attribute points to the directory of the module to load or any other
+  [module source](../../../language/modules/sources.mdx).
 * The `version` specifies the version of the module you want to use.
 
 :::warning Note


Reviews:
user: login: yottta
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T12:17:22Z
user: login: diofeher
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T14:31:03Z
user: login: ollevche
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T09:45:10Z
user: login: diofeher
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T13:22:17Z
user: login: cam72cam
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-07T12:37:44Z
user: login: cam72cam
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-23T12:51:10Z
user: login: apparentlymart
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T15:54:59Z

Review comments:
diff_hunk: @@ -32,7 +32,7 @@ ENHANCEMENTS:
 * Provider instance keys now automatically converted to string ([#2378](https://github.com/opentofu/opentofu/issues/2378))
 * Remove progress messages from commands using -concise argument ([#2549](https://github.com/opentofu/opentofu/issues/2549))
 * Upgrade aws-sdk version to include `mx-central-1` region. ([#2596](https://github.com/opentofu/opentofu/pull/2596))
-
+* Adding new remote module sources for tofu test ([#2651]https://github.com/opentofu/opentofu/pull/2651)
path: CHANGELOG.md
user: login: yottta
body: `nitpick`
Not really sure about this, but would rework this to a more clear one to the users.
```suggestion
* Remove restriction on test module sources now allowing all source types for modules during tests ([#2651]https://github.com/opentofu/opentofu/pull/2651)
```
created_at: 2025-04-01T12:17:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,7 +32,7 @@ ENHANCEMENTS:
 * Provider instance keys now automatically converted to string ([#2378](https://github.com/opentofu/opentofu/issues/2378))
 * Remove progress messages from commands using -concise argument ([#2549](https://github.com/opentofu/opentofu/issues/2549))
 * Upgrade aws-sdk version to include `mx-central-1` region. ([#2596](https://github.com/opentofu/opentofu/pull/2596))
-
+* Adding new remote module sources for tofu test ([#2651]https://github.com/opentofu/opentofu/pull/2651)
path: CHANGELOG.md
user: login: diofeher
body: That's a good idea, thanks!
created_at: 2025-04-02T14:31:03Z
author_association: MEMBER
diff_hunk: @@ -97,3 +99,82 @@ func parseTraversal(t *testing.T, addr string) hcl.Traversal {
 	}
 	return traversal
 }
+
+func assertDiagnosticsMatch(t *testing.T, want hcl.Diagnostics, got hcl.Diagnostics) {
+	t.Helper()
+
+	for i := range(want) {
path: internal/configs/test_file_test.go
user: login: ollevche
body: do we really need parentheses here?
created_at: 2025-04-03T09:45:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -97,3 +99,82 @@ func parseTraversal(t *testing.T, addr string) hcl.Traversal {
 	}
 	return traversal
 }
+
+func assertDiagnosticsMatch(t *testing.T, want hcl.Diagnostics, got hcl.Diagnostics) {
+	t.Helper()
+
+	for i := range(want) {
path: internal/configs/test_file_test.go
user: login: diofeher
body: Oh, we don't! Sorry about that, didn't notice lol gonna remove it!
created_at: 2025-04-04T13:22:17Z
author_association: MEMBER
diff_hunk: @@ -97,3 +99,82 @@ func parseTraversal(t *testing.T, addr string) hcl.Traversal {
 	}
 	return traversal
 }
+
+func assertDiagnosticsMatch(t *testing.T, want hcl.Diagnostics, got hcl.Diagnostics) {
path: internal/configs/test_file_test.go
user: login: cam72cam
body: Either move this local to the function it's used in or improve the name as it only compares the Summary field.
created_at: 2025-04-07T12:37:44Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-31T13:22:15Z
body: Reminder for the PR assignee: If this is a user-visible change, please update the changelog as part of the PR.
author_association: NONE
user: login: ollevche
created_at: 2025-04-03T10:02:55Z
body: Hey @diofeher! Could you please summarize your findings regarding the initial reasoning of this restriction under https://github.com/opentofu/opentofu/issues/2419? This is for documentation reasons, so we can revisit the context later on if needed.
author_association: CONTRIBUTOR
user: login: diofeher
created_at: 2025-04-04T13:21:05Z
body: @ollevche should I put that on the PR description or in the files itself?
author_association: MEMBER
user: login: apparentlymart
created_at: 2025-04-07T16:18:37Z
body: I also don't have any specific information on why this wasn't supported in the original implementation, but there is something interesting about the design of the test scenario language that might be a reason to be careful here.

In the main OpenTofu language, each module has its own entirely-separate namespace and objects pass from parent module to child (and vice-versa) only in specific ways:

- Values can pass in both directions using input variables and output values. In this case, both parent and child module can have their own copy of the same value but those two values are completely independent. A module explicitly defines which input variables it expects and which output values it returns.
- References to provider instances can pass either implicitly (automatic inheritance) or explicitly via the special `providers` argument in a `module` block.

However, the relationship between a test scenario file and a module it refers to in a `run` block's `module` block is _not_ fully encapsulated:

- `variable` blocks in the test scenario file get directly merged with the `variable` blocks defined in the specified module.
- `provider` blocks in the test scenario file get directly merged into the specified module.

The second of those is _already_ causing an encapsulation problem as described in https://github.com/opentofu/opentofu/issues/2603: the directory containing the test scenario file is treated as if it has its own provider requirements for the purposes of dependency installation, but it's _not_ treated as separate at runtime.

If the directory containing the test scenario files and the directory containing the module being used by the test live together in the same module package then it's perhaps defensible to say that the authors of both can be expected to keep the two consistent.

However, if a test scenario refers to a module from an entirely different module package that's maintained by someone else then suddenly that remote module is subjected to at least two additional compatibility constraints that weren't previously true:

1. It's no longer safe to add a new `variable` block to that module, because it might conflict with a `variable` block in a test scenario that depends on that module.
2. It's no longer safe to change the "local names" used for providers in the module, because that could cause a `provider` block in a test scenario to be treated as configuring an entirely different provider.

Unfortunately I think we might need to figure out what we want to change for https://github.com/opentofu/opentofu/issues/2603 first _and_ make that change before we make it valid for a test scenario to directly depend on an external module, because currently the relationship between a test scenario and the module it refers to is not well-defined or properly encapsulated, and so it's dangerous to encourage a test scenario to depend on a module that won't necessarily always update in lockstep with the test scenario. :confounded: 

author_association: CONTRIBUTOR
user: login: cam72cam
created_at: 2025-04-07T20:20:29Z
body: Thank you @apparentlymart, it seemed fairly deliberate that it was excluded previously.  I guess my question to @diofeher is, what does allowing this add as a user and is that complexity worth the effort?
author_association: MEMBER
user: login: diofeher
created_at: 2025-04-14T18:29:09Z
body: @cam72cam From my understanding, this feature  would allow creating remote helper modules that can be used to enhance the tests, without needing to copy functions locally. I'm still researching and understanding how they interact with each other, so I'll update this thread as soon as I have more understanding.
author_association: MEMBER
user: login: cam72cam
created_at: 2025-04-15T12:35:15Z
body: @diofeher for now, my preference is to close this PR and instead update the documentation to clarify that this is not supported.  I'm not convinced that we understand the testing feature and it's implications well enough to make this decision at this point.
author_association: MEMBER
user: login: diofeher
created_at: 2025-04-15T13:12:02Z
body: @cam72cam can we update the docs and put this one as a draft as we get more understanding of this issue?
author_association: MEMBER
user: login: apparentlymart
created_at: 2025-04-16T20:24:59Z
body: Notwithstanding my earlier comment, the PRs to update the docs made me notice that it apparently already supports installing from a registry-style source address, which is _effectively_ the same as supporting installation from remote-style source addresses directly because a registry is really just an extra layer of indirection over remote-style source addresses. For example, the main OpenTofu Registry is really just an index of modules from GitHub, and so it returns `git::` source addresses that OpenTofu then treats the same way as if they'd been written directly in the `source` argument.

So adding direct use of remote sources alongside the existing support for registry-style source addresses should not cause the problems I described in my previous comment because we already _have_ those problems for any module that's been published in a module registry. :confounded: 

author_association: CONTRIBUTOR
user: login: diofeher
created_at: 2025-04-16T22:42:02Z
body: @apparentlymart that's true! When I was testing I thought only local modules were being supported so I was surprised when I saw registry-style support was available, but I thought it could be ok since we would be using things only in the "registry" (although someone could point to another registry).

Now that we know that, do you think it's ok to remove the remote source constraint? For me, the way to move forward would be removing the restriction and creating an issue to research the implications of having this behavior interacting with remote stuff (security and feature-wise).
author_association: MEMBER
user: login: diofeher
created_at: 2025-04-24T15:52:28Z
body: @apparentlymart are you ok on merging this one?
author_association: MEMBER