github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/command/import.go (about) 1 package command 2 3 import ( 4 "errors" 5 "fmt" 6 "log" 7 "os" 8 "strings" 9 10 "github.com/hashicorp/hcl/v2" 11 "github.com/hashicorp/hcl/v2/hclsyntax" 12 13 "github.com/hashicorp/terraform/addrs" 14 "github.com/hashicorp/terraform/backend" 15 "github.com/hashicorp/terraform/configs" 16 "github.com/hashicorp/terraform/terraform" 17 "github.com/hashicorp/terraform/tfdiags" 18 ) 19 20 // ImportCommand is a cli.Command implementation that imports resources 21 // into the Terraform state. 22 type ImportCommand struct { 23 Meta 24 } 25 26 func (c *ImportCommand) Run(args []string) int { 27 // Get the pwd since its our default -config flag value 28 pwd, err := os.Getwd() 29 if err != nil { 30 c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err)) 31 return 1 32 } 33 34 var configPath string 35 args, err = c.Meta.process(args, true) 36 if err != nil { 37 return 1 38 } 39 40 cmdFlags := c.Meta.extendedFlagSet("import") 41 cmdFlags.IntVar(&c.Meta.parallelism, "parallelism", DefaultParallelism, "parallelism") 42 cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path") 43 cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path") 44 cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path") 45 cmdFlags.StringVar(&configPath, "config", pwd, "path") 46 cmdFlags.StringVar(&c.Meta.provider, "provider", "", "provider") 47 cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state") 48 cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout") 49 cmdFlags.BoolVar(&c.Meta.allowMissingConfig, "allow-missing-config", false, "allow missing config") 50 cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } 51 if err := cmdFlags.Parse(args); err != nil { 52 return 1 53 } 54 55 args = cmdFlags.Args() 56 if len(args) != 2 { 57 c.Ui.Error("The import command expects two arguments.") 58 cmdFlags.Usage() 59 return 1 60 } 61 62 var diags tfdiags.Diagnostics 63 64 // Parse the provided resource address. 65 traversalSrc := []byte(args[0]) 66 traversal, travDiags := hclsyntax.ParseTraversalAbs(traversalSrc, "<import-address>", hcl.Pos{Line: 1, Column: 1}) 67 diags = diags.Append(travDiags) 68 if travDiags.HasErrors() { 69 c.registerSynthConfigSource("<import-address>", traversalSrc) // so we can include a source snippet 70 c.showDiagnostics(diags) 71 c.Ui.Info(importCommandInvalidAddressReference) 72 return 1 73 } 74 addr, addrDiags := addrs.ParseAbsResourceInstance(traversal) 75 diags = diags.Append(addrDiags) 76 if addrDiags.HasErrors() { 77 c.registerSynthConfigSource("<import-address>", traversalSrc) // so we can include a source snippet 78 c.showDiagnostics(diags) 79 c.Ui.Info(importCommandInvalidAddressReference) 80 return 1 81 } 82 83 if addr.Resource.Resource.Mode != addrs.ManagedResourceMode { 84 diags = diags.Append(errors.New("A managed resource address is required. Importing into a data resource is not allowed.")) 85 c.showDiagnostics(diags) 86 return 1 87 } 88 89 if !c.dirIsConfigPath(configPath) { 90 diags = diags.Append(&hcl.Diagnostic{ 91 Severity: hcl.DiagError, 92 Summary: "No Terraform configuration files", 93 Detail: fmt.Sprintf( 94 "The directory %s does not contain any Terraform configuration files (.tf or .tf.json). To specify a different configuration directory, use the -config=\"...\" command line option.", 95 configPath, 96 ), 97 }) 98 c.showDiagnostics(diags) 99 return 1 100 } 101 102 // Load the full config, so we can verify that the target resource is 103 // already configured. 104 config, configDiags := c.loadConfig(configPath) 105 diags = diags.Append(configDiags) 106 if configDiags.HasErrors() { 107 c.showDiagnostics(diags) 108 return 1 109 } 110 111 // Verify that the given address points to something that exists in config. 112 // This is to reduce the risk that a typo in the resource address will 113 // import something that Terraform will want to immediately destroy on 114 // the next plan, and generally acts as a reassurance of user intent. 115 targetConfig := config.DescendentForInstance(addr.Module) 116 if targetConfig == nil { 117 modulePath := addr.Module.String() 118 diags = diags.Append(&hcl.Diagnostic{ 119 Severity: hcl.DiagError, 120 Summary: "Import to non-existent module", 121 Detail: fmt.Sprintf( 122 "%s is not defined in the configuration. Please add configuration for this module before importing into it.", 123 modulePath, 124 ), 125 }) 126 c.showDiagnostics(diags) 127 return 1 128 } 129 targetMod := targetConfig.Module 130 rcs := targetMod.ManagedResources 131 var rc *configs.Resource 132 resourceRelAddr := addr.Resource.Resource 133 for _, thisRc := range rcs { 134 if resourceRelAddr.Type == thisRc.Type && resourceRelAddr.Name == thisRc.Name { 135 rc = thisRc 136 break 137 } 138 } 139 if !c.Meta.allowMissingConfig && rc == nil { 140 modulePath := addr.Module.String() 141 if modulePath == "" { 142 modulePath = "the root module" 143 } 144 145 c.showDiagnostics(diags) 146 147 // This is not a diagnostic because currently our diagnostics printer 148 // doesn't support having a code example in the detail, and there's 149 // a code example in this message. 150 // TODO: Improve the diagnostics printer so we can use it for this 151 // message. 152 c.Ui.Error(fmt.Sprintf( 153 importCommandMissingResourceFmt, 154 addr, modulePath, resourceRelAddr.Type, resourceRelAddr.Name, 155 )) 156 return 1 157 } 158 159 // Also parse the user-provided provider address, if any. 160 var providerAddr addrs.AbsProviderConfig 161 if c.Meta.provider != "" { 162 traversal, travDiags := hclsyntax.ParseTraversalAbs([]byte(c.Meta.provider), `-provider=...`, hcl.Pos{Line: 1, Column: 1}) 163 diags = diags.Append(travDiags) 164 if travDiags.HasErrors() { 165 c.showDiagnostics(diags) 166 c.Ui.Info(importCommandInvalidAddressReference) 167 return 1 168 } 169 relAddr, addrDiags := configs.ParseProviderConfigCompact(traversal) 170 diags = diags.Append(addrDiags) 171 if addrDiags.HasErrors() { 172 c.showDiagnostics(diags) 173 return 1 174 } 175 providerAddr = relAddr.Absolute(addrs.RootModuleInstance) 176 } else { 177 // Use a default address inferred from the resource type. 178 // We assume the same module as the resource address here, which 179 // may get resolved to an inherited provider when we construct the 180 // import graph inside ctx.Import, called below. 181 if rc != nil && rc.ProviderConfigRef != nil { 182 providerAddr = rc.ProviderConfigAddr().Absolute(addr.Module) 183 } else { 184 providerAddr = resourceRelAddr.DefaultProviderConfig().Absolute(addr.Module) 185 } 186 } 187 188 // Check for user-supplied plugin path 189 if c.pluginPath, err = c.loadPluginPath(); err != nil { 190 c.Ui.Error(fmt.Sprintf("Error loading plugin path: %s", err)) 191 return 1 192 } 193 194 // Load the backend 195 b, backendDiags := c.Backend(&BackendOpts{ 196 Config: config.Module.Backend, 197 }) 198 diags = diags.Append(backendDiags) 199 if backendDiags.HasErrors() { 200 c.showDiagnostics(diags) 201 return 1 202 } 203 204 // We require a backend.Local to build a context. 205 // This isn't necessarily a "local.Local" backend, which provides local 206 // operations, however that is the only current implementation. A 207 // "local.Local" backend also doesn't necessarily provide local state, as 208 // that may be delegated to a "remotestate.Backend". 209 local, ok := b.(backend.Local) 210 if !ok { 211 c.Ui.Error(ErrUnsupportedLocalOp) 212 return 1 213 } 214 215 // Build the operation 216 opReq := c.Operation(b) 217 opReq.ConfigDir = configPath 218 opReq.ConfigLoader, err = c.initConfigLoader() 219 if err != nil { 220 diags = diags.Append(err) 221 c.showDiagnostics(diags) 222 return 1 223 } 224 { 225 var moreDiags tfdiags.Diagnostics 226 opReq.Variables, moreDiags = c.collectVariableValues() 227 diags = diags.Append(moreDiags) 228 if moreDiags.HasErrors() { 229 c.showDiagnostics(diags) 230 return 1 231 } 232 } 233 234 // Get the context 235 ctx, state, ctxDiags := local.Context(opReq) 236 237 defer func() { 238 err := opReq.StateLocker.Unlock(nil) 239 if err != nil { 240 c.Ui.Error(err.Error()) 241 } 242 }() 243 244 diags = diags.Append(ctxDiags) 245 if ctxDiags.HasErrors() { 246 c.showDiagnostics(diags) 247 return 1 248 } 249 250 // Perform the import. Note that as you can see it is possible for this 251 // API to import more than one resource at once. For now, we only allow 252 // one while we stabilize this feature. 253 newState, importDiags := ctx.Import(&terraform.ImportOpts{ 254 Targets: []*terraform.ImportTarget{ 255 &terraform.ImportTarget{ 256 Addr: addr, 257 ID: args[1], 258 ProviderAddr: providerAddr, 259 }, 260 }, 261 }) 262 diags = diags.Append(importDiags) 263 if diags.HasErrors() { 264 c.showDiagnostics(diags) 265 return 1 266 } 267 268 // Persist the final state 269 log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath()) 270 if err := state.WriteState(newState); err != nil { 271 c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err)) 272 return 1 273 } 274 if err := state.PersistState(); err != nil { 275 c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err)) 276 return 1 277 } 278 279 c.Ui.Output(c.Colorize().Color("[reset][green]\n" + importCommandSuccessMsg)) 280 281 if c.Meta.allowMissingConfig && rc == nil { 282 c.Ui.Output(c.Colorize().Color("[reset][yellow]\n" + importCommandAllowMissingResourceMsg)) 283 } 284 285 c.showDiagnostics(diags) 286 if diags.HasErrors() { 287 return 1 288 } 289 290 return 0 291 } 292 293 func (c *ImportCommand) Help() string { 294 helpText := ` 295 Usage: terraform import [options] ADDR ID 296 297 Import existing infrastructure into your Terraform state. 298 299 This will find and import the specified resource into your Terraform 300 state, allowing existing infrastructure to come under Terraform 301 management without having to be initially created by Terraform. 302 303 The ADDR specified is the address to import the resource to. Please 304 see the documentation online for resource addresses. The ID is a 305 resource-specific ID to identify that resource being imported. Please 306 reference the documentation for the resource type you're importing to 307 determine the ID syntax to use. It typically matches directly to the ID 308 that the provider uses. 309 310 The current implementation of Terraform import can only import resources 311 into the state. It does not generate configuration. A future version of 312 Terraform will also generate configuration. 313 314 Because of this, prior to running terraform import it is necessary to write 315 a resource configuration block for the resource manually, to which the 316 imported object will be attached. 317 318 This command will not modify your infrastructure, but it will make 319 network requests to inspect parts of your infrastructure relevant to 320 the resource being imported. 321 322 Options: 323 324 -backup=path Path to backup the existing state file before 325 modifying. Defaults to the "-state-out" path with 326 ".backup" extension. Set to "-" to disable backup. 327 328 -config=path Path to a directory of Terraform configuration files 329 to use to configure the provider. Defaults to pwd. 330 If no config files are present, they must be provided 331 via the input prompts or env vars. 332 333 -allow-missing-config Allow import when no resource configuration block exists. 334 335 -input=true Ask for input for variables if not directly set. 336 337 -lock=true Lock the state file when locking is supported. 338 339 -lock-timeout=0s Duration to retry a state lock. 340 341 -no-color If specified, output won't contain any color. 342 343 -provider=provider Deprecated: Override the provider configuration to use 344 when importing the object. By default, Terraform uses the 345 provider specified in the configuration for the target 346 resource, and that is the best behavior in most cases. 347 348 -state=PATH Path to the source state file. Defaults to the configured 349 backend, or "terraform.tfstate" 350 351 -state-out=PATH Path to the destination state file to write to. If this 352 isn't specified, the source state file will be used. This 353 can be a new or existing path. 354 355 -var 'foo=bar' Set a variable in the Terraform configuration. This 356 flag can be set multiple times. This is only useful 357 with the "-config" flag. 358 359 -var-file=foo Set variables in the Terraform configuration from 360 a file. If "terraform.tfvars" or any ".auto.tfvars" 361 files are present, they will be automatically loaded. 362 363 364 ` 365 return strings.TrimSpace(helpText) 366 } 367 368 func (c *ImportCommand) Synopsis() string { 369 return "Import existing infrastructure into Terraform" 370 } 371 372 const importCommandInvalidAddressReference = `For information on valid syntax, see: 373 https://www.terraform.io/docs/internals/resource-addressing.html` 374 375 const importCommandMissingResourceFmt = `[reset][bold][red]Error:[reset][bold] resource address %q does not exist in the configuration.[reset] 376 377 Before importing this resource, please create its configuration in %s. For example: 378 379 resource %q %q { 380 # (resource arguments) 381 } 382 ` 383 384 const importCommandSuccessMsg = `Import successful! 385 386 The resources that were imported are shown above. These resources are now in 387 your Terraform state and will henceforth be managed by Terraform. 388 ` 389 390 const importCommandAllowMissingResourceMsg = `Import does not generate resource configuration, you must create a resource 391 configuration block that matches the current or desired state manually. 392 393 If there is no matching resource configuration block for the imported 394 resource, Terraform will delete the resource on the next "terraform apply". 395 It is recommended that you run "terraform plan" to verify that the 396 configuration is correct and complete. 397 `