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