// 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 }