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