internal/tfimport/tfimport.go (644 lines of code) (raw):
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package tfimport provides utilities to import resources from a Terraform config.
package tfimport
import (
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path"
"regexp"
"strings"
"github.com/GoogleCloudPlatform/healthcare-data-protection-suite/internal/fileutil"
"github.com/GoogleCloudPlatform/healthcare-data-protection-suite/internal/runner"
"github.com/GoogleCloudPlatform/healthcare-data-protection-suite/internal/template"
"github.com/GoogleCloudPlatform/healthcare-data-protection-suite/internal/terraform"
"github.com/GoogleCloudPlatform/healthcare-data-protection-suite/internal/tfimport/importer"
)
var kubernetesImporter = &importer.SimpleImporter{
Fields: []string{"metadata"},
Tmpl: "{{or (index .metadata \"namespace\") \"default\"}}/{{.metadata.name}}",
}
var kubernetesAlphaImporter = &importer.SimpleImporter{
Fields: []string{"manifest"},
Tmpl: "{{or (index .manifest.metadata \"namespace\") \"default\"}}/{{.manifest.metadata.name}}",
}
// Importers defines all supported resource importers.
// These can be sorted in Vim using ":SortRangesByHeader" from https://github.com/jrhouston/tfk8s
var Importers = map[string]resourceImporter{
// Google provider
"google_app_engine_application": &importer.SimpleImporter{
Fields: []string{"project"},
Tmpl: "{{.project}}",
},
"google_bigquery_dataset": &importer.SimpleImporter{
Fields: []string{"project", "dataset_id"},
Tmpl: "projects/{{.project}}/datasets/{{.dataset_id}}",
},
"google_bigquery_table": &importer.SimpleImporter{
Fields: []string{"project", "dataset_id", "table_id"},
Tmpl: "{{.project}}/{{.dataset_id}}/{{.table_id}}",
},
"google_billing_account_iam_binding": &importer.SimpleImporter{
Fields: []string{"billing_account_id", "role"},
Tmpl: "{{.billing_account_id}} {{.role}}",
},
"google_billing_account_iam_member": &importer.SimpleImporter{
Fields: []string{"billing_account_id", "role", "member"},
Tmpl: "{{.billing_account_id}} {{.role}} {{.member}}",
},
"google_billing_account_iam_policy": &importer.SimpleImporter{
Fields: []string{"billing_account_id"},
Tmpl: "{{.billing_account_id}}",
},
"google_billing_budget": &importer.BillingBudget{},
"google_binary_authorization_policy": &importer.SimpleImporter{
Fields: []string{"project"},
Tmpl: "projects/{{.project}}",
},
"google_cloudbuild_trigger": &importer.SimpleImporter{
Fields: []string{"project", "name"},
Tmpl: "projects/{{.project}}/triggers/{{.name}}",
},
"google_compute_address": &importer.SimpleImporter{
Fields: []string{"project", "region", "name"},
Tmpl: "projects/{{.project}}/regions/{{.region}}/addresses/{{.name}}",
},
"google_compute_firewall": &importer.SimpleImporter{
Fields: []string{"project", "name"},
Tmpl: "projects/{{.project}}/global/firewalls/{{.name}}",
},
"google_compute_forwarding_rule": &importer.SimpleImporter{
Fields: []string{"project", "region", "name"},
Tmpl: "projects/{{.project}}/regions/{{.region}}/forwardingRules/{{.name}}",
},
"google_compute_global_address": &importer.SimpleImporter{
Fields: []string{"project", "name"},
Tmpl: "projects/{{.project}}/global/addresses/{{.name}}",
},
"google_compute_health_check": &importer.SimpleImporter{
Fields: []string{"project", "name"},
Tmpl: "projects/{{.project}}/global/healthChecks/{{.name}}",
},
"google_compute_image": &importer.SimpleImporter{
Fields: []string{"project", "name"},
Tmpl: "projects/{{.project}}/global/images/{{.name}}",
},
"google_compute_instance": &importer.SimpleImporter{
Fields: []string{"project", "zone", "name"},
Tmpl: "projects/{{.project}}/zones/{{.zone}}/instances/{{.name}}",
},
"google_compute_instance_template": &importer.SimpleImporter{
Fields: []string{"project", "name"},
Tmpl: "projects/{{.project}}/global/instanceTemplates/{{.name}}",
},
"google_compute_instance_from_template": &importer.SimpleImporter{
Fields: []string{"project", "zone", "name"},
Tmpl: "projects/{{.project}}/zones/{{.zone}}/instances/{{.name}}",
},
"google_compute_interconnect_attachment": &importer.SimpleImporter{
Fields: []string{"project", "region", "name"},
Tmpl: "projects/{{.project}}/regions/{{.region}}/interconnectAttachments/{{.name}}",
},
"google_compute_network": &importer.SimpleImporter{
Fields: []string{"project", "name"},
Tmpl: "projects/{{.project}}/global/networks/{{.name}}",
},
"google_compute_network_peering": &importer.ComputeNetworkPeering{},
"google_compute_project_metadata_item": &importer.SimpleImporter{
Fields: []string{"key"},
Tmpl: "{{.key}}",
},
"google_compute_region_backend_service": &importer.SimpleImporter{
Fields: []string{"project", "region", "name"},
Tmpl: "projects/{{.project}}/regions/{{.region}}/backendServices/{{.name}}",
},
"google_compute_route": &importer.SimpleImporter{
Fields: []string{"project", "name"},
Tmpl: "projects/{{.project}}/global/routes/{{.name}}",
},
"google_compute_router": &importer.SimpleImporter{
Fields: []string{"project", "region", "name"},
Tmpl: "projects/{{.project}}/regions/{{.region}}/routers/{{.name}}",
},
"google_compute_router_interface": &importer.SimpleImporter{
Fields: []string{"region", "router", "name"},
Tmpl: "{{.region}}/{{.router}}/{{.name}}",
},
"google_compute_router_nat": &importer.SimpleImporter{
Fields: []string{"project", "region", "router", "name"},
Tmpl: "projects/{{.project}}/regions/{{.region}}/routers/{{.router}}/{{.name}}",
},
"google_compute_router_peer": &importer.SimpleImporter{
Fields: []string{"project", "region", "router", "name"},
Tmpl: "projects/{{.project}}/regions/{{.region}}/routers/{{.router}}/{{.name}}",
},
"google_compute_shared_vpc_host_project": &importer.SimpleImporter{
Fields: []string{"project"},
Tmpl: "{{.project}}",
},
"google_compute_shared_vpc_service_project": &importer.SimpleImporter{
Fields: []string{"host_project", "service_project"},
Tmpl: "{{.host_project}}/{{.service_project}}",
},
"google_compute_subnetwork": &importer.SimpleImporter{
Fields: []string{"project", "region", "name"},
Tmpl: "projects/{{.project}}/regions/{{.region}}/subnetworks/{{.name}}",
},
"google_compute_subnetwork_iam_binding": &importer.SimpleImporter{
Fields: []string{"subnetwork", "role"},
Tmpl: "{{.subnetwork}} {{.role}}",
},
"google_compute_subnetwork_iam_member": &importer.SimpleImporter{
Fields: []string{"subnetwork", "role", "member"},
Tmpl: "{{.subnetwork}} {{.role}} {{.member}}",
},
"google_compute_subnetwork_iam_policy": &importer.SimpleImporter{
Fields: []string{"subnetwork"},
Tmpl: "{{.subnetwork}}",
},
"google_container_cluster": &importer.SimpleImporter{
Fields: []string{"project", "location", "name"},
Tmpl: "projects/{{.project}}/locations/{{.location}}/clusters/{{.name}}",
},
"google_container_node_pool": &importer.SimpleImporter{
Fields: []string{"project", "location", "cluster", "name"},
Tmpl: "{{.project}}/{{.location}}/{{.cluster}}/{{.name}}",
},
"google_dns_managed_zone": &importer.SimpleImporter{
Fields: []string{"project", "name"},
Tmpl: "projects/{{.project}}/managedZones/{{.name}}",
},
"google_dns_record_set": &importer.SimpleImporter{
Fields: []string{"project", "managed_zone", "name", "type"},
Tmpl: "{{.project}}/{{.managed_zone}}/{{.name}}/{{.type}}",
},
"google_firebase_project": &importer.SimpleImporter{
Fields: []string{"project"},
Tmpl: "projects/{{.project}}",
},
"google_folder": &importer.SimpleImporter{
// The user will always be asked for this, it cannot be automatically detemrined.
Fields: []string{"folder_id"},
Tmpl: "folders/{{.folder_id}}",
},
"google_folder_iam_binding": &importer.SimpleImporter{
Fields: []string{"folder", "role"},
Tmpl: "{{.folder}} {{.role}}",
},
"google_folder_iam_member": &importer.SimpleImporter{
Fields: []string{"folder", "role", "member"},
Tmpl: "{{.folder}} {{.role}} {{.member}}",
},
"google_folder_iam_policy": &importer.SimpleImporter{
Fields: []string{"folder"},
Tmpl: "{{.folder}}",
},
"google_folder_organization_policy": &importer.SimpleImporter{
Fields: []string{"folder", "constraint"},
Tmpl: "folders/{{.folder}}/constraints/{{.constraint}}",
},
"google_iap_tunnel_instance_iam_binding": &importer.SimpleImporter{
Fields: []string{"project", "zone", "instance", "role"},
Tmpl: "projects/{{.project}}/iap_tunnel/zones/{{.zone}}/instances/{{.instance}} {{.role}}",
},
"google_iap_tunnel_instance_iam_member": &importer.SimpleImporter{
Fields: []string{"project", "zone", "instance", "role", "member"},
Tmpl: "projects/{{.project}}/iap_tunnel/zones/{{.zone}}/instances/{{.instance}} {{.role}} {{.member}}",
},
"google_iap_tunnel_instance_iam_policy": &importer.SimpleImporter{
Fields: []string{"project", "zone", "instance"},
Tmpl: "projects/{{.project}}/iap_tunnel/zones/{{.zone}}/instances/{{.instance}}",
},
"google_kms_key_ring": &importer.SimpleImporter{
Fields: []string{"project", "location", "name"},
Tmpl: "projects/{{.project}}/locations/{{.location}}/keyRings/{{.name}}",
},
"google_logging_billing_account_sink": &importer.SimpleImporter{
Fields: []string{"billing_account", "name"},
Tmpl: "billingAccounts/{{.billing_account}}/sinks/{{.name}}",
},
"google_logging_folder_sink": &importer.SimpleImporter{
Fields: []string{"folder", "name"},
Tmpl: "folders/{{.folder}}/sinks/{{.name}}",
},
"google_logging_metric": &importer.SimpleImporter{
Fields: []string{"project", "name"},
Tmpl: "{{.project}} {{.name}}",
},
"google_logging_organization_sink": &importer.SimpleImporter{
Fields: []string{"org_id", "name"},
Tmpl: "organizations/{{.org_id}}/sinks/{{.name}}",
},
"google_logging_project_sink": &importer.SimpleImporter{
Fields: []string{"project", "name"},
Tmpl: "projects/{{.project}}/sinks/{{.name}}",
},
"google_organization_iam_audit_config": &importer.SimpleImporter{
Fields: []string{"org_id", "service"},
Tmpl: "{{.org_id}} {{.service}}",
},
"google_organization_iam_custom_role": &importer.SimpleImporter{
Fields: []string{"org_id", "role_id"},
Tmpl: "organizations/{{.org_id}}/roles/{{.role_id}}",
},
"google_organization_iam_member": &importer.SimpleImporter{
Fields: []string{"org_id", "role", "member"},
Tmpl: "{{.org_id}} {{.role}} {{.member}}",
},
"google_organization_policy": &importer.SimpleImporter{
Fields: []string{"org_id", "constraint"},
Tmpl: "{{.org_id}}/constraints/{{.constraint}}",
},
"google_project": &importer.SimpleImporter{
Fields: []string{"project_id"},
Tmpl: "{{.project_id}}",
},
"google_project_iam_audit_config": &importer.SimpleImporter{
Fields: []string{"project", "service"},
Tmpl: "{{.project}} {{.service}}",
},
"google_project_iam_binding": &importer.SimpleImporter{
Fields: []string{"project", "role"},
Tmpl: "{{.project}} {{.role}}",
},
"google_project_iam_custom_role": &importer.SimpleImporter{
Fields: []string{"project", "role_id"},
Tmpl: "projects/{{.project}}/roles/{{.role_id}}",
},
"google_project_iam_member": &importer.SimpleImporter{
Fields: []string{"project", "role", "member"},
Tmpl: "{{.project}} {{.role}} {{.member}}",
},
"google_project_organization_policy": &importer.SimpleImporter{
Fields: []string{"project", "constraint"},
Tmpl: "projects/{{.project}}:constraints/{{.constraint}}",
},
"google_project_service": &importer.SimpleImporter{
Fields: []string{"project", "service"},
Tmpl: "{{.project}}/{{.service}}",
},
"google_project_usage_export_bucket": &importer.SimpleImporter{
Fields: []string{"project"},
Tmpl: "{{.project}}",
},
"google_pubsub_subscription": &importer.SimpleImporter{
Fields: []string{"project", "name"},
Tmpl: "projects/{{.project}}/subscriptions/{{.name}}",
},
"google_pubsub_subscription_iam_binding": &importer.SimpleImporter{
Fields: []string{"project", "subscription", "role"},
Tmpl: "projects/{{.project}}/subscriptions/{{.subscription}} {{.role}}",
},
"google_pubsub_subscription_iam_member": &importer.SimpleImporter{
Fields: []string{"project", "subscription", "role", "member"},
Tmpl: "projects/{{.project}}/subscriptions/{{.subscription}} {{.role}} {{.member}}",
},
"google_pubsub_subscription_iam_policy": &importer.SimpleImporter{
Fields: []string{"project", "subscription"},
Tmpl: "projects/{{.project}}/subscriptions/{{.subscription}}",
},
"google_pubsub_topic": &importer.SimpleImporter{
Fields: []string{"project", "name"},
Tmpl: "projects/{{.project}}/topics/{{.name}}",
},
"google_pubsub_topic_iam_binding": &importer.SimpleImporter{
Fields: []string{"project", "topic", "role"},
Tmpl: "projects/{{.project}}/topics/{{.topic}} {{.role}}",
},
"google_pubsub_topic_iam_member": &importer.SimpleImporter{
Fields: []string{"project", "topic", "role", "member"},
Tmpl: "projects/{{.project}}/topics/{{.topic}} {{.role}} {{.member}}",
},
"google_pubsub_topic_iam_policy": &importer.SimpleImporter{
Fields: []string{"project", "topic"},
Tmpl: "projects/{{.project}}/topics/{{.topic}}",
},
"google_resource_manager_lien": &importer.ResourceManagerLien{},
"google_secret_manager_secret": &importer.SimpleImporter{
Fields: []string{"project", "secret_id"},
Tmpl: "projects/{{.project}}/secrets/{{.secret_id}}",
},
"google_secret_manager_secret_version": &importer.SimpleImporter{
// This field is the full path of the secret, including the project name.
Fields: []string{"secret"},
Tmpl: "{{.secret}}/versions/latest",
},
"google_service_account": &importer.SimpleImporter{
Fields: []string{"project", "account_id"},
Tmpl: "projects/{{.project}}/serviceAccounts/{{.account_id}}@{{.project}}.iam.gserviceaccount.com",
},
"google_service_account_iam_binding": &importer.SimpleImporter{
// This already includes the project. It looks like this:
// projects/my-network-project/serviceAccounts/my-sa@my-network-project.iam.gserviceaccount.com
Fields: []string{"service_account_id", "role"},
Tmpl: "{{.service_account_id}} {{.role}}",
},
"google_service_account_iam_member": &importer.SimpleImporter{
// The service_account_id already includes the project. It looks like this:
// projects/my-network-project/serviceAccounts/my-sa@my-network-project.iam.gserviceaccount.com
Fields: []string{"service_account_id", "role", "member"},
Tmpl: "{{.service_account_id}} {{.role}} {{.member}}",
},
"google_service_account_iam_policy": &importer.SimpleImporter{
// This already includes the project. It looks like this:
// projects/my-network-project/serviceAccounts/my-sa@my-network-project.iam.gserviceaccount.com
Fields: []string{"service_account_id"},
Tmpl: "{{.service_account_id}}",
},
"google_service_networking_connection": &importer.ServiceNetworkingConnection{},
"google_service_usage_consumer_quota_override": &importer.SimpleImporter{
Fields: []string{"project", "service", "metric", "limit", "name"},
Tmpl: "projects/{{.project}}/services/{{.service}}/consumerQuotaMetrics/{{.metric}}/limits/{{.limit}}/consumerOverrides/{{.name}}",
},
"google_sql_database": &importer.SimpleImporter{
Fields: []string{"project", "instance", "name"},
Tmpl: "projects/{{.project}}/instances/{{.instance}}/databases/{{.name}}",
},
"google_sql_database_instance": &importer.SimpleImporter{
Fields: []string{"project", "name"},
Tmpl: "projects/{{.project}}/instances/{{.name}}",
},
"google_sql_user": &importer.SQLUser{},
"google_storage_bucket": &importer.SimpleImporter{
Fields: []string{"project", "name"},
Tmpl: "{{.project}}/{{.name}}",
},
"google_storage_bucket_iam_binding": &importer.SimpleImporter{
Fields: []string{"bucket", "role"},
Tmpl: "{{.bucket}} {{.role}}",
},
"google_storage_bucket_iam_member": &importer.SimpleImporter{
Fields: []string{"bucket", "role", "member"},
Tmpl: "{{.bucket}} {{.role}} {{.member}}",
},
"google_storage_bucket_iam_policy": &importer.SimpleImporter{
Fields: []string{"bucket"},
Tmpl: "{{.bucket}}",
},
// GSuite provider
"gsuite_group": &importer.SimpleImporter{
Fields: []string{"email"},
Tmpl: "{{.email}}",
},
"gsuite_group_member": &importer.SimpleImporter{
Fields: []string{"group", "email"},
Tmpl: "{{.group}}:{{.email}}",
},
// Helm provider
"helm_release": &importer.SimpleImporter{
Fields: []string{"namespace", "name"},
Tmpl: "{{.namespace}}/{{.name}}",
Defaults: map[string]interface{}{
"namespace": "default",
},
},
// Kubernetes provider
"kubernetes_config_map": kubernetesImporter,
"kubernetes_namespace": &importer.SimpleImporter{
Fields: []string{"metadata"},
Tmpl: "{{.metadata.name}}",
},
"kubernetes_pod": kubernetesImporter,
"kubernetes_role": kubernetesImporter,
"kubernetes_role_binding": kubernetesImporter,
"kubernetes_service": kubernetesImporter,
"kubernetes_service_account": kubernetesImporter,
// Kubernetes generic provider (alpha)
// See https://github.com/hashicorp/terraform-provider-kubernetes-alpha
"kubernetes_manifest": kubernetesAlphaImporter,
// Random provider
"random_id": &importer.RandomID{},
"random_integer": &importer.RandomInteger{},
}
// Unimportable defines resources that are explicitly not supported by their provider.
var Unimportable = map[string]bool{
"google_bigquery_dataset_access": true,
"google_service_account_key": true,
"google_storage_bucket_object": true,
"local_file": true,
"null_resource": true,
"random_password": true,
"random_pet": true,
"random_shuffle": true,
"random_string": true,
"tls_private_key": true,
}
// RequiresInteractive lists resources which require interactivity and can't be fully automatically imported.
var RequiresInteractive = map[string]bool{
"google_billing_budget": true,
"google_resource_manager_lien": true,
}
// Resource represents a resource and an importer that can import it.
type Resource struct {
Change terraform.ResourceChange
ProviderConfig importer.ConfigMap
Importer resourceImporter
}
// resourceImporter is an interface that must be implemented by all resources to allow them to be imported.
type resourceImporter interface {
// ImportID returns an ID that Terraform can use to import this resource.
ImportID(rc terraform.ResourceChange, pcv importer.ConfigMap, interactive bool) (string, error)
}
// ImportID is a convenience function for passing a resource's information to its importer.
func (ir Resource) ImportID(interactive bool) (string, error) {
return ir.Importer.ImportID(ir.Change, ir.ProviderConfig, interactive)
}
// Importable returns an importable Resource which contains an Importer, and whether it successfully created that resource.
// pcv represents provider config values, which will be used if the resource does not have values defined.
func Importable(rc terraform.ResourceChange, pcv importer.ConfigMap, interactive bool) (*Resource, bool) {
ri, ok := Importers[rc.Kind]
if !ok {
return nil, false
}
if _, reqInteractive := RequiresInteractive[rc.Kind]; reqInteractive && !interactive {
return nil, false
}
return &Resource{
Change: rc,
ProviderConfig: pcv,
Importer: ri,
}, true
}
// Import runs `terraform import` for the given importable resource.
// It parses the output string to determine to determine if the provider said the resource doesn't exist or isn't importable.
func Import(rn runner.Runner, ir *Resource, inputDir string, terraformPath string, interactive bool) (output string, err error) {
// Try to get the ImportID()
importID, err := ir.ImportID(interactive)
if err != nil {
return "", err
}
// Run the import.
cmd := exec.Command(terraformPath, "import", ir.Change.Address, importID)
cmd.Dir = inputDir
outputBytes, err := rn.CmdCombinedOutput(cmd)
return string(outputBytes), err
}
// Regexes used in parsing the output of the `terraform import` command.
var (
reNotImportable = regexp.MustCompile(`(?i)Error:.*resource (.*) doesn't support import`)
reDoesNotExist = regexp.MustCompile(`(?i)Error:.*Cannot import non-existent.*object`)
)
// NotImportable parses the output of a `terraform import` command to determine if it indicated that a resource is not importable.
func NotImportable(output string) bool {
return reNotImportable.FindStringIndex(output) != nil
}
// DoesNotExist parses the output of a `terraform import` command to determine if it indicated that a resource does not exist.
func DoesNotExist(output string) bool {
return reDoesNotExist.FindStringIndex(output) != nil
}
// importError is an error indicating a resource failed to be imported.
// This doesn't indicate an error in running the steps around trying an import,
// it's specifically for terraform failing to import a resource for a reason
// that can't be handled (i.e. does not exist or not importable at all).
type importError struct {
// errMgs contains the error messages for all failed imports.
errMsgs []string
}
func (e *importError) Error() string {
return fmt.Sprintf("failed to find or import %v resources. Use --verbose flag to see detailed error messages.", len(e.errMsgs))
}
var summaryTmpl string = `
Summary:
Found {{.total}} importable resources
Successfully imported {{len .successes}}{{if gt (len .successes) 0}}:{{end}}
{{- range $resource := .successes}}
- {{$resource}}
{{- end}}
Skipped {{len .skipped}}{{if gt (len .skipped) 0}}:{{end}}
{{- range $resource, $reason := .skipped}}
- {{$resource}} ({{$reason}})
{{- end}}
Failed to import {{len .failures}}{{if gt (len .failures) 0}}:{{end}}
{{- range $resource := .failures}}
- {{$resource}}
{{- end}}
`
// RunArgs are the supported tfimport run arguments.
type RunArgs struct {
InputDir string
TerraformPath string
DryRun bool
Interactive bool
Verbose bool
// This is a "set" of resource types to import.
// If not nil and not empty, will import only resources which match it.
SpecificResourceTypes map[string]bool
}
// Run executes the main tfimport logic.
func Run(rn runner.Runner, importRn runner.Runner, runArgs *RunArgs) error {
// Expand the config path (ex. expand ~).
inputDir, err := fileutil.Expand(runArgs.InputDir)
if err != nil {
return fmt.Errorf("expand path %q: %v", inputDir, err)
}
var successesTotal, failuresTotal []string
skipped := make(map[string]string)
var ie *importError
for {
failuresTotal = nil
successes, err := planAndImport(rn, importRn, runArgs, skipped)
successesTotal = append(successesTotal, successes...)
if err == nil {
// Either no successes, or no errors.
// No need to retry.
break
}
if !errors.As(err, &ie) {
// It's some other kind of error and we should return it.
return err
}
// It's an importError.
failuresTotal = ie.errMsgs
// If no successes, break out of the loop.
if len(successes) <= 0 {
break
}
// Some resources imported successfully, but others didn't.
// Time to retry.
log.Println("Some imports succeeded but others did not. Retrying the import, in case dependent values have now been populated.")
}
buf, err := template.WriteBuffer(summaryTmpl, map[string]interface{}{
"total": len(successesTotal) + len(skipped) + len(failuresTotal),
"successes": successesTotal,
"skipped": skipped,
"failures": failuresTotal,
})
if err != nil {
return fmt.Errorf("building summary template: %v", err)
}
log.Printf("%s\n", buf)
if ie != nil {
return ie
}
return nil
}
// This function does the full plan and import cycle.
// If it imported some resources but failed to import others, it will return true for retry. This is a simple way to solve dependencies without having to figure out the graph.
// A specific case: GKE node pool name depends on random_id; import the random_id first, then do the cycle again and import the node pool.
// skipped is a map to be filled with skipped resources so they are skipped on subsequent runs too.
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
}