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 }