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