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
}