github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/initwd/from_module_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package initwd
     5  
     6  import (
     7  	"context"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  	"testing"
    12  
    13  	"github.com/google/go-cmp/cmp"
    14  	version "github.com/hashicorp/go-version"
    15  	"github.com/terramate-io/tf/configs"
    16  	"github.com/terramate-io/tf/configs/configload"
    17  	"github.com/terramate-io/tf/copy"
    18  	"github.com/terramate-io/tf/registry"
    19  	"github.com/terramate-io/tf/tfdiags"
    20  )
    21  
    22  func TestDirFromModule_registry(t *testing.T) {
    23  	if os.Getenv("TF_ACC") == "" {
    24  		t.Skip("this test accesses registry.terraform.io and github.com; set TF_ACC=1 to run it")
    25  	}
    26  
    27  	fixtureDir := filepath.Clean("testdata/empty")
    28  	tmpDir, done := tempChdir(t, fixtureDir)
    29  	defer done()
    30  
    31  	// the module installer runs filepath.EvalSymlinks() on the destination
    32  	// directory before copying files, and the resultant directory is what is
    33  	// returned by the install hooks. Without this, tests could fail on machines
    34  	// where the default temp dir was a symlink.
    35  	dir, err := filepath.EvalSymlinks(tmpDir)
    36  	if err != nil {
    37  		t.Error(err)
    38  	}
    39  	modsDir := filepath.Join(dir, ".terraform/modules")
    40  
    41  	hooks := &testInstallHooks{}
    42  
    43  	reg := registry.NewClient(nil, nil)
    44  	loader, cleanup := configload.NewLoaderForTests(t)
    45  	defer cleanup()
    46  	diags := DirFromModule(context.Background(), loader, dir, modsDir, "hashicorp/module-installer-acctest/aws//examples/main", reg, hooks)
    47  	assertNoDiagnostics(t, diags)
    48  
    49  	v := version.Must(version.NewVersion("0.0.2"))
    50  
    51  	wantCalls := []testInstallHookCall{
    52  		// The module specified to populate the root directory is not mentioned
    53  		// here, because the hook mechanism is defined to talk about descendent
    54  		// modules only and so a caller to InitDirFromModule is expected to
    55  		// produce its own user-facing announcement about the root module being
    56  		// installed.
    57  
    58  		// Note that "root" in the following examples is, confusingly, the
    59  		// label on the module block in the example we've installed here:
    60  		//     module "root" {
    61  
    62  		{
    63  			Name:        "Download",
    64  			ModuleAddr:  "root",
    65  			PackageAddr: "registry.terraform.io/hashicorp/module-installer-acctest/aws",
    66  			Version:     v,
    67  		},
    68  		{
    69  			Name:       "Install",
    70  			ModuleAddr: "root",
    71  			Version:    v,
    72  			// NOTE: This local path and the other paths derived from it below
    73  			// can vary depending on how the registry is implemented. At the
    74  			// time of writing this test, registry.terraform.io returns
    75  			// git repository source addresses and so this path refers to the
    76  			// root of the git clone, but historically the registry referred
    77  			// to GitHub-provided tar archives which meant that there was an
    78  			// extra level of subdirectory here for the typical directory
    79  			// nesting in tar archives, which would've been reflected as
    80  			// an extra segment on this path. If this test fails due to an
    81  			// additional path segment in future, then a change to the upstream
    82  			// registry might be the root cause.
    83  			LocalPath: filepath.Join(dir, ".terraform/modules/root"),
    84  		},
    85  		{
    86  			Name:       "Install",
    87  			ModuleAddr: "root.child_a",
    88  			LocalPath:  filepath.Join(dir, ".terraform/modules/root/modules/child_a"),
    89  		},
    90  		{
    91  			Name:       "Install",
    92  			ModuleAddr: "root.child_a.child_b",
    93  			LocalPath:  filepath.Join(dir, ".terraform/modules/root/modules/child_b"),
    94  		},
    95  	}
    96  
    97  	if diff := cmp.Diff(wantCalls, hooks.Calls); diff != "" {
    98  		t.Fatalf("wrong installer calls\n%s", diff)
    99  	}
   100  
   101  	loader, err = configload.NewLoader(&configload.Config{
   102  		ModulesDir: modsDir,
   103  	})
   104  	if err != nil {
   105  		t.Fatal(err)
   106  	}
   107  
   108  	// Make sure the configuration is loadable now.
   109  	// (This ensures that correct information is recorded in the manifest.)
   110  	config, loadDiags := loader.LoadConfig(".")
   111  	if assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) {
   112  		return
   113  	}
   114  
   115  	wantTraces := map[string]string{
   116  		"":                     "in example",
   117  		"root":                 "in root module",
   118  		"root.child_a":         "in child_a module",
   119  		"root.child_a.child_b": "in child_b module",
   120  	}
   121  	gotTraces := map[string]string{}
   122  	config.DeepEach(func(c *configs.Config) {
   123  		path := strings.Join(c.Path, ".")
   124  		if c.Module.Variables["v"] == nil {
   125  			gotTraces[path] = "<missing>"
   126  			return
   127  		}
   128  		varDesc := c.Module.Variables["v"].Description
   129  		gotTraces[path] = varDesc
   130  	})
   131  	assertResultDeepEqual(t, gotTraces, wantTraces)
   132  }
   133  
   134  func TestDirFromModule_submodules(t *testing.T) {
   135  	fixtureDir := filepath.Clean("testdata/empty")
   136  	fromModuleDir, err := filepath.Abs("./testdata/local-modules")
   137  	if err != nil {
   138  		t.Fatal(err)
   139  	}
   140  
   141  	// DirFromModule will expand ("canonicalize") the pathnames, so we must do
   142  	// the same for our "wantCalls" comparison values. Otherwise this test
   143  	// will fail when building in a source tree with symlinks in $PWD.
   144  	//
   145  	// See also: https://github.com/terramate-io/tf/issues/26014
   146  	//
   147  	fromModuleDirRealpath, err := filepath.EvalSymlinks(fromModuleDir)
   148  	if err != nil {
   149  		t.Error(err)
   150  	}
   151  
   152  	tmpDir, done := tempChdir(t, fixtureDir)
   153  	defer done()
   154  
   155  	hooks := &testInstallHooks{}
   156  	dir, err := filepath.EvalSymlinks(tmpDir)
   157  	if err != nil {
   158  		t.Error(err)
   159  	}
   160  	modInstallDir := filepath.Join(dir, ".terraform/modules")
   161  
   162  	loader, cleanup := configload.NewLoaderForTests(t)
   163  	defer cleanup()
   164  	diags := DirFromModule(context.Background(), loader, dir, modInstallDir, fromModuleDir, nil, hooks)
   165  	assertNoDiagnostics(t, diags)
   166  	wantCalls := []testInstallHookCall{
   167  		{
   168  			Name:       "Install",
   169  			ModuleAddr: "child_a",
   170  			LocalPath:  filepath.Join(fromModuleDirRealpath, "child_a"),
   171  		},
   172  		{
   173  			Name:       "Install",
   174  			ModuleAddr: "child_a.child_b",
   175  			LocalPath:  filepath.Join(fromModuleDirRealpath, "child_a/child_b"),
   176  		},
   177  	}
   178  
   179  	if assertResultDeepEqual(t, hooks.Calls, wantCalls) {
   180  		return
   181  	}
   182  
   183  	loader, err = configload.NewLoader(&configload.Config{
   184  		ModulesDir: modInstallDir,
   185  	})
   186  	if err != nil {
   187  		t.Fatal(err)
   188  	}
   189  
   190  	// Make sure the configuration is loadable now.
   191  	// (This ensures that correct information is recorded in the manifest.)
   192  	config, loadDiags := loader.LoadConfig(".")
   193  	if assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) {
   194  		return
   195  	}
   196  	wantTraces := map[string]string{
   197  		"":                "in root module",
   198  		"child_a":         "in child_a module",
   199  		"child_a.child_b": "in child_b module",
   200  	}
   201  	gotTraces := map[string]string{}
   202  
   203  	config.DeepEach(func(c *configs.Config) {
   204  		path := strings.Join(c.Path, ".")
   205  		if c.Module.Variables["v"] == nil {
   206  			gotTraces[path] = "<missing>"
   207  			return
   208  		}
   209  		varDesc := c.Module.Variables["v"].Description
   210  		gotTraces[path] = varDesc
   211  	})
   212  	assertResultDeepEqual(t, gotTraces, wantTraces)
   213  }
   214  
   215  // submodulesWithProvider is identical to above, except that the configuration
   216  // would fail to load for some reason. We still want the module to be installed
   217  // for use cases like testing or CDKTF, and will only emit warnings for config
   218  // errors.
   219  func TestDirFromModule_submodulesWithProvider(t *testing.T) {
   220  	fixtureDir := filepath.Clean("testdata/empty")
   221  	fromModuleDir, err := filepath.Abs("./testdata/local-module-missing-provider")
   222  	if err != nil {
   223  		t.Fatal(err)
   224  	}
   225  
   226  	tmpDir, done := tempChdir(t, fixtureDir)
   227  	defer done()
   228  
   229  	hooks := &testInstallHooks{}
   230  	dir, err := filepath.EvalSymlinks(tmpDir)
   231  	if err != nil {
   232  		t.Error(err)
   233  	}
   234  	modInstallDir := filepath.Join(dir, ".terraform/modules")
   235  
   236  	loader, cleanup := configload.NewLoaderForTests(t)
   237  	defer cleanup()
   238  	diags := DirFromModule(context.Background(), loader, dir, modInstallDir, fromModuleDir, nil, hooks)
   239  
   240  	for _, d := range diags {
   241  		if d.Severity() != tfdiags.Warning {
   242  			t.Errorf("expected warning, got %v", diags.Err())
   243  		}
   244  	}
   245  }
   246  
   247  // TestDirFromModule_rel_submodules is similar to the test above, but the
   248  // from-module is relative to the install dir ("../"):
   249  // https://github.com/terramate-io/tf/issues/23010
   250  func TestDirFromModule_rel_submodules(t *testing.T) {
   251  	// This test creates a tmpdir with the following directory structure:
   252  	// - tmpdir/local-modules (with contents of testdata/local-modules)
   253  	// - tmpdir/empty: the workDir we CD into for the test
   254  	// - tmpdir/empty/target (target, the destination for init -from-module)
   255  	tmpDir := t.TempDir()
   256  	fromModuleDir := filepath.Join(tmpDir, "local-modules")
   257  	workDir := filepath.Join(tmpDir, "empty")
   258  	if err := os.Mkdir(fromModuleDir, os.ModePerm); err != nil {
   259  		t.Fatal(err)
   260  	}
   261  	if err := copy.CopyDir(fromModuleDir, "testdata/local-modules"); err != nil {
   262  		t.Fatal(err)
   263  	}
   264  	if err := os.Mkdir(workDir, os.ModePerm); err != nil {
   265  		t.Fatal(err)
   266  	}
   267  
   268  	targetDir := filepath.Join(tmpDir, "target")
   269  	if err := os.Mkdir(targetDir, os.ModePerm); err != nil {
   270  		t.Fatal(err)
   271  	}
   272  	oldDir, err := os.Getwd()
   273  	if err != nil {
   274  		t.Fatal(err)
   275  	}
   276  	err = os.Chdir(targetDir)
   277  	if err != nil {
   278  		t.Fatalf("failed to switch to temp dir %s: %s", tmpDir, err)
   279  	}
   280  	t.Cleanup(func() {
   281  		os.Chdir(oldDir)
   282  	})
   283  
   284  	hooks := &testInstallHooks{}
   285  
   286  	modInstallDir := ".terraform/modules"
   287  	sourceDir := "../local-modules"
   288  	loader, cleanup := configload.NewLoaderForTests(t)
   289  	defer cleanup()
   290  	diags := DirFromModule(context.Background(), loader, ".", modInstallDir, sourceDir, nil, hooks)
   291  	assertNoDiagnostics(t, diags)
   292  	wantCalls := []testInstallHookCall{
   293  		{
   294  			Name:       "Install",
   295  			ModuleAddr: "child_a",
   296  			LocalPath:  filepath.Join(sourceDir, "child_a"),
   297  		},
   298  		{
   299  			Name:       "Install",
   300  			ModuleAddr: "child_a.child_b",
   301  			LocalPath:  filepath.Join(sourceDir, "child_a/child_b"),
   302  		},
   303  	}
   304  
   305  	if assertResultDeepEqual(t, hooks.Calls, wantCalls) {
   306  		return
   307  	}
   308  
   309  	loader, err = configload.NewLoader(&configload.Config{
   310  		ModulesDir: modInstallDir,
   311  	})
   312  	if err != nil {
   313  		t.Fatal(err)
   314  	}
   315  
   316  	// Make sure the configuration is loadable now.
   317  	// (This ensures that correct information is recorded in the manifest.)
   318  	config, loadDiags := loader.LoadConfig(".")
   319  	if assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) {
   320  		return
   321  	}
   322  	wantTraces := map[string]string{
   323  		"":                "in root module",
   324  		"child_a":         "in child_a module",
   325  		"child_a.child_b": "in child_b module",
   326  	}
   327  	gotTraces := map[string]string{}
   328  
   329  	config.DeepEach(func(c *configs.Config) {
   330  		path := strings.Join(c.Path, ".")
   331  		if c.Module.Variables["v"] == nil {
   332  			gotTraces[path] = "<missing>"
   333  			return
   334  		}
   335  		varDesc := c.Module.Variables["v"].Description
   336  		gotTraces[path] = varDesc
   337  	})
   338  	assertResultDeepEqual(t, gotTraces, wantTraces)
   339  }