github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/initwd/from_module.go (about) 1 package initwd 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "log" 7 "os" 8 "path/filepath" 9 "sort" 10 "strings" 11 12 "github.com/hashicorp/terraform/internal/copy" 13 "github.com/hashicorp/terraform/internal/earlyconfig" 14 "github.com/hashicorp/terraform/internal/getmodules" 15 16 version "github.com/hashicorp/go-version" 17 "github.com/hashicorp/terraform-config-inspect/tfconfig" 18 "github.com/hashicorp/terraform/internal/modsdir" 19 "github.com/hashicorp/terraform/internal/registry" 20 "github.com/hashicorp/terraform/internal/tfdiags" 21 ) 22 23 const initFromModuleRootCallName = "root" 24 const initFromModuleRootKeyPrefix = initFromModuleRootCallName + "." 25 26 // DirFromModule populates the given directory (which must exist and be 27 // empty) with the contents of the module at the given source address. 28 // 29 // It does this by installing the given module and all of its descendent 30 // modules in a temporary root directory and then copying the installed 31 // files into suitable locations. As a consequence, any diagnostics it 32 // generates will reveal the location of this temporary directory to the 33 // user. 34 // 35 // This rather roundabout installation approach is taken to ensure that 36 // installation proceeds in a manner identical to normal module installation. 37 // 38 // If the given source address specifies a sub-directory of the given 39 // package then only the sub-directory and its descendents will be copied 40 // into the given root directory, which will cause any relative module 41 // references using ../ from that module to be unresolvable. Error diagnostics 42 // are produced in that case, to prompt the user to rewrite the source strings 43 // to be absolute references to the original remote module. 44 func DirFromModule(rootDir, modulesDir, sourceAddr string, reg *registry.Client, hooks ModuleInstallHooks) tfdiags.Diagnostics { 45 var diags tfdiags.Diagnostics 46 47 // The way this function works is pretty ugly, but we accept it because 48 // -from-module is a less important case than normal module installation 49 // and so it's better to keep this ugly complexity out here rather than 50 // adding even more complexity to the normal module installer. 51 52 // The target directory must exist but be empty. 53 { 54 entries, err := ioutil.ReadDir(rootDir) 55 if err != nil { 56 if os.IsNotExist(err) { 57 diags = diags.Append(tfdiags.Sourceless( 58 tfdiags.Error, 59 "Target directory does not exist", 60 fmt.Sprintf("Cannot initialize non-existent directory %s.", rootDir), 61 )) 62 } else { 63 diags = diags.Append(tfdiags.Sourceless( 64 tfdiags.Error, 65 "Failed to read target directory", 66 fmt.Sprintf("Error reading %s to ensure it is empty: %s.", rootDir, err), 67 )) 68 } 69 return diags 70 } 71 haveEntries := false 72 for _, entry := range entries { 73 if entry.Name() == "." || entry.Name() == ".." || entry.Name() == ".terraform" { 74 continue 75 } 76 haveEntries = true 77 } 78 if haveEntries { 79 diags = diags.Append(tfdiags.Sourceless( 80 tfdiags.Error, 81 "Can't populate non-empty directory", 82 fmt.Sprintf("The target directory %s is not empty, so it cannot be initialized with the -from-module=... option.", rootDir), 83 )) 84 return diags 85 } 86 } 87 88 instDir := filepath.Join(rootDir, ".terraform/init-from-module") 89 inst := NewModuleInstaller(instDir, reg) 90 log.Printf("[DEBUG] installing modules in %s to initialize working directory from %q", instDir, sourceAddr) 91 os.RemoveAll(instDir) // if this fails then we'll fail on MkdirAll below too 92 err := os.MkdirAll(instDir, os.ModePerm) 93 if err != nil { 94 diags = diags.Append(tfdiags.Sourceless( 95 tfdiags.Error, 96 "Failed to create temporary directory", 97 fmt.Sprintf("Failed to create temporary directory %s: %s.", instDir, err), 98 )) 99 return diags 100 } 101 102 instManifest := make(modsdir.Manifest) 103 retManifest := make(modsdir.Manifest) 104 105 fakeFilename := fmt.Sprintf("-from-module=%q", sourceAddr) 106 fakePos := tfconfig.SourcePos{ 107 Filename: fakeFilename, 108 Line: 1, 109 } 110 111 // -from-module allows relative paths but it's different than a normal 112 // module address where it'd be resolved relative to the module call 113 // (which is synthetic, here.) To address this, we'll just patch up any 114 // relative paths to be absolute paths before we run, ensuring we'll 115 // get the right result. This also, as an important side-effect, ensures 116 // that the result will be "downloaded" with go-getter (copied from the 117 // source location), rather than just recorded as a relative path. 118 { 119 maybePath := filepath.ToSlash(sourceAddr) 120 if maybePath == "." || strings.HasPrefix(maybePath, "./") || strings.HasPrefix(maybePath, "../") { 121 if wd, err := os.Getwd(); err == nil { 122 sourceAddr = filepath.Join(wd, sourceAddr) 123 log.Printf("[TRACE] -from-module relative path rewritten to absolute path %s", sourceAddr) 124 } 125 } 126 } 127 128 // Now we need to create an artificial root module that will seed our 129 // installation process. 130 fakeRootModule := &tfconfig.Module{ 131 ModuleCalls: map[string]*tfconfig.ModuleCall{ 132 initFromModuleRootCallName: { 133 Name: initFromModuleRootCallName, 134 Source: sourceAddr, 135 Pos: fakePos, 136 }, 137 }, 138 } 139 140 // wrapHooks filters hook notifications to only include Download calls 141 // and to trim off the initFromModuleRootCallName prefix. We'll produce 142 // our own Install notifications directly below. 143 wrapHooks := installHooksInitDir{ 144 Wrapped: hooks, 145 } 146 fetcher := getmodules.NewPackageFetcher() 147 _, instDiags := inst.installDescendentModules(fakeRootModule, rootDir, instManifest, true, wrapHooks, fetcher) 148 diags = append(diags, instDiags...) 149 if instDiags.HasErrors() { 150 return diags 151 } 152 153 // If all of that succeeded then we'll now migrate what was installed 154 // into the final directory structure. 155 err = os.MkdirAll(modulesDir, os.ModePerm) 156 if err != nil { 157 diags = diags.Append(tfdiags.Sourceless( 158 tfdiags.Error, 159 "Failed to create local modules directory", 160 fmt.Sprintf("Failed to create modules directory %s: %s.", modulesDir, err), 161 )) 162 return diags 163 } 164 165 recordKeys := make([]string, 0, len(instManifest)) 166 for k := range instManifest { 167 recordKeys = append(recordKeys, k) 168 } 169 sort.Strings(recordKeys) 170 171 for _, recordKey := range recordKeys { 172 record := instManifest[recordKey] 173 174 if record.Key == initFromModuleRootCallName { 175 // We've found the module the user requested, which we must 176 // now copy into rootDir so it can be used directly. 177 log.Printf("[TRACE] copying new root module from %s to %s", record.Dir, rootDir) 178 err := copy.CopyDir(rootDir, record.Dir) 179 if err != nil { 180 diags = diags.Append(tfdiags.Sourceless( 181 tfdiags.Error, 182 "Failed to copy root module", 183 fmt.Sprintf("Error copying root module %q from %s to %s: %s.", sourceAddr, record.Dir, rootDir, err), 184 )) 185 continue 186 } 187 188 // We'll try to load the newly-copied module here just so we can 189 // sniff for any module calls that ../ out of the root directory 190 // and must thus be rewritten to be absolute addresses again. 191 // For now we can't do this rewriting automatically, but we'll 192 // generate an error to help the user do it manually. 193 mod, _ := earlyconfig.LoadModule(rootDir) // ignore diagnostics since we're just doing value-add here anyway 194 if mod != nil { 195 for _, mc := range mod.ModuleCalls { 196 if pathTraversesUp(mc.Source) { 197 packageAddr, givenSubdir := getmodules.SplitPackageSubdir(sourceAddr) 198 newSubdir := filepath.Join(givenSubdir, mc.Source) 199 if pathTraversesUp(newSubdir) { 200 // This should never happen in any reasonable 201 // configuration since this suggests a path that 202 // traverses up out of the package root. We'll just 203 // ignore this, since we'll fail soon enough anyway 204 // trying to resolve this path when this module is 205 // loaded. 206 continue 207 } 208 209 var newAddr = packageAddr 210 if newSubdir != "" { 211 newAddr = fmt.Sprintf("%s//%s", newAddr, filepath.ToSlash(newSubdir)) 212 } 213 diags = diags.Append(tfdiags.Sourceless( 214 tfdiags.Error, 215 "Root module references parent directory", 216 fmt.Sprintf("The requested module %q refers to a module via its parent directory. To use this as a new root module this source string must be rewritten as a remote source address, such as %q.", sourceAddr, newAddr), 217 )) 218 continue 219 } 220 } 221 } 222 223 retManifest[""] = modsdir.Record{ 224 Key: "", 225 Dir: rootDir, 226 } 227 continue 228 } 229 230 if !strings.HasPrefix(record.Key, initFromModuleRootKeyPrefix) { 231 // Ignore the *real* root module, whose key is empty, since 232 // we're only interested in the module named "root" and its 233 // descendents. 234 continue 235 } 236 237 newKey := record.Key[len(initFromModuleRootKeyPrefix):] 238 instPath := filepath.Join(modulesDir, newKey) 239 tempPath := filepath.Join(instDir, record.Key) 240 241 // tempPath won't be present for a module that was installed from 242 // a relative path, so in that case we just record the installation 243 // directory and assume it was already copied into place as part 244 // of its parent. 245 if _, err := os.Stat(tempPath); err != nil { 246 if !os.IsNotExist(err) { 247 diags = diags.Append(tfdiags.Sourceless( 248 tfdiags.Error, 249 "Failed to stat temporary module install directory", 250 fmt.Sprintf("Error from stat %s for module %s: %s.", instPath, newKey, err), 251 )) 252 continue 253 } 254 255 var parentKey string 256 if lastDot := strings.LastIndexByte(newKey, '.'); lastDot != -1 { 257 parentKey = newKey[:lastDot] 258 } 259 260 var parentOld modsdir.Record 261 // "" is the root module; all other modules get `root.` added as a prefix 262 if parentKey == "" { 263 parentOld = instManifest[parentKey] 264 } else { 265 parentOld = instManifest[initFromModuleRootKeyPrefix+parentKey] 266 } 267 parentNew := retManifest[parentKey] 268 269 // We need to figure out which portion of our directory is the 270 // parent package path and which portion is the subdirectory 271 // under that. 272 var baseDirRel string 273 baseDirRel, err = filepath.Rel(parentOld.Dir, record.Dir) 274 if err != nil { 275 // This error may occur when installing a local module with a 276 // relative path, for e.g. if the source is in a directory above 277 // the destination ("../") 278 if parentOld.Dir == "." { 279 absDir, err := filepath.Abs(parentOld.Dir) 280 if err != nil { 281 diags = diags.Append(tfdiags.Sourceless( 282 tfdiags.Error, 283 "Failed to determine module install directory", 284 fmt.Sprintf("Error determine relative source directory for module %s: %s.", newKey, err), 285 )) 286 continue 287 } 288 baseDirRel, err = filepath.Rel(absDir, record.Dir) 289 if err != nil { 290 diags = diags.Append(tfdiags.Sourceless( 291 tfdiags.Error, 292 "Failed to determine relative module source location", 293 fmt.Sprintf("Error determining relative source for module %s: %s.", newKey, err), 294 )) 295 continue 296 } 297 } else { 298 diags = diags.Append(tfdiags.Sourceless( 299 tfdiags.Error, 300 "Failed to determine relative module source location", 301 fmt.Sprintf("Error determining relative source for module %s: %s.", newKey, err), 302 )) 303 } 304 } 305 306 newDir := filepath.Join(parentNew.Dir, baseDirRel) 307 log.Printf("[TRACE] relative reference for %s rewritten from %s to %s", newKey, record.Dir, newDir) 308 newRecord := record // shallow copy 309 newRecord.Dir = newDir 310 newRecord.Key = newKey 311 retManifest[newKey] = newRecord 312 hooks.Install(newRecord.Key, newRecord.Version, newRecord.Dir) 313 continue 314 } 315 316 err = os.MkdirAll(instPath, os.ModePerm) 317 if err != nil { 318 diags = diags.Append(tfdiags.Sourceless( 319 tfdiags.Error, 320 "Failed to create module install directory", 321 fmt.Sprintf("Error creating directory %s for module %s: %s.", instPath, newKey, err), 322 )) 323 continue 324 } 325 326 // We copy rather than "rename" here because renaming between directories 327 // can be tricky in edge-cases like network filesystems, etc. 328 log.Printf("[TRACE] copying new module %s from %s to %s", newKey, record.Dir, instPath) 329 err := copy.CopyDir(instPath, tempPath) 330 if err != nil { 331 diags = diags.Append(tfdiags.Sourceless( 332 tfdiags.Error, 333 "Failed to copy descendent module", 334 fmt.Sprintf("Error copying module %q from %s to %s: %s.", newKey, tempPath, rootDir, err), 335 )) 336 continue 337 } 338 339 subDir, err := filepath.Rel(tempPath, record.Dir) 340 if err != nil { 341 // Should never happen, because we constructed both directories 342 // from the same base and so they must have a common prefix. 343 panic(err) 344 } 345 346 newRecord := record // shallow copy 347 newRecord.Dir = filepath.Join(instPath, subDir) 348 newRecord.Key = newKey 349 retManifest[newKey] = newRecord 350 hooks.Install(newRecord.Key, newRecord.Version, newRecord.Dir) 351 } 352 353 retManifest.WriteSnapshotToDir(modulesDir) 354 if err != nil { 355 diags = diags.Append(tfdiags.Sourceless( 356 tfdiags.Error, 357 "Failed to write module manifest", 358 fmt.Sprintf("Error writing module manifest: %s.", err), 359 )) 360 } 361 362 if !diags.HasErrors() { 363 // Try to clean up our temporary directory, but don't worry if we don't 364 // succeed since it shouldn't hurt anything. 365 os.RemoveAll(instDir) 366 } 367 368 return diags 369 } 370 371 func pathTraversesUp(path string) bool { 372 return strings.HasPrefix(filepath.ToSlash(path), "../") 373 } 374 375 // installHooksInitDir is an adapter wrapper for an InstallHooks that 376 // does some fakery to make downloads look like they are happening in their 377 // final locations, rather than in the temporary loader we use. 378 // 379 // It also suppresses "Install" calls entirely, since InitDirFromModule 380 // does its own installation steps after the initial installation pass 381 // has completed. 382 type installHooksInitDir struct { 383 Wrapped ModuleInstallHooks 384 ModuleInstallHooksImpl 385 } 386 387 func (h installHooksInitDir) Download(moduleAddr, packageAddr string, version *version.Version) { 388 if !strings.HasPrefix(moduleAddr, initFromModuleRootKeyPrefix) { 389 // We won't announce the root module, since hook implementations 390 // don't expect to see that and the caller will usually have produced 391 // its own user-facing notification about what it's doing anyway. 392 return 393 } 394 395 trimAddr := moduleAddr[len(initFromModuleRootKeyPrefix):] 396 h.Wrapped.Download(trimAddr, packageAddr, version) 397 }