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  `