From 9a7f715809bc1cec8faed6bcd51ffd17879e2749 Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Tue, 14 Jan 2025 13:43:33 +1100 Subject: [PATCH 01/12] refactor(internal): swap to resource and endpoint mapping Previously, we relied on manually mapping the supported resources. This was for a number of reasons: 1. Not all resources supported a `List` operation. 2. Resources were not 1:1 to the API endpoint in Terraform. 3. Resources did not have full attribute coverage and not having a way to flag incomplete resources could result in outages if they were applied. With the release of the Cloudflare Terraform Provider v5 and the SDKs, we're now in a position to make the internals more dynamic as they are all powered by the OpenAPI schemas and eliminates the previously encountered constraints. Under the covers, we have swapped to a mapping file that is generated by an out of band process while the HTTP client and data handling is now using the v4 Go library. --- go.mod | 7 +- go.sum | 16 + internal/app/cf-terraforming/cmd/generate.go | 1267 +---------------- .../cmd/resource_to_endpoint_mapping.go | 73 + 4 files changed, 130 insertions(+), 1233 deletions(-) create mode 100644 internal/app/cf-terraforming/cmd/resource_to_endpoint_mapping.go diff --git a/go.mod b/go.mod index 91ffe2ec0..c241432ab 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.10.0 - github.com/zclconf/go-cty v1.16.0 + github.com/zclconf/go-cty v1.16.1 ) require ( @@ -26,6 +26,7 @@ require ( github.com/agext/levenshtein v1.2.1 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/cloudflare/circl v1.3.7 // indirect + github.com/cloudflare/cloudflare-go/v4 v4.0.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/goccy/go-json v0.10.4 // indirect @@ -47,6 +48,10 @@ require ( github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/crypto v0.31.0 // indirect diff --git a/go.sum b/go.sum index 8db9c5337..d95715f92 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vc github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cloudflare/cloudflare-go v0.113.0 h1:qnOXmA6RbgZ4rg5gNBK5QGk0Pzbv8pnUYV3C4+8CU6w= github.com/cloudflare/cloudflare-go v0.113.0/go.mod h1:Dlm4BAnycHc0i8yLxQZb9b+OlMwYOAoDJsUOEFgpVvo= +github.com/cloudflare/cloudflare-go/v4 v4.0.0 h1:qVtUvfsnH2n7aCZOIapbiE3w/FPNrHp7s578OLIdbo8= +github.com/cloudflare/cloudflare-go/v4 v4.0.0/go.mod h1:XcYpLe7Mf6FN87kXzEWVnJ6z+vskW/k6eUqgqfhFE9k= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= @@ -135,10 +137,24 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/zclconf/go-cty v1.16.0 h1:xPKEhst+BW5D0wxebMZkxgapvOE/dw7bFTlgSc9nD6w= github.com/zclconf/go-cty v1.16.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty v1.16.1 h1:a5TZEPzBFFR53udlIKApXzj8JIF4ZNQ6abH79z5R1S0= +github.com/zclconf/go-cty v1.16.1/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= diff --git a/internal/app/cf-terraforming/cmd/generate.go b/internal/app/cf-terraforming/cmd/generate.go index 509b63ec2..2f5f4a134 100644 --- a/internal/app/cf-terraforming/cmd/generate.go +++ b/internal/app/cf-terraforming/cmd/generate.go @@ -4,12 +4,14 @@ import ( "context" "encoding/json" "fmt" + "io" "io/ioutil" + "net/http" "os" "sort" "strings" - cloudflare "github.com/cloudflare/cloudflare-go" + "github.com/cloudflare/cloudflare-go/v4" "github.com/hashicorp/go-version" "github.com/hashicorp/hc-install/product" "github.com/hashicorp/hc-install/releases" @@ -17,6 +19,7 @@ import ( "github.com/hashicorp/terraform-exec/tfexec" "github.com/spf13/cobra" "github.com/spf13/viper" + "github.com/tidwall/gjson" "github.com/zclconf/go-cty/cty" ) @@ -91,1249 +94,45 @@ func generateResources() func(cmd *cobra.Command, args []string) { resources := strings.Split(resourceType, ",") for _, resourceType := range resources { r := s.ResourceSchemas[resourceType] - log.Debugf("beginning to read and build %s resources", resourceType) + log.Debugf("beginning to read and build %q resources", resourceType) + + if resourceToEndpoint[resourceType] == "" { + log.Debugf("did not find API endpoint for %q. skipping...", resourceType) + continue + } // Initialise `resourceCount` outside of the switch for supported resources // to allow it to be referenced further down in the loop that outputs the // newly generated resources. resourceCount := 0 - // Lazy approach to restrict support to known resources due to Go's type - // restrictions and the need to explicitly map out the structs. - var jsonStructData []interface{} - - var identifier *cloudflare.ResourceContainer - if accountID != "" { - identifier = cloudflare.AccountIdentifier(accountID) - } else { - identifier = cloudflare.ZoneIdentifier(zoneID) - } - - switch resourceType { - case "cloudflare_access_application": - jsonPayload, _, err := api.ListAccessApplications(context.Background(), identifier, cloudflare.ListAccessApplicationsParams{}) - if err != nil { - log.Fatal(err) - } - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - case "cloudflare_access_group": - jsonPayload, _, err := api.ListAccessGroups(context.Background(), identifier, cloudflare.ListAccessGroupsParams{}) - if err != nil { - log.Fatal(err) - } - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - case "cloudflare_access_identity_provider": - jsonPayload, _, err := api.ListAccessIdentityProviders(context.Background(), identifier, cloudflare.ListAccessIdentityProvidersParams{}) - if err != nil { - log.Fatal(err) - } - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - case "cloudflare_access_service_token": - jsonPayload, _, err := api.ListAccessServiceTokens(context.Background(), identifier, cloudflare.ListAccessServiceTokensParams{}) - if err != nil { - log.Fatal(err) - } - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - case "cloudflare_access_mutual_tls_certificate": - jsonPayload, _, err := api.ListAccessMutualTLSCertificates(context.Background(), identifier, cloudflare.ListAccessMutualTLSCertificatesParams{}) - if err != nil { - log.Fatal(err) - } - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - case "cloudflare_access_rule": - if accountID != "" { - jsonPayload, err := api.ListAccountAccessRules(context.Background(), accountID, cloudflare.AccessRule{}, 1) - if err != nil { - log.Fatal(err) - } - - resourceCount = len(jsonPayload.Result) - m, _ := json.Marshal(jsonPayload.Result) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - } else { - jsonPayload, err := api.ListZoneAccessRules(context.Background(), zoneID, cloudflare.AccessRule{}, 1) - if err != nil { - log.Fatal(err) - } - - resourceCount = len(jsonPayload.Result) - m, _ := json.Marshal(jsonPayload.Result) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - } - case "cloudflare_account_member": - jsonPayload, _, err := api.AccountMembers(context.Background(), accountID, cloudflare.PaginationOptions{}) - if err != nil { - log.Fatal(err) - } - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - - // remap email and role_ids into the right structure. - for i := 0; i < resourceCount; i++ { - jsonStructData[i].(map[string]interface{})["email_address"] = jsonStructData[i].(map[string]interface{})["user"].(map[string]interface{})["email"] - roleIDs := []string{} - for _, role := range jsonStructData[i].(map[string]interface{})["roles"].([]interface{}) { - roleIDs = append(roleIDs, role.(map[string]interface{})["id"].(string)) - } - jsonStructData[i].(map[string]interface{})["role_ids"] = roleIDs - } - case "cloudflare_argo": - jsonPayload := []cloudflare.ArgoFeatureSetting{} - - argoSmartRouting, err := api.ArgoSmartRouting(context.Background(), zoneID) - if err != nil { - log.Fatal(err) - } - jsonPayload = append(jsonPayload, argoSmartRouting) - - argoTieredCaching, err := api.ArgoTieredCaching(context.Background(), zoneID) - if err != nil { - log.Fatal(err) - } - jsonPayload = append(jsonPayload, argoTieredCaching) - - resourceCount = 1 - - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - - for i, b := range jsonStructData { - key := b.(map[string]interface{})["id"].(string) - jsonStructData[0].(map[string]interface{})[key] = jsonStructData[i].(map[string]interface{})["value"] - } - case "cloudflare_api_shield": - jsonPayload := []cloudflare.APIShield{} - apiShieldConfig, _, err := api.GetAPIShieldConfiguration(context.Background(), identifier) - if err != nil { - log.Fatal(err) - } - // the response can contain an empty APIShield struct. Verify we have data before we attempt to do anything - jsonPayload = append(jsonPayload, apiShieldConfig) - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - - // this is only every a 1:1 so we can just verify if the 0th element has they key we expect - jsonStructData[0].(map[string]interface{})["id"] = zoneID - - if jsonStructData[0].(map[string]interface{})["auth_id_characteristics"] == nil { - // force a no resources return by setting resourceCount to 0 - resourceCount = 0 - } - case "cloudflare_user_agent_blocking_rule": - page := 1 - var jsonPayload []cloudflare.UserAgentRule - for { - res, err := api.ListUserAgentRules(context.Background(), zoneID, page) - if err != nil { - log.Fatal(err) - } - - jsonPayload = append(jsonPayload, res.Result...) - res.ResultInfo = res.ResultInfo.Next() - - if res.ResultInfo.Done() { - break - } - page = page + 1 - } - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err := json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - case "cloudflare_bot_management": - botManagement, err := api.GetBotManagement(context.Background(), identifier) - if err != nil { - log.Fatal(err) - } - var jsonPayload []cloudflare.BotManagement - jsonPayload = append(jsonPayload, botManagement) - - resourceCount = 1 - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - - jsonStructData[0].(map[string]interface{})["id"] = zoneID - case "cloudflare_byo_ip_prefix": - jsonPayload, err := api.ListPrefixes(context.Background(), accountID) - if err != nil { - log.Fatal(err) - } - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - - // remap ID to prefix_id and advertised to advertisement on the JSON payloads. - for i := 0; i < resourceCount; i++ { - jsonStructData[i].(map[string]interface{})["prefix_id"] = jsonStructData[i].(map[string]interface{})["id"] - - if jsonStructData[i].(map[string]interface{})["advertised"].(bool) { - jsonStructData[i].(map[string]interface{})["advertisement"] = "on" - } else { - jsonStructData[i].(map[string]interface{})["advertisement"] = "off" - } - } - case "cloudflare_certificate_pack": - jsonPayload, err := api.ListCertificatePacks(context.Background(), zoneID) - if err != nil { - log.Fatal(err) - } - - var customerManagedCertificates []cloudflare.CertificatePack - for _, r := range jsonPayload { - if r.Type != "universal" { - customerManagedCertificates = append(customerManagedCertificates, r) - } - } - jsonPayload = customerManagedCertificates - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - case "cloudflare_custom_pages": - if accountID != "" { - acc := cloudflare.CustomPageOptions{AccountID: accountID} - jsonPayload, err := api.CustomPages(context.Background(), &acc) - if err != nil { - log.Fatal(err) - } - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - } else { - zo := cloudflare.CustomPageOptions{ZoneID: zoneID} - jsonPayload, err := api.CustomPages(context.Background(), &zo) - if err != nil { - log.Fatal(err) - } + var ( + jsonStructData []interface{} + params map[string]interface{} + result *http.Response + ) - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - } - - var newJsonStructData []interface{} - // remap ID to the "type" field - for i := 0; i < resourceCount; i++ { - jsonStructData[i].(map[string]interface{})["type"] = jsonStructData[i].(map[string]interface{})["id"] - // we only want repsonses that have 'url' - if jsonStructData[i].(map[string]interface{})["url"] != nil { - newJsonStructData = append(newJsonStructData, jsonStructData[i]) - } - } - jsonStructData = newJsonStructData - resourceCount = len(jsonStructData) - - case "cloudflare_custom_hostname_fallback_origin": - var jsonPayload []cloudflare.CustomHostnameFallbackOrigin - apiCall, err := api.CustomHostnameFallbackOrigin(context.Background(), zoneID) - if err != nil { - log.Fatal(err) - } - - if apiCall.Origin != "" { - resourceCount = 1 - jsonPayload = append(jsonPayload, apiCall) - } - - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - - for i := 0; i < resourceCount; i++ { - jsonStructData[i].(map[string]interface{})["id"] = sanitiseTerraformResourceName(jsonStructData[i].(map[string]interface{})["origin"].(string)) - jsonStructData[i].(map[string]interface{})["status"] = nil - } - case "cloudflare_filter": - jsonPayload, _, err := api.Filters(context.Background(), identifier, cloudflare.FilterListParams{}) - if err != nil { - log.Fatal(err) - } - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - case "cloudflare_firewall_rule": - jsonPayload, _, err := api.FirewallRules(context.Background(), identifier, cloudflare.FirewallRuleListParams{}) - if err != nil { - log.Fatal(err) - } - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - - // remap Filter.ID to `filter_id` on the JSON payloads. - for i := 0; i < resourceCount; i++ { - jsonStructData[i].(map[string]interface{})["filter_id"] = jsonStructData[i].(map[string]interface{})["filter"].(map[string]interface{})["id"] - } - case "cloudflare_custom_hostname": - jsonPayload, _, err := api.CustomHostnames(context.Background(), zoneID, 1, cloudflare.CustomHostname{}) - if err != nil { - log.Fatal(err) - } - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - - for i := 0; i < resourceCount; i++ { - jsonStructData[i].(map[string]interface{})["ssl"].(map[string]interface{})["validation_errors"] = nil - } - case "cloudflare_custom_ssl": - jsonPayload, err := api.ListSSL(context.Background(), zoneID) - if err != nil { - log.Fatal(err) - } - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - case "cloudflare_healthcheck": - jsonPayload, err := api.Healthchecks(context.Background(), zoneID) - if err != nil { - log.Fatal(err) - } - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - case "cloudflare_list": - jsonPayload, err := api.ListLists(context.Background(), identifier, cloudflare.ListListsParams{}) - if err != nil { - log.Fatal(err) - } - - m, err := json.Marshal(jsonPayload) - if err != nil { - log.Fatal(err) - } - - if err = json.Unmarshal(m, &jsonStructData); err != nil { - log.Fatal(err) - } - resourceCount = len(jsonPayload) - - for i := 0; i < resourceCount; i++ { - listID := jsonPayload[i].ID - kind := jsonPayload[i].Kind - - listItems, err := api.ListListItems(context.Background(), identifier, cloudflare.ListListItemsParams{ID: listID}) - if err != nil { - log.Fatal(err) - } - items := make([]interface{}, 0) - - for _, listItem := range listItems { - if kind == "" { - continue - } - - value := map[string]interface{}{} - switch kind { - case "ip": - if listItem.IP == nil { - continue - } - value["ip"] = *listItem.IP - case "asn": - if listItem.ASN == nil { - continue - } - value["asn"] = int(*listItem.ASN) - case "hostname": - if listItem.Hostname == nil { - continue - } - value["hostname"] = map[string]interface{}{ - "url_hostname": listItem.Hostname.UrlHostname, - } - case "redirect": - if listItem.Redirect == nil { - continue - } - redirect := map[string]interface{}{ - "source_url": listItem.Redirect.SourceUrl, - "target_url": listItem.Redirect.TargetUrl, - } - if listItem.Redirect.IncludeSubdomains != nil { - redirect["include_subdomains"] = boolToEnabledOrDisabled(*listItem.Redirect.IncludeSubdomains) - } - if listItem.Redirect.SubpathMatching != nil { - redirect["subpath_matching"] = boolToEnabledOrDisabled(*listItem.Redirect.SubpathMatching) - } - if listItem.Redirect.StatusCode != nil { - redirect["status_code"] = *listItem.Redirect.StatusCode - } - if listItem.Redirect.PreserveQueryString != nil { - redirect["preserve_query_string"] = boolToEnabledOrDisabled(*listItem.Redirect.PreserveQueryString) - } - if listItem.Redirect.PreservePathSuffix != nil { - redirect["preserve_path_suffix"] = boolToEnabledOrDisabled(*listItem.Redirect.PreservePathSuffix) - } - value["redirect"] = redirect - } - items = append(items, map[string]interface{}{ - "comment": listItem.Comment, - "value": value, - }) - } - jsonStructData[i].(map[string]interface{})["item"] = items - } - case "cloudflare_load_balancer": - jsonPayload, err := api.ListLoadBalancers(context.Background(), identifier, cloudflare.ListLoadBalancerParams{}) - if err != nil { - log.Fatal(err) - } - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - - for i := 0; i < resourceCount; i++ { - jsonStructData[i].(map[string]interface{})["default_pool_ids"] = jsonStructData[i].(map[string]interface{})["default_pools"] - jsonStructData[i].(map[string]interface{})["fallback_pool_id"] = jsonStructData[i].(map[string]interface{})["fallback_pool"] - - if jsonStructData[i].(map[string]interface{})["country_pools"] != nil { - original := jsonStructData[i].(map[string]interface{})["country_pools"] - jsonStructData[i].(map[string]interface{})["country_pools"] = []interface{}{} - - for country, popIDs := range original.(map[string]interface{}) { - jsonStructData[i].(map[string]interface{})["country_pools"] = append(jsonStructData[i].(map[string]interface{})["country_pools"].([]interface{}), map[string]interface{}{"country": country, "pool_ids": popIDs}) - } - } - - if jsonStructData[i].(map[string]interface{})["region_pools"] != nil { - original := jsonStructData[i].(map[string]interface{})["region_pools"] - jsonStructData[i].(map[string]interface{})["region_pools"] = []interface{}{} - - for region, popIDs := range original.(map[string]interface{}) { - jsonStructData[i].(map[string]interface{})["region_pools"] = append(jsonStructData[i].(map[string]interface{})["region_pools"].([]interface{}), map[string]interface{}{"region": region, "pool_ids": popIDs}) - } - } - - if jsonStructData[i].(map[string]interface{})["pop_pools"] != nil { - original := jsonStructData[i].(map[string]interface{})["pop_pools"] - jsonStructData[i].(map[string]interface{})["pop_pools"] = []interface{}{} - - for pop, popIDs := range original.(map[string]interface{}) { - jsonStructData[i].(map[string]interface{})["pop_pools"] = append(jsonStructData[i].(map[string]interface{})["pop_pools"].([]interface{}), map[string]interface{}{"pop": pop, "pool_ids": popIDs}) - } - } - } - - case "cloudflare_load_balancer_pool": - jsonPayload, err := api.ListLoadBalancerPools(context.Background(), identifier, cloudflare.ListLoadBalancerPoolParams{}) - if err != nil { - log.Fatal(err) - } - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - - for i := 0; i < resourceCount; i++ { - for originCounter := range jsonStructData[i].(map[string]interface{})["origins"].([]interface{}) { - if jsonStructData[i].(map[string]interface{})["origins"].([]interface{})[originCounter].(map[string]interface{})["header"] != nil { - jsonStructData[i].(map[string]interface{})["origins"].([]interface{})[originCounter].(map[string]interface{})["header"].(map[string]interface{})["header"] = "Host" - jsonStructData[i].(map[string]interface{})["origins"].([]interface{})[originCounter].(map[string]interface{})["header"].(map[string]interface{})["values"] = jsonStructData[i].(map[string]interface{})["origins"].([]interface{})[originCounter].(map[string]interface{})["header"].(map[string]interface{})["Host"] - } - } - } - case "cloudflare_load_balancer_monitor": - jsonPayload, err := api.ListLoadBalancerMonitors(context.Background(), identifier, cloudflare.ListLoadBalancerMonitorParams{}) - if err != nil { - log.Fatal(err) - } - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - case "cloudflare_logpush_job": - jsonPayload, err := api.ListLogpushJobs(context.Background(), identifier, cloudflare.ListLogpushJobsParams{}) - if err != nil { - log.Fatal(err) - } - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - - for i := 0; i < resourceCount; i++ { - // Workaround for LogpushJob.Filter being empty with a custom - // marshaler and returning `{"where":{}}` as the "empty" value. - if jsonStructData[i].(map[string]interface{})["filter"] == `{"where":{}}` { - jsonStructData[i].(map[string]interface{})["filter"] = nil - } - } - case "cloudflare_managed_headers": - // only grab the enabled headers - jsonPayload, err := api.ListZoneManagedHeaders(context.Background(), cloudflare.ResourceIdentifier(zoneID), cloudflare.ListManagedHeadersParams{Status: "enabled"}) - if err != nil { - log.Fatal(err) - } + placeholderReplacer := strings.NewReplacer("{account_id}", accountID, "{zone_id}", zoneID) + endpoint := placeholderReplacer.Replace(resourceToEndpoint[resourceType]) - var managedHeaders []cloudflare.ManagedHeaders - managedHeaders = append(managedHeaders, jsonPayload) + client := cloudflare.NewClient() - resourceCount = len(managedHeaders) - m, _ := json.Marshal(managedHeaders) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - - for i := 0; i < resourceCount; i++ { - jsonStructData[i].(map[string]interface{})["id"] = zoneID - } - case "cloudflare_origin_ca_certificate": - jsonPayload, err := api.ListOriginCACertificates(context.Background(), cloudflare.ListOriginCertificatesParams{ZoneID: zoneID}) - if err != nil { - log.Fatal(err) - } - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - case "cloudflare_page_rule": - jsonPayload, err := api.ListPageRules(context.Background(), zoneID) - if err != nil { - log.Fatal(err) - } - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - - for i := 0; i < resourceCount; i++ { - jsonStructData[i].(map[string]interface{})["target"] = jsonStructData[i].(map[string]interface{})["targets"].([]interface{})[0].(map[string]interface{})["constraint"].(map[string]interface{})["value"] - jsonStructData[i].(map[string]interface{})["actions"] = flattenAttrMap(jsonStructData[i].(map[string]interface{})["actions"].([]interface{})) - - // Have to remap the cache_ttl_by_status to conform to Terraform's more human-friendly structure. - if cache, ok := jsonStructData[i].(map[string]interface{})["actions"].(map[string]interface{})["cache_ttl_by_status"].(map[string]interface{}); ok { - cache_ttl_by_status := []map[string]interface{}{} - - for codes, ttl := range cache { - if ttl == "no-cache" { - ttl = 0 - } else if ttl == "no-store" { - ttl = -1 - } - elem := map[string]interface{}{ - "codes": codes, - "ttl": ttl, - } - - cache_ttl_by_status = append(cache_ttl_by_status, elem) - } - - sort.SliceStable(cache_ttl_by_status, func(i int, j int) bool { - return cache_ttl_by_status[i]["codes"].(string) < cache_ttl_by_status[j]["codes"].(string) - }) - - jsonStructData[i].(map[string]interface{})["actions"].(map[string]interface{})["cache_ttl_by_status"] = cache_ttl_by_status - } - - // Remap cache_key_fields.query_string.include & .exclude wildcards (not in an array) to the appropriate "ignore" field value in Terraform. - if c, ok := jsonStructData[i].(map[string]interface{})["actions"].(map[string]interface{})["cache_key_fields"].(map[string]interface{}); ok { - if s, sok := c["query_string"].(map[string]interface{})["include"].(string); sok && s == "*" { - jsonStructData[i].(map[string]interface{})["actions"].(map[string]interface{})["cache_key_fields"].(map[string]interface{})["query_string"].(map[string]interface{})["include"] = nil - jsonStructData[i].(map[string]interface{})["actions"].(map[string]interface{})["cache_key_fields"].(map[string]interface{})["query_string"].(map[string]interface{})["ignore"] = false - } - if s, sok := c["query_string"].(map[string]interface{})["exclude"].(string); sok && s == "*" { - jsonStructData[i].(map[string]interface{})["actions"].(map[string]interface{})["cache_key_fields"].(map[string]interface{})["query_string"].(map[string]interface{})["exclude"] = nil - jsonStructData[i].(map[string]interface{})["actions"].(map[string]interface{})["cache_key_fields"].(map[string]interface{})["query_string"].(map[string]interface{})["ignore"] = true - } - } - } - case "cloudflare_rate_limit": - jsonPayload, err := api.ListAllRateLimits(context.Background(), zoneID) - if err != nil { - log.Fatal(err) - } - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - - for i := 0; i < resourceCount; i++ { - var bypassItems []string - - // Remap match.request.url to match.request.url_pattern - jsonStructData[i].(map[string]interface{})["match"].(map[string]interface{})["request"].(map[string]interface{})["url_pattern"] = jsonStructData[i].(map[string]interface{})["match"].(map[string]interface{})["request"].(map[string]interface{})["url"] - - // Remap bypass to bypass_url_patterns - if jsonStructData[i].(map[string]interface{})["bypass"] != nil { - for _, item := range jsonStructData[i].(map[string]interface{})["bypass"].([]interface{}) { - bypassItems = append(bypassItems, item.(map[string]interface{})["value"].(string)) - } - jsonStructData[i].(map[string]interface{})["bypass_url_patterns"] = bypassItems - } - - // Remap match.response.status to match.response.statuses - jsonStructData[i].(map[string]interface{})["match"].(map[string]interface{})["response"].(map[string]interface{})["statuses"] = jsonStructData[i].(map[string]interface{})["match"].(map[string]interface{})["response"].(map[string]interface{})["status"] - } - - case "cloudflare_record": - jsonPayload, _, err := api.ListDNSRecords(context.Background(), identifier, cloudflare.ListDNSRecordsParams{}) - if err != nil { - log.Fatal(err) - } - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - - zone, _ := api.ZoneDetails(context.Background(), identifier.Identifier) - - for i := 0; i < resourceCount; i++ { - // Drop the proxiable values as they are not usable - jsonStructData[i].(map[string]interface{})["proxiable"] = nil - jsonStructData[i].(map[string]interface{})["value"] = nil - - if jsonStructData[i].(map[string]interface{})["name"].(string) != zone.Name { - jsonStructData[i].(map[string]interface{})["name"] = strings.ReplaceAll(jsonStructData[i].(map[string]interface{})["name"].(string), "."+zone.Name, "") - } - } - case "cloudflare_ruleset": - jsonPayload, err := api.ListRulesets(context.Background(), identifier, cloudflare.ListRulesetsParams{}) - if err != nil { - log.Fatal(err) - } - - var nonManagedRules []cloudflare.Ruleset - - // A little annoying but makes more sense doing it this way. Only append - // the non-managed rules to the usable nonManagedRules variable instead - // of attempting to delete from an existing slice and just reassign. - for _, r := range jsonPayload { - if r.Kind != string(cloudflare.RulesetKindManaged) { - nonManagedRules = append(nonManagedRules, r) - } - } - jsonPayload = nonManagedRules - ruleHeaders := map[string][]map[string]interface{}{} - for i, rule := range nonManagedRules { - ruleset, _ := api.GetRuleset(context.Background(), identifier, rule.ID) - jsonPayload[i].Rules = ruleset.Rules - - if ruleset.Rules != nil { - for _, rule := range ruleset.Rules { - if rule.ActionParameters != nil && rule.ActionParameters.Headers != nil { - // Sort the headers to have deterministic config output - keys := make([]string, 0, len(rule.ActionParameters.Headers)) - for k := range rule.ActionParameters.Headers { - keys = append(keys, k) - } - sort.Strings(keys) - - // The structure of the API response for headers differs from the - // structure terraform requires. So we collect all the headers - // indexed by rule.ID to massage the jsonStructData later - for _, headerName := range keys { - header := map[string]interface{}{ - "name": headerName, - "operation": rule.ActionParameters.Headers[headerName].Operation, - "expression": rule.ActionParameters.Headers[headerName].Expression, - "value": rule.ActionParameters.Headers[headerName].Value, - } - ruleHeaders[rule.ID] = append(ruleHeaders[rule.ID], header) - } - } - } - } - } - - sort.Slice(jsonPayload, func(i, j int) bool { - return jsonPayload[i].Phase < jsonPayload[j].Phase - }) - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - - // Make the rules have the correct header structure - for i, ruleset := range jsonStructData { - if ruleset.(map[string]interface{})["rules"] != nil { - for j, rule := range ruleset.(map[string]interface{})["rules"].([]interface{}) { - ID := rule.(map[string]interface{})["id"] - if ID != nil { - headers, exists := ruleHeaders[ID.(string)] - if exists { - jsonStructData[i].(map[string]interface{})["rules"].([]interface{})[j].(map[string]interface{})["action_parameters"].(map[string]interface{})["headers"] = headers - } - } - } - } - } - - // log custom fields specific transformation fields - logCustomFieldsTransform := []string{"cookie_fields", "request_fields", "response_fields"} - - for i := 0; i < resourceCount; i++ { - rules := jsonStructData[i].(map[string]interface{})["rules"] - if rules != nil { - for ruleCounter := range rules.([]interface{}) { - // should the `ref` be the default `id`, don't output it - // as we don't need to track a computed default. - id := rules.([]interface{})[ruleCounter].(map[string]interface{})["id"] - ref := rules.([]interface{})[ruleCounter].(map[string]interface{})["ref"] - if id == ref { - rules.([]interface{})[ruleCounter].(map[string]interface{})["ref"] = nil - } - - actionParams := rules.([]interface{})[ruleCounter].(map[string]interface{})["action_parameters"] - if actionParams != nil { - // check for log custom fields that need to be transformed - for _, logCustomFields := range logCustomFieldsTransform { - // check if the field exists and make sure it has at least one element - if actionParams.(map[string]interface{})[logCustomFields] != nil && len(actionParams.(map[string]interface{})[logCustomFields].([]interface{})) > 0 { - // Create a new list to store the data in. - var newLogCustomFields []interface{} - // iterate over each of the keys and add them to a generic list - for logCustomFieldsCounter := range actionParams.(map[string]interface{})[logCustomFields].([]interface{}) { - newLogCustomFields = append(newLogCustomFields, actionParams.(map[string]interface{})[logCustomFields].([]interface{})[logCustomFieldsCounter].(map[string]interface{})["name"]) - } - actionParams.(map[string]interface{})[logCustomFields] = newLogCustomFields - } - } - - // check if our ruleset is of action 'skip' - if rules.([]interface{})[ruleCounter].(map[string]interface{})["action"] == "skip" { - for rule := range actionParams.(map[string]interface{}) { - // "rules" is the only map[string][]string we need to remap. The others are all []string and are handled naturally. - if rule == "rules" { - for key, value := range actionParams.(map[string]interface{})[rule].(map[string]interface{}) { - var rulesList []string - for _, val := range value.([]interface{}) { - rulesList = append(rulesList, val.(string)) - } - actionParams.(map[string]interface{})[rule].(map[string]interface{})[key] = strings.Join(rulesList, ",") - } - } - } - } - - // Cache Rules transformation - if jsonStructData[i].(map[string]interface{})["phase"] == "http_request_cache_settings" { - if ck, ok := rules.([]interface{})[ruleCounter].(map[string]interface{})["action_parameters"].(map[string]interface{})["cache_key"]; ok { - if c, cok := ck.(map[string]interface{})["custom_key"]; cok { - if qs, qok := c.(map[string]interface{})["query_string"]; qok { - if s, sok := qs.(map[string]interface{})["include"]; sok && s == "*" { - rules.([]interface{})[ruleCounter].(map[string]interface{})["action_parameters"].(map[string]interface{})["cache_key"].(map[string]interface{})["custom_key"].(map[string]interface{})["query_string"].(map[string]interface{})["include"] = []interface{}{"*"} - } - if s, sok := qs.(map[string]interface{})["exclude"]; sok && s == "*" { - rules.([]interface{})[ruleCounter].(map[string]interface{})["action_parameters"].(map[string]interface{})["cache_key"].(map[string]interface{})["custom_key"].(map[string]interface{})["query_string"].(map[string]interface{})["exclude"] = []interface{}{"*"} - } - } - } - } - } - } - } - } - } - case "cloudflare_spectrum_application": - jsonPayload, err := api.SpectrumApplications(context.Background(), zoneID) - if err != nil { - log.Fatal(err) - } - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - case "cloudflare_teams_list": - jsonPayload, _, err := api.ListTeamsLists(context.Background(), identifier, cloudflare.ListTeamListsParams{}) - if err != nil { - log.Fatal(err) - } - // get items for the lists and add it the specific list struct - for i, TeamsList := range jsonPayload { - items_struct, _, err := api.ListTeamsListItems( - context.Background(), - identifier, - cloudflare.ListTeamsListItemsParams{ListID: TeamsList.ID}) - if err != nil { - log.Fatal(err) - } - TeamsList.Items = append(TeamsList.Items, items_struct...) - jsonPayload[i] = TeamsList - } - m, err := json.Marshal(jsonPayload) - if err != nil { - log.Fatal(err) - } - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - resourceCount = len(jsonPayload) - - // converting the items to value field and not the otherway around - for i := 0; i < resourceCount; i++ { - if jsonStructData[i].(map[string]interface{})["items"] != nil && len(jsonStructData[i].(map[string]interface{})["items"].([]interface{})) > 0 { - // new interface for storing data - var newItems []interface{} - for _, item := range jsonStructData[i].(map[string]interface{})["items"].([]interface{}) { - newItems = append(newItems, item.(map[string]interface{})["value"]) - } - jsonStructData[i].(map[string]interface{})["items"] = newItems - } - } - case "cloudflare_teams_location": - jsonPayload, _, err := api.TeamsLocations(context.Background(), accountID) - if err != nil { - log.Fatal(err) - } - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - case "cloudflare_teams_proxy_endpoint": - jsonPayload, _, err := api.TeamsProxyEndpoints(context.Background(), accountID) - if err != nil { - log.Fatal(err) - } - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - case "cloudflare_teams_rule": - jsonPayload, err := api.TeamsRules(context.Background(), accountID) - if err != nil { - log.Fatal(err) - } - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - // check for empty descriptions - for i := 0; i < resourceCount; i++ { - if jsonStructData[i].(map[string]interface{})["description"] == "" { - jsonStructData[i].(map[string]interface{})["description"] = "default" - } - } - case "cloudflare_tunnel": - log.Debug("only requesting the first 1000 active Cloudflare Tunnels due to the service not providing correct pagination responses") - jsonPayload, _, err := api.ListTunnels( - context.Background(), - identifier, - cloudflare.TunnelListParams{ - IsDeleted: cloudflare.BoolPtr(false), - ResultInfo: cloudflare.ResultInfo{ - PerPage: 1000, - Page: 1, - }, - }) - if err != nil { - log.Fatal(err) - } - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - - for i := 0; i < resourceCount; i++ { - secret, err := api.GetTunnelToken( - context.Background(), - identifier, - jsonStructData[i].(map[string]interface{})["id"].(string), - ) - if err != nil { - log.Fatal(err) - } - jsonStructData[i].(map[string]interface{})["secret"] = secret - jsonStructData[i].(map[string]interface{})["account_id"] = accountID - - jsonStructData[i].(map[string]interface{})["connections"] = nil - } - case "cloudflare_turnstile_widget": - jsonPayload, _, err := api.ListTurnstileWidgets(context.Background(), identifier, cloudflare.ListTurnstileWidgetParams{}) - if err != nil { - log.Fatal(err) - } - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - - for i := 0; i < resourceCount; i++ { - jsonStructData[i].(map[string]interface{})["id"] = jsonStructData[i].(map[string]interface{})["sitekey"] - - // We always want to emit a list of domains, even if it is empty. - // The empty list is used to enable the "Allow on any hostname" feature, it is *not* a default value. - if jsonStructData[i].(map[string]interface{})["domains"] == nil { - jsonStructData[i].(map[string]interface{})["domains"] = []string{} - } - } - case "cloudflare_url_normalization_settings": - jsonPayload, err := api.URLNormalizationSettings(context.Background(), &cloudflare.ResourceContainer{Identifier: zoneID, Level: cloudflare.ZoneRouteLevel}) - if err != nil { - log.Fatal(err) - } - var newJsonPayload []interface{} - newJsonPayload = append(newJsonPayload, jsonPayload) - resourceCount = len(newJsonPayload) - m, _ := json.Marshal(newJsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - - // this is only every a 1:1 so we can just verify if the 0th element has they key we expect - jsonStructData[0].(map[string]interface{})["id"] = zoneID - case "cloudflare_waiting_room": - jsonPayload, err := api.ListWaitingRooms(context.Background(), zoneID) - if err != nil { - log.Fatal(err) - } - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - - for i := 0; i < resourceCount; i++ { - if jsonStructData[i].(map[string]interface{})["queueing_status_code"].(float64) == 0 { - jsonStructData[i].(map[string]interface{})["queueing_status_code"] = nil - } - } - case "cloudflare_waiting_room_event": - waitingRooms, err := api.ListWaitingRooms(context.Background(), zoneID) - if err != nil { - log.Fatal(err) - } - for i := 0; i < len(waitingRooms); i++ { - roomEvents, err := api.ListWaitingRoomEvents(context.Background(), zoneID, waitingRooms[i].ID) - if err != nil { - log.Fatal(err) - } - m, err := json.Marshal(roomEvents) - if err != nil { - log.Fatal(err) - } - jsonRoomEvents := []interface{}{} - err = json.Unmarshal(m, &jsonRoomEvents) - if err != nil { - log.Fatal(err) - } - for i := 0; i < len(jsonRoomEvents); i++ { - jsonRoomEvents[i].(map[string]interface{})["waiting_room_id"] = waitingRooms[i].ID - } - jsonStructData = append(jsonStructData, jsonRoomEvents...) - } - resourceCount = len(jsonStructData) - case "cloudflare_waiting_room_rules": - waitingRooms, err := api.ListWaitingRooms(context.Background(), zoneID) - if err != nil { - log.Fatal(err) - } - roomRules := []struct { - ID string `json:"id"` - WaitingRoomID string `json:"waiting_room_id"` - Rules []cloudflare.WaitingRoomRule `json:"rules"` - }{} - for i := 0; i < len(waitingRooms); i++ { - rules, err := api.ListWaitingRoomRules(context.Background(), cloudflare.ZoneIdentifier(zoneID), cloudflare.ListWaitingRoomRuleParams{ - WaitingRoomID: waitingRooms[i].ID, - }) - if err != nil { - log.Fatal(err) - } - roomRules = append(roomRules, struct { - ID string `json:"id"` - WaitingRoomID string `json:"waiting_room_id"` - Rules []cloudflare.WaitingRoomRule `json:"rules"` - }{ - ID: waitingRooms[i].ID, - WaitingRoomID: waitingRooms[i].ID, - Rules: rules, - }) - } - resourceCount = len(roomRules) - m, err := json.Marshal(roomRules) - if err != nil { - log.Fatal(err) - } - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - case "cloudflare_waiting_room_settings": - waitingRoomSettings, err := api.GetWaitingRoomSettings(context.Background(), cloudflare.ZoneIdentifier(zoneID)) - if err != nil { - log.Fatal(err) - } - var jsonPayload []cloudflare.WaitingRoomSettings - jsonPayload = append(jsonPayload, waitingRoomSettings) - - resourceCount = 1 - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - - jsonStructData[0].(map[string]interface{})["id"] = zoneID - jsonStructData[0].(map[string]interface{})["search_engine_crawler_bypass"] = waitingRoomSettings.SearchEngineCrawlerBypass - case "cloudflare_workers_kv_namespace": - jsonPayload, _, err := api.ListWorkersKVNamespaces(context.Background(), identifier, cloudflare.ListWorkersKVNamespacesParams{}) - if err != nil { - log.Fatal(err) - } - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - case "cloudflare_worker_route": - jsonPayload, err := api.ListWorkerRoutes(context.Background(), identifier, cloudflare.ListWorkerRoutesParams{}) - if err != nil { - log.Fatal(err) - } - resourceCount = len(jsonPayload.Routes) - m, _ := json.Marshal(jsonPayload.Routes) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - - // remap "script_name" to the "script" value. - for i := 0; i < resourceCount; i++ { - jsonStructData[i].(map[string]interface{})["script_name"] = jsonStructData[i].(map[string]interface{})["script"] - } - case "cloudflare_zone": - jsonPayload, err := api.ListZones(context.Background()) - if err != nil { - log.Fatal(err) - } - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - - // - remap "zone" to the "name" value - // - remap "plan" to "legacy_id" value - // - drop meta and name_servers - // - pull in the account_id field - for i := 0; i < resourceCount; i++ { - jsonStructData[i].(map[string]interface{})["zone"] = jsonStructData[i].(map[string]interface{})["name"] - jsonStructData[i].(map[string]interface{})["plan"] = jsonStructData[i].(map[string]interface{})["plan"].(map[string]interface{})["legacy_id"].(string) - jsonStructData[i].(map[string]interface{})["meta"] = nil - jsonStructData[i].(map[string]interface{})["name_servers"] = nil - jsonStructData[i].(map[string]interface{})["status"] = nil - jsonStructData[i].(map[string]interface{})["account_id"] = jsonStructData[i].(map[string]interface{})["account"].(map[string]interface{})["id"].(string) - } - case "cloudflare_zone_lockdown": - jsonPayload, _, err := api.ListZoneLockdowns(context.Background(), identifier, cloudflare.LockdownListParams{}) - if err != nil { - log.Fatal(err) - } - - resourceCount = len(jsonPayload) - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - - case "cloudflare_zone_settings_override": - jsonPayload, err := api.ZoneSettings(context.Background(), zoneID) - if err != nil { - log.Fatal(err) - } - - resourceCount = 1 - m, _ := json.Marshal(jsonPayload.Result) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } - - zoneSettingsStruct := make(map[string]interface{}) - for _, data := range jsonStructData { - keyName := data.(map[string]interface{})["id"].(string) - value := data.(map[string]interface{})["value"] - zoneSettingsStruct[keyName] = value - } - - // Remap all settings under "settings" block as well as some of the - // attributes that are not 1:1 with the API. - for i := 0; i < resourceCount; i++ { - jsonStructData[i].(map[string]interface{})["id"] = zoneID - jsonStructData[i].(map[string]interface{})["settings"] = zoneSettingsStruct - - // zero RTT - jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["zero_rtt"] = jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["0rtt"] - - // Mobile subdomain redirects - if jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["mobile_redirect"].(map[string]interface{})["status"] == "off" { - jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["mobile_redirect"] = nil - } + err := client.Get(context.Background(), endpoint, params, &result) + if err != nil { + log.Fatalf("failed to fetch API endpoint: %s", err) + } - // HSTS - jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["security_header"].(map[string]interface{})["enabled"] = jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["security_header"].(map[string]interface{})["strict_transport_security"].(map[string]interface{})["enabled"] - jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["security_header"].(map[string]interface{})["include_subdomains"] = jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["security_header"].(map[string]interface{})["strict_transport_security"].(map[string]interface{})["include_subdomains"] - jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["security_header"].(map[string]interface{})["max_age"] = jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["security_header"].(map[string]interface{})["strict_transport_security"].(map[string]interface{})["max_age"] - jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["security_header"].(map[string]interface{})["preload"] = jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["security_header"].(map[string]interface{})["strict_transport_security"].(map[string]interface{})["preload"] - jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["security_header"].(map[string]interface{})["nosniff"] = jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["security_header"].(map[string]interface{})["strict_transport_security"].(map[string]interface{})["nosniff"] + body, err := io.ReadAll(result.Body) + if err != nil { + log.Fatalln(err) + } - // tls_1_2_only is deprecated in favour of min_tls - jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["tls_1_2_only"] = nil - } - case "cloudflare_tiered_cache": - tieredCache, err := api.GetTieredCache(context.Background(), &cloudflare.ResourceContainer{Identifier: zoneID}) - if err != nil { - log.Fatal(err) - } - var jsonPayload []cloudflare.TieredCache - jsonPayload = append(jsonPayload, tieredCache) + value := gjson.Get(string(body), "result") + json.Unmarshal([]byte(value.String()), &jsonStructData) - resourceCount = 1 - m, _ := json.Marshal(jsonPayload) - err = json.Unmarshal(m, &jsonStructData) - if err != nil { - log.Fatal(err) - } + resourceCount = len(jsonStructData) + log.Debugf("found %d resources to write out for %q", resourceCount, resourceType) - jsonStructData[0].(map[string]interface{})["id"] = zoneID - jsonStructData[0].(map[string]interface{})["cache_type"] = tieredCache.Type.String() - default: - fmt.Fprintf(cmd.OutOrStderr(), "%q is not yet supported for automatic generation", resourceType) - return - } // If we don't have any resources to generate, just bail out early. if resourceCount == 0 { fmt.Fprintf(cmd.OutOrStderr(), "no resources of type %q found to generate", resourceType) @@ -1361,6 +160,10 @@ func generateResources() func(cmd *cobra.Command, args []string) { } resource := rootBody.AppendNewBlock("resource", []string{resourceType, resourceID}).Body() + if r == nil { + log.Fatalf("failed to find %q in the initialized provider schema", resourceType) + } + sortedBlockAttributes := make([]string, 0, len(r.Block.Attributes)) for k := range r.Block.Attributes { sortedBlockAttributes = append(sortedBlockAttributes, k) @@ -1413,7 +216,7 @@ func generateResources() func(cmd *cobra.Command, args []string) { case ty.IsObjectType(): fmt.Printf("object found. attrName %s\n", attrName) default: - log.Debugf("attribute %q (attribute type of %q) has not been generated", attrName, ty.FriendlyName()) + log.Debugf("attribute %q has not been generated", attrName) } } diff --git a/internal/app/cf-terraforming/cmd/resource_to_endpoint_mapping.go b/internal/app/cf-terraforming/cmd/resource_to_endpoint_mapping.go new file mode 100644 index 000000000..960671932 --- /dev/null +++ b/internal/app/cf-terraforming/cmd/resource_to_endpoint_mapping.go @@ -0,0 +1,73 @@ +// This file is automatically generated by the OpenAPI schemas. Any manual edits here will be overwritten on the next update. + +package cmd + +var resourceToEndpoint = map[string]string{ + "cloudflare_account_member": "/accounts/{account_id}/members", + "cloudflare_account_role": "/accounts/{account_id}/roles", + "cloudflare_account_token": "/accounts/{account_id}/tokens", + "cloudflare_api_token_permissions_groups": "/accounts/{account_id}/tokens/permission_groups", + "cloudflare_api_token": "/user/tokens", + "cloudflare_load_balancer_monitor": "/accounts/{account_id}/load_balancers/monitors", + "cloudflare_load_balancer_pool": "/accounts/{account_id}/load_balancers/pools", + "cloudflare_certificate_pack": "/zones/{zone_id}/ssl/certificate_packs", + "cloudflare_dns_record": "/zones/{zone_id}/dns_records", + "cloudflare_email_routing_rule": "/zones/{zone_id}/email/routing/rules", + "cloudflare_email_routing_address": "/accounts/{account_id}/email/routing/addresses", + "cloudflare_zone_lockdown": "/zones/{zone_id}/firewall/lockdowns", + "cloudflare_firewall_rule": "/zones/{zone_id}/firewall/rules", + "cloudflare_access_rule": "/{account_or_zone}/{account_or_zone_id}/firewall/access_rules/rules", + "cloudflare_user_agent_blocking_rule": "/zones/{zone_id}/firewall/ua_rules", + "cloudflare_logpush_job": "/{account_or_zone}/{account_or_zone_id}/logpush/jobs", + "cloudflare_waiting_room_event": "/zones/{zone_id}/waiting_rooms/{waiting_room_id}/events", + "cloudflare_web3_hostname": "/zones/{zone_id}/web3/hostnames", + "cloudflare_workers_script": "/accounts/{account_id}/workers/scripts", + "cloudflare_workers_custom_domain": "/accounts/{account_id}/workers/domains", + "cloudflare_workers_kv_namespace": "/accounts/{account_id}/storage/kv/namespaces", + "cloudflare_api_shield_operation": "/zones/{zone_id}/api_gateway/operations", + "cloudflare_api_shield_schema": "/zones/{zone_id}/api_gateway/user_schemas", + "cloudflare_page_shield_policy": "/zones/{zone_id}/page_shield/policies", + "cloudflare_page_shield_connections": "/zones/{zone_id}/page_shield/connections", + "cloudflare_page_shield_scripts": "/zones/{zone_id}/page_shield/scripts", + "cloudflare_page_shield_cookies": "/zones/{zone_id}/page_shield/cookies", + "cloudflare_spectrum_application": "/zones/{zone_id}/spectrum/apps", + "cloudflare_regional_hostname": "/zones/{zone_id}/addressing/regional_hostnames", + "cloudflare_address_map": "/accounts/{account_id}/addressing/address_maps", + "cloudflare_byo_ip_prefix": "/accounts/{account_id}/addressing/prefixes", + "cloudflare_image": "/accounts/{account_id}/images/v1", + "cloudflare_image_variant": "/accounts/{account_id}/images/v1/variants", + "cloudflare_magic_wan_gre_tunnel": "/accounts/{account_id}/magic/gre_tunnels", + "cloudflare_magic_wan_ipsec_tunnel": "/accounts/{account_id}/magic/ipsec_tunnels", + "cloudflare_magic_wan_static_route": "/accounts/{account_id}/magic/routes", + "cloudflare_magic_transit_site": "/accounts/{account_id}/magic/sites", + "cloudflare_magic_transit_site_acl": "/accounts/{account_id}/magic/sites/{site_id}/acls", + "cloudflare_magic_transit_site_lan": "/accounts/{account_id}/magic/sites/{site_id}/lans", + "cloudflare_magic_transit_site_wan": "/accounts/{account_id}/magic/sites/{site_id}/wans", + "cloudflare_magic_transit_connector": "/accounts/{account_id}/magic/connectors", + "cloudflare_magic_network_monitoring_rule": "/accounts/{account_id}/mnm/rules", + "cloudflare_pages_project": "/accounts/{account_id}/pages/projects", + "cloudflare_pages_domain": "/accounts/{account_id}/pages/projects/{project_name}/domains", + "cloudflare_registrar_domain": "/accounts/{account_id}/registrar/domains", + "cloudflare_list": "/accounts/{account_id}/rules/lists", + "cloudflare_list_item": "/accounts/{account_id}/rules/lists/{list_id}/items", + "cloudflare_stream_live_input": "/accounts/{account_id}/stream/live_inputs", + "cloudflare_stream_watermark": "/accounts/{account_id}/stream/watermarks", + "cloudflare_notification_policy": "/accounts/{account_id}/alerting/v3/policies", + "cloudflare_d1_database": "/accounts/{account_id}/d1/database", + "cloudflare_r2_bucket": "/accounts/{account_id}/r2/buckets", + "cloudflare_zero_trust_access_identity_provider": "/{account_or_zone}/{account_or_zone_id}/access/identity_providers", + "cloudflare_zero_trust_organization": "/{account_or_zone}/{account_or_zone_id}/access/organizations", + "cloudflare_zero_trust_tunnel_cloudflared": "/accounts/{account_id}/cfd_tunnel", + "cloudflare_turnstile_widget": "/accounts/{account_id}/challenges/widgets", + "cloudflare_hyperdrive_config": "/accounts/{account_id}/hyperdrive/configs", + "cloudflare_web_analytics_site": "/accounts/{account_id}/rum/site_info/list", + "cloudflare_web_analytics_rule": "/accounts/{account_id}/rum/v2/{ruleset_id}/rules", + "cloudflare_snippet_rules": "/zones/{zone_id}/snippets/snippet_rules", + "cloudflare_calls_sfu_app": "/accounts/{account_id}/calls/apps", + "cloudflare_calls_turn_app": "/accounts/{account_id}/calls/turn_keys", + "cloudflare_permission_group": "/accounts/{account_id}/iam/permission_groups", + "cloudflare_resource_group": "/accounts/{account_id}/iam/resource_groups", + "cloudflare_cloud_connector_rules": "/zones/{zone_id}/cloud_connector/rules", + "cloudflare_leaked_credential_check_rule": "/zones/{zone_id}/leaked-credential-checks/detections", + "cloudflare_content_scanning_expression": "/zones/{zone_id}/content-upload-scan/payloads", +} From 31888da9bcf834b96fc5d21cfc1f85b9fab22b01 Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Tue, 14 Jan 2025 14:55:11 +1100 Subject: [PATCH 02/12] add scripts/build-resource-to-endpoint-mapping for building mapping file --- .../cmd/resource_to_endpoint_mapping.go | 3 +- scripts/build-resource-to-endpoint-mapping | 67 +++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100755 scripts/build-resource-to-endpoint-mapping diff --git a/internal/app/cf-terraforming/cmd/resource_to_endpoint_mapping.go b/internal/app/cf-terraforming/cmd/resource_to_endpoint_mapping.go index 960671932..ee1fc7575 100644 --- a/internal/app/cf-terraforming/cmd/resource_to_endpoint_mapping.go +++ b/internal/app/cf-terraforming/cmd/resource_to_endpoint_mapping.go @@ -1,5 +1,4 @@ -// This file is automatically generated by the OpenAPI schemas. Any manual edits here will be overwritten on the next update. - +// This file is automatically generated. Any manual edits here will be overwritten on the next update. package cmd var resourceToEndpoint = map[string]string{ diff --git a/scripts/build-resource-to-endpoint-mapping b/scripts/build-resource-to-endpoint-mapping new file mode 100755 index 000000000..f9169e0c8 --- /dev/null +++ b/scripts/build-resource-to-endpoint-mapping @@ -0,0 +1,67 @@ +#!/usr/bin/env ruby + +require "yaml" + +if ARGV.length != 1 + puts "path to stainless configuration must be provided as the only argument" + exit 1 +end + +config = YAML.load_file(ARGV[0]) +mappingPath = "internal/app/cf-terraforming/cmd/resource_to_endpoint_mapping.go" +output = "" + +def terraform_name(v) + return v.dig("terraform", "name") || "" +end + +def has_subresources(v) + return !v["subresources"].nil? +end + +def list_endpoint(v) + operation = v.dig("methods", "list") + if operation.nil? + return nil + end + + # if we have a hash it means the endpoint has a + # nested structure and we need the `endpoint` value + # instead of just the `list` operation value. + if operation.is_a?(Hash) + return nil if !operation.dig("endpoint").start_with?("get") + + return operation.dig("endpoint").delete_prefix("get ") + end + + return nil if !operation.start_with?("get") + return operation.delete_prefix("get ") +end + +def has_terraform_configured(output, hash) + hash.each do |k, v| + next if terraform_name(v) == "" + next if list_endpoint(v).nil? + + output << "\"cloudflare_#{terraform_name(v)}\": \"#{list_endpoint(v)}\",\n" + has_terraform_configured(output, v["subresources"]) if has_subresources(v) + end +end + +config["resources"].keys.each do |k, v| + c = config["resources"][k].dig("subresources") + next if c.nil? + has_terraform_configured(output, c) +end + + +File.write(mappingPath, %Q( +// This file is automatically generated. Any manual edits here will be overwritten on the next update. +package cmd + +var resourceToEndpoint = map[string]string{ +#{output} +} +)) + +Kernel.system("gofmt -s -w #{mappingPath}") From 95422b58847b95d00c1f30e98570718a9e2b8696 Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Tue, 14 Jan 2025 14:55:45 +1100 Subject: [PATCH 03/12] handle combined endpoints --- internal/app/cf-terraforming/cmd/generate.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/internal/app/cf-terraforming/cmd/generate.go b/internal/app/cf-terraforming/cmd/generate.go index 2f5f4a134..d152d928e 100644 --- a/internal/app/cf-terraforming/cmd/generate.go +++ b/internal/app/cf-terraforming/cmd/generate.go @@ -112,8 +112,21 @@ func generateResources() func(cmd *cobra.Command, args []string) { result *http.Response ) + endpoint := resourceToEndpoint[resourceType] + + // if we encounter a combined endpoint, we need to rewrite to use the correct + // endpoint depending on what parameters are being provided. + if strings.Contains(endpoint, "{account_or_zone}") { + if accountID != "" { + endpoint = strings.Replace(endpoint, "/{account_or_zone}/{account_or_zone_id}/", "/accounts/{account_id}/", 1) + } else { + endpoint = strings.Replace(endpoint, "/{account_or_zone}/{account_or_zone_id}/", "/zones/{zone_id}/", 1) + } + } + + // replace the URL placeholders with the actual values we have. placeholderReplacer := strings.NewReplacer("{account_id}", accountID, "{zone_id}", zoneID) - endpoint := placeholderReplacer.Replace(resourceToEndpoint[resourceType]) + endpoint = placeholderReplacer.Replace(endpoint) client := cloudflare.NewClient() From 519664385c22967e0b8a3aae2c8b59dff1a932aa Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Tue, 14 Jan 2025 14:55:59 +1100 Subject: [PATCH 04/12] update README support matrix --- README.md | 66 ++----------------------------------------------------- 1 file changed, 2 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index f83a5fb95..a6ba822e1 100644 --- a/README.md +++ b/README.md @@ -205,70 +205,8 @@ existing binary, or you wish to provide a Terraform compatible binary (such as ## Supported Resources -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_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 | ❌ | ❌ | -| [cloudflare_access_rule](https://www.terraform.io/docs/providers/cloudflare/r/access_rule) | Account | ✅ | ✅ | -| [cloudflare_access_service_token](https://www.terraform.io/docs/providers/cloudflare/r/access_service_token) | Account | ✅ | ❌ | -| [cloudflare_account_member](https://www.terraform.io/docs/providers/cloudflare/r/account_member) | Account | ✅ | ✅ | -| [cloudflare_api_shield](https://www.terraform.io/docs/providers/cloudflare/r/api_shield) | Zone | ✅ | ❌ | -| [cloudflare_api_token](https://www.terraform.io/docs/providers/cloudflare/r/api_token) | User | ❌ | ❌ | -| [cloudflare_argo](https://www.terraform.io/docs/providers/cloudflare/r/argo) | Zone | ✅ | ✅ | -| [cloudflare_authenticated_origin_pulls](https://www.terraform.io/docs/providers/cloudflare/r/authenticated_origin_pulls) | Zone | ❌ | ❌ | -| [cloudflare_authenticated_origin_pulls_certificate](https://www.terraform.io/docs/providers/cloudflare/r/authenticated_origin_pulls_certificate) | Zone | ❌ | ❌ | -| [cloudflare_bot_management](https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs/resources/bot_management) | Zone | ✅ | ✅ | -| [cloudflare_byo_ip_prefix](https://www.terraform.io/docs/providers/cloudflare/r/byo_ip_prefix) | Account | ✅ | ✅ | -| [cloudflare_certificate_pack](https://www.terraform.io/docs/providers/cloudflare/r/certificate_pack) | Zone | ✅ | ✅ | -| [cloudflare_custom_hostname](https://www.terraform.io/docs/providers/cloudflare/r/custom_hostname) | Zone | ✅ | ✅ | -| [cloudflare_custom_hostname_fallback_origin](https://www.terraform.io/docs/providers/cloudflare/r/custom_hostname_fallback_origin) | Account | ✅ | ❌ | -| [cloudflare_custom_pages](https://www.terraform.io/docs/providers/cloudflare/r/custom_pages) | Account or Zone | ✅ | ✅ | -| [cloudflare_custom_ssl](https://www.terraform.io/docs/providers/cloudflare/r/custom_ssl) | Zone | ✅ | ✅ | -| [cloudflare_filter](https://www.terraform.io/docs/providers/cloudflare/r/filter) | Zone | ✅ | ✅ | -| [cloudflare_firewall_rule](https://www.terraform.io/docs/providers/cloudflare/r/firewall_rule) | Zone | ✅ | ✅ | -| [cloudflare_healthcheck](https://www.terraform.io/docs/providers/cloudflare/r/healthcheck) | Zone | ✅ | ✅ | -| [cloudflare_ip_list](https://www.terraform.io/docs/providers/cloudflare/r/ip_list) | Account | ❌ | ✅ | -| [cloudflare_list](https://www.terraform.io/docs/providers/cloudflare/r/list) | Account | ✅ | ❌ | -| [cloudflare_load_balancer](https://www.terraform.io/docs/providers/cloudflare/r/load_balancer) | Zone | ✅ | ✅ | -| [cloudflare_load_balancer_monitor](https://www.terraform.io/docs/providers/cloudflare/r/load_balancer_monitor) | Account | ✅ | ✅ | -| [cloudflare_load_balancer_pool](https://www.terraform.io/docs/providers/cloudflare/r/load_balancer_pool) | Account | ✅ | ✅ | -| [cloudflare_logpull_retention](https://www.terraform.io/docs/providers/cloudflare/r/logpull_retention) | Zone | ❌ | ❌ | -| [cloudflare_logpush_job](https://www.terraform.io/docs/providers/cloudflare/r/logpush_job) | Zone | ✅ | ❌ | -| [cloudflare_logpush_ownership_challenge](https://www.terraform.io/docs/providers/cloudflare/r/logpush_ownership_challenge) | Zone | ❌ | ❌ | -| [cloudflare_magic_firewall_ruleset](https://www.terraform.io/docs/providers/cloudflare/r/magic_firewall_ruleset) | Account | ❌ | ❌ | -| [cloudflare_origin_ca_certificate](https://www.terraform.io/docs/providers/cloudflare/r/origin_ca_certificate) | Zone | ✅ | ✅ | -| [cloudflare_page_rule](https://www.terraform.io/docs/providers/cloudflare/r/page_rule) | Zone | ✅ | ✅ | -| [cloudflare_rate_limit](https://www.terraform.io/docs/providers/cloudflare/r/rate_limit) | Zone | ✅ | ✅ | -| [cloudflare_record](https://www.terraform.io/docs/providers/cloudflare/r/record) | Zone | ✅ | ✅ | -| [cloudflare_ruleset](https://www.terraform.io/docs/providers/cloudflare/r/ruleset) | Account or Zone | ✅ | ✅ | -| [cloudflare_spectrum_application](https://www.terraform.io/docs/providers/cloudflare/r/spectrum_application) | Zone | ✅ | ✅ | -| [cloudflare_tiered_cache](https://www.terraform.io/docs/providers/cloudflare/r/tiered_cache) | Zone | ✅ | ❌ | -| [cloudflare_teams_list](https://www.terraform.io/docs/providers/cloudflare/r/teams_list) | Account | ✅ | ✅ | -| [cloudflare_teams_location](https://www.terraform.io/docs/providers/cloudflare/r/teams_location) | Account | ✅ | ✅ | -| [cloudflare_teams_proxy_endpoint](https://www.terraform.io/docs/providers/cloudflare/r/teams_proxy_endpoint) | Account | ✅ | ✅ | -| [cloudflare_teams_rule](https://www.terraform.io/docs/providers/cloudflare/r/teams_rule) | Account | ✅ | ✅ | -| [cloudflare_tunnel](https://www.terraform.io/docs/providers/cloudflare/r/tunnel) | Account | ✅ | ✅ | -| [cloudflare_turnstile_widget](https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs/resources/turnstile_widget) | Account | ✅ | ✅ | -| [cloudflare_url_normalization_settings](https://www.terraform.io/docs/providers/cloudflare/r/url_normalization_settings) | Zone | ✅ | ❌ | -| [cloudflare_waf_group](https://www.terraform.io/docs/providers/cloudflare/r/waf_group) | Zone | ❌ | ❌ | -| [cloudflare_waf_override](https://www.terraform.io/docs/providers/cloudflare/r/waf_override) | Zone | ✅ | ✅ | -| [cloudflare_waf_package](https://www.terraform.io/docs/providers/cloudflare/r/waf_package) | Zone | ✅ | ❌ | -| [cloudflare_waf_rule](https://www.terraform.io/docs/providers/cloudflare/r/waf_rule) | Zone | ❌ | ❌ | -| [cloudflare_waiting_room](https://www.terraform.io/docs/providers/cloudflare/r/waiting_room) | Zone | ✅ | ✅ | -| [cloudflare_worker_cron_trigger](https://www.terraform.io/docs/providers/cloudflare/r/worker_cron_trigger) | Account | ❌ | ❌ | -| [cloudflare_worker_route](https://www.terraform.io/docs/providers/cloudflare/r/worker_route) | Zone | ✅ | ✅ | -| [cloudflare_worker_script](https://www.terraform.io/docs/providers/cloudflare/r/worker_script) | Account | ❌ | ❌ | -| [cloudflare_workers_kv](https://www.terraform.io/docs/providers/cloudflare/r/workers_kv) | Account | ❌ | ❌ | -| [cloudflare_workers_kv_namespace](https://www.terraform.io/docs/providers/cloudflare/r/workers_kv_namespace) | Account | ✅ | ✅ | -| [cloudflare_zone](https://www.terraform.io/docs/providers/cloudflare/r/zone) | Account | ✅ | ✅ | -| [cloudflare_zone_dnssec](https://www.terraform.io/docs/providers/cloudflare/r/zone_dnssec) | Zone | ❌ | ❌ | -| [cloudflare_zone_lockdown](https://www.terraform.io/docs/providers/cloudflare/r/zone_lockdown) | Zone | ✅ | ✅ | -| [cloudflare_zone_settings_override](https://www.terraform.io/docs/providers/cloudflare/r/zone_settings_override) | Zone | ✅ | ❌ | +Any resource that is released within the Terraform Provider is automatically +supported. ## Testing From 9cc22576cc0aec46283296b178271a4642009ee5 Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Tue, 14 Jan 2025 15:02:07 +1100 Subject: [PATCH 05/12] no need for a body with .Get --- internal/app/cf-terraforming/cmd/generate.go | 3 +-- scripts/build-resource-to-endpoint-mapping | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/internal/app/cf-terraforming/cmd/generate.go b/internal/app/cf-terraforming/cmd/generate.go index d152d928e..9cc150b82 100644 --- a/internal/app/cf-terraforming/cmd/generate.go +++ b/internal/app/cf-terraforming/cmd/generate.go @@ -108,7 +108,6 @@ func generateResources() func(cmd *cobra.Command, args []string) { var ( jsonStructData []interface{} - params map[string]interface{} result *http.Response ) @@ -130,7 +129,7 @@ func generateResources() func(cmd *cobra.Command, args []string) { client := cloudflare.NewClient() - err := client.Get(context.Background(), endpoint, params, &result) + err := client.Get(context.Background(), endpoint, nil, &result) if err != nil { log.Fatalf("failed to fetch API endpoint: %s", err) } diff --git a/scripts/build-resource-to-endpoint-mapping b/scripts/build-resource-to-endpoint-mapping index f9169e0c8..e1edbe647 100755 --- a/scripts/build-resource-to-endpoint-mapping +++ b/scripts/build-resource-to-endpoint-mapping @@ -21,9 +21,7 @@ end def list_endpoint(v) operation = v.dig("methods", "list") - if operation.nil? - return nil - end + return nil if operation.nil? # if we have a hash it means the endpoint has a # nested structure and we need the `endpoint` value From b7cae1a8c144e617ca2e7a65179c72362b9da6b7 Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Fri, 17 Jan 2025 14:49:51 +1100 Subject: [PATCH 06/12] clean up logic for generating the mapping --- scripts/build-resource-to-endpoint-mapping | 77 +++++++++++++++++----- 1 file changed, 59 insertions(+), 18 deletions(-) diff --git a/scripts/build-resource-to-endpoint-mapping b/scripts/build-resource-to-endpoint-mapping index e1edbe647..b59fcde98 100755 --- a/scripts/build-resource-to-endpoint-mapping +++ b/scripts/build-resource-to-endpoint-mapping @@ -12,44 +12,85 @@ mappingPath = "internal/app/cf-terraforming/cmd/resource_to_endpoint_mapping.go" output = "" def terraform_name(v) - return v.dig("terraform", "name") || "" + if v.is_a?(Hash) + # don't generate if we've explicitly disabled the resource + return nil if v.dig("terraform") == false + return nil if v.dig("terraform", "resource") == false + + return v.dig("terraform", "name") + elsif v.is_a?(String) + return v + end + + return nil end def has_subresources(v) - return !v["subresources"].nil? + return false unless v.is_a?(Hash) + return v.has_key?("subresources") end def list_endpoint(v) - operation = v.dig("methods", "list") - return nil if operation.nil? + return nil unless v.is_a?(Hash) + list_operation = v.dig("methods", "list") + get_operation = v.dig("methods", "get") + + return nil if list_operation.nil? && get_operation.nil? + + if list_operation.is_a?(String) + return nil if !list_operation.start_with?("get") + return list_operation.delete_prefix("get ") + end + + if list_operation.is_a?(String) + return nil if !list_operation.start_with?("get") + end + + if get_operation.is_a?(String) + return nil if !get_operation.start_with?("get") + return get_operation.delete_prefix("get ") + end + + if get_operation.is_a?(String) + return nil if !get_operation.start_with?("get") + end # if we have a hash it means the endpoint has a # nested structure and we need the `endpoint` value # instead of just the `list` operation value. - if operation.is_a?(Hash) - return nil if !operation.dig("endpoint").start_with?("get") + if list_operation.is_a?(Hash) && !list_operation.nil? + return nil if !list_operation.dig("endpoint").start_with?("get") + return list_operation.dig("endpoint").delete_prefix("get ") + end - return operation.dig("endpoint").delete_prefix("get ") + # if we have a hash it means the endpoint has a + # nested structure and we need the `endpoint` value + # instead of just the `list` operation value. + if get_operation.is_a?(Hash) && !get_operation.nil? + return nil if !get_operation.dig("endpoint").start_with?("get") + return get_operation.dig("endpoint").delete_prefix("get ") end - return nil if !operation.start_with?("get") - return operation.delete_prefix("get ") + return nil end -def has_terraform_configured(output, hash) - hash.each do |k, v| - next if terraform_name(v) == "" - next if list_endpoint(v).nil? +def build_output(output, config) + terraform_name = terraform_name(config) + endpoint = list_endpoint(config) + + if !terraform_name.nil? && !endpoint.nil? + output << "\"cloudflare_#{terraform_name}\": \"#{endpoint}\",\n" + end - output << "\"cloudflare_#{terraform_name(v)}\": \"#{list_endpoint(v)}\",\n" - has_terraform_configured(output, v["subresources"]) if has_subresources(v) + return if config["subresources"].nil? + config["subresources"].each do |k, v| + build_output(output, v) end end config["resources"].keys.each do |k, v| - c = config["resources"][k].dig("subresources") - next if c.nil? - has_terraform_configured(output, c) + c = config["resources"][k] + build_output(output, c) end From 98b652795253784836e0d5c20aa2ccb57feadc43 Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Fri, 17 Jan 2025 14:50:18 +1100 Subject: [PATCH 07/12] commit updated mapping --- .../cmd/resource_to_endpoint_mapping.go | 240 +++++++++++++----- 1 file changed, 173 insertions(+), 67 deletions(-) diff --git a/internal/app/cf-terraforming/cmd/resource_to_endpoint_mapping.go b/internal/app/cf-terraforming/cmd/resource_to_endpoint_mapping.go index ee1fc7575..56d358284 100644 --- a/internal/app/cf-terraforming/cmd/resource_to_endpoint_mapping.go +++ b/internal/app/cf-terraforming/cmd/resource_to_endpoint_mapping.go @@ -2,71 +2,177 @@ package cmd var resourceToEndpoint = map[string]string{ - "cloudflare_account_member": "/accounts/{account_id}/members", - "cloudflare_account_role": "/accounts/{account_id}/roles", - "cloudflare_account_token": "/accounts/{account_id}/tokens", - "cloudflare_api_token_permissions_groups": "/accounts/{account_id}/tokens/permission_groups", - "cloudflare_api_token": "/user/tokens", - "cloudflare_load_balancer_monitor": "/accounts/{account_id}/load_balancers/monitors", - "cloudflare_load_balancer_pool": "/accounts/{account_id}/load_balancers/pools", - "cloudflare_certificate_pack": "/zones/{zone_id}/ssl/certificate_packs", - "cloudflare_dns_record": "/zones/{zone_id}/dns_records", - "cloudflare_email_routing_rule": "/zones/{zone_id}/email/routing/rules", - "cloudflare_email_routing_address": "/accounts/{account_id}/email/routing/addresses", - "cloudflare_zone_lockdown": "/zones/{zone_id}/firewall/lockdowns", - "cloudflare_firewall_rule": "/zones/{zone_id}/firewall/rules", - "cloudflare_access_rule": "/{account_or_zone}/{account_or_zone_id}/firewall/access_rules/rules", - "cloudflare_user_agent_blocking_rule": "/zones/{zone_id}/firewall/ua_rules", - "cloudflare_logpush_job": "/{account_or_zone}/{account_or_zone_id}/logpush/jobs", - "cloudflare_waiting_room_event": "/zones/{zone_id}/waiting_rooms/{waiting_room_id}/events", - "cloudflare_web3_hostname": "/zones/{zone_id}/web3/hostnames", - "cloudflare_workers_script": "/accounts/{account_id}/workers/scripts", - "cloudflare_workers_custom_domain": "/accounts/{account_id}/workers/domains", - "cloudflare_workers_kv_namespace": "/accounts/{account_id}/storage/kv/namespaces", - "cloudflare_api_shield_operation": "/zones/{zone_id}/api_gateway/operations", - "cloudflare_api_shield_schema": "/zones/{zone_id}/api_gateway/user_schemas", - "cloudflare_page_shield_policy": "/zones/{zone_id}/page_shield/policies", - "cloudflare_page_shield_connections": "/zones/{zone_id}/page_shield/connections", - "cloudflare_page_shield_scripts": "/zones/{zone_id}/page_shield/scripts", - "cloudflare_page_shield_cookies": "/zones/{zone_id}/page_shield/cookies", - "cloudflare_spectrum_application": "/zones/{zone_id}/spectrum/apps", - "cloudflare_regional_hostname": "/zones/{zone_id}/addressing/regional_hostnames", - "cloudflare_address_map": "/accounts/{account_id}/addressing/address_maps", - "cloudflare_byo_ip_prefix": "/accounts/{account_id}/addressing/prefixes", - "cloudflare_image": "/accounts/{account_id}/images/v1", - "cloudflare_image_variant": "/accounts/{account_id}/images/v1/variants", - "cloudflare_magic_wan_gre_tunnel": "/accounts/{account_id}/magic/gre_tunnels", - "cloudflare_magic_wan_ipsec_tunnel": "/accounts/{account_id}/magic/ipsec_tunnels", - "cloudflare_magic_wan_static_route": "/accounts/{account_id}/magic/routes", - "cloudflare_magic_transit_site": "/accounts/{account_id}/magic/sites", - "cloudflare_magic_transit_site_acl": "/accounts/{account_id}/magic/sites/{site_id}/acls", - "cloudflare_magic_transit_site_lan": "/accounts/{account_id}/magic/sites/{site_id}/lans", - "cloudflare_magic_transit_site_wan": "/accounts/{account_id}/magic/sites/{site_id}/wans", - "cloudflare_magic_transit_connector": "/accounts/{account_id}/magic/connectors", - "cloudflare_magic_network_monitoring_rule": "/accounts/{account_id}/mnm/rules", - "cloudflare_pages_project": "/accounts/{account_id}/pages/projects", - "cloudflare_pages_domain": "/accounts/{account_id}/pages/projects/{project_name}/domains", - "cloudflare_registrar_domain": "/accounts/{account_id}/registrar/domains", - "cloudflare_list": "/accounts/{account_id}/rules/lists", - "cloudflare_list_item": "/accounts/{account_id}/rules/lists/{list_id}/items", - "cloudflare_stream_live_input": "/accounts/{account_id}/stream/live_inputs", - "cloudflare_stream_watermark": "/accounts/{account_id}/stream/watermarks", - "cloudflare_notification_policy": "/accounts/{account_id}/alerting/v3/policies", - "cloudflare_d1_database": "/accounts/{account_id}/d1/database", - "cloudflare_r2_bucket": "/accounts/{account_id}/r2/buckets", - "cloudflare_zero_trust_access_identity_provider": "/{account_or_zone}/{account_or_zone_id}/access/identity_providers", - "cloudflare_zero_trust_organization": "/{account_or_zone}/{account_or_zone_id}/access/organizations", - "cloudflare_zero_trust_tunnel_cloudflared": "/accounts/{account_id}/cfd_tunnel", - "cloudflare_turnstile_widget": "/accounts/{account_id}/challenges/widgets", - "cloudflare_hyperdrive_config": "/accounts/{account_id}/hyperdrive/configs", - "cloudflare_web_analytics_site": "/accounts/{account_id}/rum/site_info/list", - "cloudflare_web_analytics_rule": "/accounts/{account_id}/rum/v2/{ruleset_id}/rules", - "cloudflare_snippet_rules": "/zones/{zone_id}/snippets/snippet_rules", - "cloudflare_calls_sfu_app": "/accounts/{account_id}/calls/apps", - "cloudflare_calls_turn_app": "/accounts/{account_id}/calls/turn_keys", - "cloudflare_permission_group": "/accounts/{account_id}/iam/permission_groups", - "cloudflare_resource_group": "/accounts/{account_id}/iam/resource_groups", - "cloudflare_cloud_connector_rules": "/zones/{zone_id}/cloud_connector/rules", - "cloudflare_leaked_credential_check_rule": "/zones/{zone_id}/leaked-credential-checks/detections", - "cloudflare_content_scanning_expression": "/zones/{zone_id}/content-upload-scan/payloads", + "cloudflare_account": "/accounts", + "cloudflare_account_member": "/accounts/{account_id}/members", + "cloudflare_account_subscription": "/accounts/{account_id}/subscriptions", + "cloudflare_account_token": "/accounts/{account_id}/tokens", + "cloudflare_origin_ca_certificate": "/certificates", + "cloudflare_user": "/user", + "cloudflare_api_token": "/user/tokens", + "cloudflare_zone": "/zones", + "cloudflare_zone_setting": "/zones/{zone_id}/settings/{setting_id}", + "cloudflare_zone_hold": "/zones/{zone_id}/hold", + "cloudflare_zone_subscription": "/zones/{identifier}/subscription", + "cloudflare_load_balancer": "/zones/{zone_id}/load_balancers", + "cloudflare_load_balancer_monitor": "/accounts/{account_id}/load_balancers/monitors", + "cloudflare_load_balancer_pool": "/accounts/{account_id}/load_balancers/pools", + "cloudflare_zone_cache_reserve": "/zones/{zone_id}/cache/cache_reserve", + "cloudflare_tiered_cache": "/zones/{zone_id}/cache/tiered_cache_smart_topology_enable", + "cloudflare_zone_cache_variants": "/zones/{zone_id}/cache/variants", + "cloudflare_regional_tiered_cache": "/zones/{zone_id}/cache/regional_tiered_cache", + "cloudflare_certificate_pack": "/zones/{zone_id}/ssl/certificate_packs", + "cloudflare_total_tls": "/zones/{zone_id}/acm/total_tls", + "cloudflare_argo_smart_routing": "/zones/{zone_id}/argo/smart_routing", + "cloudflare_argo_tiered_caching": "/zones/{zone_id}/argo/tiered_caching", + "cloudflare_custom_ssl": "/zones/{zone_id}/custom_certificates", + "cloudflare_custom_hostname": "/zones/{zone_id}/custom_hostnames", + "cloudflare_custom_hostname_fallback_origin": "/zones/{zone_id}/custom_hostnames/fallback_origin", + "cloudflare_dns_firewall": "/accounts/{account_id}/dns_firewall", + "cloudflare_zone_dnssec": "/zones/{zone_id}/dnssec", + "cloudflare_dns_record": "/zones/{zone_id}/dns_records", + "cloudflare_dns_zone_transfers_incoming": "/zones/{zone_id}/secondary_dns/incoming", + "cloudflare_dns_zone_transfers_outgoing": "/zones/{zone_id}/secondary_dns/outgoing", + "cloudflare_dns_zone_transfers_acl": "/accounts/{account_id}/secondary_dns/acls", + "cloudflare_dns_zone_transfers_peer": "/accounts/{account_id}/secondary_dns/peers", + "cloudflare_dns_zone_transfers_tsig": "/accounts/{account_id}/secondary_dns/tsigs", + "cloudflare_email_security_block_sender": "/accounts/{account_id}/email-security/settings/block_senders", + "cloudflare_email_security_impersonation_registry": "/accounts/{account_id}/email-security/settings/impersonation_registry", + "cloudflare_email_security_trusted_domains": "/accounts/{account_id}/email-security/settings/trusted_domains", + "cloudflare_email_routing_settings": "/zones/{zone_id}/email/routing", + "cloudflare_email_routing_dns": "/zones/{zone_id}/email/routing/dns", + "cloudflare_email_routing_rule": "/zones/{zone_id}/email/routing/rules", + "cloudflare_email_routing_catch_all": "/zones/{zone_id}/email/routing/rules/catch_all", + "cloudflare_email_routing_address": "/accounts/{account_id}/email/routing/addresses", + "cloudflare_filter": "/zones/{zone_id}/filters", + "cloudflare_zone_lockdown": "/zones/{zone_id}/firewall/lockdowns", + "cloudflare_firewall_rule": "/zones/{zone_id}/firewall/rules", + "cloudflare_access_rule": "/{account_or_zone}/{account_or_zone_id}/firewall/access_rules/rules", + "cloudflare_user_agent_blocking_rule": "/zones/{zone_id}/firewall/ua_rules", + "cloudflare_healthcheck": "/zones/{zone_id}/healthchecks", + "cloudflare_keyless_certificate": "/zones/{zone_id}/keyless_certificates", + "cloudflare_logpush_job": "/{account_or_zone}/{account_or_zone_id}/logpush/jobs", + "cloudflare_logpull_retention": "/zones/{zone_id}/logs/control/retention/flag", + "cloudflare_authenticated_origin_pulls_certificate": "/zones/{zone_id}/origin_tls_client_auth", + "cloudflare_authenticated_origin_pulls": "/zones/{zone_id}/origin_tls_client_auth/hostnames/{hostname}", + "cloudflare_page_rule": "/zones/{zone_id}/pagerules", + "cloudflare_rate_limit": "/zones/{zone_id}/rate_limits", + "cloudflare_waiting_room": "/zones/{zone_id}/waiting_rooms", + "cloudflare_waiting_room_event": "/zones/{zone_id}/waiting_rooms/{waiting_room_id}/events", + "cloudflare_waiting_room_rules": "/zones/{zone_id}/waiting_rooms/{waiting_room_id}/rules", + "cloudflare_waiting_room_settings": "/zones/{zone_id}/waiting_rooms/settings", + "cloudflare_web3_hostname": "/zones/{zone_id}/web3/hostnames", + "cloudflare_workers_script": "/accounts/{account_id}/workers/scripts", + "cloudflare_workers_script_subdomain": "/accounts/{account_id}/workers/scripts/{script_name}/subdomain", + "cloudflare_workers_cron_trigger": "/accounts/{account_id}/workers/scripts/{script_name}/schedules", + "cloudflare_workers_deployment": "/accounts/{account_id}/workers/scripts/{script_name}/deployments", + "cloudflare_workers_custom_domain": "/accounts/{account_id}/workers/domains", + "cloudflare_workers_kv_namespace": "/accounts/{account_id}/storage/kv/namespaces", + "cloudflare_workers_kv": "/accounts/{account_id}/storage/kv/namespaces/{namespace_id}/values/{key_name}", + "cloudflare_queue": "/accounts/{account_id}/queues", + "cloudflare_queue_consumer": "/accounts/{account_id}/queues/{queue_id}/consumers", + "cloudflare_api_shield": "/zones/{zone_id}/api_gateway/configuration", + "cloudflare_api_shield_discovery_operation": "/zones/{zone_id}/api_gateway/discovery/operations", + "cloudflare_api_shield_operation": "/zones/{zone_id}/api_gateway/operations", + "cloudflare_api_shield_operation_schema_validation_settings": "/zones/{zone_id}/api_gateway/operations/{operation_id}/schema_validation", + "cloudflare_api_shield_schema_validation_settings": "/zones/{zone_id}/api_gateway/settings/schema_validation", + "cloudflare_api_shield_schema": "/zones/{zone_id}/api_gateway/user_schemas", + "cloudflare_managed_transforms": "/zones/{zone_id}/managed_headers", + "cloudflare_page_shield_policy": "/zones/{zone_id}/page_shield/policies", + "cloudflare_ruleset": "/{account_or_zone}/{account_or_zone_id}/rulesets", + "cloudflare_url_normalization_settings": "/zones/{zone_id}/url_normalization", + "cloudflare_spectrum_application": "/zones/{zone_id}/spectrum/apps/{app_id}", + "cloudflare_regional_hostname": "/zones/{zone_id}/addressing/regional_hostnames", + "cloudflare_address_map": "/accounts/{account_id}/addressing/address_maps", + "cloudflare_byo_ip_prefix": "/accounts/{account_id}/addressing/prefixes", + "cloudflare_image": "/accounts/{account_id}/images/v1", + "cloudflare_image_variant": "/accounts/{account_id}/images/v1/variants", + "cloudflare_magic_wan_gre_tunnel": "/accounts/{account_id}/magic/gre_tunnels", + "cloudflare_magic_wan_ipsec_tunnel": "/accounts/{account_id}/magic/ipsec_tunnels", + "cloudflare_magic_wan_static_route": "/accounts/{account_id}/magic/routes", + "cloudflare_magic_transit_site": "/accounts/{account_id}/magic/sites", + "cloudflare_magic_transit_site_acl": "/accounts/{account_id}/magic/sites/{site_id}/acls", + "cloudflare_magic_transit_site_lan": "/accounts/{account_id}/magic/sites/{site_id}/lans", + "cloudflare_magic_transit_site_wan": "/accounts/{account_id}/magic/sites/{site_id}/wans", + "cloudflare_magic_transit_connector": "/accounts/{account_id}/magic/connectors", + "cloudflare_magic_network_monitoring_configuration": "/accounts/{account_id}/mnm/config", + "cloudflare_magic_network_monitoring_rule": "/accounts/{account_id}/mnm/rules", + "cloudflare_mtls_certificate": "/accounts/{account_id}/mtls_certificates", + "cloudflare_pages_project": "/accounts/{account_id}/pages/projects", + "cloudflare_pages_domain": "/accounts/{account_id}/pages/projects/{project_name}/domains", + "cloudflare_registrar_domain": "/accounts/{account_id}/registrar/domains", + "cloudflare_list": "/accounts/{account_id}/rules/lists", + "cloudflare_list_item": "/accounts/{account_id}/rules/lists/{list_id}/items", + "cloudflare_stream": "/accounts/{account_id}/stream", + "cloudflare_stream_audio_track": "/accounts/{account_id}/stream/{identifier}/audio", + "cloudflare_stream_key": "/accounts/{account_id}/stream/keys", + "cloudflare_stream_live_input": "/accounts/{account_id}/stream/live_inputs", + "cloudflare_stream_watermark": "/accounts/{account_id}/stream/watermarks", + "cloudflare_stream_webhook": "/accounts/{account_id}/stream/webhook", + "cloudflare_stream_caption_language": "/accounts/{account_id}/stream/{identifier}/captions/{language}", + "cloudflare_stream_download": "/accounts/{account_id}/stream/{identifier}/downloads", + "cloudflare_notification_policy_webhooks": "/accounts/{account_id}/alerting/v3/destinations/webhooks", + "cloudflare_notification_policy": "/accounts/{account_id}/alerting/v3/policies", + "cloudflare_d1_database": "/accounts/{account_id}/d1/database", + "cloudflare_r2_bucket": "/accounts/{account_id}/r2/buckets", + "cloudflare_r2_custom_domain": "/accounts/{account_id}/r2/buckets/{bucket_name}/domains/custom", + "cloudflare_r2_managed_domain": "/accounts/{account_id}/r2/buckets/{bucket_name}/domains/managed", + "cloudflare_workers_for_platforms_dispatch_namespace": "/accounts/{account_id}/workers/dispatch/namespaces", + "cloudflare_workers_secret": "/accounts/{account_id}/workers/dispatch/namespaces/{dispatch_namespace}/scripts/{script_name}/secrets", + "cloudflare_zero_trust_dex_test": "/accounts/{account_id}/devices/dex_tests", + "cloudflare_zero_trust_device_managed_networks": "/accounts/{account_id}/devices/networks", + "cloudflare_zero_trust_device_default_profile": "/accounts/{account_id}/devices/policy", + "cloudflare_zero_trust_device_default_profile_local_domain_fallback": "/accounts/{account_id}/devices/policy/fallback_domains", + "cloudflare_zero_trust_device_default_profile_certificates": "/zones/{zone_id}/devices/policy/certificates", + "cloudflare_zero_trust_device_custom_profile": "/accounts/{account_id}/devices/policies", + "cloudflare_zero_trust_device_custom_profile_local_domain_fallback": "/accounts/{account_id}/devices/policy/{policy_id}/fallback_domains", + "cloudflare_zero_trust_device_posture_rule": "/accounts/{account_id}/devices/posture", + "cloudflare_zero_trust_device_posture_integration": "/accounts/{account_id}/devices/posture/integration", + "cloudflare_zero_trust_access_identity_provider": "/{account_or_zone}/{account_or_zone_id}/access/identity_providers", + "cloudflare_zero_trust_organization": "/{account_or_zone}/{account_or_zone_id}/access/organizations", + "cloudflare_zero_trust_access_infrastructure_target": "/accounts/{account_id}/infrastructure/targets", + "cloudflare_zero_trust_access_application": "/{account_or_zone}/{account_or_zone_id}/access/apps", + "cloudflare_zero_trust_access_short_lived_certificate": "/{account_or_zone}/{account_or_zone_id}/access/apps/ca", + "cloudflare_zero_trust_access_mtls_certificate": "/{account_or_zone}/{account_or_zone_id}/access/certificates", + "cloudflare_zero_trust_access_mtls_hostname_settings": "/{account_or_zone}/{account_or_zone_id}/access/certificates/settings", + "cloudflare_zero_trust_access_group": "/{account_or_zone}/{account_or_zone_id}/access/groups", + "cloudflare_zero_trust_access_service_token": "/{account_or_zone}/{account_or_zone_id}/access/service_tokens", + "cloudflare_zero_trust_access_key_configuration": "/accounts/{account_id}/access/keys", + "cloudflare_zero_trust_access_custom_page": "/accounts/{account_id}/access/custom_pages", + "cloudflare_zero_trust_access_tag": "/accounts/{account_id}/access/tags", + "cloudflare_zero_trust_access_policy": "/accounts/{account_id}/access/policies", + "cloudflare_zero_trust_tunnel_cloudflared": "/accounts/{account_id}/cfd_tunnel", + "cloudflare_zero_trust_tunnel_cloudflared_config": "/accounts/{account_id}/cfd_tunnel/{tunnel_id}/configurations", + "cloudflare_zero_trust_dlp_dataset": "/accounts/{account_id}/dlp/datasets", + "cloudflare_zero_trust_dlp_custom_profile": "/accounts/{account_id}/dlp/profiles/custom/{profile_id}", + "cloudflare_zero_trust_dlp_predefined_profile": "/accounts/{account_id}/dlp/profiles/predefined/{profile_id}", + "cloudflare_zero_trust_dlp_entry": "/accounts/{account_id}/dlp/entries", + "cloudflare_zero_trust_gateway_categories": "/accounts/{account_id}/gateway/categories", + "cloudflare_zero_trust_gateway_app_types": "/accounts/{account_id}/gateway/app_types", + "cloudflare_zero_trust_gateway_settings": "/accounts/{account_id}/gateway/configuration", + "cloudflare_zero_trust_list": "/accounts/{account_id}/gateway/lists", + "cloudflare_zero_trust_dns_location": "/accounts/{account_id}/gateway/locations", + "cloudflare_zero_trust_gateway_proxy_endpoint": "/accounts/{account_id}/gateway/proxy_endpoints", + "cloudflare_zero_trust_gateway_policy": "/accounts/{account_id}/gateway/rules", + "cloudflare_zero_trust_gateway_certificate": "/accounts/{account_id}/gateway/certificates", + "cloudflare_zero_trust_tunnel_cloudflared_route": "/accounts/{account_id}/teamnet/routes", + "cloudflare_zero_trust_tunnel_cloudflared_virtual_network": "/accounts/{account_id}/teamnet/virtual_networks", + "cloudflare_zero_trust_risk_behavior": "/accounts/{account_id}/zt_risk_scoring/behaviors", + "cloudflare_zero_trust_risk_scoring_integration": "/accounts/{account_id}/zt_risk_scoring/integrations/{integration_id}", + "cloudflare_turnstile_widget": "/accounts/{account_id}/challenges/widgets", + "cloudflare_hyperdrive_config": "/accounts/{account_id}/hyperdrive/configs", + "cloudflare_web_analytics_site": "/accounts/{account_id}/rum/site_info/list", + "cloudflare_web_analytics_rule": "/accounts/{account_id}/rum/v2/{ruleset_id}/rules", + "cloudflare_bot_management": "/zones/{zone_id}/bot_management", + "cloudflare_observatory_scheduled_test": "/zones/{zone_id}/speed_api/schedule/{url}", + "cloudflare_hostname_tls_setting": "/zones/{zone_id}/hostnames/settings/{setting_id}", + "cloudflare_snippets": "/zones/{zone_id}/snippets", + "cloudflare_snippet_rules": "/zones/{zone_id}/snippets/snippet_rules", + "cloudflare_calls_sfu_app": "/accounts/{account_id}/calls/apps", + "cloudflare_calls_turn_app": "/accounts/{account_id}/calls/turn_keys", + "cloudflare_cloudforce_one_request_priority": "/accounts/{account_identifier}/cloudforce-one/requests/priority/{priority_identifer}", + "cloudflare_cloudforce_one_request_asset": "/accounts/{account_identifier}/cloudforce-one/requests/{request_identifier}/asset/{asset_identifer}", + "cloudflare_cloud_connector_rules": "/zones/{zone_id}/cloud_connector/rules", + "cloudflare_leaked_credential_check": "/zones/{zone_id}/leaked-credential-checks", + "cloudflare_leaked_credential_check_rule": "/zones/{zone_id}/leaked-credential-checks/detections", + "cloudflare_content_scanning_expression": "/zones/{zone_id}/content-upload-scan/payloads", } From ce03b108acb346f8987265d6085290e804b2b197 Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Fri, 17 Jan 2025 14:50:24 +1100 Subject: [PATCH 08/12] internal(generate): run new/old approach alongside Updates the generate handler to keep the old way for v4 however, gate the new automatic approach for v5. --- internal/app/cf-terraforming/cmd/generate.go | 1294 +++++++++++++++++- 1 file changed, 1266 insertions(+), 28 deletions(-) diff --git a/internal/app/cf-terraforming/cmd/generate.go b/internal/app/cf-terraforming/cmd/generate.go index 9cc150b82..8ca3e65fa 100644 --- a/internal/app/cf-terraforming/cmd/generate.go +++ b/internal/app/cf-terraforming/cmd/generate.go @@ -5,12 +5,12 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "net/http" "os" "sort" "strings" + cfv0 "github.com/cloudflare/cloudflare-go" "github.com/cloudflare/cloudflare-go/v4" "github.com/hashicorp/go-version" "github.com/hashicorp/hc-install/product" @@ -50,7 +50,7 @@ func generateResources() func(cmd *cobra.Command, args []string) { // Download terraform if no existing binary was provided if execPath == "" { - tmpDir, err := ioutil.TempDir("", "tfinstall") + tmpDir, err := os.MkdirTemp("", "tfinstall") if err != nil { log.Fatal(err) } @@ -80,6 +80,10 @@ func generateResources() func(cmd *cobra.Command, args []string) { log.Fatal(err) } + _, providerVersion, err := tf.Version(context.Background(), true) + providerVersionString := providerVersion[providerRegistryHostname+"/cloudflare/cloudflare"].String() + log.Debugf("detected provider version: %s", providerVersionString) + log.Debug("reading Terraform schema for Cloudflare provider") ps, err := tf.ProvidersSchema(context.Background()) if err != nil { @@ -105,44 +109,1278 @@ func generateResources() func(cmd *cobra.Command, args []string) { // to allow it to be referenced further down in the loop that outputs the // newly generated resources. resourceCount := 0 + var jsonStructData []interface{} + + if strings.HasPrefix(providerVersionString, "5") { + var result *http.Response + + endpoint := resourceToEndpoint[resourceType] + + // if we encounter a combined endpoint, we need to rewrite to use the correct + // endpoint depending on what parameters are being provided. + if strings.Contains(endpoint, "{account_or_zone}") { + if accountID != "" { + endpoint = strings.Replace(endpoint, "/{account_or_zone}/{account_or_zone_id}/", "/accounts/{account_id}/", 1) + } else { + endpoint = strings.Replace(endpoint, "/{account_or_zone}/{account_or_zone_id}/", "/zones/{zone_id}/", 1) + } + } + + // replace the URL placeholders with the actual values we have. + placeholderReplacer := strings.NewReplacer("{account_id}", accountID, "{zone_id}", zoneID) + endpoint = placeholderReplacer.Replace(endpoint) + + client := cloudflare.NewClient() + + err := client.Get(context.Background(), endpoint, nil, &result) + if err != nil { + log.Fatalf("failed to fetch API endpoint: %s", err) + } - var ( - jsonStructData []interface{} - result *http.Response - ) + body, err := io.ReadAll(result.Body) + if err != nil { + log.Fatalln(err) + } - endpoint := resourceToEndpoint[resourceType] + value := gjson.Get(string(body), "result") + json.Unmarshal([]byte(value.String()), &jsonStructData) - // if we encounter a combined endpoint, we need to rewrite to use the correct - // endpoint depending on what parameters are being provided. - if strings.Contains(endpoint, "{account_or_zone}") { + resourceCount = len(jsonStructData) + } else { + var identifier *cfv0.ResourceContainer if accountID != "" { - endpoint = strings.Replace(endpoint, "/{account_or_zone}/{account_or_zone_id}/", "/accounts/{account_id}/", 1) + identifier = cfv0.AccountIdentifier(accountID) } else { - endpoint = strings.Replace(endpoint, "/{account_or_zone}/{account_or_zone_id}/", "/zones/{zone_id}/", 1) + identifier = cfv0.ZoneIdentifier(zoneID) } - } - // replace the URL placeholders with the actual values we have. - placeholderReplacer := strings.NewReplacer("{account_id}", accountID, "{zone_id}", zoneID) - endpoint = placeholderReplacer.Replace(endpoint) + switch resourceType { + case "cloudflare_access_application": + jsonPayload, _, err := api.ListAccessApplications(context.Background(), identifier, cfv0.ListAccessApplicationsParams{}) + if err != nil { + log.Fatal(err) + } - client := cloudflare.NewClient() + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + case "cloudflare_access_group": + jsonPayload, _, err := api.ListAccessGroups(context.Background(), identifier, cfv0.ListAccessGroupsParams{}) + if err != nil { + log.Fatal(err) + } - err := client.Get(context.Background(), endpoint, nil, &result) - if err != nil { - log.Fatalf("failed to fetch API endpoint: %s", err) - } + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + case "cloudflare_access_identity_provider": + jsonPayload, _, err := api.ListAccessIdentityProviders(context.Background(), identifier, cfv0.ListAccessIdentityProvidersParams{}) + if err != nil { + log.Fatal(err) + } - body, err := io.ReadAll(result.Body) - if err != nil { - log.Fatalln(err) - } + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + case "cloudflare_access_service_token": + jsonPayload, _, err := api.ListAccessServiceTokens(context.Background(), identifier, cfv0.ListAccessServiceTokensParams{}) + if err != nil { + log.Fatal(err) + } + + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + case "cloudflare_access_mutual_tls_certificate": + jsonPayload, _, err := api.ListAccessMutualTLSCertificates(context.Background(), identifier, cfv0.ListAccessMutualTLSCertificatesParams{}) + if err != nil { + log.Fatal(err) + } + + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + case "cloudflare_access_rule": + if accountID != "" { + jsonPayload, err := api.ListAccountAccessRules(context.Background(), accountID, cfv0.AccessRule{}, 1) + if err != nil { + log.Fatal(err) + } + + resourceCount = len(jsonPayload.Result) + m, _ := json.Marshal(jsonPayload.Result) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + } else { + jsonPayload, err := api.ListZoneAccessRules(context.Background(), zoneID, cfv0.AccessRule{}, 1) + if err != nil { + log.Fatal(err) + } + + resourceCount = len(jsonPayload.Result) + m, _ := json.Marshal(jsonPayload.Result) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + } + case "cloudflare_account_member": + jsonPayload, _, err := api.AccountMembers(context.Background(), accountID, cfv0.PaginationOptions{}) + if err != nil { + log.Fatal(err) + } + + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + + // remap email and role_ids into the right structure. + for i := 0; i < resourceCount; i++ { + jsonStructData[i].(map[string]interface{})["email_address"] = jsonStructData[i].(map[string]interface{})["user"].(map[string]interface{})["email"] + roleIDs := []string{} + for _, role := range jsonStructData[i].(map[string]interface{})["roles"].([]interface{}) { + roleIDs = append(roleIDs, role.(map[string]interface{})["id"].(string)) + } + jsonStructData[i].(map[string]interface{})["role_ids"] = roleIDs + } + case "cloudflare_argo": + jsonPayload := []cfv0.ArgoFeatureSetting{} + + argoSmartRouting, err := api.ArgoSmartRouting(context.Background(), zoneID) + if err != nil { + log.Fatal(err) + } + jsonPayload = append(jsonPayload, argoSmartRouting) + + argoTieredCaching, err := api.ArgoTieredCaching(context.Background(), zoneID) + if err != nil { + log.Fatal(err) + } + jsonPayload = append(jsonPayload, argoTieredCaching) + + resourceCount = 1 + + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + + for i, b := range jsonStructData { + key := b.(map[string]interface{})["id"].(string) + jsonStructData[0].(map[string]interface{})[key] = jsonStructData[i].(map[string]interface{})["value"] + } + case "cloudflare_api_shield": + jsonPayload := []cfv0.APIShield{} + apiShieldConfig, _, err := api.GetAPIShieldConfiguration(context.Background(), identifier) + if err != nil { + log.Fatal(err) + } + // the response can contain an empty APIShield struct. Verify we have data before we attempt to do anything + jsonPayload = append(jsonPayload, apiShieldConfig) + + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + + // this is only every a 1:1 so we can just verify if the 0th element has they key we expect + jsonStructData[0].(map[string]interface{})["id"] = zoneID + + if jsonStructData[0].(map[string]interface{})["auth_id_characteristics"] == nil { + // force a no resources return by setting resourceCount to 0 + resourceCount = 0 + } + case "cloudflare_user_agent_blocking_rule": + page := 1 + var jsonPayload []cfv0.UserAgentRule + for { + res, err := api.ListUserAgentRules(context.Background(), zoneID, page) + if err != nil { + log.Fatal(err) + } + + jsonPayload = append(jsonPayload, res.Result...) + res.ResultInfo = res.ResultInfo.Next() + + if res.ResultInfo.Done() { + break + } + page = page + 1 + } + + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err := json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + case "cloudflare_bot_management": + botManagement, err := api.GetBotManagement(context.Background(), identifier) + if err != nil { + log.Fatal(err) + } + var jsonPayload []cfv0.BotManagement + jsonPayload = append(jsonPayload, botManagement) + + resourceCount = 1 + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + + jsonStructData[0].(map[string]interface{})["id"] = zoneID + case "cloudflare_byo_ip_prefix": + jsonPayload, err := api.ListPrefixes(context.Background(), accountID) + if err != nil { + log.Fatal(err) + } + + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + + // remap ID to prefix_id and advertised to advertisement on the JSON payloads. + for i := 0; i < resourceCount; i++ { + jsonStructData[i].(map[string]interface{})["prefix_id"] = jsonStructData[i].(map[string]interface{})["id"] + + if jsonStructData[i].(map[string]interface{})["advertised"].(bool) { + jsonStructData[i].(map[string]interface{})["advertisement"] = "on" + } else { + jsonStructData[i].(map[string]interface{})["advertisement"] = "off" + } + } + case "cloudflare_certificate_pack": + jsonPayload, err := api.ListCertificatePacks(context.Background(), zoneID) + if err != nil { + log.Fatal(err) + } + + var customerManagedCertificates []cfv0.CertificatePack + for _, r := range jsonPayload { + if r.Type != "universal" { + customerManagedCertificates = append(customerManagedCertificates, r) + } + } + jsonPayload = customerManagedCertificates - value := gjson.Get(string(body), "result") - json.Unmarshal([]byte(value.String()), &jsonStructData) + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + case "cloudflare_custom_pages": + if accountID != "" { + acc := cfv0.CustomPageOptions{AccountID: accountID} + jsonPayload, err := api.CustomPages(context.Background(), &acc) + if err != nil { + log.Fatal(err) + } + + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + } else { + zo := cfv0.CustomPageOptions{ZoneID: zoneID} + jsonPayload, err := api.CustomPages(context.Background(), &zo) + if err != nil { + log.Fatal(err) + } + + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + } + + var newJsonStructData []interface{} + // remap ID to the "type" field + for i := 0; i < resourceCount; i++ { + jsonStructData[i].(map[string]interface{})["type"] = jsonStructData[i].(map[string]interface{})["id"] + // we only want repsonses that have 'url' + if jsonStructData[i].(map[string]interface{})["url"] != nil { + newJsonStructData = append(newJsonStructData, jsonStructData[i]) + } + } + jsonStructData = newJsonStructData + resourceCount = len(jsonStructData) + + case "cloudflare_custom_hostname_fallback_origin": + var jsonPayload []cfv0.CustomHostnameFallbackOrigin + apiCall, err := api.CustomHostnameFallbackOrigin(context.Background(), zoneID) + if err != nil { + log.Fatal(err) + } + + if apiCall.Origin != "" { + resourceCount = 1 + jsonPayload = append(jsonPayload, apiCall) + } + + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + + for i := 0; i < resourceCount; i++ { + jsonStructData[i].(map[string]interface{})["id"] = sanitiseTerraformResourceName(jsonStructData[i].(map[string]interface{})["origin"].(string)) + jsonStructData[i].(map[string]interface{})["status"] = nil + } + case "cloudflare_filter": + jsonPayload, _, err := api.Filters(context.Background(), identifier, cfv0.FilterListParams{}) + if err != nil { + log.Fatal(err) + } + + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + case "cloudflare_firewall_rule": + jsonPayload, _, err := api.FirewallRules(context.Background(), identifier, cfv0.FirewallRuleListParams{}) + if err != nil { + log.Fatal(err) + } + + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + + // remap Filter.ID to `filter_id` on the JSON payloads. + for i := 0; i < resourceCount; i++ { + jsonStructData[i].(map[string]interface{})["filter_id"] = jsonStructData[i].(map[string]interface{})["filter"].(map[string]interface{})["id"] + } + case "cloudflare_custom_hostname": + jsonPayload, _, err := api.CustomHostnames(context.Background(), zoneID, 1, cfv0.CustomHostname{}) + if err != nil { + log.Fatal(err) + } + + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + + for i := 0; i < resourceCount; i++ { + jsonStructData[i].(map[string]interface{})["ssl"].(map[string]interface{})["validation_errors"] = nil + } + case "cloudflare_custom_ssl": + jsonPayload, err := api.ListSSL(context.Background(), zoneID) + if err != nil { + log.Fatal(err) + } + + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + case "cloudflare_healthcheck": + jsonPayload, err := api.Healthchecks(context.Background(), zoneID) + if err != nil { + log.Fatal(err) + } + + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + case "cloudflare_list": + jsonPayload, err := api.ListLists(context.Background(), identifier, cfv0.ListListsParams{}) + if err != nil { + log.Fatal(err) + } + + m, err := json.Marshal(jsonPayload) + if err != nil { + log.Fatal(err) + } + + if err = json.Unmarshal(m, &jsonStructData); err != nil { + log.Fatal(err) + } + resourceCount = len(jsonPayload) + + for i := 0; i < resourceCount; i++ { + listID := jsonPayload[i].ID + kind := jsonPayload[i].Kind + + listItems, err := api.ListListItems(context.Background(), identifier, cfv0.ListListItemsParams{ID: listID}) + if err != nil { + log.Fatal(err) + } + items := make([]interface{}, 0) + + for _, listItem := range listItems { + if kind == "" { + continue + } + + value := map[string]interface{}{} + switch kind { + case "ip": + if listItem.IP == nil { + continue + } + value["ip"] = *listItem.IP + case "asn": + if listItem.ASN == nil { + continue + } + value["asn"] = int(*listItem.ASN) + case "hostname": + if listItem.Hostname == nil { + continue + } + value["hostname"] = map[string]interface{}{ + "url_hostname": listItem.Hostname.UrlHostname, + } + case "redirect": + if listItem.Redirect == nil { + continue + } + redirect := map[string]interface{}{ + "source_url": listItem.Redirect.SourceUrl, + "target_url": listItem.Redirect.TargetUrl, + } + if listItem.Redirect.IncludeSubdomains != nil { + redirect["include_subdomains"] = boolToEnabledOrDisabled(*listItem.Redirect.IncludeSubdomains) + } + if listItem.Redirect.SubpathMatching != nil { + redirect["subpath_matching"] = boolToEnabledOrDisabled(*listItem.Redirect.SubpathMatching) + } + if listItem.Redirect.StatusCode != nil { + redirect["status_code"] = *listItem.Redirect.StatusCode + } + if listItem.Redirect.PreserveQueryString != nil { + redirect["preserve_query_string"] = boolToEnabledOrDisabled(*listItem.Redirect.PreserveQueryString) + } + if listItem.Redirect.PreservePathSuffix != nil { + redirect["preserve_path_suffix"] = boolToEnabledOrDisabled(*listItem.Redirect.PreservePathSuffix) + } + value["redirect"] = redirect + } + items = append(items, map[string]interface{}{ + "comment": listItem.Comment, + "value": value, + }) + } + jsonStructData[i].(map[string]interface{})["item"] = items + } + case "cloudflare_load_balancer": + jsonPayload, err := api.ListLoadBalancers(context.Background(), identifier, cfv0.ListLoadBalancerParams{}) + if err != nil { + log.Fatal(err) + } + + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + + for i := 0; i < resourceCount; i++ { + jsonStructData[i].(map[string]interface{})["default_pool_ids"] = jsonStructData[i].(map[string]interface{})["default_pools"] + jsonStructData[i].(map[string]interface{})["fallback_pool_id"] = jsonStructData[i].(map[string]interface{})["fallback_pool"] + + if jsonStructData[i].(map[string]interface{})["country_pools"] != nil { + original := jsonStructData[i].(map[string]interface{})["country_pools"] + jsonStructData[i].(map[string]interface{})["country_pools"] = []interface{}{} + + for country, popIDs := range original.(map[string]interface{}) { + jsonStructData[i].(map[string]interface{})["country_pools"] = append(jsonStructData[i].(map[string]interface{})["country_pools"].([]interface{}), map[string]interface{}{"country": country, "pool_ids": popIDs}) + } + } + + if jsonStructData[i].(map[string]interface{})["region_pools"] != nil { + original := jsonStructData[i].(map[string]interface{})["region_pools"] + jsonStructData[i].(map[string]interface{})["region_pools"] = []interface{}{} + + for region, popIDs := range original.(map[string]interface{}) { + jsonStructData[i].(map[string]interface{})["region_pools"] = append(jsonStructData[i].(map[string]interface{})["region_pools"].([]interface{}), map[string]interface{}{"region": region, "pool_ids": popIDs}) + } + } + + if jsonStructData[i].(map[string]interface{})["pop_pools"] != nil { + original := jsonStructData[i].(map[string]interface{})["pop_pools"] + jsonStructData[i].(map[string]interface{})["pop_pools"] = []interface{}{} + + for pop, popIDs := range original.(map[string]interface{}) { + jsonStructData[i].(map[string]interface{})["pop_pools"] = append(jsonStructData[i].(map[string]interface{})["pop_pools"].([]interface{}), map[string]interface{}{"pop": pop, "pool_ids": popIDs}) + } + } + } + + case "cloudflare_load_balancer_pool": + jsonPayload, err := api.ListLoadBalancerPools(context.Background(), identifier, cfv0.ListLoadBalancerPoolParams{}) + if err != nil { + log.Fatal(err) + } + + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + + for i := 0; i < resourceCount; i++ { + for originCounter := range jsonStructData[i].(map[string]interface{})["origins"].([]interface{}) { + if jsonStructData[i].(map[string]interface{})["origins"].([]interface{})[originCounter].(map[string]interface{})["header"] != nil { + jsonStructData[i].(map[string]interface{})["origins"].([]interface{})[originCounter].(map[string]interface{})["header"].(map[string]interface{})["header"] = "Host" + jsonStructData[i].(map[string]interface{})["origins"].([]interface{})[originCounter].(map[string]interface{})["header"].(map[string]interface{})["values"] = jsonStructData[i].(map[string]interface{})["origins"].([]interface{})[originCounter].(map[string]interface{})["header"].(map[string]interface{})["Host"] + } + } + } + case "cloudflare_load_balancer_monitor": + jsonPayload, err := api.ListLoadBalancerMonitors(context.Background(), identifier, cfv0.ListLoadBalancerMonitorParams{}) + if err != nil { + log.Fatal(err) + } + + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + case "cloudflare_logpush_job": + jsonPayload, err := api.ListLogpushJobs(context.Background(), identifier, cfv0.ListLogpushJobsParams{}) + if err != nil { + log.Fatal(err) + } + + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + + for i := 0; i < resourceCount; i++ { + // Workaround for LogpushJob.Filter being empty with a custom + // marshaler and returning `{"where":{}}` as the "empty" value. + if jsonStructData[i].(map[string]interface{})["filter"] == `{"where":{}}` { + jsonStructData[i].(map[string]interface{})["filter"] = nil + } + } + case "cloudflare_managed_headers": + // only grab the enabled headers + jsonPayload, err := api.ListZoneManagedHeaders(context.Background(), cfv0.ResourceIdentifier(zoneID), cfv0.ListManagedHeadersParams{Status: "enabled"}) + if err != nil { + log.Fatal(err) + } + + var managedHeaders []cfv0.ManagedHeaders + managedHeaders = append(managedHeaders, jsonPayload) + + resourceCount = len(managedHeaders) + m, _ := json.Marshal(managedHeaders) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + + for i := 0; i < resourceCount; i++ { + jsonStructData[i].(map[string]interface{})["id"] = zoneID + } + case "cloudflare_origin_ca_certificate": + jsonPayload, err := api.ListOriginCACertificates(context.Background(), cfv0.ListOriginCertificatesParams{ZoneID: zoneID}) + if err != nil { + log.Fatal(err) + } + + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + case "cloudflare_page_rule": + jsonPayload, err := api.ListPageRules(context.Background(), zoneID) + if err != nil { + log.Fatal(err) + } + + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + + for i := 0; i < resourceCount; i++ { + jsonStructData[i].(map[string]interface{})["target"] = jsonStructData[i].(map[string]interface{})["targets"].([]interface{})[0].(map[string]interface{})["constraint"].(map[string]interface{})["value"] + jsonStructData[i].(map[string]interface{})["actions"] = flattenAttrMap(jsonStructData[i].(map[string]interface{})["actions"].([]interface{})) + + // Have to remap the cache_ttl_by_status to conform to Terraform's more human-friendly structure. + if cache, ok := jsonStructData[i].(map[string]interface{})["actions"].(map[string]interface{})["cache_ttl_by_status"].(map[string]interface{}); ok { + cache_ttl_by_status := []map[string]interface{}{} + + for codes, ttl := range cache { + if ttl == "no-cache" { + ttl = 0 + } else if ttl == "no-store" { + ttl = -1 + } + elem := map[string]interface{}{ + "codes": codes, + "ttl": ttl, + } + + cache_ttl_by_status = append(cache_ttl_by_status, elem) + } + + sort.SliceStable(cache_ttl_by_status, func(i int, j int) bool { + return cache_ttl_by_status[i]["codes"].(string) < cache_ttl_by_status[j]["codes"].(string) + }) + + jsonStructData[i].(map[string]interface{})["actions"].(map[string]interface{})["cache_ttl_by_status"] = cache_ttl_by_status + } + + // Remap cache_key_fields.query_string.include & .exclude wildcards (not in an array) to the appropriate "ignore" field value in Terraform. + if c, ok := jsonStructData[i].(map[string]interface{})["actions"].(map[string]interface{})["cache_key_fields"].(map[string]interface{}); ok { + if s, sok := c["query_string"].(map[string]interface{})["include"].(string); sok && s == "*" { + jsonStructData[i].(map[string]interface{})["actions"].(map[string]interface{})["cache_key_fields"].(map[string]interface{})["query_string"].(map[string]interface{})["include"] = nil + jsonStructData[i].(map[string]interface{})["actions"].(map[string]interface{})["cache_key_fields"].(map[string]interface{})["query_string"].(map[string]interface{})["ignore"] = false + } + if s, sok := c["query_string"].(map[string]interface{})["exclude"].(string); sok && s == "*" { + jsonStructData[i].(map[string]interface{})["actions"].(map[string]interface{})["cache_key_fields"].(map[string]interface{})["query_string"].(map[string]interface{})["exclude"] = nil + jsonStructData[i].(map[string]interface{})["actions"].(map[string]interface{})["cache_key_fields"].(map[string]interface{})["query_string"].(map[string]interface{})["ignore"] = true + } + } + } + case "cloudflare_rate_limit": + jsonPayload, err := api.ListAllRateLimits(context.Background(), zoneID) + if err != nil { + log.Fatal(err) + } + + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + + for i := 0; i < resourceCount; i++ { + var bypassItems []string + + // Remap match.request.url to match.request.url_pattern + jsonStructData[i].(map[string]interface{})["match"].(map[string]interface{})["request"].(map[string]interface{})["url_pattern"] = jsonStructData[i].(map[string]interface{})["match"].(map[string]interface{})["request"].(map[string]interface{})["url"] + + // Remap bypass to bypass_url_patterns + if jsonStructData[i].(map[string]interface{})["bypass"] != nil { + for _, item := range jsonStructData[i].(map[string]interface{})["bypass"].([]interface{}) { + bypassItems = append(bypassItems, item.(map[string]interface{})["value"].(string)) + } + jsonStructData[i].(map[string]interface{})["bypass_url_patterns"] = bypassItems + } + + // Remap match.response.status to match.response.statuses + jsonStructData[i].(map[string]interface{})["match"].(map[string]interface{})["response"].(map[string]interface{})["statuses"] = jsonStructData[i].(map[string]interface{})["match"].(map[string]interface{})["response"].(map[string]interface{})["status"] + } + + case "cloudflare_record": + jsonPayload, _, err := api.ListDNSRecords(context.Background(), identifier, cfv0.ListDNSRecordsParams{}) + if err != nil { + log.Fatal(err) + } + + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + + zone, _ := api.ZoneDetails(context.Background(), identifier.Identifier) + + for i := 0; i < resourceCount; i++ { + // Drop the proxiable values as they are not usable + jsonStructData[i].(map[string]interface{})["proxiable"] = nil + jsonStructData[i].(map[string]interface{})["value"] = nil + + if jsonStructData[i].(map[string]interface{})["name"].(string) != zone.Name { + jsonStructData[i].(map[string]interface{})["name"] = strings.ReplaceAll(jsonStructData[i].(map[string]interface{})["name"].(string), "."+zone.Name, "") + } + } + case "cloudflare_ruleset": + jsonPayload, err := api.ListRulesets(context.Background(), identifier, cfv0.ListRulesetsParams{}) + if err != nil { + log.Fatal(err) + } + + var nonManagedRules []cfv0.Ruleset + + // A little annoying but makes more sense doing it this way. Only append + // the non-managed rules to the usable nonManagedRules variable instead + // of attempting to delete from an existing slice and just reassign. + for _, r := range jsonPayload { + if r.Kind != string(cfv0.RulesetKindManaged) { + nonManagedRules = append(nonManagedRules, r) + } + } + jsonPayload = nonManagedRules + ruleHeaders := map[string][]map[string]interface{}{} + for i, rule := range nonManagedRules { + ruleset, _ := api.GetRuleset(context.Background(), identifier, rule.ID) + jsonPayload[i].Rules = ruleset.Rules + + if ruleset.Rules != nil { + for _, rule := range ruleset.Rules { + if rule.ActionParameters != nil && rule.ActionParameters.Headers != nil { + // Sort the headers to have deterministic config output + keys := make([]string, 0, len(rule.ActionParameters.Headers)) + for k := range rule.ActionParameters.Headers { + keys = append(keys, k) + } + sort.Strings(keys) + + // The structure of the API response for headers differs from the + // structure terraform requires. So we collect all the headers + // indexed by rule.ID to massage the jsonStructData later + for _, headerName := range keys { + header := map[string]interface{}{ + "name": headerName, + "operation": rule.ActionParameters.Headers[headerName].Operation, + "expression": rule.ActionParameters.Headers[headerName].Expression, + "value": rule.ActionParameters.Headers[headerName].Value, + } + ruleHeaders[rule.ID] = append(ruleHeaders[rule.ID], header) + } + } + } + } + } + + sort.Slice(jsonPayload, func(i, j int) bool { + return jsonPayload[i].Phase < jsonPayload[j].Phase + }) + + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + + // Make the rules have the correct header structure + for i, ruleset := range jsonStructData { + if ruleset.(map[string]interface{})["rules"] != nil { + for j, rule := range ruleset.(map[string]interface{})["rules"].([]interface{}) { + ID := rule.(map[string]interface{})["id"] + if ID != nil { + headers, exists := ruleHeaders[ID.(string)] + if exists { + jsonStructData[i].(map[string]interface{})["rules"].([]interface{})[j].(map[string]interface{})["action_parameters"].(map[string]interface{})["headers"] = headers + } + } + } + } + } + + // log custom fields specific transformation fields + logCustomFieldsTransform := []string{"cookie_fields", "request_fields", "response_fields"} + + for i := 0; i < resourceCount; i++ { + rules := jsonStructData[i].(map[string]interface{})["rules"] + if rules != nil { + for ruleCounter := range rules.([]interface{}) { + // should the `ref` be the default `id`, don't output it + // as we don't need to track a computed default. + id := rules.([]interface{})[ruleCounter].(map[string]interface{})["id"] + ref := rules.([]interface{})[ruleCounter].(map[string]interface{})["ref"] + if id == ref { + rules.([]interface{})[ruleCounter].(map[string]interface{})["ref"] = nil + } + + actionParams := rules.([]interface{})[ruleCounter].(map[string]interface{})["action_parameters"] + if actionParams != nil { + // check for log custom fields that need to be transformed + for _, logCustomFields := range logCustomFieldsTransform { + // check if the field exists and make sure it has at least one element + if actionParams.(map[string]interface{})[logCustomFields] != nil && len(actionParams.(map[string]interface{})[logCustomFields].([]interface{})) > 0 { + // Create a new list to store the data in. + var newLogCustomFields []interface{} + // iterate over each of the keys and add them to a generic list + for logCustomFieldsCounter := range actionParams.(map[string]interface{})[logCustomFields].([]interface{}) { + newLogCustomFields = append(newLogCustomFields, actionParams.(map[string]interface{})[logCustomFields].([]interface{})[logCustomFieldsCounter].(map[string]interface{})["name"]) + } + actionParams.(map[string]interface{})[logCustomFields] = newLogCustomFields + } + } + + // check if our ruleset is of action 'skip' + if rules.([]interface{})[ruleCounter].(map[string]interface{})["action"] == "skip" { + for rule := range actionParams.(map[string]interface{}) { + // "rules" is the only map[string][]string we need to remap. The others are all []string and are handled naturally. + if rule == "rules" { + for key, value := range actionParams.(map[string]interface{})[rule].(map[string]interface{}) { + var rulesList []string + for _, val := range value.([]interface{}) { + rulesList = append(rulesList, val.(string)) + } + actionParams.(map[string]interface{})[rule].(map[string]interface{})[key] = strings.Join(rulesList, ",") + } + } + } + } + + // Cache Rules transformation + if jsonStructData[i].(map[string]interface{})["phase"] == "http_request_cache_settings" { + if ck, ok := rules.([]interface{})[ruleCounter].(map[string]interface{})["action_parameters"].(map[string]interface{})["cache_key"]; ok { + if c, cok := ck.(map[string]interface{})["custom_key"]; cok { + if qs, qok := c.(map[string]interface{})["query_string"]; qok { + if s, sok := qs.(map[string]interface{})["include"]; sok && s == "*" { + rules.([]interface{})[ruleCounter].(map[string]interface{})["action_parameters"].(map[string]interface{})["cache_key"].(map[string]interface{})["custom_key"].(map[string]interface{})["query_string"].(map[string]interface{})["include"] = []interface{}{"*"} + } + if s, sok := qs.(map[string]interface{})["exclude"]; sok && s == "*" { + rules.([]interface{})[ruleCounter].(map[string]interface{})["action_parameters"].(map[string]interface{})["cache_key"].(map[string]interface{})["custom_key"].(map[string]interface{})["query_string"].(map[string]interface{})["exclude"] = []interface{}{"*"} + } + } + } + } + } + } + } + } + } + case "cloudflare_spectrum_application": + jsonPayload, err := api.SpectrumApplications(context.Background(), zoneID) + if err != nil { + log.Fatal(err) + } + + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + case "cloudflare_teams_list": + jsonPayload, _, err := api.ListTeamsLists(context.Background(), identifier, cfv0.ListTeamListsParams{}) + if err != nil { + log.Fatal(err) + } + // get items for the lists and add it the specific list struct + for i, TeamsList := range jsonPayload { + items_struct, _, err := api.ListTeamsListItems( + context.Background(), + identifier, + cfv0.ListTeamsListItemsParams{ListID: TeamsList.ID}) + if err != nil { + log.Fatal(err) + } + TeamsList.Items = append(TeamsList.Items, items_struct...) + jsonPayload[i] = TeamsList + } + m, err := json.Marshal(jsonPayload) + if err != nil { + log.Fatal(err) + } + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + resourceCount = len(jsonPayload) + + // converting the items to value field and not the otherway around + for i := 0; i < resourceCount; i++ { + if jsonStructData[i].(map[string]interface{})["items"] != nil && len(jsonStructData[i].(map[string]interface{})["items"].([]interface{})) > 0 { + // new interface for storing data + var newItems []interface{} + for _, item := range jsonStructData[i].(map[string]interface{})["items"].([]interface{}) { + newItems = append(newItems, item.(map[string]interface{})["value"]) + } + jsonStructData[i].(map[string]interface{})["items"] = newItems + } + } + case "cloudflare_teams_location": + jsonPayload, _, err := api.TeamsLocations(context.Background(), accountID) + if err != nil { + log.Fatal(err) + } + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + case "cloudflare_teams_proxy_endpoint": + jsonPayload, _, err := api.TeamsProxyEndpoints(context.Background(), accountID) + if err != nil { + log.Fatal(err) + } + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + case "cloudflare_teams_rule": + jsonPayload, err := api.TeamsRules(context.Background(), accountID) + if err != nil { + log.Fatal(err) + } + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + // check for empty descriptions + for i := 0; i < resourceCount; i++ { + if jsonStructData[i].(map[string]interface{})["description"] == "" { + jsonStructData[i].(map[string]interface{})["description"] = "default" + } + } + case "cloudflare_tunnel": + log.Debug("only requesting the first 1000 active Cloudflare Tunnels due to the service not providing correct pagination responses") + jsonPayload, _, err := api.ListTunnels( + context.Background(), + identifier, + cfv0.TunnelListParams{ + IsDeleted: cfv0.BoolPtr(false), + ResultInfo: cfv0.ResultInfo{ + PerPage: 1000, + Page: 1, + }, + }) + if err != nil { + log.Fatal(err) + } + + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + + for i := 0; i < resourceCount; i++ { + secret, err := api.GetTunnelToken( + context.Background(), + identifier, + jsonStructData[i].(map[string]interface{})["id"].(string), + ) + if err != nil { + log.Fatal(err) + } + jsonStructData[i].(map[string]interface{})["secret"] = secret + jsonStructData[i].(map[string]interface{})["account_id"] = accountID + + jsonStructData[i].(map[string]interface{})["connections"] = nil + } + case "cloudflare_turnstile_widget": + jsonPayload, _, err := api.ListTurnstileWidgets(context.Background(), identifier, cfv0.ListTurnstileWidgetParams{}) + if err != nil { + log.Fatal(err) + } + + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + + for i := 0; i < resourceCount; i++ { + jsonStructData[i].(map[string]interface{})["id"] = jsonStructData[i].(map[string]interface{})["sitekey"] + + // We always want to emit a list of domains, even if it is empty. + // The empty list is used to enable the "Allow on any hostname" feature, it is *not* a default value. + if jsonStructData[i].(map[string]interface{})["domains"] == nil { + jsonStructData[i].(map[string]interface{})["domains"] = []string{} + } + } + case "cloudflare_url_normalization_settings": + jsonPayload, err := api.URLNormalizationSettings(context.Background(), &cfv0.ResourceContainer{Identifier: zoneID, Level: cfv0.ZoneRouteLevel}) + if err != nil { + log.Fatal(err) + } + var newJsonPayload []interface{} + newJsonPayload = append(newJsonPayload, jsonPayload) + resourceCount = len(newJsonPayload) + m, _ := json.Marshal(newJsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + + // this is only every a 1:1 so we can just verify if the 0th element has they key we expect + jsonStructData[0].(map[string]interface{})["id"] = zoneID + case "cloudflare_waiting_room": + jsonPayload, err := api.ListWaitingRooms(context.Background(), zoneID) + if err != nil { + log.Fatal(err) + } + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + + for i := 0; i < resourceCount; i++ { + if jsonStructData[i].(map[string]interface{})["queueing_status_code"].(float64) == 0 { + jsonStructData[i].(map[string]interface{})["queueing_status_code"] = nil + } + } + case "cloudflare_waiting_room_event": + waitingRooms, err := api.ListWaitingRooms(context.Background(), zoneID) + if err != nil { + log.Fatal(err) + } + for i := 0; i < len(waitingRooms); i++ { + roomEvents, err := api.ListWaitingRoomEvents(context.Background(), zoneID, waitingRooms[i].ID) + if err != nil { + log.Fatal(err) + } + m, err := json.Marshal(roomEvents) + if err != nil { + log.Fatal(err) + } + jsonRoomEvents := []interface{}{} + err = json.Unmarshal(m, &jsonRoomEvents) + if err != nil { + log.Fatal(err) + } + for i := 0; i < len(jsonRoomEvents); i++ { + jsonRoomEvents[i].(map[string]interface{})["waiting_room_id"] = waitingRooms[i].ID + } + jsonStructData = append(jsonStructData, jsonRoomEvents...) + } + resourceCount = len(jsonStructData) + case "cloudflare_waiting_room_rules": + waitingRooms, err := api.ListWaitingRooms(context.Background(), zoneID) + if err != nil { + log.Fatal(err) + } + roomRules := []struct { + ID string `json:"id"` + WaitingRoomID string `json:"waiting_room_id"` + Rules []cfv0.WaitingRoomRule `json:"rules"` + }{} + for i := 0; i < len(waitingRooms); i++ { + rules, err := api.ListWaitingRoomRules(context.Background(), cfv0.ZoneIdentifier(zoneID), cfv0.ListWaitingRoomRuleParams{ + WaitingRoomID: waitingRooms[i].ID, + }) + if err != nil { + log.Fatal(err) + } + roomRules = append(roomRules, struct { + ID string `json:"id"` + WaitingRoomID string `json:"waiting_room_id"` + Rules []cfv0.WaitingRoomRule `json:"rules"` + }{ + ID: waitingRooms[i].ID, + WaitingRoomID: waitingRooms[i].ID, + Rules: rules, + }) + } + resourceCount = len(roomRules) + m, err := json.Marshal(roomRules) + if err != nil { + log.Fatal(err) + } + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + case "cloudflare_waiting_room_settings": + waitingRoomSettings, err := api.GetWaitingRoomSettings(context.Background(), cfv0.ZoneIdentifier(zoneID)) + if err != nil { + log.Fatal(err) + } + var jsonPayload []cfv0.WaitingRoomSettings + jsonPayload = append(jsonPayload, waitingRoomSettings) + + resourceCount = 1 + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + + jsonStructData[0].(map[string]interface{})["id"] = zoneID + jsonStructData[0].(map[string]interface{})["search_engine_crawler_bypass"] = waitingRoomSettings.SearchEngineCrawlerBypass + case "cloudflare_workers_kv_namespace": + jsonPayload, _, err := api.ListWorkersKVNamespaces(context.Background(), identifier, cfv0.ListWorkersKVNamespacesParams{}) + if err != nil { + log.Fatal(err) + } + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + case "cloudflare_worker_route": + jsonPayload, err := api.ListWorkerRoutes(context.Background(), identifier, cfv0.ListWorkerRoutesParams{}) + if err != nil { + log.Fatal(err) + } + resourceCount = len(jsonPayload.Routes) + m, _ := json.Marshal(jsonPayload.Routes) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + + // remap "script_name" to the "script" value. + for i := 0; i < resourceCount; i++ { + jsonStructData[i].(map[string]interface{})["script_name"] = jsonStructData[i].(map[string]interface{})["script"] + } + case "cloudflare_zone": + jsonPayload, err := api.ListZones(context.Background()) + if err != nil { + log.Fatal(err) + } + + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + + // - remap "zone" to the "name" value + // - remap "plan" to "legacy_id" value + // - drop meta and name_servers + // - pull in the account_id field + for i := 0; i < resourceCount; i++ { + jsonStructData[i].(map[string]interface{})["zone"] = jsonStructData[i].(map[string]interface{})["name"] + jsonStructData[i].(map[string]interface{})["plan"] = jsonStructData[i].(map[string]interface{})["plan"].(map[string]interface{})["legacy_id"].(string) + jsonStructData[i].(map[string]interface{})["meta"] = nil + jsonStructData[i].(map[string]interface{})["name_servers"] = nil + jsonStructData[i].(map[string]interface{})["status"] = nil + jsonStructData[i].(map[string]interface{})["account_id"] = jsonStructData[i].(map[string]interface{})["account"].(map[string]interface{})["id"].(string) + } + case "cloudflare_zone_lockdown": + jsonPayload, _, err := api.ListZoneLockdowns(context.Background(), identifier, cfv0.LockdownListParams{}) + if err != nil { + log.Fatal(err) + } + + resourceCount = len(jsonPayload) + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + + case "cloudflare_zone_settings_override": + jsonPayload, err := api.ZoneSettings(context.Background(), zoneID) + if err != nil { + log.Fatal(err) + } + + resourceCount = 1 + m, _ := json.Marshal(jsonPayload.Result) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + + zoneSettingsStruct := make(map[string]interface{}) + for _, data := range jsonStructData { + keyName := data.(map[string]interface{})["id"].(string) + value := data.(map[string]interface{})["value"] + zoneSettingsStruct[keyName] = value + } + + // Remap all settings under "settings" block as well as some of the + // attributes that are not 1:1 with the API. + for i := 0; i < resourceCount; i++ { + jsonStructData[i].(map[string]interface{})["id"] = zoneID + jsonStructData[i].(map[string]interface{})["settings"] = zoneSettingsStruct + + // zero RTT + jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["zero_rtt"] = jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["0rtt"] + + // Mobile subdomain redirects + if jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["mobile_redirect"].(map[string]interface{})["status"] == "off" { + jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["mobile_redirect"] = nil + } + + // HSTS + jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["security_header"].(map[string]interface{})["enabled"] = jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["security_header"].(map[string]interface{})["strict_transport_security"].(map[string]interface{})["enabled"] + jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["security_header"].(map[string]interface{})["include_subdomains"] = jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["security_header"].(map[string]interface{})["strict_transport_security"].(map[string]interface{})["include_subdomains"] + jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["security_header"].(map[string]interface{})["max_age"] = jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["security_header"].(map[string]interface{})["strict_transport_security"].(map[string]interface{})["max_age"] + jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["security_header"].(map[string]interface{})["preload"] = jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["security_header"].(map[string]interface{})["strict_transport_security"].(map[string]interface{})["preload"] + jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["security_header"].(map[string]interface{})["nosniff"] = jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["security_header"].(map[string]interface{})["strict_transport_security"].(map[string]interface{})["nosniff"] + + // tls_1_2_only is deprecated in favour of min_tls + jsonStructData[i].(map[string]interface{})["settings"].(map[string]interface{})["tls_1_2_only"] = nil + } + case "cloudflare_tiered_cache": + tieredCache, err := api.GetTieredCache(context.Background(), &cfv0.ResourceContainer{Identifier: zoneID}) + if err != nil { + log.Fatal(err) + } + var jsonPayload []cfv0.TieredCache + jsonPayload = append(jsonPayload, tieredCache) + + resourceCount = 1 + m, _ := json.Marshal(jsonPayload) + err = json.Unmarshal(m, &jsonStructData) + if err != nil { + log.Fatal(err) + } + + jsonStructData[0].(map[string]interface{})["id"] = zoneID + jsonStructData[0].(map[string]interface{})["cache_type"] = tieredCache.Type.String() + default: + fmt.Fprintf(cmd.OutOrStderr(), "%q is not yet supported for automatic generation", resourceType) + return + } + } - resourceCount = len(jsonStructData) log.Debugf("found %d resources to write out for %q", resourceCount, resourceType) // If we don't have any resources to generate, just bail out early. From 46db758425351ffea2c903281fc9a93fc787582e Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Fri, 17 Jan 2025 14:57:20 +1100 Subject: [PATCH 09/12] include mentions for v4 and v5 --- README.md | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a6ba822e1..b1d8beb97 100644 --- a/README.md +++ b/README.md @@ -205,8 +205,77 @@ existing binary, or you wish to provide a Terraform compatible binary (such as ## Supported Resources +### v5 + Any resource that is released within the Terraform Provider is automatically -supported. +supported for generation. Import support is not yet implemented. + +### v4 + +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_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 | ❌ | ❌ | +| [cloudflare_access_rule](https://www.terraform.io/docs/providers/cloudflare/r/access_rule) | Account | ✅ | ✅ | +| [cloudflare_access_service_token](https://www.terraform.io/docs/providers/cloudflare/r/access_service_token) | Account | ✅ | ❌ | +| [cloudflare_account_member](https://www.terraform.io/docs/providers/cloudflare/r/account_member) | Account | ✅ | ✅ | +| [cloudflare_api_shield](https://www.terraform.io/docs/providers/cloudflare/r/api_shield) | Zone | ✅ | ❌ | +| [cloudflare_api_token](https://www.terraform.io/docs/providers/cloudflare/r/api_token) | User | ❌ | ❌ | +| [cloudflare_argo](https://www.terraform.io/docs/providers/cloudflare/r/argo) | Zone | ✅ | ✅ | +| [cloudflare_authenticated_origin_pulls](https://www.terraform.io/docs/providers/cloudflare/r/authenticated_origin_pulls) | Zone | ❌ | ❌ | +| [cloudflare_authenticated_origin_pulls_certificate](https://www.terraform.io/docs/providers/cloudflare/r/authenticated_origin_pulls_certificate) | Zone | ❌ | ❌ | +| [cloudflare_bot_management](https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs/resources/bot_management) | Zone | ✅ | ✅ | +| [cloudflare_byo_ip_prefix](https://www.terraform.io/docs/providers/cloudflare/r/byo_ip_prefix) | Account | ✅ | ✅ | +| [cloudflare_certificate_pack](https://www.terraform.io/docs/providers/cloudflare/r/certificate_pack) | Zone | ✅ | ✅ | +| [cloudflare_custom_hostname](https://www.terraform.io/docs/providers/cloudflare/r/custom_hostname) | Zone | ✅ | ✅ | +| [cloudflare_custom_hostname_fallback_origin](https://www.terraform.io/docs/providers/cloudflare/r/custom_hostname_fallback_origin) | Account | ✅ | ❌ | +| [cloudflare_custom_pages](https://www.terraform.io/docs/providers/cloudflare/r/custom_pages) | Account or Zone | ✅ | ✅ | +| [cloudflare_custom_ssl](https://www.terraform.io/docs/providers/cloudflare/r/custom_ssl) | Zone | ✅ | ✅ | +| [cloudflare_filter](https://www.terraform.io/docs/providers/cloudflare/r/filter) | Zone | ✅ | ✅ | +| [cloudflare_firewall_rule](https://www.terraform.io/docs/providers/cloudflare/r/firewall_rule) | Zone | ✅ | ✅ | +| [cloudflare_healthcheck](https://www.terraform.io/docs/providers/cloudflare/r/healthcheck) | Zone | ✅ | ✅ | +| [cloudflare_ip_list](https://www.terraform.io/docs/providers/cloudflare/r/ip_list) | Account | ❌ | ✅ | +| [cloudflare_list](https://www.terraform.io/docs/providers/cloudflare/r/list) | Account | ✅ | ❌ | +| [cloudflare_load_balancer](https://www.terraform.io/docs/providers/cloudflare/r/load_balancer) | Zone | ✅ | ✅ | +| [cloudflare_load_balancer_monitor](https://www.terraform.io/docs/providers/cloudflare/r/load_balancer_monitor) | Account | ✅ | ✅ | +| [cloudflare_load_balancer_pool](https://www.terraform.io/docs/providers/cloudflare/r/load_balancer_pool) | Account | ✅ | ✅ | +| [cloudflare_logpull_retention](https://www.terraform.io/docs/providers/cloudflare/r/logpull_retention) | Zone | ❌ | ❌ | +| [cloudflare_logpush_job](https://www.terraform.io/docs/providers/cloudflare/r/logpush_job) | Zone | ✅ | ❌ | +| [cloudflare_logpush_ownership_challenge](https://www.terraform.io/docs/providers/cloudflare/r/logpush_ownership_challenge) | Zone | ❌ | ❌ | +| [cloudflare_magic_firewall_ruleset](https://www.terraform.io/docs/providers/cloudflare/r/magic_firewall_ruleset) | Account | ❌ | ❌ | +| [cloudflare_origin_ca_certificate](https://www.terraform.io/docs/providers/cloudflare/r/origin_ca_certificate) | Zone | ✅ | ✅ | +| [cloudflare_page_rule](https://www.terraform.io/docs/providers/cloudflare/r/page_rule) | Zone | ✅ | ✅ | +| [cloudflare_rate_limit](https://www.terraform.io/docs/providers/cloudflare/r/rate_limit) | Zone | ✅ | ✅ | +| [cloudflare_record](https://www.terraform.io/docs/providers/cloudflare/r/record) | Zone | ✅ | ✅ | +| [cloudflare_ruleset](https://www.terraform.io/docs/providers/cloudflare/r/ruleset) | Account or Zone | ✅ | ✅ | +| [cloudflare_spectrum_application](https://www.terraform.io/docs/providers/cloudflare/r/spectrum_application) | Zone | ✅ | ✅ | +| [cloudflare_tiered_cache](https://www.terraform.io/docs/providers/cloudflare/r/tiered_cache) | Zone | ✅ | ❌ | +| [cloudflare_teams_list](https://www.terraform.io/docs/providers/cloudflare/r/teams_list) | Account | ✅ | ✅ | +| [cloudflare_teams_location](https://www.terraform.io/docs/providers/cloudflare/r/teams_location) | Account | ✅ | ✅ | +| [cloudflare_teams_proxy_endpoint](https://www.terraform.io/docs/providers/cloudflare/r/teams_proxy_endpoint) | Account | ✅ | ✅ | +| [cloudflare_teams_rule](https://www.terraform.io/docs/providers/cloudflare/r/teams_rule) | Account | ✅ | ✅ | +| [cloudflare_tunnel](https://www.terraform.io/docs/providers/cloudflare/r/tunnel) | Account | ✅ | ✅ | +| [cloudflare_turnstile_widget](https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs/resources/turnstile_widget) | Account | ✅ | ✅ | +| [cloudflare_url_normalization_settings](https://www.terraform.io/docs/providers/cloudflare/r/url_normalization_settings) | Zone | ✅ | ❌ | +| [cloudflare_waf_group](https://www.terraform.io/docs/providers/cloudflare/r/waf_group) | Zone | ❌ | ❌ | +| [cloudflare_waf_override](https://www.terraform.io/docs/providers/cloudflare/r/waf_override) | Zone | ✅ | ✅ | +| [cloudflare_waf_package](https://www.terraform.io/docs/providers/cloudflare/r/waf_package) | Zone | ✅ | ❌ | +| [cloudflare_waf_rule](https://www.terraform.io/docs/providers/cloudflare/r/waf_rule) | Zone | ❌ | ❌ | +| [cloudflare_waiting_room](https://www.terraform.io/docs/providers/cloudflare/r/waiting_room) | Zone | ✅ | ✅ | +| [cloudflare_worker_cron_trigger](https://www.terraform.io/docs/providers/cloudflare/r/worker_cron_trigger) | Account | ❌ | ❌ | +| [cloudflare_worker_route](https://www.terraform.io/docs/providers/cloudflare/r/worker_route) | Zone | ✅ | ✅ | +| [cloudflare_worker_script](https://www.terraform.io/docs/providers/cloudflare/r/worker_script) | Account | ❌ | ❌ | +| [cloudflare_workers_kv](https://www.terraform.io/docs/providers/cloudflare/r/workers_kv) | Account | ❌ | ❌ | +| [cloudflare_workers_kv_namespace](https://www.terraform.io/docs/providers/cloudflare/r/workers_kv_namespace) | Account | ✅ | ✅ | +| [cloudflare_zone](https://www.terraform.io/docs/providers/cloudflare/r/zone) | Account | ✅ | ✅ | +| [cloudflare_zone_dnssec](https://www.terraform.io/docs/providers/cloudflare/r/zone_dnssec) | Zone | ❌ | ❌ | +| [cloudflare_zone_lockdown](https://www.terraform.io/docs/providers/cloudflare/r/zone_lockdown) | Zone | ✅ | ✅ | +| [cloudflare_zone_settings_override](https://www.terraform.io/docs/providers/cloudflare/r/zone_settings_override) | Zone | ✅ | ❌ | ## Testing From 859178eae72314417aba88e6057104bf62c78820 Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Fri, 17 Jan 2025 15:03:15 +1100 Subject: [PATCH 10/12] fix lints --- .golintci.yml | 2 ++ internal/app/cf-terraforming/cmd/generate.go | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.golintci.yml b/.golintci.yml index 7a5b24bff..6bf3bcd45 100644 --- a/.golintci.yml +++ b/.golintci.yml @@ -4,6 +4,8 @@ run: tests: true skip-dirs-use-default: true modules-download-mode: readonly + skip-files: + - internal/app/cf-terraforming/cmd/resource_to_endpoint_mapping.go linters: disable-all: true diff --git a/internal/app/cf-terraforming/cmd/generate.go b/internal/app/cf-terraforming/cmd/generate.go index 8ca3e65fa..ea7dbf1fa 100644 --- a/internal/app/cf-terraforming/cmd/generate.go +++ b/internal/app/cf-terraforming/cmd/generate.go @@ -143,7 +143,10 @@ func generateResources() func(cmd *cobra.Command, args []string) { } value := gjson.Get(string(body), "result") - json.Unmarshal([]byte(value.String()), &jsonStructData) + err = json.Unmarshal([]byte(value.String()), &jsonStructData) + if err != nil { + log.Fatalf("failed to unmarshal result: %s", err) + } resourceCount = len(jsonStructData) } else { From 2d4ded607c0da386391cd7ebfd495a96a69aca99 Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Fri, 17 Jan 2025 15:13:47 +1100 Subject: [PATCH 11/12] only check the mapped values for v5 --- internal/app/cf-terraforming/cmd/generate.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/app/cf-terraforming/cmd/generate.go b/internal/app/cf-terraforming/cmd/generate.go index ea7dbf1fa..576339453 100644 --- a/internal/app/cf-terraforming/cmd/generate.go +++ b/internal/app/cf-terraforming/cmd/generate.go @@ -100,11 +100,6 @@ func generateResources() func(cmd *cobra.Command, args []string) { r := s.ResourceSchemas[resourceType] log.Debugf("beginning to read and build %q resources", resourceType) - if resourceToEndpoint[resourceType] == "" { - log.Debugf("did not find API endpoint for %q. skipping...", resourceType) - continue - } - // Initialise `resourceCount` outside of the switch for supported resources // to allow it to be referenced further down in the loop that outputs the // newly generated resources. @@ -112,6 +107,11 @@ func generateResources() func(cmd *cobra.Command, args []string) { var jsonStructData []interface{} if strings.HasPrefix(providerVersionString, "5") { + if resourceToEndpoint[resourceType] == "" { + log.Debugf("did not find API endpoint for %q. skipping...", resourceType) + continue + } + var result *http.Response endpoint := resourceToEndpoint[resourceType] From a940366de5b150e6ca4ba965c0e954653259240c Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Fri, 17 Jan 2025 15:19:51 +1100 Subject: [PATCH 12/12] fix test assertion --- testdata/terraform/cloudflare_teams_location/test.tf | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/testdata/terraform/cloudflare_teams_location/test.tf b/testdata/terraform/cloudflare_teams_location/test.tf index 1e9cf8502..3b56eb640 100644 --- a/testdata/terraform/cloudflare_teams_location/test.tf +++ b/testdata/terraform/cloudflare_teams_location/test.tf @@ -3,7 +3,8 @@ resource "cloudflare_teams_location" "terraform_managed_resource" { client_default = false ecs_support = false name = "Austin Office Location" - networks { + networks = [{ + id = "" network = "192.0.2.1/32" - } + }] }