func planAndImport()

in internal/tfimport/tfimport.go [638:804]


func planAndImport(rn, importRn runner.Runner, runArgs *RunArgs, skipped map[string]string) (successes []string, err error) {
	// Create Terraform command runners.
	tfCmdOutput := func(args ...string) ([]byte, error) {
		cmd := exec.Command(runArgs.TerraformPath, args...)
		cmd.Dir = runArgs.InputDir
		return rn.CmdOutput(cmd)
	}

	// Init is safe to run on an already-initialized config dir.
	if out, err := tfCmdOutput("init"); err != nil {
		return nil, fmt.Errorf("init: %v\v%v", err, string(out))
	}

	// Generate and load the plan using a temp file.
	// Use the .tf files input dir in case the system can't write to /tmp.
	tmpfile, err := ioutil.TempFile(runArgs.InputDir, "plan-for-import-*.tfplan")
	if err != nil {
		return nil, fmt.Errorf("create temp file: %v", err)
	}
	defer os.Remove(tmpfile.Name())
	planName := path.Base(tmpfile.Name())
	if out, err := tfCmdOutput("plan", "-out", planName); err != nil {
		return nil, fmt.Errorf("plan: %v\n%v", err, string(out))
	}
	b, err := tfCmdOutput("show", "-json", planName)
	if err != nil {
		return nil, fmt.Errorf("show: %v\n%v", err, string(b))
	}

	// Load only "create" changes.
	createChanges, err := terraform.ReadPlanChanges(b, []string{"create"})
	if err != nil {
		return nil, fmt.Errorf("read Terraform plan changes: %q", err)
	}

	// Import all importable create changes.
	var importCmds []string
	var notImportableMsgs []string
	var failures []string
	for _, cc := range createChanges {
		// If previously skipped, skip again
		if _, ok := skipped[cc.Address]; ok {
			continue
		}

		// Get the provider config values (pcv) for this particular resource.
		// This is needed to determine if it's possible to import the resource.
		pcv, err := terraform.ReadProviderConfigValues(b, cc.Kind, cc.Name)
		if err != nil {
			return nil, fmt.Errorf("read provider config values from the Terraform plan: %q", err)
		}

		// Try to convert to an importable resource.
		ir, ok := Importable(cc, pcv, runArgs.Interactive)
		if !ok {
			notImportableMsg := fmt.Sprintf("Resource %q of type %q not importable\n", cc.Address, cc.Kind)
			skipped[cc.Address] = notImportableMsg
			log.Println(notImportableMsg)
			if runArgs.DryRun {
				notImportableMsgs = append(notImportableMsgs, notImportableMsg)
			}
			continue
		}

		log.Printf("Found importable resource: %q\n", ir.Change.Address)

		// Check against specific resources list, if present.
		if len(runArgs.SpecificResourceTypes) > 0 {
			if _, ok := runArgs.SpecificResourceTypes[ir.Change.Kind]; !ok {
				log.Printf("Skipping %v, not in list of specific resource types to import", ir.Change.Address)
				continue
			}
		}

		// Attempt the import.
		output, err := Import(importRn, ir, runArgs.InputDir, runArgs.TerraformPath, runArgs.Interactive)

		// In dry-run mode, the output is the command to run.
		if runArgs.DryRun {
			cmd := output
			if err != nil {
				cmd = err.Error()
			} else {
				// The last arg in import could be several space-separated strings. These need to be quoted together.
				args := strings.SplitN(cmd, " ", 4)
				if len(args) == 4 {
					cmd = fmt.Sprintf("%v %v %v %q\n", args[0], args[1], args[2], args[3])
				}
			}

			// If the output isn't command with 4 parts, just print it as-is.
			importCmds = append(importCmds, cmd)

			// Treat it as skipped for the purposes of reporting "importable" resources.
			skipped[cc.Address] = "dry run"
			continue
		}

		// Handle the different outcomes of the import attempt.
		var iie *importer.InsufficientInfoErr
		var se *importer.SkipErr
		switch {
		// err will only be nil when the import succeed.
		// Import succeeded, print the success output.
		case err == nil:
			// Import succeeded.
			log.Printf("Successfully imported %v\n", cc.Address)
			successes = append(successes, cc.Address)

		// Check if the user manually skipped the import.
		case errors.As(err, &se):
			skipped[cc.Address] = "manually skipped"

		// Check if the error indicates insufficient information.
		case errors.As(err, &iie):
			log.Printf("Insufficient information to import %q, missing fields %s\n", cc.Address, strings.Join(iie.MissingFields, ","))

			msg := fmt.Sprintf("%v (insufficient information)", cc.Address)
			if runArgs.Verbose {
				msg = fmt.Sprintf("%v ; insufficient information; full error: %v", cc.Address, err)
			}
			failures = append(failures, msg)

		// Check if error indicates resource is not importable or does not exist.
		// err will be `exit code 1` even when it failed because the resource is not importable or already exists.
		case NotImportable(output):
			msg := fmt.Sprintf("Import not supported by provider for resource %q\n", cc.Address)
			log.Print(msg)
			skipped[cc.Address] = msg
		case DoesNotExist(output):
			msg := fmt.Sprintf("Resource %q does not exist, not importing\n", cc.Address)
			log.Print(msg)
			skipped[cc.Address] = msg

		// Important to handle this last.
		default:
			log.Printf("Failed to import %q\n", cc.Address)

			msg := fmt.Sprintf("%v (error while running terraform import)", cc.Address)
			if runArgs.Verbose {
				msg = fmt.Sprintf("%v ; full error: %v\n%v", cc.Address, err, output)
			}
			failures = append(failures, msg)
		}
	}

	if runArgs.DryRun {
		if len(importCmds) > 0 {
			log.Printf("Import commands:")
			fmt.Printf("cd %v\n", runArgs.InputDir)
			fmt.Printf("%v\n", strings.Join(importCmds, ""))
		}

		if len(notImportableMsgs) > 0 {
			log.Printf("The following resources are NOT importable:")
			fmt.Printf("%v\n", strings.Join(notImportableMsgs, ""))
		}

		return nil, nil
	}

	if len(failures) > 0 {
		return successes, &importError{failures}
	}

	return successes, nil
}