configs/configupgrade: detect possible relative module sources (#20646)

* configs/configupgrade: detect possible relative module sources

If a module source appears to be a relative local path but does not have
a preceding ./, print a #TODO message for the user.

* internal/initwd: limit go-getter detectors to those supported by terraform
* internal/initwd: move isMaybeRelativeLocalPath check into getWithGoGetter

To avoid making two calls to getter.Detect, which potentially makes
non-trivial API calls, the "isMaybeRelativeLocalPath" check was moved to
a later step and a custom error type was added so user-friendly
diagnostics could be displayed in the event that a possible relative local
path was detected.
This commit is contained in:
Kristin Laemmert 2019-03-13 11:17:14 -07:00 committed by GitHub
parent 29f2776cc7
commit a15a4acf2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 176 additions and 43 deletions

View File

@ -150,7 +150,7 @@ command and dealing with them before running this command again.
Providers: c.providerResolver(), Providers: c.providerResolver(),
Provisioners: c.provisionerFactories(), Provisioners: c.provisionerFactories(),
} }
newSources, upgradeDiags := upgrader.Upgrade(sources) newSources, upgradeDiags := upgrader.Upgrade(sources, dir)
diags = diags.Append(upgradeDiags) diags = diags.Append(upgradeDiags)
if upgradeDiags.HasErrors() { if upgradeDiags.HasErrors() {
c.showDiagnostics(diags) c.showDiagnostics(diags)

View File

@ -25,6 +25,7 @@ type analysis struct {
ResourceProviderType map[addrs.Resource]string ResourceProviderType map[addrs.Resource]string
ResourceHasCount map[addrs.Resource]bool ResourceHasCount map[addrs.Resource]bool
VariableTypes map[string]string VariableTypes map[string]string
ModuleDir string
} }
// analyze processes the configuration files included inside the receiver // analyze processes the configuration files included inside the receiver

View File

@ -0,0 +1,3 @@
module "foo" {
source = "foo"
}

View File

@ -0,0 +1,3 @@
module "foo" {
source = "foo"
}

View File

@ -0,0 +1,3 @@
terraform {
required_version = ">= 0.12"
}

View File

@ -0,0 +1,3 @@
module "foo" {
source = "module"
}

View File

@ -0,0 +1,3 @@
output "foo" {
value = "hello"
}

View File

@ -0,0 +1,10 @@
module "foo" {
# TF-UPGRADE-TODO: In Terraform v0.11 and earlier, it was possible to
# reference a relative module source without a preceding ./, but it is no
# longer supported in Terraform v0.12.
#
# If the below module source is indeed a relative local path, add ./ to the
# start of the source string. If that is not the case, then leave it as-is
# and remove this TODO comment.
source = "module"
}

View File

@ -0,0 +1,3 @@
output "foo" {
value = "hello"
}

View File

@ -0,0 +1,3 @@
terraform {
required_version = ">= 0.12"
}

View File

@ -30,11 +30,12 @@ import (
// warnings are also represented as "TF-UPGRADE-TODO:" comments in the // warnings are also represented as "TF-UPGRADE-TODO:" comments in the
// generated source files so that users can visit them all and decide what to // generated source files so that users can visit them all and decide what to
// do with them. // do with them.
func (u *Upgrader) Upgrade(input ModuleSources) (ModuleSources, tfdiags.Diagnostics) { func (u *Upgrader) Upgrade(input ModuleSources, dir string) (ModuleSources, tfdiags.Diagnostics) {
ret := make(ModuleSources) ret := make(ModuleSources)
var diags tfdiags.Diagnostics var diags tfdiags.Diagnostics
an, err := u.analyze(input) an, err := u.analyze(input)
an.ModuleDir = dir
if err != nil { if err != nil {
diags = diags.Append(err) diags = diags.Append(err)
return ret, diags return ret, diags

View File

@ -3,6 +3,8 @@ package configupgrade
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"os"
"path/filepath"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@ -14,6 +16,7 @@ import (
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/registry/regsrc"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags" "github.com/hashicorp/terraform/tfdiags"
) )
@ -657,6 +660,55 @@ func connectionBlockRule(filename string, resourceType string, an *analysis, adh
} }
} }
func moduleSourceRule(filename string, an *analysis) bodyItemRule {
return func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
val, ok := item.Val.(*hcl1ast.LiteralType)
if !ok {
diags = diags.Append(&hcl2.Diagnostic{
Severity: hcl2.DiagError,
Summary: "Invalid source argument",
Detail: `The "source" argument must be a single string containing the module source.`,
Subject: hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(),
})
return diags
}
if val.Token.Type != hcl1token.STRING {
diags = diags.Append(&hcl2.Diagnostic{
Severity: hcl2.DiagError,
Summary: "Invalid source argument",
Detail: `The "source" argument must be a single string containing the module source.`,
Subject: hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(),
})
return diags
}
litVal := val.Token.Value().(string)
if isMaybeRelativeLocalPath(litVal, an.ModuleDir) {
diags = diags.Append(&hcl2.Diagnostic{
Severity: hcl2.DiagWarning,
Summary: "Possible relative module source",
Detail: "Terraform cannot determine the given module source, but it appears to be a relative path",
Subject: hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(),
})
buf.WriteString(
"# TF-UPGRADE-TODO: In Terraform v0.11 and earlier, it was possible to\n" +
"# reference a relative module source without a preceding ./, but it is no\n" +
"# longer supported in Terraform v0.12.\n" +
"#\n" +
"# If the below module source is indeed a relative local path, add ./ to the\n" +
"# start of the source string. If that is not the case, then leave it as-is\n" +
"# and remove this TODO comment.\n",
)
}
newVal, exprDiags := upgradeExpr(val, filename, false, an)
diags = diags.Append(exprDiags)
buf.WriteString("source = " + string(newVal) + "\n")
return diags
}
}
// Prior to Terraform 0.12 providers were able to supply default connection // Prior to Terraform 0.12 providers were able to supply default connection
// settings that would partially populate the "connection" block with // settings that would partially populate the "connection" block with
// automatically-selected values. // automatically-selected values.
@ -873,3 +925,42 @@ var resourceTypeAutomaticConnectionExprs = map[string]map[string]string{
)`, )`,
}, },
} }
// copied directly from internal/initwd/getter.go
var localSourcePrefixes = []string{
"./",
"../",
".\\",
"..\\",
}
// isMaybeRelativeLocalPath tries to catch situations where a module source is
// an improperly-referenced relative path, such as "module" instead of
// "./module". This is a simple check that could return a false positive in the
// unlikely-yet-plausible case that a module source is for eg. a github
// repository that also looks exactly like an existing relative path. This
// should only be used to return a warning.
func isMaybeRelativeLocalPath(addr, dir string) bool {
for _, prefix := range localSourcePrefixes {
if strings.HasPrefix(addr, prefix) {
// it is _definitely_ a relative path
return false
}
}
_, err := regsrc.ParseModuleSource(addr)
if err == nil {
// it is a registry source
return false
}
possibleRelPath := filepath.Join(dir, addr)
_, err = os.Stat(possibleRelPath)
if err == nil {
// If there is no error, something exists at what would be the relative
// path, if the module source started with ./
return true
}
return false
}

View File

@ -209,7 +209,7 @@ func (u *Upgrader) upgradeNativeSyntaxFile(filename string, src []byte, an *anal
// start with the straightforward mapping of those and override // start with the straightforward mapping of those and override
// the special lifecycle arguments below. // the special lifecycle arguments below.
rules := justAttributesBodyRules(filename, body, an) rules := justAttributesBodyRules(filename, body, an)
rules["source"] = noInterpAttributeRule(filename, cty.String, an) rules["source"] = moduleSourceRule(filename, an)
rules["version"] = noInterpAttributeRule(filename, cty.String, an) rules["version"] = noInterpAttributeRule(filename, cty.String, an)
rules["providers"] = func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics { rules["providers"] = func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics var diags tfdiags.Diagnostics

View File

@ -53,7 +53,7 @@ func TestUpgradeValid(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
gotSrc, diags := u.Upgrade(inputSrc) gotSrc, diags := u.Upgrade(inputSrc, inputDir)
if diags.HasErrors() { if diags.HasErrors() {
t.Error(diags.Err()) t.Error(diags.Err())
} }
@ -101,7 +101,7 @@ func TestUpgradeRenameJSON(t *testing.T) {
u := &Upgrader{ u := &Upgrader{
Providers: providers.ResolverFixed(testProviders), Providers: providers.ResolverFixed(testProviders),
} }
gotSrc, diags := u.Upgrade(inputSrc) gotSrc, diags := u.Upgrade(inputSrc, inputDir)
if diags.HasErrors() { if diags.HasErrors() {
t.Error(diags.Err()) t.Error(diags.Err())
} }

View File

@ -89,6 +89,10 @@ func (g reusingGetter) getWithGoGetter(instPath, addr string) (string, error) {
return "", err return "", err
} }
if isMaybeRelativeLocalPath(realAddr) {
return "", &MaybeRelativePathErr{addr}
}
var realSubDir string var realSubDir string
realAddr, realSubDir = splitAddrSubdir(realAddr) realAddr, realSubDir = splitAddrSubdir(realAddr)
if realSubDir != "" { if realSubDir != "" {
@ -187,17 +191,19 @@ func isRegistrySourceAddr(addr string) bool {
return err == nil return err == nil
} }
func isMaybeRelativeLocalPath(addr, path string) bool { type MaybeRelativePathErr struct {
realAddr, err := getter.Detect(addr, path, getter.Detectors) Addr string
// this error will be handled by the next function }
if err != nil {
return false func (e *MaybeRelativePathErr) Error() string {
} else { return fmt.Sprintf("Terraform cannot determine the module source for %s", e.Addr)
if strings.HasPrefix(realAddr, "file://") { }
_, err := os.Stat(realAddr[7:])
if err != nil { func isMaybeRelativeLocalPath(addr string) bool {
return true if strings.HasPrefix(addr, "file://") {
} _, err := os.Stat(addr[7:])
if err != nil {
return true
} }
} }
return false return false

View File

@ -199,22 +199,6 @@ func (i *ModuleInstaller) installDescendentModules(rootMod *tfconfig.Module, roo
diags = append(diags, mDiags...) diags = append(diags, mDiags...)
return mod, v, diags return mod, v, diags
case isMaybeRelativeLocalPath(req.SourceAddr, instPath):
log.Printf(
"[TRACE] ModuleInstaller: %s looks like a local path but is missing ./ or ../",
req.SourceAddr,
)
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to locate local module source",
fmt.Sprintf(
"%s looks like a relative path, but Terraform cannot determine the module source. "+
"Add ./ at the start of the source string if this is a relative path.",
req.SourceAddr,
),
))
return nil, nil, diags
default: default:
log.Printf("[TRACE] ModuleInstaller: %s address %q will be handled by go-getter", key, req.SourceAddr) log.Printf("[TRACE] ModuleInstaller: %s address %q will be handled by go-getter", key, req.SourceAddr)
@ -487,17 +471,36 @@ func (i *ModuleInstaller) installGoGetterModule(req *earlyconfig.ModuleRequest,
modDir, err := getter.getWithGoGetter(instPath, req.SourceAddr) modDir, err := getter.getWithGoGetter(instPath, req.SourceAddr)
if err != nil { if err != nil {
// Errors returned by go-getter have very inconsistent quality as if err, ok := err.(*MaybeRelativePathErr); ok {
// end-user error messages, but for now we're accepting that because log.Printf(
// we have no way to recognize any specific errors to improve them "[TRACE] ModuleInstaller: %s looks like a local path but is missing ./ or ../",
// and masking the error entirely would hide valuable diagnostic req.SourceAddr,
// information from the user. )
diags = diags.Append(tfdiags.Sourceless( diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error, tfdiags.Error,
"Failed to download module", "Module not found",
fmt.Sprintf("Error attempting to download module %q (%s:%d) source code from %q: %s", req.Name, req.CallPos.Filename, req.CallPos.Line, packageAddr, err), fmt.Sprintf(
)) "The module address %q could not be resolved.\n\n"+
"If you intended this as a path relative to the current "+
"module, use \"./%s\" instead. The \"./\" prefix "+
"indicates that the address is a relative filesystem path.",
req.SourceAddr, req.SourceAddr,
),
))
} else {
// Errors returned by go-getter have very inconsistent quality as
// end-user error messages, but for now we're accepting that because
// we have no way to recognize any specific errors to improve them
// and masking the error entirely would hide valuable diagnostic
// information from the user.
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to download module",
fmt.Sprintf("Error attempting to download module %q (%s:%d) source code from %q: %s", req.Name, req.CallPos.Filename, req.CallPos.Line, packageAddr, err),
))
}
return nil, diags return nil, diags
} }
log.Printf("[TRACE] ModuleInstaller: %s %q was downloaded to %s", key, req.SourceAddr, modDir) log.Printf("[TRACE] ModuleInstaller: %s %q was downloaded to %s", key, req.SourceAddr, modDir)

View File

@ -107,7 +107,7 @@ func TestModuleInstaller_error(t *testing.T) {
if !diags.HasErrors() { if !diags.HasErrors() {
t.Fatal("expected error") t.Fatal("expected error")
} else { } else {
assertDiagnosticSummary(t, diags, "Failed to locate local module source") assertDiagnosticSummary(t, diags, "Module not found")
} }
} }