opentofu/internal/initwd/from_module_test.go
Alisdair McDiarmid 8df065a2fe initwd: Switch from earlyconfig to configs
This is a mostly mechanical refactor with a handful of changes which
are necessary due to the semantic difference between earlyconfig and
configs.

When parsing root and descendant modules in the module installer, we now
check the core version requirements inline. If the Terraform version is
incompatible, we drop any other module loader diagnostics. This ensures
that future language additions don't clutter the output and confuse the
user.

We also add two new checks during the module load process:

* Don't try to load a module with a `nil` source address. This is a
  necessary change due to the move away from earlyconfig.

* Don't try to load a module with a blank name (i.e. `module ""`).
  Because our module loading manifest uses the stringified module path
  as its map key, this causes a collision with the root module, and a
  later panic. This is the bug which triggered this refactor in the
  first place.
2023-03-06 09:14:28 -05:00

305 lines
9.1 KiB
Go

package initwd
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configload"
"github.com/hashicorp/terraform/internal/copy"
"github.com/hashicorp/terraform/internal/registry"
"github.com/hashicorp/terraform/internal/tfdiags"
)
func TestDirFromModule_registry(t *testing.T) {
if os.Getenv("TF_ACC") == "" {
t.Skip("this test accesses registry.terraform.io and github.com; set TF_ACC=1 to run it")
}
fixtureDir := filepath.Clean("testdata/empty")
tmpDir, done := tempChdir(t, fixtureDir)
defer done()
// the module installer runs filepath.EvalSymlinks() on the destination
// directory before copying files, and the resultant directory is what is
// returned by the install hooks. Without this, tests could fail on machines
// where the default temp dir was a symlink.
dir, err := filepath.EvalSymlinks(tmpDir)
if err != nil {
t.Error(err)
}
modsDir := filepath.Join(dir, ".terraform/modules")
hooks := &testInstallHooks{}
reg := registry.NewClient(nil, nil)
loader, cleanup := configload.NewLoaderForTests(t)
defer cleanup()
diags := DirFromModule(context.Background(), loader, dir, modsDir, "hashicorp/module-installer-acctest/aws//examples/main", reg, hooks)
assertNoDiagnostics(t, diags)
v := version.Must(version.NewVersion("0.0.2"))
wantCalls := []testInstallHookCall{
// The module specified to populate the root directory is not mentioned
// here, because the hook mechanism is defined to talk about descendent
// modules only and so a caller to InitDirFromModule is expected to
// produce its own user-facing announcement about the root module being
// installed.
// Note that "root" in the following examples is, confusingly, the
// label on the module block in the example we've installed here:
// module "root" {
{
Name: "Download",
ModuleAddr: "root",
PackageAddr: "registry.terraform.io/hashicorp/module-installer-acctest/aws",
Version: v,
},
{
Name: "Install",
ModuleAddr: "root",
Version: v,
// NOTE: This local path and the other paths derived from it below
// can vary depending on how the registry is implemented. At the
// time of writing this test, registry.terraform.io returns
// git repository source addresses and so this path refers to the
// root of the git clone, but historically the registry referred
// to GitHub-provided tar archives which meant that there was an
// extra level of subdirectory here for the typical directory
// nesting in tar archives, which would've been reflected as
// an extra segment on this path. If this test fails due to an
// additional path segment in future, then a change to the upstream
// registry might be the root cause.
LocalPath: filepath.Join(dir, ".terraform/modules/root"),
},
{
Name: "Install",
ModuleAddr: "root.child_a",
LocalPath: filepath.Join(dir, ".terraform/modules/root/modules/child_a"),
},
{
Name: "Install",
ModuleAddr: "root.child_a.child_b",
LocalPath: filepath.Join(dir, ".terraform/modules/root/modules/child_b"),
},
}
if diff := cmp.Diff(wantCalls, hooks.Calls); diff != "" {
t.Fatalf("wrong installer calls\n%s", diff)
}
loader, err = configload.NewLoader(&configload.Config{
ModulesDir: modsDir,
})
if err != nil {
t.Fatal(err)
}
// Make sure the configuration is loadable now.
// (This ensures that correct information is recorded in the manifest.)
config, loadDiags := loader.LoadConfig(".")
if assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) {
return
}
wantTraces := map[string]string{
"": "in example",
"root": "in root module",
"root.child_a": "in child_a module",
"root.child_a.child_b": "in child_b module",
}
gotTraces := map[string]string{}
config.DeepEach(func(c *configs.Config) {
path := strings.Join(c.Path, ".")
if c.Module.Variables["v"] == nil {
gotTraces[path] = "<missing>"
return
}
varDesc := c.Module.Variables["v"].Description
gotTraces[path] = varDesc
})
assertResultDeepEqual(t, gotTraces, wantTraces)
}
func TestDirFromModule_submodules(t *testing.T) {
fixtureDir := filepath.Clean("testdata/empty")
fromModuleDir, err := filepath.Abs("./testdata/local-modules")
if err != nil {
t.Fatal(err)
}
// DirFromModule will expand ("canonicalize") the pathnames, so we must do
// the same for our "wantCalls" comparison values. Otherwise this test
// will fail when building in a source tree with symlinks in $PWD.
//
// See also: https://github.com/hashicorp/terraform/issues/26014
//
fromModuleDirRealpath, err := filepath.EvalSymlinks(fromModuleDir)
if err != nil {
t.Error(err)
}
tmpDir, done := tempChdir(t, fixtureDir)
defer done()
hooks := &testInstallHooks{}
dir, err := filepath.EvalSymlinks(tmpDir)
if err != nil {
t.Error(err)
}
modInstallDir := filepath.Join(dir, ".terraform/modules")
loader, cleanup := configload.NewLoaderForTests(t)
defer cleanup()
diags := DirFromModule(context.Background(), loader, dir, modInstallDir, fromModuleDir, nil, hooks)
assertNoDiagnostics(t, diags)
wantCalls := []testInstallHookCall{
{
Name: "Install",
ModuleAddr: "child_a",
LocalPath: filepath.Join(fromModuleDirRealpath, "child_a"),
},
{
Name: "Install",
ModuleAddr: "child_a.child_b",
LocalPath: filepath.Join(fromModuleDirRealpath, "child_a/child_b"),
},
}
if assertResultDeepEqual(t, hooks.Calls, wantCalls) {
return
}
loader, err = configload.NewLoader(&configload.Config{
ModulesDir: modInstallDir,
})
if err != nil {
t.Fatal(err)
}
// Make sure the configuration is loadable now.
// (This ensures that correct information is recorded in the manifest.)
config, loadDiags := loader.LoadConfig(".")
if assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) {
return
}
wantTraces := map[string]string{
"": "in root module",
"child_a": "in child_a module",
"child_a.child_b": "in child_b module",
}
gotTraces := map[string]string{}
config.DeepEach(func(c *configs.Config) {
path := strings.Join(c.Path, ".")
if c.Module.Variables["v"] == nil {
gotTraces[path] = "<missing>"
return
}
varDesc := c.Module.Variables["v"].Description
gotTraces[path] = varDesc
})
assertResultDeepEqual(t, gotTraces, wantTraces)
}
// TestDirFromModule_rel_submodules is similar to the test above, but the
// from-module is relative to the install dir ("../"):
// https://github.com/hashicorp/terraform/issues/23010
func TestDirFromModule_rel_submodules(t *testing.T) {
// This test creates a tmpdir with the following directory structure:
// - tmpdir/local-modules (with contents of testdata/local-modules)
// - tmpdir/empty: the workDir we CD into for the test
// - tmpdir/empty/target (target, the destination for init -from-module)
tmpDir := t.TempDir()
fromModuleDir := filepath.Join(tmpDir, "local-modules")
workDir := filepath.Join(tmpDir, "empty")
if err := os.Mkdir(fromModuleDir, os.ModePerm); err != nil {
t.Fatal(err)
}
if err := copy.CopyDir(fromModuleDir, "testdata/local-modules"); err != nil {
t.Fatal(err)
}
if err := os.Mkdir(workDir, os.ModePerm); err != nil {
t.Fatal(err)
}
targetDir := filepath.Join(tmpDir, "target")
if err := os.Mkdir(targetDir, os.ModePerm); err != nil {
t.Fatal(err)
}
oldDir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
err = os.Chdir(targetDir)
if err != nil {
t.Fatalf("failed to switch to temp dir %s: %s", tmpDir, err)
}
t.Cleanup(func() {
os.Chdir(oldDir)
})
hooks := &testInstallHooks{}
modInstallDir := ".terraform/modules"
sourceDir := "../local-modules"
loader, cleanup := configload.NewLoaderForTests(t)
defer cleanup()
diags := DirFromModule(context.Background(), loader, ".", modInstallDir, sourceDir, nil, hooks)
assertNoDiagnostics(t, diags)
wantCalls := []testInstallHookCall{
{
Name: "Install",
ModuleAddr: "child_a",
LocalPath: filepath.Join(sourceDir, "child_a"),
},
{
Name: "Install",
ModuleAddr: "child_a.child_b",
LocalPath: filepath.Join(sourceDir, "child_a/child_b"),
},
}
if assertResultDeepEqual(t, hooks.Calls, wantCalls) {
return
}
loader, err = configload.NewLoader(&configload.Config{
ModulesDir: modInstallDir,
})
if err != nil {
t.Fatal(err)
}
// Make sure the configuration is loadable now.
// (This ensures that correct information is recorded in the manifest.)
config, loadDiags := loader.LoadConfig(".")
if assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) {
return
}
wantTraces := map[string]string{
"": "in root module",
"child_a": "in child_a module",
"child_a.child_b": "in child_b module",
}
gotTraces := map[string]string{}
config.DeepEach(func(c *configs.Config) {
path := strings.Join(c.Path, ".")
if c.Module.Variables["v"] == nil {
gotTraces[path] = "<missing>"
return
}
varDesc := c.Module.Variables["v"].Description
gotTraces[path] = varDesc
})
assertResultDeepEqual(t, gotTraces, wantTraces)
}