github.com/pulumi/terraform@v1.4.0/pkg/addrs/module_source.go (about)

     1  package addrs
     2  
     3  import (
     4  	"fmt"
     5  	"path"
     6  	"strings"
     7  
     8  	tfaddr "github.com/hashicorp/terraform-registry-address"
     9  	"github.com/pulumi/terraform/pkg/getmodules"
    10  )
    11  
    12  // ModuleSource is the general type for all three of the possible module source
    13  // address types. The concrete implementations of this are ModuleSourceLocal,
    14  // ModuleSourceRegistry, and ModuleSourceRemote.
    15  type ModuleSource interface {
    16  	// String returns a full representation of the address, including any
    17  	// additional components that are typically implied by omission in
    18  	// user-written addresses.
    19  	//
    20  	// We typically use this longer representation in error message, in case
    21  	// the inclusion of normally-omitted components is helpful in debugging
    22  	// unexpected behavior.
    23  	String() string
    24  
    25  	// ForDisplay is similar to String but instead returns a representation of
    26  	// the idiomatic way to write the address in configuration, omitting
    27  	// components that are commonly just implied in addresses written by
    28  	// users.
    29  	//
    30  	// We typically use this shorter representation in informational messages,
    31  	// such as the note that we're about to start downloading a package.
    32  	ForDisplay() string
    33  
    34  	moduleSource()
    35  }
    36  
    37  var _ ModuleSource = ModuleSourceLocal("")
    38  var _ ModuleSource = ModuleSourceRegistry{}
    39  var _ ModuleSource = ModuleSourceRemote{}
    40  
    41  var moduleSourceLocalPrefixes = []string{
    42  	"./",
    43  	"../",
    44  	".\\",
    45  	"..\\",
    46  }
    47  
    48  // ParseModuleSource parses a module source address as given in the "source"
    49  // argument inside a "module" block in the configuration.
    50  //
    51  // For historical reasons this syntax is a bit overloaded, supporting three
    52  // different address types:
    53  //   - Local paths starting with either ./ or ../, which are special because
    54  //     Terraform considers them to belong to the same "package" as the caller.
    55  //   - Module registry addresses, given as either NAMESPACE/NAME/SYSTEM or
    56  //     HOST/NAMESPACE/NAME/SYSTEM, in which case the remote registry serves
    57  //     as an indirection over the third address type that follows.
    58  //   - Various URL-like and other heuristically-recognized strings which
    59  //     we currently delegate to the external library go-getter.
    60  //
    61  // There is some ambiguity between the module registry addresses and go-getter's
    62  // very liberal heuristics and so this particular function will typically treat
    63  // an invalid registry address as some other sort of remote source address
    64  // rather than returning an error. If you know that you're expecting a
    65  // registry address in particular, use ParseModuleSourceRegistry instead, which
    66  // can therefore expose more detailed error messages about registry address
    67  // parsing in particular.
    68  func ParseModuleSource(raw string) (ModuleSource, error) {
    69  	if isModuleSourceLocal(raw) {
    70  		localAddr, err := parseModuleSourceLocal(raw)
    71  		if err != nil {
    72  			// This is to make sure we really return a nil ModuleSource in
    73  			// this case, rather than an interface containing the zero
    74  			// value of ModuleSourceLocal.
    75  			return nil, err
    76  		}
    77  		return localAddr, nil
    78  	}
    79  
    80  	// For historical reasons, whether an address is a registry
    81  	// address is defined only by whether it can be successfully
    82  	// parsed as one, and anything else must fall through to be
    83  	// parsed as a direct remote source, where go-getter might
    84  	// then recognize it as a filesystem path. This is odd
    85  	// but matches behavior we've had since Terraform v0.10 which
    86  	// existing modules may be relying on.
    87  	// (Notice that this means that there's never any path where
    88  	// the registry source parse error gets returned to the caller,
    89  	// which is annoying but has been true for many releases
    90  	// without it posing a serious problem in practice.)
    91  	if ret, err := ParseModuleSourceRegistry(raw); err == nil {
    92  		return ret, nil
    93  	}
    94  
    95  	// If we get down here then we treat everything else as a
    96  	// remote address. In practice there's very little that
    97  	// go-getter doesn't consider invalid input, so even invalid
    98  	// nonsense will probably interpreted as _something_ here
    99  	// and then fail during installation instead. We can't
   100  	// really improve this situation for historical reasons.
   101  	remoteAddr, err := parseModuleSourceRemote(raw)
   102  	if err != nil {
   103  		// This is to make sure we really return a nil ModuleSource in
   104  		// this case, rather than an interface containing the zero
   105  		// value of ModuleSourceRemote.
   106  		return nil, err
   107  	}
   108  	return remoteAddr, nil
   109  }
   110  
   111  // ModuleSourceLocal is a ModuleSource representing a local path reference
   112  // from the caller's directory to the callee's directory within the same
   113  // module package.
   114  //
   115  // A "module package" here means a set of modules distributed together in
   116  // the same archive, repository, or similar. That's a significant distinction
   117  // because we always download and cache entire module packages at once,
   118  // and then create relative references within the same directory in order
   119  // to ensure all modules in the package are looking at a consistent filesystem
   120  // layout. We also assume that modules within a package are maintained together,
   121  // which means that cross-cutting maintenence across all of them would be
   122  // possible.
   123  //
   124  // The actual value of a ModuleSourceLocal is a normalized relative path using
   125  // forward slashes, even on operating systems that have other conventions,
   126  // because we're representing traversal within the logical filesystem
   127  // represented by the containing package, not actually within the physical
   128  // filesystem we unpacked the package into. We should typically not construct
   129  // ModuleSourceLocal values directly, except in tests where we can ensure
   130  // the value meets our assumptions. Use ParseModuleSource instead if the
   131  // input string is not hard-coded in the program.
   132  type ModuleSourceLocal string
   133  
   134  func parseModuleSourceLocal(raw string) (ModuleSourceLocal, error) {
   135  	// As long as we have a suitable prefix (detected by ParseModuleSource)
   136  	// there is no failure case for local paths: we just use the "path"
   137  	// package's cleaning logic to remove any redundant "./" and "../"
   138  	// sequences and any duplicate slashes and accept whatever that
   139  	// produces.
   140  
   141  	// Although using backslashes (Windows-style) is non-idiomatic, we do
   142  	// allow it and just normalize it away, so the rest of Terraform will
   143  	// only see the forward-slash form.
   144  	if strings.Contains(raw, `\`) {
   145  		// Note: We use string replacement rather than filepath.ToSlash
   146  		// here because the filepath package behavior varies by current
   147  		// platform, but we want to interpret configured paths the same
   148  		// across all platforms: these are virtual paths within a module
   149  		// package, not physical filesystem paths.
   150  		raw = strings.ReplaceAll(raw, `\`, "/")
   151  	}
   152  
   153  	// Note that we could've historically blocked using "//" in a path here
   154  	// in order to avoid confusion with the subdir syntax in remote addresses,
   155  	// but we historically just treated that as the same as a single slash
   156  	// and so we continue to do that now for compatibility. Clean strips those
   157  	// out and reduces them to just a single slash.
   158  	clean := path.Clean(raw)
   159  
   160  	// However, we do need to keep a single "./" on the front if it isn't
   161  	// a "../" path, or else it would be ambigous with the registry address
   162  	// syntax.
   163  	if !strings.HasPrefix(clean, "../") {
   164  		clean = "./" + clean
   165  	}
   166  
   167  	return ModuleSourceLocal(clean), nil
   168  }
   169  
   170  func isModuleSourceLocal(raw string) bool {
   171  	for _, prefix := range moduleSourceLocalPrefixes {
   172  		if strings.HasPrefix(raw, prefix) {
   173  			return true
   174  		}
   175  	}
   176  	return false
   177  }
   178  
   179  func (s ModuleSourceLocal) moduleSource() {}
   180  
   181  func (s ModuleSourceLocal) String() string {
   182  	// We assume that our underlying string was already normalized at
   183  	// construction, so we just return it verbatim.
   184  	return string(s)
   185  }
   186  
   187  func (s ModuleSourceLocal) ForDisplay() string {
   188  	return string(s)
   189  }
   190  
   191  // ModuleSourceRegistry is a ModuleSource representing a module listed in a
   192  // Terraform module registry.
   193  //
   194  // A registry source isn't a direct source location but rather an indirection
   195  // over a ModuleSourceRemote. The job of a registry is to translate the
   196  // combination of a ModuleSourceRegistry and a module version number into
   197  // a concrete ModuleSourceRemote that Terraform will then download and
   198  // install.
   199  type ModuleSourceRegistry tfaddr.Module
   200  
   201  // DefaultModuleRegistryHost is the hostname used for registry-based module
   202  // source addresses that do not have an explicit hostname.
   203  const DefaultModuleRegistryHost = tfaddr.DefaultModuleRegistryHost
   204  
   205  // ParseModuleSourceRegistry is a variant of ParseModuleSource which only
   206  // accepts module registry addresses, and will reject any other address type.
   207  //
   208  // Use this instead of ParseModuleSource if you know from some other surrounding
   209  // context that an address is intended to be a registry address rather than
   210  // some other address type, which will then allow for better error reporting
   211  // due to the additional information about user intent.
   212  func ParseModuleSourceRegistry(raw string) (ModuleSource, error) {
   213  	// Before we delegate to the "real" function we'll just make sure this
   214  	// doesn't look like a local source address, so we can return a better
   215  	// error message for that situation.
   216  	if isModuleSourceLocal(raw) {
   217  		return ModuleSourceRegistry{}, fmt.Errorf("can't use local directory %q as a module registry address", raw)
   218  	}
   219  
   220  	src, err := tfaddr.ParseModuleSource(raw)
   221  	if err != nil {
   222  		return nil, err
   223  	}
   224  	return ModuleSourceRegistry{
   225  		Package: src.Package,
   226  		Subdir:  src.Subdir,
   227  	}, nil
   228  }
   229  
   230  func (s ModuleSourceRegistry) moduleSource() {}
   231  
   232  func (s ModuleSourceRegistry) String() string {
   233  	if s.Subdir != "" {
   234  		return s.Package.String() + "//" + s.Subdir
   235  	}
   236  	return s.Package.String()
   237  }
   238  
   239  func (s ModuleSourceRegistry) ForDisplay() string {
   240  	if s.Subdir != "" {
   241  		return s.Package.ForDisplay() + "//" + s.Subdir
   242  	}
   243  	return s.Package.ForDisplay()
   244  }
   245  
   246  // ModuleSourceRemote is a ModuleSource representing a remote location from
   247  // which we can retrieve a module package.
   248  //
   249  // A ModuleSourceRemote can optionally include a "subdirectory" path, which
   250  // means that it's selecting a sub-directory of the given package to use as
   251  // the entry point into the package.
   252  type ModuleSourceRemote struct {
   253  	// Package is the address of the remote package that the requested
   254  	// module belongs to.
   255  	Package ModulePackage
   256  
   257  	// If Subdir is non-empty then it represents a sub-directory within the
   258  	// remote package which will serve as the entry-point for the package.
   259  	//
   260  	// Subdir uses a normalized forward-slash-based path syntax within the
   261  	// virtual filesystem represented by the final package. It will never
   262  	// include `../` or `./` sequences.
   263  	Subdir string
   264  }
   265  
   266  func parseModuleSourceRemote(raw string) (ModuleSourceRemote, error) {
   267  	var subDir string
   268  	raw, subDir = getmodules.SplitPackageSubdir(raw)
   269  	if strings.HasPrefix(subDir, "../") {
   270  		return ModuleSourceRemote{}, fmt.Errorf("subdirectory path %q leads outside of the module package", subDir)
   271  	}
   272  
   273  	// A remote source address is really just a go-getter address resulting
   274  	// from go-getter's "detect" phase, which adds on the prefix specifying
   275  	// which protocol it should use and possibly also adjusts the
   276  	// protocol-specific part into different syntax.
   277  	//
   278  	// Note that for historical reasons this can potentially do network
   279  	// requests in order to disambiguate certain address types, although
   280  	// that's a legacy thing that is only for some specific, less-commonly-used
   281  	// address types. Most just do local string manipulation. We should
   282  	// aim to remove the network requests over time, if possible.
   283  	norm, moreSubDir, err := getmodules.NormalizePackageAddress(raw)
   284  	if err != nil {
   285  		// We must pass through the returned error directly here because
   286  		// the getmodules package has some special error types it uses
   287  		// for certain cases where the UI layer might want to include a
   288  		// more helpful error message.
   289  		return ModuleSourceRemote{}, err
   290  	}
   291  
   292  	if moreSubDir != "" {
   293  		switch {
   294  		case subDir != "":
   295  			// The detector's own subdir goes first, because the
   296  			// subdir we were given is conceptually relative to
   297  			// the subdirectory that we just detected.
   298  			subDir = path.Join(moreSubDir, subDir)
   299  		default:
   300  			subDir = path.Clean(moreSubDir)
   301  		}
   302  		if strings.HasPrefix(subDir, "../") {
   303  			// This would suggest a bug in a go-getter detector, but
   304  			// we'll catch it anyway to avoid doing something confusing
   305  			// downstream.
   306  			return ModuleSourceRemote{}, fmt.Errorf("detected subdirectory path %q of %q leads outside of the module package", subDir, norm)
   307  		}
   308  	}
   309  
   310  	return ModuleSourceRemote{
   311  		Package: ModulePackage(norm),
   312  		Subdir:  subDir,
   313  	}, nil
   314  }
   315  
   316  func (s ModuleSourceRemote) moduleSource() {}
   317  
   318  func (s ModuleSourceRemote) String() string {
   319  	base := s.Package.String()
   320  
   321  	if s.Subdir != "" {
   322  		// Address contains query string
   323  		if strings.Contains(base, "?") {
   324  			parts := strings.SplitN(base, "?", 2)
   325  			return parts[0] + "//" + s.Subdir + "?" + parts[1]
   326  		}
   327  		return base + "//" + s.Subdir
   328  	}
   329  	return base
   330  }
   331  
   332  func (s ModuleSourceRemote) ForDisplay() string {
   333  	// The two string representations are identical for this address type.
   334  	// This isn't really entirely true to the idea of "ForDisplay" since
   335  	// it'll often include some additional components added in by the
   336  	// go-getter detectors, but we don't have any function to turn a
   337  	// "detected" string back into an idiomatic shorthand the user might've
   338  	// entered.
   339  	return s.String()
   340  }
   341  
   342  // FromRegistry can be called on a remote source address that was returned
   343  // from a module registry, passing in the original registry source address
   344  // that the registry was asked about, in order to get the effective final
   345  // remote source address.
   346  //
   347  // Specifically, this method handles the situations where one or both of
   348  // the two addresses contain subdirectory paths, combining both when necessary
   349  // in order to ensure that both the registry's given path and the user's
   350  // given path are both respected.
   351  //
   352  // This will return nonsense if given a registry address other than the one
   353  // that generated the reciever via a registry lookup.
   354  func (s ModuleSourceRemote) FromRegistry(given ModuleSourceRegistry) ModuleSourceRemote {
   355  	ret := s // not a pointer, so this is a shallow copy
   356  
   357  	switch {
   358  	case s.Subdir != "" && given.Subdir != "":
   359  		ret.Subdir = path.Join(s.Subdir, given.Subdir)
   360  	case given.Subdir != "":
   361  		ret.Subdir = given.Subdir
   362  	}
   363  
   364  	return ret
   365  }