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