diff --git a/README.md b/README.md index b3c6f260b..43ff7e670 100644 --- a/README.md +++ b/README.md @@ -135,16 +135,30 @@ If you use another OS, you will need to download the release directly from ## Importing with Terraform state -`cf-terraforming` will output the `terraform import` compatible commands for you -when you invoke the `import` command. This command assumes you have already ran -`cf-terraforming generate ...` to output your resources. +`cf-terraforming` has the ability to generate the configuration for you to import +existing resources. -In the future we aim to automate this however for now, it is a manual step to -allow flexibility in directory structure. +Depending on your version of Terraform, you can generate the `import` block +(Terraform 1.5+) using the `--modern-import-block` flag or the `terraform import` +compatible CLI output (all versions). + +This command assumes you have already ran `cf-terraforming generate ...` to +output your resources. + +``` +# All versions of Terraform +$ cf-terraforming import \ + --resource-type "cloudflare_record" \ + --email $CLOUDFLARE_EMAIL \ + --key $CLOUDFLARE_API_KEY \ + --zone $CLOUDFLARE_ZONE_ID +``` ``` +# Terraform 1.5+ only $ cf-terraforming import \ --resource-type "cloudflare_record" \ + --modern-import-block \ --email $CLOUDFLARE_EMAIL \ --key $CLOUDFLARE_API_KEY \ --zone $CLOUDFLARE_ZONE_ID @@ -157,7 +171,7 @@ Any resources not listed are currently not supported. | Resource | Resource Scope | Generate Supported | Import Supported | | ------------------------------------------------------------------------------------------------------------------------------------------------ | --------------- | ------------------ | ---------------- | | [cloudflare_access_application](https://www.terraform.io/docs/providers/cloudflare/r/access_application) | Account | ✅ | ❌ | -| [cloudflare_access_group](https://www.terraform.io/docs/providers/cloudflare/r/access_group) | Account | ✅ | ✅ | +| [cloudflare_access_group](https://www.terraform.io/docs/providers/cloudflare/r/access_group) | Account | ✅ | ✅ | | [cloudflare_access_identity_provider](https://www.terraform.io/docs/providers/cloudflare/r/access_identity_provider) | Account | ✅ | ❌ | | [cloudflare_access_mutual_tls_certificate](https://www.terraform.io/docs/providers/cloudflare/r/access_mutual_tls_certificate) | Account | ✅ | ❌ | | [cloudflare_access_policy](https://www.terraform.io/docs/providers/cloudflare/r/access_policy) | Account | ❌ | ❌ | diff --git a/internal/app/cf-terraforming/cmd/import.go b/internal/app/cf-terraforming/cmd/import.go index 9f82d5c4f..a4bc0bd59 100644 --- a/internal/app/cf-terraforming/cmd/import.go +++ b/internal/app/cf-terraforming/cmd/import.go @@ -9,7 +9,9 @@ import ( "time" "github.com/cloudflare/cloudflare-go" + "github.com/hashicorp/hcl/v2/hclwrite" "github.com/spf13/cobra" + "github.com/zclconf/go-cty/cty" ) // resourceImportStringFormats contains a mapping of the resource type to the @@ -483,16 +485,43 @@ func runImport() func(cmd *cobra.Command, args []string) { return } + importFile := hclwrite.NewEmptyFile() + importBody := importFile.Body() + for _, data := range jsonStructData { - fmt.Fprint(cmd.OutOrStdout(), buildCompositeID(resourceType, data.(map[string]interface{})["id"].(string))) + id := data.(map[string]interface{})["id"].(string) + + if useModernImportBlock { + idvalue := buildRawImportAddress(resourceType, id) + imp := importBody.AppendNewBlock("import", []string{}).Body() + imp.SetAttributeRaw("to", hclwrite.TokensForIdentifier(fmt.Sprintf("%s.%s", resourceType, fmt.Sprintf("%s_%s", terraformResourceNamePrefix, id)))) + imp.SetAttributeValue("id", cty.StringVal(idvalue)) + importFile.Body().AppendNewline() + } else { + fmt.Fprint(cmd.OutOrStdout(), buildTerraformImportCommand(resourceType, id)) + } + } + + if useModernImportBlock { + // don't format the output; there is a bug in hclwrite.Format that + // splits incorrectly on certain characters. instead, manually + // insert new lines on the block. + fmt.Fprint(cmd.OutOrStdout(), string(importFile.Bytes())) } } } -// buildCompositeID takes the resourceType and resourceID in order to lookup the -// resource type import string and then return a suitable composite value that -// is compatible with `terraform import`. -func buildCompositeID(resourceType, resourceID string) string { +// buildTerraformImportCommand takes the resourceType and resourceID in order to +// lookup the resource type import string and then return a suitable composite +// value that is compatible with `terraform import`. +func buildTerraformImportCommand(resourceType, resourceID string) string { + resourceImportAddress := buildRawImportAddress(resourceType, resourceID) + return fmt.Sprintf("%s %s.%s_%s %s\n", terraformImportCmdPrefix, resourceType, terraformResourceNamePrefix, resourceID, resourceImportAddress) +} + +// buildRawImportAddress takes the resourceType and resourceID in order to lookup +// the resource type import string and then return a suitable address. +func buildRawImportAddress(resourceType, resourceID string) string { if _, ok := resourceImportStringFormats[resourceType]; !ok { log.Fatalf("%s does not have an import format defined", resourceType) } @@ -508,8 +537,7 @@ func buildCompositeID(resourceType, resourceID string) string { identiferValue = zoneID } - s := "" - s += fmt.Sprintf("%s %s.%s_%s %s", terraformImportCmdPrefix, resourceType, terraformResourceNamePrefix, resourceID, resourceImportStringFormats[resourceType]) + s := resourceImportStringFormats[resourceType] replacer := strings.NewReplacer( ":identifier_type", identiferType, ":identifier_value", identiferValue, @@ -517,7 +545,6 @@ func buildCompositeID(resourceType, resourceID string) string { ":account_id", accountID, ":id", resourceID, ) - s += "\n" return replacer.Replace(s) } diff --git a/internal/app/cf-terraforming/cmd/root.go b/internal/app/cf-terraforming/cmd/root.go index e19a1e83c..2bf958ca4 100644 --- a/internal/app/cf-terraforming/cmd/root.go +++ b/internal/app/cf-terraforming/cmd/root.go @@ -10,7 +10,7 @@ import ( var log = logrus.New() var cfgFile, zoneID, hostname, apiEmail, apiKey, apiToken, accountID, terraformInstallPath, terraformBinaryPath string -var verbose bool +var verbose, useModernImportBlock bool var api *cloudflare.API var terraformImportCmdPrefix = "terraform import" var terraformResourceNamePrefix = "terraform_managed_resource" @@ -118,6 +118,8 @@ func init() { if err = viper.BindEnv("terraform-install-path", "CLOUDFLARE_TERRAFORM_INSTALL_PATH"); err != nil { log.Fatal(err) } + + rootCmd.PersistentFlags().BoolVarP(&useModernImportBlock, "modern-import-block", "", false, "Whether to generate HCL import blocks for generated resources instead of `terraform import` compatible CLI commands. This is only compatible with Terraform 1.5+") } // initConfig reads in config file and ENV variables if set.