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 }