mirror of
https://github.com/hashicorp/terraform.git
synced 2026-03-21 18:10:30 -04:00
217 lines
8.7 KiB
Go
217 lines
8.7 KiB
Go
// Copyright IBM Corp. 2014, 2026
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package moduleaddrs
|
|
|
|
import (
|
|
"fmt"
|
|
"path"
|
|
"strings"
|
|
|
|
tfaddr "github.com/hashicorp/terraform-registry-address"
|
|
|
|
"github.com/hashicorp/terraform/internal/addrs"
|
|
)
|
|
|
|
// We have some of the module address parsers in here, rather than in
|
|
// package addrs, because right now our remote source address normalization
|
|
// is inextricably tied to the external go-getter library, which means any
|
|
// package that calls these functions must indirectly depend on go-getter.
|
|
//
|
|
// Package addrs is imported from almost everywhere, so any dependency it
|
|
// has becomes an indirect dependency of everything else. Only a few callers
|
|
// actually need to parse module source addresses, so it's pragmatic to have
|
|
// just those callers import this package, whereas packages that only need
|
|
// to work with addresses that were already parsed -- or don't need to interact
|
|
// with module source addresses _at all_ -- can avoid indirectly depending
|
|
// on go-getter and all of its various third-party dependencies.
|
|
|
|
var moduleSourceLocalPrefixes = []string{
|
|
"./",
|
|
"../",
|
|
".\\",
|
|
"..\\",
|
|
}
|
|
|
|
// ParseModuleSource parses a module source address as given in the "source"
|
|
// argument inside a "module" block in the configuration.
|
|
//
|
|
// For historical reasons this syntax is a bit overloaded, supporting three
|
|
// different address types:
|
|
// - Local paths starting with either ./ or ../, which are special because
|
|
// Terraform considers them to belong to the same "package" as the caller.
|
|
// - Module registry addresses, given as either NAMESPACE/NAME/SYSTEM or
|
|
// HOST/NAMESPACE/NAME/SYSTEM, in which case the remote registry serves
|
|
// as an indirection over the third address type that follows.
|
|
// - Various URL-like and other heuristically-recognized strings which
|
|
// we currently delegate to the external library go-getter.
|
|
//
|
|
// There is some ambiguity between the module registry addresses and go-getter's
|
|
// very liberal heuristics and so this particular function will typically treat
|
|
// an invalid registry address as some other sort of remote source address
|
|
// rather than returning an error. If you know that you're expecting a
|
|
// registry address in particular, use [ParseModuleSourceRegistry] instead, which
|
|
// can therefore expose more detailed error messages about registry address
|
|
// parsing in particular.
|
|
func ParseModuleSource(raw string) (addrs.ModuleSource, error) {
|
|
if isModuleSourceLocal(raw) {
|
|
localAddr, err := parseModuleSourceLocal(raw)
|
|
if err != nil {
|
|
// This is to make sure we really return a nil ModuleSource in
|
|
// this case, rather than an interface containing the zero
|
|
// value of ModuleSourceLocal.
|
|
return nil, err
|
|
}
|
|
return localAddr, nil
|
|
}
|
|
|
|
// For historical reasons, whether an address is a registry
|
|
// address is defined only by whether it can be successfully
|
|
// parsed as one, and anything else must fall through to be
|
|
// parsed as a direct remote source, where go-getter might
|
|
// then recognize it as a filesystem path. This is odd
|
|
// but matches behavior we've had since Terraform v0.10 which
|
|
// existing modules may be relying on.
|
|
// (Notice that this means that there's never any path where
|
|
// the registry source parse error gets returned to the caller,
|
|
// which is annoying but has been true for many releases
|
|
// without it posing a serious problem in practice.)
|
|
if ret, err := ParseModuleSourceRegistry(raw); err == nil {
|
|
return ret, nil
|
|
}
|
|
|
|
// If we get down here then we treat everything else as a
|
|
// remote address. In practice there's very little that
|
|
// go-getter doesn't consider invalid input, so even invalid
|
|
// nonsense will probably interpreted as _something_ here
|
|
// and then fail during installation instead. We can't
|
|
// really improve this situation for historical reasons.
|
|
remoteAddr, err := parseModuleSourceRemote(raw)
|
|
if err != nil {
|
|
// This is to make sure we really return a nil ModuleSource in
|
|
// this case, rather than an interface containing the zero
|
|
// value of ModuleSourceRemote.
|
|
return nil, err
|
|
}
|
|
return remoteAddr, nil
|
|
}
|
|
|
|
func parseModuleSourceLocal(raw string) (addrs.ModuleSourceLocal, error) {
|
|
// As long as we have a suitable prefix (detected by ParseModuleSource)
|
|
// there is no failure case for local paths: we just use the "path"
|
|
// package's cleaning logic to remove any redundant "./" and "../"
|
|
// sequences and any duplicate slashes and accept whatever that
|
|
// produces.
|
|
|
|
// Although using backslashes (Windows-style) is non-idiomatic, we do
|
|
// allow it and just normalize it away, so the rest of Terraform will
|
|
// only see the forward-slash form.
|
|
if strings.Contains(raw, `\`) {
|
|
// Note: We use string replacement rather than filepath.ToSlash
|
|
// here because the filepath package behavior varies by current
|
|
// platform, but we want to interpret configured paths the same
|
|
// across all platforms: these are virtual paths within a module
|
|
// package, not physical filesystem paths.
|
|
raw = strings.ReplaceAll(raw, `\`, "/")
|
|
}
|
|
|
|
// Note that we could've historically blocked using "//" in a path here
|
|
// in order to avoid confusion with the subdir syntax in remote addresses,
|
|
// but we historically just treated that as the same as a single slash
|
|
// and so we continue to do that now for compatibility. Clean strips those
|
|
// out and reduces them to just a single slash.
|
|
clean := path.Clean(raw)
|
|
|
|
// However, we do need to keep a single "./" on the front if it isn't
|
|
// a "../" path, or else it would be ambigous with the registry address
|
|
// syntax.
|
|
if !strings.HasPrefix(clean, "../") {
|
|
clean = "./" + clean
|
|
}
|
|
|
|
return addrs.ModuleSourceLocal(clean), nil
|
|
}
|
|
|
|
func isModuleSourceLocal(raw string) bool {
|
|
for _, prefix := range moduleSourceLocalPrefixes {
|
|
if strings.HasPrefix(raw, prefix) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ParseModuleSourceRegistry is a variant of ParseModuleSource which only
|
|
// accepts module registry addresses, and will reject any other address type.
|
|
//
|
|
// Use this instead of ParseModuleSource if you know from some other surrounding
|
|
// context that an address is intended to be a registry address rather than
|
|
// some other address type, which will then allow for better error reporting
|
|
// due to the additional information about user intent.
|
|
func ParseModuleSourceRegistry(raw string) (addrs.ModuleSource, error) {
|
|
// Before we delegate to the "real" function we'll just make sure this
|
|
// doesn't look like a local source address, so we can return a better
|
|
// error message for that situation.
|
|
if isModuleSourceLocal(raw) {
|
|
return addrs.ModuleSourceRegistry{}, fmt.Errorf("can't use local directory %q as a module registry address", raw)
|
|
}
|
|
|
|
src, err := tfaddr.ParseModuleSource(raw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return addrs.ModuleSourceRegistry{
|
|
Package: src.Package,
|
|
Subdir: src.Subdir,
|
|
}, nil
|
|
}
|
|
|
|
func parseModuleSourceRemote(raw string) (addrs.ModuleSourceRemote, error) {
|
|
var subDir string
|
|
raw, subDir = SplitPackageSubdir(raw)
|
|
if strings.HasPrefix(subDir, "../") {
|
|
return addrs.ModuleSourceRemote{}, fmt.Errorf("subdirectory path %q leads outside of the module package", subDir)
|
|
}
|
|
|
|
// A remote source address is really just a go-getter address resulting
|
|
// from go-getter's "detect" phase, which adds on the prefix specifying
|
|
// which protocol it should use and possibly also adjusts the
|
|
// protocol-specific part into different syntax.
|
|
//
|
|
// Note that for historical reasons this can potentially do network
|
|
// requests in order to disambiguate certain address types, although
|
|
// that's a legacy thing that is only for some specific, less-commonly-used
|
|
// address types. Most just do local string manipulation. We should
|
|
// aim to remove the network requests over time, if possible.
|
|
norm, moreSubDir, err := NormalizePackageAddress(raw)
|
|
if err != nil {
|
|
// We must pass through the returned error directly here because
|
|
// the getmodules package has some special error types it uses
|
|
// for certain cases where the UI layer might want to include a
|
|
// more helpful error message.
|
|
return addrs.ModuleSourceRemote{}, err
|
|
}
|
|
|
|
if moreSubDir != "" {
|
|
switch {
|
|
case subDir != "":
|
|
// The detector's own subdir goes first, because the
|
|
// subdir we were given is conceptually relative to
|
|
// the subdirectory that we just detected.
|
|
subDir = path.Join(moreSubDir, subDir)
|
|
default:
|
|
subDir = path.Clean(moreSubDir)
|
|
}
|
|
if strings.HasPrefix(subDir, "../") {
|
|
// This would suggest a bug in a go-getter detector, but
|
|
// we'll catch it anyway to avoid doing something confusing
|
|
// downstream.
|
|
return addrs.ModuleSourceRemote{}, fmt.Errorf("detected subdirectory path %q of %q leads outside of the module package", subDir, norm)
|
|
}
|
|
}
|
|
|
|
return addrs.ModuleSourceRemote{
|
|
Package: addrs.ModulePackage(norm),
|
|
Subdir: subDir,
|
|
}, nil
|
|
}
|