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