github.com/kevinklinger/open_terraform@v1.3.6/noninternal/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/kevinklinger/open_terraform/noninternal/addrs" 14 "github.com/kevinklinger/open_terraform/noninternal/backend" 15 "github.com/kevinklinger/open_terraform/noninternal/command/arguments" 16 "github.com/kevinklinger/open_terraform/noninternal/command/views" 17 "github.com/kevinklinger/open_terraform/noninternal/configs" 18 "github.com/kevinklinger/open_terraform/noninternal/terraform" 19 "github.com/kevinklinger/open_terraform/noninternal/tfdiags" 20 ) 21 22 // ImportCommand is a cli.Command implementation that imports resources 23 // into the Terraform state. 24 type ImportCommand struct { 25 Meta 26 } 27 28 func (c *ImportCommand) Run(args []string) int { 29 // Get the pwd since its our default -config flag value 30 pwd, err := os.Getwd() 31 if err != nil { 32 c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err)) 33 return 1 34 } 35 36 var configPath string 37 args = c.Meta.process(args) 38 39 cmdFlags := c.Meta.extendedFlagSet("import") 40 cmdFlags.BoolVar(&c.ignoreRemoteVersion, "ignore-remote-version", false, "continue even if remote and local Terraform versions are incompatible") 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.BoolVar(&c.Meta.stateLock, "lock", true, "lock state") 47 cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout") 48 cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } 49 if err := cmdFlags.Parse(args); err != nil { 50 return 1 51 } 52 53 args = cmdFlags.Args() 54 if len(args) != 2 { 55 c.Ui.Error("The import command expects two arguments.") 56 cmdFlags.Usage() 57 return 1 58 } 59 60 var diags tfdiags.Diagnostics 61 62 // Parse the provided resource address. 63 traversalSrc := []byte(args[0]) 64 traversal, travDiags := hclsyntax.ParseTraversalAbs(traversalSrc, "<import-address>", hcl.Pos{Line: 1, Column: 1}) 65 diags = diags.Append(travDiags) 66 if travDiags.HasErrors() { 67 c.registerSynthConfigSource("<import-address>", traversalSrc) // so we can include a source snippet 68 c.showDiagnostics(diags) 69 c.Ui.Info(importCommandInvalidAddressReference) 70 return 1 71 } 72 addr, addrDiags := addrs.ParseAbsResourceInstance(traversal) 73 diags = diags.Append(addrDiags) 74 if addrDiags.HasErrors() { 75 c.registerSynthConfigSource("<import-address>", traversalSrc) // so we can include a source snippet 76 c.showDiagnostics(diags) 77 c.Ui.Info(importCommandInvalidAddressReference) 78 return 1 79 } 80 81 if addr.Resource.Resource.Mode != addrs.ManagedResourceMode { 82 diags = diags.Append(errors.New("A managed resource address is required. Importing into a data resource is not allowed.")) 83 c.showDiagnostics(diags) 84 return 1 85 } 86 87 if !c.dirIsConfigPath(configPath) { 88 diags = diags.Append(&hcl.Diagnostic{ 89 Severity: hcl.DiagError, 90 Summary: "No Terraform configuration files", 91 Detail: fmt.Sprintf( 92 "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.", 93 configPath, 94 ), 95 }) 96 c.showDiagnostics(diags) 97 return 1 98 } 99 100 // Load the full config, so we can verify that the target resource is 101 // already configured. 102 config, configDiags := c.loadConfig(configPath) 103 diags = diags.Append(configDiags) 104 if configDiags.HasErrors() { 105 c.showDiagnostics(diags) 106 return 1 107 } 108 109 // Verify that the given address points to something that exists in config. 110 // This is to reduce the risk that a typo in the resource address will 111 // import something that Terraform will want to immediately destroy on 112 // the next plan, and generally acts as a reassurance of user intent. 113 targetConfig := config.DescendentForInstance(addr.Module) 114 if targetConfig == nil { 115 modulePath := addr.Module.String() 116 diags = diags.Append(&hcl.Diagnostic{ 117 Severity: hcl.DiagError, 118 Summary: "Import to non-existent module", 119 Detail: fmt.Sprintf( 120 "%s is not defined in the configuration. Please add configuration for this module before importing into it.", 121 modulePath, 122 ), 123 }) 124 c.showDiagnostics(diags) 125 return 1 126 } 127 targetMod := targetConfig.Module 128 rcs := targetMod.ManagedResources 129 var rc *configs.Resource 130 resourceRelAddr := addr.Resource.Resource 131 for _, thisRc := range rcs { 132 if resourceRelAddr.Type == thisRc.Type && resourceRelAddr.Name == thisRc.Name { 133 rc = thisRc 134 break 135 } 136 } 137 if rc == nil { 138 modulePath := addr.Module.String() 139 if modulePath == "" { 140 modulePath = "the root module" 141 } 142 143 c.showDiagnostics(diags) 144 145 // This is not a diagnostic because currently our diagnostics printer 146 // doesn't support having a code example in the detail, and there's 147 // a code example in this message. 148 // TODO: Improve the diagnostics printer so we can use it for this 149 // message. 150 c.Ui.Error(fmt.Sprintf( 151 importCommandMissingResourceFmt, 152 addr, modulePath, resourceRelAddr.Type, resourceRelAddr.Name, 153 )) 154 return 1 155 } 156 157 // Check for user-supplied plugin path 158 if c.pluginPath, err = c.loadPluginPath(); err != nil { 159 c.Ui.Error(fmt.Sprintf("Error loading plugin path: %s", err)) 160 return 1 161 } 162 163 // Load the backend 164 b, backendDiags := c.Backend(&BackendOpts{ 165 Config: config.Module.Backend, 166 }) 167 diags = diags.Append(backendDiags) 168 if backendDiags.HasErrors() { 169 c.showDiagnostics(diags) 170 return 1 171 } 172 173 // We require a backend.Local to build a context. 174 // This isn't necessarily a "local.Local" backend, which provides local 175 // operations, however that is the only current implementation. A 176 // "local.Local" backend also doesn't necessarily provide local state, as 177 // that may be delegated to a "remotestate.Backend". 178 local, ok := b.(backend.Local) 179 if !ok { 180 c.Ui.Error(ErrUnsupportedLocalOp) 181 return 1 182 } 183 184 // Build the operation 185 opReq := c.Operation(b) 186 opReq.ConfigDir = configPath 187 opReq.ConfigLoader, err = c.initConfigLoader() 188 if err != nil { 189 diags = diags.Append(err) 190 c.showDiagnostics(diags) 191 return 1 192 } 193 opReq.Hooks = []terraform.Hook{c.uiHook()} 194 { 195 var moreDiags tfdiags.Diagnostics 196 opReq.Variables, moreDiags = c.collectVariableValues() 197 diags = diags.Append(moreDiags) 198 if moreDiags.HasErrors() { 199 c.showDiagnostics(diags) 200 return 1 201 } 202 } 203 opReq.View = views.NewOperation(arguments.ViewHuman, c.RunningInAutomation, c.View) 204 205 // Check remote Terraform version is compatible 206 remoteVersionDiags := c.remoteVersionCheck(b, opReq.Workspace) 207 diags = diags.Append(remoteVersionDiags) 208 c.showDiagnostics(diags) 209 if diags.HasErrors() { 210 return 1 211 } 212 213 // Get the context 214 lr, state, ctxDiags := local.LocalRun(opReq) 215 diags = diags.Append(ctxDiags) 216 if ctxDiags.HasErrors() { 217 c.showDiagnostics(diags) 218 return 1 219 } 220 221 // Successfully creating the context can result in a lock, so ensure we release it 222 defer func() { 223 diags := opReq.StateLocker.Unlock() 224 if diags.HasErrors() { 225 c.showDiagnostics(diags) 226 } 227 }() 228 229 // Perform the import. Note that as you can see it is possible for this 230 // API to import more than one resource at once. For now, we only allow 231 // one while we stabilize this feature. 232 newState, importDiags := lr.Core.Import(lr.Config, lr.InputState, &terraform.ImportOpts{ 233 Targets: []*terraform.ImportTarget{ 234 { 235 Addr: addr, 236 ID: args[1], 237 }, 238 }, 239 240 // The LocalRun idea is designed around our primary operations, so 241 // the input variables end up represented as plan options even though 242 // this particular operation isn't really a plan. 243 SetVariables: lr.PlanOpts.SetVariables, 244 }) 245 diags = diags.Append(importDiags) 246 if diags.HasErrors() { 247 c.showDiagnostics(diags) 248 return 1 249 } 250 251 // Get schemas, if possible, before writing state 252 var schemas *terraform.Schemas 253 if isCloudMode(b) { 254 var schemaDiags tfdiags.Diagnostics 255 schemas, schemaDiags = c.MaybeGetSchemas(newState, nil) 256 diags = diags.Append(schemaDiags) 257 } 258 259 // Persist the final state 260 log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath()) 261 if err := state.WriteState(newState); err != nil { 262 c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err)) 263 return 1 264 } 265 if err := state.PersistState(schemas); err != nil { 266 c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err)) 267 return 1 268 } 269 270 c.Ui.Output(c.Colorize().Color("[reset][green]\n" + importCommandSuccessMsg)) 271 272 c.showDiagnostics(diags) 273 if diags.HasErrors() { 274 return 1 275 } 276 277 return 0 278 } 279 280 func (c *ImportCommand) Help() string { 281 helpText := ` 282 Usage: terraform [global options] import [options] ADDR ID 283 284 Import existing infrastructure into your Terraform state. 285 286 This will find and import the specified resource into your Terraform 287 state, allowing existing infrastructure to come under Terraform 288 management without having to be initially created by Terraform. 289 290 The ADDR specified is the address to import the resource to. Please 291 see the documentation online for resource addresses. The ID is a 292 resource-specific ID to identify that resource being imported. Please 293 reference the documentation for the resource type you're importing to 294 determine the ID syntax to use. It typically matches directly to the ID 295 that the provider uses. 296 297 The current implementation of Terraform import can only import resources 298 into the state. It does not generate configuration. A future version of 299 Terraform will also generate configuration. 300 301 Because of this, prior to running terraform import it is necessary to write 302 a resource configuration block for the resource manually, to which the 303 imported object will be attached. 304 305 This command will not modify your infrastructure, but it will make 306 network requests to inspect parts of your infrastructure relevant to 307 the resource being imported. 308 309 Options: 310 311 -config=path Path to a directory of Terraform configuration files 312 to use to configure the provider. Defaults to pwd. 313 If no config files are present, they must be provided 314 via the input prompts or env vars. 315 316 -input=false Disable interactive input prompts. 317 318 -lock=false Don't hold a state lock during the operation. This is 319 dangerous if others might concurrently run commands 320 against the same workspace. 321 322 -lock-timeout=0s Duration to retry a state lock. 323 324 -no-color If specified, output won't contain any color. 325 326 -var 'foo=bar' Set a variable in the Terraform configuration. This 327 flag can be set multiple times. This is only useful 328 with the "-config" flag. 329 330 -var-file=foo Set variables in the Terraform configuration from 331 a file. If "terraform.tfvars" or any ".auto.tfvars" 332 files are present, they will be automatically loaded. 333 334 -ignore-remote-version A rare option used for the remote backend only. See 335 the remote backend documentation for more information. 336 337 -state, state-out, and -backup are legacy options supported for the local 338 backend only. For more information, see the local backend's documentation. 339 340 ` 341 return strings.TrimSpace(helpText) 342 } 343 344 func (c *ImportCommand) Synopsis() string { 345 return "Associate existing infrastructure with a Terraform resource" 346 } 347 348 const importCommandInvalidAddressReference = `For information on valid syntax, see: 349 https://www.terraform.io/docs/cli/state/resource-addressing.html` 350 351 const importCommandMissingResourceFmt = `[reset][bold][red]Error:[reset][bold] resource address %q does not exist in the configuration.[reset] 352 353 Before importing this resource, please create its configuration in %s. For example: 354 355 resource %q %q { 356 # (resource arguments) 357 } 358 ` 359 360 const importCommandSuccessMsg = `Import successful! 361 362 The resources that were imported are shown above. These resources are now in 363 your Terraform state and will henceforth be managed by Terraform. 364 `