internal/initwd: fix panics with relative submodules in DirFromModule (#25250)

* internal/initwd: fix panics with relative submodules in DirFromModule

There were two related issues here:

1. panic with any local module with submodules
1. panic with a relative directory that was above the workdir ("../")

The first panic was caused by the local installer looking up the root
module with the (nonexistant) key "root.", instead of "".

The second panic was caused by the installer trying to determine the
relative path from ".". This was fixed by detecting "." as the source
path and using the absolute path for the call to filepath.Rel.

Added test cases for both panics and updated the existing e2e tests with
the correct install paths.
This commit is contained in:
Kristin Laemmert 2020-06-17 13:24:56 -04:00 committed by GitHub
parent 1a43187e49
commit 1b8f4566fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 214 additions and 18 deletions

View File

@ -254,21 +254,52 @@ func DirFromModule(rootDir, modulesDir, sourceAddr string, reg *registry.Client,
var parentKey string var parentKey string
if lastDot := strings.LastIndexByte(newKey, '.'); lastDot != -1 { if lastDot := strings.LastIndexByte(newKey, '.'); lastDot != -1 {
parentKey = newKey[:lastDot] parentKey = newKey[:lastDot]
} else {
parentKey = "" // parent is the root module
} }
parentOld := instManifest[initFromModuleRootKeyPrefix+parentKey] var parentOld modsdir.Record
// "" is the root module; all other modules get `root.` added as a prefix
if parentKey == "" {
parentOld = instManifest[parentKey]
} else {
parentOld = instManifest[initFromModuleRootKeyPrefix+parentKey]
}
parentNew := retManifest[parentKey] parentNew := retManifest[parentKey]
// We need to figure out which portion of our directory is the // We need to figure out which portion of our directory is the
// parent package path and which portion is the subdirectory // parent package path and which portion is the subdirectory
// under that. // under that.
baseDirRel, err := filepath.Rel(parentOld.Dir, record.Dir) var baseDirRel string
baseDirRel, err = filepath.Rel(parentOld.Dir, record.Dir)
if err != nil { if err != nil {
// Should never happen, because we constructed both directories // This error may occur when installing a local module with a
// from the same base and so they must have a common prefix. // relative path, for e.g. if the source is in a directory above
panic(err) // the destination ("../")
if parentOld.Dir == "." {
absDir, err := filepath.Abs(parentOld.Dir)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to determine module install directory",
fmt.Sprintf("Error determine relative source directory for module %s: %s.", newKey, err),
))
continue
}
baseDirRel, err = filepath.Rel(absDir, record.Dir)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to determine relative module source location",
fmt.Sprintf("Error determining relative source for module %s: %s.", newKey, err),
))
continue
}
} else {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to determine relative module source location",
fmt.Sprintf("Error determining relative source for module %s: %s.", newKey, err),
))
}
} }
newDir := filepath.Join(parentNew.Dir, baseDirRel) newDir := filepath.Join(parentNew.Dir, baseDirRel)

View File

@ -1,6 +1,8 @@
package initwd package initwd
import ( import (
"fmt"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -9,6 +11,7 @@ import (
version "github.com/hashicorp/go-version" version "github.com/hashicorp/go-version"
"github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/configs/configload" "github.com/hashicorp/terraform/configs/configload"
"github.com/hashicorp/terraform/internal/copydir"
"github.com/hashicorp/terraform/registry" "github.com/hashicorp/terraform/registry"
"github.com/hashicorp/terraform/tfdiags" "github.com/hashicorp/terraform/tfdiags"
) )
@ -20,6 +23,7 @@ func TestDirFromModule_registry(t *testing.T) {
fixtureDir := filepath.Clean("testdata/empty") fixtureDir := filepath.Clean("testdata/empty")
tmpDir, done := tempChdir(t, fixtureDir) tmpDir, done := tempChdir(t, fixtureDir)
defer done()
// the module installer runs filepath.EvalSymlinks() on the destination // the module installer runs filepath.EvalSymlinks() on the destination
// directory before copying files, and the resultant directory is what is // directory before copying files, and the resultant directory is what is
@ -30,7 +34,6 @@ func TestDirFromModule_registry(t *testing.T) {
t.Error(err) t.Error(err)
} }
modsDir := filepath.Join(dir, ".terraform/modules") modsDir := filepath.Join(dir, ".terraform/modules")
defer done()
hooks := &testInstallHooks{} hooks := &testInstallHooks{}
@ -38,7 +41,7 @@ func TestDirFromModule_registry(t *testing.T) {
diags := DirFromModule(dir, modsDir, "hashicorp/module-installer-acctest/aws//examples/main", reg, hooks) diags := DirFromModule(dir, modsDir, "hashicorp/module-installer-acctest/aws//examples/main", reg, hooks)
assertNoDiagnostics(t, diags) assertNoDiagnostics(t, diags)
v := version.Must(version.NewVersion("0.0.1")) v := version.Must(version.NewVersion("0.0.2"))
wantCalls := []testInstallHookCall{ wantCalls := []testInstallHookCall{
// The module specified to populate the root directory is not mentioned // The module specified to populate the root directory is not mentioned
@ -61,17 +64,17 @@ func TestDirFromModule_registry(t *testing.T) {
Name: "Install", Name: "Install",
ModuleAddr: "root", ModuleAddr: "root",
Version: v, Version: v,
LocalPath: filepath.Join(dir, ".terraform/modules/root/hashicorp-terraform-aws-module-installer-acctest-5e87aff"), LocalPath: filepath.Join(dir, fmt.Sprintf(".terraform/modules/root/terraform-aws-module-installer-acctest-%s", v)),
}, },
{ {
Name: "Install", Name: "Install",
ModuleAddr: "root.child_a", ModuleAddr: "root.child_a",
LocalPath: filepath.Join(dir, ".terraform/modules/root/hashicorp-terraform-aws-module-installer-acctest-5e87aff/modules/child_a"), LocalPath: filepath.Join(dir, fmt.Sprintf(".terraform/modules/root/terraform-aws-module-installer-acctest-%s/modules/child_a", v)),
}, },
{ {
Name: "Install", Name: "Install",
ModuleAddr: "root.child_a.child_b", ModuleAddr: "root.child_a.child_b",
LocalPath: filepath.Join(dir, ".terraform/modules/root/hashicorp-terraform-aws-module-installer-acctest-5e87aff/modules/child_b"), LocalPath: filepath.Join(dir, fmt.Sprintf(".terraform/modules/root/terraform-aws-module-installer-acctest-%s/modules/child_b", v)),
}, },
} }
@ -111,3 +114,165 @@ func TestDirFromModule_registry(t *testing.T) {
}) })
assertResultDeepEqual(t, gotTraces, wantTraces) 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)
}
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")
diags := DirFromModule(dir, modInstallDir, fromModuleDir, nil, hooks)
assertNoDiagnostics(t, diags)
wantCalls := []testInstallHookCall{
{
Name: "Install",
ModuleAddr: "child_a",
LocalPath: filepath.Join(fromModuleDir, "child_a"),
},
{
Name: "Install",
ModuleAddr: "child_a.child_b",
LocalPath: filepath.Join(fromModuleDir, "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, err := ioutil.TempDir("", "terraform-configload")
if err != nil {
t.Fatal(err)
}
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 := copydir.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)
}
defer os.Chdir(oldDir)
defer os.RemoveAll(tmpDir)
hooks := &testInstallHooks{}
modInstallDir := ".terraform/modules"
sourceDir := "../local-modules"
diags := DirFromModule(".", 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)
}

View File

@ -268,7 +268,7 @@ func TestLoaderInstallModules_registry(t *testing.T) {
Name: "Install", Name: "Install",
ModuleAddr: "acctest_child_a", ModuleAddr: "acctest_child_a",
Version: v, Version: v,
LocalPath: filepath.Join(dir, ".terraform/modules/acctest_child_a/hashicorp-terraform-aws-module-installer-acctest-853d038/modules/child_a"), LocalPath: filepath.Join(dir, ".terraform/modules/acctest_child_a/terraform-aws-module-installer-acctest-0.0.1/modules/child_a"),
}, },
// acctest_child_a.child_b // acctest_child_a.child_b
@ -276,7 +276,7 @@ func TestLoaderInstallModules_registry(t *testing.T) {
{ {
Name: "Install", Name: "Install",
ModuleAddr: "acctest_child_a.child_b", ModuleAddr: "acctest_child_a.child_b",
LocalPath: filepath.Join(dir, ".terraform/modules/acctest_child_a/hashicorp-terraform-aws-module-installer-acctest-853d038/modules/child_b"), LocalPath: filepath.Join(dir, ".terraform/modules/acctest_child_a/terraform-aws-module-installer-acctest-0.0.1/modules/child_b"),
}, },
// acctest_child_b accesses //modules/child_b directly // acctest_child_b accesses //modules/child_b directly
@ -290,7 +290,7 @@ func TestLoaderInstallModules_registry(t *testing.T) {
Name: "Install", Name: "Install",
ModuleAddr: "acctest_child_b", ModuleAddr: "acctest_child_b",
Version: v, Version: v,
LocalPath: filepath.Join(dir, ".terraform/modules/acctest_child_b/hashicorp-terraform-aws-module-installer-acctest-853d038/modules/child_b"), LocalPath: filepath.Join(dir, ".terraform/modules/acctest_child_b/terraform-aws-module-installer-acctest-0.0.1/modules/child_b"),
}, },
// acctest_root // acctest_root
@ -304,7 +304,7 @@ func TestLoaderInstallModules_registry(t *testing.T) {
Name: "Install", Name: "Install",
ModuleAddr: "acctest_root", ModuleAddr: "acctest_root",
Version: v, Version: v,
LocalPath: filepath.Join(dir, ".terraform/modules/acctest_root/hashicorp-terraform-aws-module-installer-acctest-853d038"), LocalPath: filepath.Join(dir, ".terraform/modules/acctest_root/terraform-aws-module-installer-acctest-0.0.1"),
}, },
// acctest_root.child_a // acctest_root.child_a
@ -312,7 +312,7 @@ func TestLoaderInstallModules_registry(t *testing.T) {
{ {
Name: "Install", Name: "Install",
ModuleAddr: "acctest_root.child_a", ModuleAddr: "acctest_root.child_a",
LocalPath: filepath.Join(dir, ".terraform/modules/acctest_root/hashicorp-terraform-aws-module-installer-acctest-853d038/modules/child_a"), LocalPath: filepath.Join(dir, ".terraform/modules/acctest_root/terraform-aws-module-installer-acctest-0.0.1/modules/child_a"),
}, },
// acctest_root.child_a.child_b // acctest_root.child_a.child_b
@ -320,7 +320,7 @@ func TestLoaderInstallModules_registry(t *testing.T) {
{ {
Name: "Install", Name: "Install",
ModuleAddr: "acctest_root.child_a.child_b", ModuleAddr: "acctest_root.child_a.child_b",
LocalPath: filepath.Join(dir, ".terraform/modules/acctest_root/hashicorp-terraform-aws-module-installer-acctest-853d038/modules/child_b"), LocalPath: filepath.Join(dir, ".terraform/modules/acctest_root/terraform-aws-module-installer-acctest-0.0.1/modules/child_b"),
}, },
} }