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  `