mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-24 08:00:17 -06:00
369 lines
14 KiB
Go
369 lines
14 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package addrs
|
|
|
|
import (
|
|
"fmt"
|
|
"path"
|
|
"strings"
|
|
|
|
tfaddr "github.com/hashicorp/terraform-registry-address"
|
|
"github.com/placeholderplaceholderplaceholder/opentf/internal/getmodules"
|
|
)
|
|
|
|
// ModuleSource is the general type for all three of the possible module source
|
|
// address types. The concrete implementations of this are ModuleSourceLocal,
|
|
// ModuleSourceRegistry, and ModuleSourceRemote.
|
|
type ModuleSource interface {
|
|
// String returns a full representation of the address, including any
|
|
// additional components that are typically implied by omission in
|
|
// user-written addresses.
|
|
//
|
|
// We typically use this longer representation in error message, in case
|
|
// the inclusion of normally-omitted components is helpful in debugging
|
|
// unexpected behavior.
|
|
String() string
|
|
|
|
// ForDisplay is similar to String but instead returns a representation of
|
|
// the idiomatic way to write the address in configuration, omitting
|
|
// components that are commonly just implied in addresses written by
|
|
// users.
|
|
//
|
|
// We typically use this shorter representation in informational messages,
|
|
// such as the note that we're about to start downloading a package.
|
|
ForDisplay() string
|
|
|
|
moduleSource()
|
|
}
|
|
|
|
var _ ModuleSource = ModuleSourceLocal("")
|
|
var _ ModuleSource = ModuleSourceRegistry{}
|
|
var _ ModuleSource = ModuleSourceRemote{}
|
|
|
|
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
|
|
// OpenTF 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) (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 OpenTF 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
|
|
}
|
|
|
|
// ModuleSourceLocal is a ModuleSource representing a local path reference
|
|
// from the caller's directory to the callee's directory within the same
|
|
// module package.
|
|
//
|
|
// A "module package" here means a set of modules distributed together in
|
|
// the same archive, repository, or similar. That's a significant distinction
|
|
// because we always download and cache entire module packages at once,
|
|
// and then create relative references within the same directory in order
|
|
// to ensure all modules in the package are looking at a consistent filesystem
|
|
// layout. We also assume that modules within a package are maintained together,
|
|
// which means that cross-cutting maintenence across all of them would be
|
|
// possible.
|
|
//
|
|
// The actual value of a ModuleSourceLocal is a normalized relative path using
|
|
// forward slashes, even on operating systems that have other conventions,
|
|
// because we're representing traversal within the logical filesystem
|
|
// represented by the containing package, not actually within the physical
|
|
// filesystem we unpacked the package into. We should typically not construct
|
|
// ModuleSourceLocal values directly, except in tests where we can ensure
|
|
// the value meets our assumptions. Use ParseModuleSource instead if the
|
|
// input string is not hard-coded in the program.
|
|
type ModuleSourceLocal string
|
|
|
|
func parseModuleSourceLocal(raw string) (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 OpenTF 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 ModuleSourceLocal(clean), nil
|
|
}
|
|
|
|
func isModuleSourceLocal(raw string) bool {
|
|
for _, prefix := range moduleSourceLocalPrefixes {
|
|
if strings.HasPrefix(raw, prefix) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (s ModuleSourceLocal) moduleSource() {}
|
|
|
|
func (s ModuleSourceLocal) String() string {
|
|
// We assume that our underlying string was already normalized at
|
|
// construction, so we just return it verbatim.
|
|
return string(s)
|
|
}
|
|
|
|
func (s ModuleSourceLocal) ForDisplay() string {
|
|
return string(s)
|
|
}
|
|
|
|
// ModuleSourceRegistry is a ModuleSource representing a module listed in a
|
|
// OpenTF module registry.
|
|
//
|
|
// A registry source isn't a direct source location but rather an indirection
|
|
// over a ModuleSourceRemote. The job of a registry is to translate the
|
|
// combination of a ModuleSourceRegistry and a module version number into
|
|
// a concrete ModuleSourceRemote that OpenTF will then download and
|
|
// install.
|
|
type ModuleSourceRegistry tfaddr.Module
|
|
|
|
// DefaultModuleRegistryHost is the hostname used for registry-based module
|
|
// source addresses that do not have an explicit hostname.
|
|
const DefaultModuleRegistryHost = tfaddr.DefaultModuleRegistryHost
|
|
|
|
// 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) (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 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 ModuleSourceRegistry{
|
|
Package: src.Package,
|
|
Subdir: src.Subdir,
|
|
}, nil
|
|
}
|
|
|
|
func (s ModuleSourceRegistry) moduleSource() {}
|
|
|
|
func (s ModuleSourceRegistry) String() string {
|
|
if s.Subdir != "" {
|
|
return s.Package.String() + "//" + s.Subdir
|
|
}
|
|
return s.Package.String()
|
|
}
|
|
|
|
func (s ModuleSourceRegistry) ForDisplay() string {
|
|
if s.Subdir != "" {
|
|
return s.Package.ForDisplay() + "//" + s.Subdir
|
|
}
|
|
return s.Package.ForDisplay()
|
|
}
|
|
|
|
// ModuleSourceRemote is a ModuleSource representing a remote location from
|
|
// which we can retrieve a module package.
|
|
//
|
|
// A ModuleSourceRemote can optionally include a "subdirectory" path, which
|
|
// means that it's selecting a sub-directory of the given package to use as
|
|
// the entry point into the package.
|
|
type ModuleSourceRemote struct {
|
|
// Package is the address of the remote package that the requested
|
|
// module belongs to.
|
|
Package ModulePackage
|
|
|
|
// If Subdir is non-empty then it represents a sub-directory within the
|
|
// remote package which will serve as the entry-point for the package.
|
|
//
|
|
// Subdir uses a normalized forward-slash-based path syntax within the
|
|
// virtual filesystem represented by the final package. It will never
|
|
// include `../` or `./` sequences.
|
|
Subdir string
|
|
}
|
|
|
|
func parseModuleSourceRemote(raw string) (ModuleSourceRemote, error) {
|
|
var subDir string
|
|
raw, subDir = getmodules.SplitPackageSubdir(raw)
|
|
if strings.HasPrefix(subDir, "../") {
|
|
return 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 := getmodules.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 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 ModuleSourceRemote{}, fmt.Errorf("detected subdirectory path %q of %q leads outside of the module package", subDir, norm)
|
|
}
|
|
}
|
|
|
|
return ModuleSourceRemote{
|
|
Package: ModulePackage(norm),
|
|
Subdir: subDir,
|
|
}, nil
|
|
}
|
|
|
|
func (s ModuleSourceRemote) moduleSource() {}
|
|
|
|
func (s ModuleSourceRemote) String() string {
|
|
base := s.Package.String()
|
|
|
|
if s.Subdir != "" {
|
|
// Address contains query string
|
|
if strings.Contains(base, "?") {
|
|
parts := strings.SplitN(base, "?", 2)
|
|
return parts[0] + "//" + s.Subdir + "?" + parts[1]
|
|
}
|
|
return base + "//" + s.Subdir
|
|
}
|
|
return base
|
|
}
|
|
|
|
func (s ModuleSourceRemote) ForDisplay() string {
|
|
// The two string representations are identical for this address type.
|
|
// This isn't really entirely true to the idea of "ForDisplay" since
|
|
// it'll often include some additional components added in by the
|
|
// go-getter detectors, but we don't have any function to turn a
|
|
// "detected" string back into an idiomatic shorthand the user might've
|
|
// entered.
|
|
return s.String()
|
|
}
|
|
|
|
// FromRegistry can be called on a remote source address that was returned
|
|
// from a module registry, passing in the original registry source address
|
|
// that the registry was asked about, in order to get the effective final
|
|
// remote source address.
|
|
//
|
|
// Specifically, this method handles the situations where one or both of
|
|
// the two addresses contain subdirectory paths, combining both when necessary
|
|
// in order to ensure that both the registry's given path and the user's
|
|
// given path are both respected.
|
|
//
|
|
// This will return nonsense if given a registry address other than the one
|
|
// that generated the reciever via a registry lookup.
|
|
func (s ModuleSourceRemote) FromRegistry(given ModuleSourceRegistry) ModuleSourceRemote {
|
|
ret := s // not a pointer, so this is a shallow copy
|
|
|
|
switch {
|
|
case s.Subdir != "" && given.Subdir != "":
|
|
ret.Subdir = path.Join(s.Subdir, given.Subdir)
|
|
case given.Subdir != "":
|
|
ret.Subdir = given.Subdir
|
|
}
|
|
|
|
return ret
|
|
}
|