diff --git a/.github/workflows/acceptance-tests.yml b/.github/workflows/acceptance-tests.yml
index d087db3ef0..1e94a92be6 100644
--- a/.github/workflows/acceptance-tests.yml
+++ b/.github/workflows/acceptance-tests.yml
@@ -50,7 +50,7 @@ jobs:
# run: go test ./internal/services/... -v -sweep="1" -timeout 60m
- name: Run acceptance tests
# note: not all resources are covered here, only passing ones should be included here (for now).
- run: go test ./internal/services/{account,account_api_token_permission_groups,account_dns_settings,account_dns_settings_internal_view,account_permission_group,account_role,account_subscription,account_token,address_map,api_shield,api_shield_discovery_operation,api_shield_schema,api_token_permission_groups,argo_smart_routing,argo_tiered_caching,authenticated_origin_pulls,authenticated_origin_pulls_certificate,authenticated_origin_pulls_settings,botnet_feed_config_asn,byo_ip_prefix,calls_sfu_app,calls_turn_app,certificate_pack,cloud_connector_rules,cloudforce_one_request,cloudforce_one_request_asset,cloudforce_one_request_message,cloudforce_one_request_priority,content_scanning_expression,custom_pages,custom_ssl,dcv_delegation,dns_record,dns_settings_internal_view,dns_zone_transfers_acl,dns_zone_transfers_peer,dns_zone_transfers_tsig,email_routing_address,email_routing_catch_all,email_routing_dns,email_routing_rule,email_security_block_sender,email_security_impersonation_registry,email_security_trusted_domains,image,image_variant,ip_ranges,leaked_credential_check,leaked_credential_check_rule,list,logpull_retention,logpush_dataset_field,logpush_dataset_job,logpush_job,logpush_ownership_challenge,magic_network_monitoring_configuration,magic_network_monitoring_rule,magic_transit_connector,magic_transit_site,magic_transit_site_acl,magic_transit_site_lan,magic_transit_site_wan,magic_wan_gre_tunnel,magic_wan_ipsec_tunnel,magic_wan_static_route,observatory_scheduled_test,origin_ca_certificate,page_rule,page_shield_connections,page_shield_cookies,page_shield_policy,page_shield_scripts,pages_domain,pages_project,queue,queue_consumer,r2_bucket,r2_bucket_cors,r2_bucket_event_notification,r2_bucket_lifecycle,r2_bucket_lock,r2_bucket_sippy,r2_custom_domain,r2_managed_domain,regional_hostname,regional_tiered_cache,registrar_domain,resource_group,ruleset,snippet_rules,snippets,stream,stream_audio_track,stream_caption_language,stream_download,stream_key,stream_live_input,stream_watermark,stream_webhook,tiered_cache,turnstile_widget,url_normalization_settings,user,waiting_room_settings,workers_cron_trigger,workers_custom_domain,workers_deployment,workers_for_platforms_dispatch_namespace,workers_kv_namespace,workers_route,workers_script,workers_script_subdomain,zero_trust_access_infrastructure_target,zero_trust_access_key_configuration,zero_trust_access_mtls_hostname_settings,zero_trust_access_service_token,zero_trust_access_tag,zero_trust_device_custom_profile,zero_trust_device_default_profile,zero_trust_device_default_profile_certificates,zero_trust_device_managed_networks,zero_trust_dlp_custom_profile,zero_trust_dlp_dataset,zero_trust_dlp_entry,zero_trust_dlp_predefined_profile,zero_trust_gateway_app_types,zero_trust_gateway_categories,zero_trust_gateway_certificate,zero_trust_gateway_logging,zero_trust_gateway_proxy_endpoint,zero_trust_list,zero_trust_risk_behavior,zero_trust_risk_scoring_integration,zero_trust_tunnel_cloudflared,zero_trust_tunnel_cloudflared_route,zero_trust_tunnel_cloudflared_token,zero_trust_tunnel_warp_connector_token,zone,zone_cache_reserve,zone_cache_variants,zone_dns_settings,zone_hold,zone_lockdown,zone_setting,zone_subscription} -run "^TestAcc" -count 1
+ run: ./scripts/run-ci-acceptance-tests
env:
TF_ACC: 1
# when all resources support sweepers, re-enable.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 90882aa1e0..effb11b0a8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -15,6 +15,7 @@ on:
jobs:
lint:
runs-on: ${{ github.repository == 'stainless-sdks/cloudflare-terraform' && 'depot-ubuntu-24.04' || 'lx64' }}
+ if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@v4
@@ -31,6 +32,7 @@ jobs:
test:
runs-on: ${{ github.repository == 'stainless-sdks/cloudflare-terraform' && 'depot-ubuntu-24.04' || 'lx64' }}
+ if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@v4
diff --git a/.stats.yml b/.stats.yml
index 990c6efb47..e991ae6841 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 1752
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cloudflare%2Fcloudflare-b9cf8d97f015bd3c7509f68b4dea2c37dc5f97183372064702ea540b6dd999f8.yml
-openapi_spec_hash: 83243c9ee06f88d0fa91e9b185d8a42e
-config_hash: cce40d4d65a4d67d5df957a75a15b567
+configured_endpoints: 1759
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cloudflare%2Fcloudflare-2960e6379741ec36e2d9ce4951603341a209d327c23b8dd612059d5d54d6a462.yml
+openapi_spec_hash: 20d3ceeadc6efd9590f482c695bf82ab
+config_hash: 920bb1b417565d337cbdb7c39e77be5b
diff --git a/bin/check-release-environment b/bin/check-release-environment
new file mode 100644
index 0000000000..3eb4ed5816
--- /dev/null
+++ b/bin/check-release-environment
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+
+errors=()
+
+if [ -z "${GPG_SIGNING_KEY}" ]; then
+ errors+=("The GPG_SIGNING_KEY secret has not been set. Please set it in either this repository's secrets or your organization secrets")
+fi
+
+if [ -z "${GPG_SIGNING_PASSWORD}" ]; then
+ errors+=("The GPG_SIGNING_PASSWORD secret has not been set. Please set it in either this repository's secrets or your organization secrets")
+fi
+
+lenErrors=${#errors[@]}
+
+if [[ lenErrors -gt 0 ]]; then
+ echo -e "Found the following errors in the release environment:\n"
+
+ for error in "${errors[@]}"; do
+ echo -e "- $error\n"
+ done
+
+ exit 1
+fi
+
+echo "The environment is ready to push releases!"
diff --git a/docs/data-sources/logpush_dataset_job.md b/docs/data-sources/logpush_dataset_job.md
index 6ff9322462..b7ecdd80a9 100644
--- a/docs/data-sources/logpush_dataset_job.md
+++ b/docs/data-sources/logpush_dataset_job.md
@@ -39,8 +39,8 @@ Available values: "access_requests", "audit_logs", "biso_user_actions", "casb_fi
- `frequency` (String, Deprecated) This field is deprecated. Please use `max_upload_*` parameters instead. The frequency at which Cloudflare sends batches of logs to your destination. Setting frequency to high sends your logs in larger quantities of smaller files. Setting frequency to low sends logs in smaller quantities of larger files.
Available values: "high", "low".
- `id` (Number) Unique id of the job.
-- `kind` (String) The kind parameter (optional) is used to differentiate between Logpush and Edge Log Delivery jobs. Currently, Edge Log Delivery is only supported for the `http_requests` dataset.
-Available values: "edge".
+- `kind` (String) The kind parameter (optional) is used to differentiate between Logpush and Edge Log Delivery jobs (when supported by the dataset).
+Available values: "", "edge".
- `last_complete` (String) Records the last time for which logs have been successfully pushed. If the last successful push was for logs range 2018-07-23T10:00:00Z to 2018-07-23T10:01:00Z then the value of this field will be 2018-07-23T10:01:00Z. If the job has never run or has just been enabled and hasn't run yet then the field will be empty.
- `last_error` (String) Records the last time the job failed. If not null, the job is currently failing. If null, the job has either never failed or has run successfully at least once since last failure. See also the error_message field.
- `logpull_options` (String, Deprecated) This field is deprecated. Use `output_options` instead. Configuration string. It specifies things like requested fields and timestamp formats. If migrating from the logpull api, copy the url (full url or just the query string) of your call here, and logpush will keep on making this call for you, setting start and end times appropriately.
diff --git a/docs/data-sources/logpush_job.md b/docs/data-sources/logpush_job.md
index 0bdd8960ab..5bfb7c06ef 100644
--- a/docs/data-sources/logpush_job.md
+++ b/docs/data-sources/logpush_job.md
@@ -38,8 +38,8 @@ Available values: "access_requests", "audit_logs", "biso_user_actions", "casb_fi
- `frequency` (String, Deprecated) This field is deprecated. Please use `max_upload_*` parameters instead. The frequency at which Cloudflare sends batches of logs to your destination. Setting frequency to high sends your logs in larger quantities of smaller files. Setting frequency to low sends logs in smaller quantities of larger files.
Available values: "high", "low".
- `id` (Number) Unique id of the job.
-- `kind` (String) The kind parameter (optional) is used to differentiate between Logpush and Edge Log Delivery jobs. Currently, Edge Log Delivery is only supported for the `http_requests` dataset.
-Available values: "edge".
+- `kind` (String) The kind parameter (optional) is used to differentiate between Logpush and Edge Log Delivery jobs (when supported by the dataset).
+Available values: "", "edge".
- `last_complete` (String) Records the last time for which logs have been successfully pushed. If the last successful push was for logs range 2018-07-23T10:00:00Z to 2018-07-23T10:01:00Z then the value of this field will be 2018-07-23T10:01:00Z. If the job has never run or has just been enabled and hasn't run yet then the field will be empty.
- `last_error` (String) Records the last time the job failed. If not null, the job is currently failing. If null, the job has either never failed or has run successfully at least once since last failure. See also the error_message field.
- `logpull_options` (String, Deprecated) This field is deprecated. Use `output_options` instead. Configuration string. It specifies things like requested fields and timestamp formats. If migrating from the logpull api, copy the url (full url or just the query string) of your call here, and logpush will keep on making this call for you, setting start and end times appropriately.
diff --git a/docs/data-sources/logpush_jobs.md b/docs/data-sources/logpush_jobs.md
index 66cbf404a8..a847d32a09 100644
--- a/docs/data-sources/logpush_jobs.md
+++ b/docs/data-sources/logpush_jobs.md
@@ -44,8 +44,8 @@ Available values: "access_requests", "audit_logs", "biso_user_actions", "casb_fi
- `frequency` (String, Deprecated) This field is deprecated. Please use `max_upload_*` parameters instead. The frequency at which Cloudflare sends batches of logs to your destination. Setting frequency to high sends your logs in larger quantities of smaller files. Setting frequency to low sends logs in smaller quantities of larger files.
Available values: "high", "low".
- `id` (Number) Unique id of the job.
-- `kind` (String) The kind parameter (optional) is used to differentiate between Logpush and Edge Log Delivery jobs. Currently, Edge Log Delivery is only supported for the `http_requests` dataset.
-Available values: "edge".
+- `kind` (String) The kind parameter (optional) is used to differentiate between Logpush and Edge Log Delivery jobs (when supported by the dataset).
+Available values: "", "edge".
- `last_complete` (String) Records the last time for which logs have been successfully pushed. If the last successful push was for logs range 2018-07-23T10:00:00Z to 2018-07-23T10:01:00Z then the value of this field will be 2018-07-23T10:01:00Z. If the job has never run or has just been enabled and hasn't run yet then the field will be empty.
- `last_error` (String) Records the last time the job failed. If not null, the job is currently failing. If null, the job has either never failed or has run successfully at least once since last failure. See also the error_message field.
- `logpull_options` (String, Deprecated) This field is deprecated. Use `output_options` instead. Configuration string. It specifies things like requested fields and timestamp formats. If migrating from the logpull api, copy the url (full url or just the query string) of your call here, and logpush will keep on making this call for you, setting start and end times appropriately.
diff --git a/docs/data-sources/workers_kv.md b/docs/data-sources/workers_kv.md
index 65490c3ecd..b9f77974aa 100644
--- a/docs/data-sources/workers_kv.md
+++ b/docs/data-sources/workers_kv.md
@@ -24,7 +24,7 @@ data "cloudflare_workers_kv" "example_workers_kv" {
### Required
-- `account_id` (String) Identifier
+- `account_id` (String) Identifier.
- `key_name` (String) A key's name. The name may be at most 512 bytes. All printable, non-whitespace characters are valid. Use percent-encoding to define key names as part of a URL.
- `namespace_id` (String) Namespace identifier tag.
diff --git a/docs/data-sources/workers_kv_namespace.md b/docs/data-sources/workers_kv_namespace.md
index b8edea9d87..48a13be296 100644
--- a/docs/data-sources/workers_kv_namespace.md
+++ b/docs/data-sources/workers_kv_namespace.md
@@ -23,7 +23,7 @@ data "cloudflare_workers_kv_namespace" "example_workers_kv_namespace" {
### Required
-- `account_id` (String) Identifier
+- `account_id` (String) Identifier.
### Optional
diff --git a/docs/data-sources/workers_kv_namespaces.md b/docs/data-sources/workers_kv_namespaces.md
index 265660c806..abcd983ae1 100644
--- a/docs/data-sources/workers_kv_namespaces.md
+++ b/docs/data-sources/workers_kv_namespaces.md
@@ -24,7 +24,7 @@ data "cloudflare_workers_kv_namespaces" "example_workers_kv_namespaces" {
### Required
-- `account_id` (String) Identifier
+- `account_id` (String) Identifier.
### Optional
diff --git a/docs/data-sources/zero_trust_dlp_custom_profile.md b/docs/data-sources/zero_trust_dlp_custom_profile.md
index 0354e2f7bd..85dd9ced1f 100644
--- a/docs/data-sources/zero_trust_dlp_custom_profile.md
+++ b/docs/data-sources/zero_trust_dlp_custom_profile.md
@@ -75,7 +75,7 @@ Cannot be set to false if secret is true
- `pattern` (Attributes) (see [below for nested schema](#nestedatt--entries--pattern))
- `profile_id` (String)
- `secret` (Boolean)
-- `type` (String) Available values: "custom", "predefined", "integration", "exact_data", "document_template", "word_list".
+- `type` (String) Available values: "custom", "predefined", "integration", "exact_data", "document_fingerprint", "word_list".
- `updated_at` (String)
- `word_list` (String)
diff --git a/docs/data-sources/zero_trust_dlp_entries.md b/docs/data-sources/zero_trust_dlp_entries.md
index 8ce909b225..3ea327e881 100644
--- a/docs/data-sources/zero_trust_dlp_entries.md
+++ b/docs/data-sources/zero_trust_dlp_entries.md
@@ -48,7 +48,7 @@ Cannot be set to false if secret is true
- `pattern` (Attributes) (see [below for nested schema](#nestedatt--result--pattern))
- `profile_id` (String)
- `secret` (Boolean)
-- `type` (String) Available values: "custom", "predefined", "integration", "exact_data", "document_template", "word_list".
+- `type` (String) Available values: "custom", "predefined", "integration", "exact_data", "document_fingerprint", "word_list".
- `updated_at` (String)
- `word_list` (String)
diff --git a/docs/data-sources/zero_trust_dlp_entry.md b/docs/data-sources/zero_trust_dlp_entry.md
index 4dbefa1420..80d12ca282 100644
--- a/docs/data-sources/zero_trust_dlp_entry.md
+++ b/docs/data-sources/zero_trust_dlp_entry.md
@@ -42,7 +42,7 @@ Cannot be set to false if secret is true
- `pattern` (Attributes) (see [below for nested schema](#nestedatt--pattern))
- `profile_id` (String)
- `secret` (Boolean)
-- `type` (String) Available values: "custom", "predefined", "integration", "exact_data", "document_template", "word_list".
+- `type` (String) Available values: "custom", "predefined", "integration", "exact_data", "document_fingerprint", "word_list".
- `updated_at` (String)
- `word_list` (String)
diff --git a/docs/data-sources/zero_trust_dlp_predefined_profile.md b/docs/data-sources/zero_trust_dlp_predefined_profile.md
index 9a1110f234..539f0af48d 100644
--- a/docs/data-sources/zero_trust_dlp_predefined_profile.md
+++ b/docs/data-sources/zero_trust_dlp_predefined_profile.md
@@ -75,7 +75,7 @@ Cannot be set to false if secret is true
- `pattern` (Attributes) (see [below for nested schema](#nestedatt--entries--pattern))
- `profile_id` (String)
- `secret` (Boolean)
-- `type` (String) Available values: "custom", "predefined", "integration", "exact_data", "document_template", "word_list".
+- `type` (String) Available values: "custom", "predefined", "integration", "exact_data", "document_fingerprint", "word_list".
- `updated_at` (String)
- `word_list` (String)
diff --git a/docs/data-sources/zero_trust_gateway_settings.md b/docs/data-sources/zero_trust_gateway_settings.md
index d92708fb56..5580f651c8 100644
--- a/docs/data-sources/zero_trust_gateway_settings.md
+++ b/docs/data-sources/zero_trust_gateway_settings.md
@@ -37,7 +37,6 @@ Read-Only:
- `activity_log` (Attributes) Activity log settings. (see [below for nested schema](#nestedatt--settings--activity_log))
- `antivirus` (Attributes) Anti-virus settings. (see [below for nested schema](#nestedatt--settings--antivirus))
-- `app_control_settings` (Attributes) Setting to enable App Control (see [below for nested schema](#nestedatt--settings--app_control_settings))
- `block_page` (Attributes) Block page layout settings. (see [below for nested schema](#nestedatt--settings--block_page))
- `body_scanning` (Attributes) DLP body scanning settings. (see [below for nested schema](#nestedatt--settings--body_scanning))
- `browser_isolation` (Attributes) Browser isolation settings. (see [below for nested schema](#nestedatt--settings--browser_isolation))
@@ -80,14 +79,6 @@ Read-Only:
-
-### Nested Schema for `settings.app_control_settings`
-
-Read-Only:
-
-- `enabled` (Boolean) Enable App Control
-
-
### Nested Schema for `settings.block_page`
diff --git a/docs/resources/dns_record.md b/docs/resources/dns_record.md
index f01b7488c9..a23cb0b36e 100644
--- a/docs/resources/dns_record.md
+++ b/docs/resources/dns_record.md
@@ -15,6 +15,7 @@ description: |-
resource "cloudflare_dns_record" "example_dns_record" {
zone_id = "023e105f4ecef8ad9ca31a8372d0c353"
name = "example.com"
+ ttl = 3600
type = "A"
comment = "Domain verification record"
content = "198.51.100.4"
@@ -24,7 +25,6 @@ resource "cloudflare_dns_record" "example_dns_record" {
ipv6_only = true
}
tags = ["owner:dns-team"]
- ttl = 3600
}
```
diff --git a/docs/resources/image.md b/docs/resources/image.md
index fbde4aabf7..1ca40ab66f 100644
--- a/docs/resources/image.md
+++ b/docs/resources/image.md
@@ -14,6 +14,9 @@ description: |-
```terraform
resource "cloudflare_image" "example_image" {
account_id = "023e105f4ecef8ad9ca31a8372d0c353"
+ id = {
+
+ }
file = {
}
@@ -31,6 +34,7 @@ resource "cloudflare_image" "example_image" {
### Required
- `account_id` (String) Account identifier tag.
+- `id` (String) An optional custom unique identifier for your image.
### Optional
@@ -42,7 +46,6 @@ resource "cloudflare_image" "example_image" {
### Read-Only
- `filename` (String) Image file name.
-- `id` (String) Image unique identifier.
- `meta` (String) User modifiable key-value store. Can be used for keeping references to another system of record for managing images. Metadata must not exceed 1024 bytes.
- `uploaded` (String) When the media item was uploaded.
- `variants` (List of String) Object specifying available variants for an image.
diff --git a/docs/resources/logpush_job.md b/docs/resources/logpush_job.md
index fc716e6256..e59f7a05ef 100644
--- a/docs/resources/logpush_job.md
+++ b/docs/resources/logpush_job.md
@@ -19,7 +19,7 @@ resource "cloudflare_logpush_job" "example_logpush_job" {
enabled = false
filter = "{\"where\":{\"and\":[{\"key\":\"ClientRequestPath\",\"operator\":\"contains\",\"value\":\"/static\"},{\"key\":\"ClientRequestHost\",\"operator\":\"eq\",\"value\":\"example.com\"}]}}"
frequency = "high"
- kind = "edge"
+ kind = ""
logpull_options = "fields=RayID,ClientIP,EdgeStartTimestamp×tamps=rfc3339"
max_upload_bytes = 5000000
max_upload_interval_seconds = 30
@@ -59,8 +59,8 @@ Available values: "access_requests", "audit_logs", "biso_user_actions", "casb_fi
- `filter` (String) The filters to select the events to include and/or remove from your logs. For more information, refer to [Filters](https://developers.cloudflare.com/logs/reference/filters/).
- `frequency` (String, Deprecated) This field is deprecated. Please use `max_upload_*` parameters instead. The frequency at which Cloudflare sends batches of logs to your destination. Setting frequency to high sends your logs in larger quantities of smaller files. Setting frequency to low sends logs in smaller quantities of larger files.
Available values: "high", "low".
-- `kind` (String) The kind parameter (optional) is used to differentiate between Logpush and Edge Log Delivery jobs. Currently, Edge Log Delivery is only supported for the `http_requests` dataset.
-Available values: "edge".
+- `kind` (String) The kind parameter (optional) is used to differentiate between Logpush and Edge Log Delivery jobs (when supported by the dataset).
+Available values: "", "edge".
- `logpull_options` (String, Deprecated) This field is deprecated. Use `output_options` instead. Configuration string. It specifies things like requested fields and timestamp formats. If migrating from the logpull api, copy the url (full url or just the query string) of your call here, and logpush will keep on making this call for you, setting start and end times appropriately.
- `max_upload_bytes` (Number) The maximum uncompressed file size of a batch of logs. This setting value must be between `5 MB` and `1 GB`, or `0` to disable it. Note that you cannot set a minimum file size; this means that log files may be much smaller than this batch size. This parameter is not available for jobs with `edge` as its kind.
- `max_upload_interval_seconds` (Number) The maximum interval in seconds for log batches. This setting must be between 30 and 300 seconds (5 minutes), or `0` to disable it. Note that you cannot specify a minimum interval for log batches; this means that log files may be sent in shorter intervals than this. This parameter is only used for jobs with `edge` as its kind.
diff --git a/docs/resources/ruleset.md b/docs/resources/ruleset.md
index d5364357b5..eb997bfe9f 100644
--- a/docs/resources/ruleset.md
+++ b/docs/resources/ruleset.md
@@ -19,7 +19,6 @@ resource "cloudflare_ruleset" "example_ruleset" {
zone_id = "zone_id"
description = "My ruleset to execute managed rulesets"
rules = [{
- id = "3a03d665bac047339bb530ecb439a90d"
action = "block"
action_parameters = {
response = {
diff --git a/docs/resources/workers_kv.md b/docs/resources/workers_kv.md
index baa91ac1bd..66b4d7df92 100644
--- a/docs/resources/workers_kv.md
+++ b/docs/resources/workers_kv.md
@@ -16,8 +16,10 @@ resource "cloudflare_workers_kv" "example_workers_kv" {
account_id = "023e105f4ecef8ad9ca31a8372d0c353"
namespace_id = "0f2ac74b498b48028cb68387c421e279"
key_name = "My-Key"
- metadata = "{\"someMetadataKey\": \"someMetadataValue\"}"
value = "Some Value"
+ metadata = {
+
+ }
}
```
@@ -26,14 +28,14 @@ resource "cloudflare_workers_kv" "example_workers_kv" {
### Required
-- `account_id` (String) Identifier
+- `account_id` (String) Identifier.
- `key_name` (String) A key's name. The name may be at most 512 bytes. All printable, non-whitespace characters are valid. Use percent-encoding to define key names as part of a URL.
- `namespace_id` (String) Namespace identifier tag.
- `value` (String) A byte sequence to be stored, up to 25 MiB in length.
### Optional
-- `metadata` (String) Arbitrary JSON to be associated with a key/value pair.
+- `metadata` (String)
### Read-Only
diff --git a/docs/resources/workers_kv_namespace.md b/docs/resources/workers_kv_namespace.md
index c72b3cfd7a..11aefebc96 100644
--- a/docs/resources/workers_kv_namespace.md
+++ b/docs/resources/workers_kv_namespace.md
@@ -23,7 +23,7 @@ resource "cloudflare_workers_kv_namespace" "example_workers_kv_namespace" {
### Required
-- `account_id` (String) Identifier
+- `account_id` (String) Identifier.
- `title` (String) A human-readable string name for a Namespace.
### Read-Only
diff --git a/docs/resources/zero_trust_dlp_custom_profile.md b/docs/resources/zero_trust_dlp_custom_profile.md
index ee81a368aa..f3df2be443 100644
--- a/docs/resources/zero_trust_dlp_custom_profile.md
+++ b/docs/resources/zero_trust_dlp_custom_profile.md
@@ -119,7 +119,7 @@ Required:
- `enabled` (Boolean)
- `entry_id` (String)
-- `entry_type` (String) Available values: "custom", "predefined", "integration", "exact_data".
+- `entry_type` (String) Available values: "custom", "predefined", "integration", "exact_data", "document_fingerprint".
## Import
diff --git a/docs/resources/zero_trust_gateway_settings.md b/docs/resources/zero_trust_gateway_settings.md
index 9360f68bca..69c7b8736f 100644
--- a/docs/resources/zero_trust_gateway_settings.md
+++ b/docs/resources/zero_trust_gateway_settings.md
@@ -29,9 +29,6 @@ resource "cloudflare_zero_trust_gateway_settings" "example_zero_trust_gateway_se
support_url = "support_url"
}
}
- app_control_settings = {
- enabled = false
- }
block_page = {
background_color = "background_color"
enabled = true
@@ -107,7 +104,6 @@ Optional:
- `activity_log` (Attributes) Activity log settings. (see [below for nested schema](#nestedatt--settings--activity_log))
- `antivirus` (Attributes) Anti-virus settings. (see [below for nested schema](#nestedatt--settings--antivirus))
-- `app_control_settings` (Attributes) Setting to enable App Control (see [below for nested schema](#nestedatt--settings--app_control_settings))
- `block_page` (Attributes) Block page layout settings. (see [below for nested schema](#nestedatt--settings--block_page))
- `body_scanning` (Attributes) DLP body scanning settings. (see [below for nested schema](#nestedatt--settings--body_scanning))
- `browser_isolation` (Attributes) Browser isolation settings. (see [below for nested schema](#nestedatt--settings--browser_isolation))
@@ -150,14 +146,6 @@ Optional:
-
-### Nested Schema for `settings.app_control_settings`
-
-Optional:
-
-- `enabled` (Boolean) Enable App Control
-
-
### Nested Schema for `settings.block_page`
diff --git a/docs/resources/zero_trust_list.md b/docs/resources/zero_trust_list.md
index 673c738349..c68dae0406 100644
--- a/docs/resources/zero_trust_list.md
+++ b/docs/resources/zero_trust_list.md
@@ -37,7 +37,7 @@ Available values: "SERIAL", "URL", "DOMAIN", "EMAIL", "IP".
### Optional
- `description` (String) The description of the list.
-- `items` (Attributes List) The items in the list. (see [below for nested schema](#nestedatt--items))
+- `items` (Attributes List) items to add to the list. (see [below for nested schema](#nestedatt--items))
### Read-Only
@@ -54,10 +54,6 @@ Optional:
- `description` (String) The description of the list item, if present
- `value` (String) The value of the item in a list.
-Read-Only:
-
-- `created_at` (String)
-
## Import
Import is supported using the following syntax:
diff --git a/examples/data-sources/cloudflare_user_agent_blocking_rules/data-source.tf b/examples/data-sources/cloudflare_user_agent_blocking_rules/data-source.tf
index bc748a5e59..3dbee13a7b 100644
--- a/examples/data-sources/cloudflare_user_agent_blocking_rules/data-source.tf
+++ b/examples/data-sources/cloudflare_user_agent_blocking_rules/data-source.tf
@@ -1,6 +1,6 @@
data "cloudflare_user_agent_blocking_rules" "example_user_agent_blocking_rules" {
zone_id = "023e105f4ecef8ad9ca31a8372d0c353"
description = "abusive"
- description_search = "abusive"
- ua_search = "Safari"
+ paused = false
+ user_agent = "Safari"
}
diff --git a/examples/data-sources/cloudflare_zero_trust_tunnel_warp_connector/data-source.tf b/examples/data-sources/cloudflare_zero_trust_tunnel_warp_connector/data-source.tf
new file mode 100644
index 0000000000..2a15409e6e
--- /dev/null
+++ b/examples/data-sources/cloudflare_zero_trust_tunnel_warp_connector/data-source.tf
@@ -0,0 +1,4 @@
+data "cloudflare_zero_trust_tunnel_warp_connector" "example_zero_trust_tunnel_warp_connector" {
+ account_id = "699d98642c564d2e855e9661899b7252"
+ tunnel_id = "f70ff985-a4ef-4643-bbbc-4a0ed4fc8415"
+}
diff --git a/examples/data-sources/cloudflare_zero_trust_tunnel_warp_connectors/data-source.tf b/examples/data-sources/cloudflare_zero_trust_tunnel_warp_connectors/data-source.tf
new file mode 100644
index 0000000000..eaf356bffa
--- /dev/null
+++ b/examples/data-sources/cloudflare_zero_trust_tunnel_warp_connectors/data-source.tf
@@ -0,0 +1,12 @@
+data "cloudflare_zero_trust_tunnel_warp_connectors" "example_zero_trust_tunnel_warp_connectors" {
+ account_id = "699d98642c564d2e855e9661899b7252"
+ exclude_prefix = "vpc1-"
+ existed_at = "2019-10-12T07%3A20%3A50.52Z"
+ include_prefix = "vpc1-"
+ is_deleted = true
+ name = "blog"
+ status = "healthy"
+ uuid = "f70ff985-a4ef-4643-bbbc-4a0ed4fc8415"
+ was_active_at = "2009-11-10T23:00:00Z"
+ was_inactive_at = "2009-11-10T23:00:00Z"
+}
diff --git a/examples/resources/cloudflare_account_token/resource.tf b/examples/resources/cloudflare_account_token/resource.tf
index 3b832bb766..894d97bb2e 100644
--- a/examples/resources/cloudflare_account_token/resource.tf
+++ b/examples/resources/cloudflare_account_token/resource.tf
@@ -17,8 +17,7 @@ resource "cloudflare_account_token" "example_account_token" {
}
}]
resources = {
- "com.cloudflare.api.account.zone.22b1de5f1c0e4b3ea97bb1e963b06a43" = "*"
- "com.cloudflare.api.account.zone.eb78d65290b24279ba6f44721b3ea3c4" = "*"
+ foo = "string"
}
}]
condition = {
diff --git a/examples/resources/cloudflare_api_token/resource.tf b/examples/resources/cloudflare_api_token/resource.tf
index 60b090078e..8886b0b8ca 100644
--- a/examples/resources/cloudflare_api_token/resource.tf
+++ b/examples/resources/cloudflare_api_token/resource.tf
@@ -16,8 +16,7 @@ resource "cloudflare_api_token" "example_api_token" {
}
}]
resources = {
- "com.cloudflare.api.account.zone.22b1de5f1c0e4b3ea97bb1e963b06a43" = "*"
- "com.cloudflare.api.account.zone.eb78d65290b24279ba6f44721b3ea3c4" = "*"
+ foo = "string"
}
}]
condition = {
diff --git a/examples/resources/cloudflare_image/resource.tf b/examples/resources/cloudflare_image/resource.tf
index aa3749a6e6..8486556b5e 100644
--- a/examples/resources/cloudflare_image/resource.tf
+++ b/examples/resources/cloudflare_image/resource.tf
@@ -1,8 +1,7 @@
resource "cloudflare_image" "example_image" {
account_id = "023e105f4ecef8ad9ca31a8372d0c353"
- file = {
-
- }
+ id = "id"
+ file = null
metadata = {
}
diff --git a/examples/resources/cloudflare_logpull_retention/import.sh b/examples/resources/cloudflare_logpull_retention/import.sh
new file mode 100755
index 0000000000..e3e1e65882
--- /dev/null
+++ b/examples/resources/cloudflare_logpull_retention/import.sh
@@ -0,0 +1 @@
+$ terraform import cloudflare_logpull_retention.example ''
diff --git a/examples/resources/cloudflare_logpush_job/resource.tf b/examples/resources/cloudflare_logpush_job/resource.tf
index db27efd0b9..b9c3dbe6a1 100644
--- a/examples/resources/cloudflare_logpush_job/resource.tf
+++ b/examples/resources/cloudflare_logpush_job/resource.tf
@@ -5,7 +5,7 @@ resource "cloudflare_logpush_job" "example_logpush_job" {
enabled = false
filter = "{\"where\":{\"and\":[{\"key\":\"ClientRequestPath\",\"operator\":\"contains\",\"value\":\"/static\"},{\"key\":\"ClientRequestHost\",\"operator\":\"eq\",\"value\":\"example.com\"}]}}"
frequency = "high"
- kind = "edge"
+ kind = ""
logpull_options = "fields=RayID,ClientIP,EdgeStartTimestamp×tamps=rfc3339"
max_upload_bytes = 5000000
max_upload_interval_seconds = 30
diff --git a/examples/resources/cloudflare_ruleset/resource.tf b/examples/resources/cloudflare_ruleset/resource.tf
index c255defc5a..7378c2d833 100644
--- a/examples/resources/cloudflare_ruleset/resource.tf
+++ b/examples/resources/cloudflare_ruleset/resource.tf
@@ -5,7 +5,6 @@ resource "cloudflare_ruleset" "example_ruleset" {
zone_id = "zone_id"
description = "My ruleset to execute managed rulesets"
rules = [{
- id = "3a03d665bac047339bb530ecb439a90d"
action = "block"
action_parameters = {
response = {
diff --git a/examples/resources/cloudflare_user_agent_blocking_rule/import.sh b/examples/resources/cloudflare_user_agent_blocking_rule/import.sh
new file mode 100755
index 0000000000..af5b863405
--- /dev/null
+++ b/examples/resources/cloudflare_user_agent_blocking_rule/import.sh
@@ -0,0 +1 @@
+$ terraform import cloudflare_user_agent_blocking_rule.example '/'
diff --git a/examples/resources/cloudflare_user_agent_blocking_rule/resource.tf b/examples/resources/cloudflare_user_agent_blocking_rule/resource.tf
index 186b6bc4b7..a692ceb9a7 100644
--- a/examples/resources/cloudflare_user_agent_blocking_rule/resource.tf
+++ b/examples/resources/cloudflare_user_agent_blocking_rule/resource.tf
@@ -5,4 +5,6 @@ resource "cloudflare_user_agent_blocking_rule" "example_user_agent_blocking_rule
value = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)"
}
mode = "challenge"
+ description = "Prevent multiple login failures to mitigate brute force attacks"
+ paused = false
}
diff --git a/examples/resources/cloudflare_workers_kv/resource.tf b/examples/resources/cloudflare_workers_kv/resource.tf
index 0747967d69..7c9bfa2900 100644
--- a/examples/resources/cloudflare_workers_kv/resource.tf
+++ b/examples/resources/cloudflare_workers_kv/resource.tf
@@ -2,6 +2,8 @@ resource "cloudflare_workers_kv" "example_workers_kv" {
account_id = "023e105f4ecef8ad9ca31a8372d0c353"
namespace_id = "0f2ac74b498b48028cb68387c421e279"
key_name = "My-Key"
- metadata = "{\"someMetadataKey\": \"someMetadataValue\"}"
value = "Some Value"
+ metadata = {
+
+ }
}
diff --git a/examples/resources/cloudflare_zero_trust_gateway_settings/resource.tf b/examples/resources/cloudflare_zero_trust_gateway_settings/resource.tf
index 148f41f287..1bff7690d5 100644
--- a/examples/resources/cloudflare_zero_trust_gateway_settings/resource.tf
+++ b/examples/resources/cloudflare_zero_trust_gateway_settings/resource.tf
@@ -15,9 +15,6 @@ resource "cloudflare_zero_trust_gateway_settings" "example_zero_trust_gateway_se
support_url = "support_url"
}
}
- app_control_settings = {
- enabled = false
- }
block_page = {
background_color = "background_color"
enabled = true
diff --git a/examples/resources/cloudflare_zero_trust_tunnel_warp_connector/import.sh b/examples/resources/cloudflare_zero_trust_tunnel_warp_connector/import.sh
new file mode 100755
index 0000000000..cf3b69b07d
--- /dev/null
+++ b/examples/resources/cloudflare_zero_trust_tunnel_warp_connector/import.sh
@@ -0,0 +1 @@
+$ terraform import cloudflare_zero_trust_tunnel_warp_connector.example '/'
diff --git a/examples/resources/cloudflare_zero_trust_tunnel_warp_connector/resource.tf b/examples/resources/cloudflare_zero_trust_tunnel_warp_connector/resource.tf
new file mode 100644
index 0000000000..da85cff42c
--- /dev/null
+++ b/examples/resources/cloudflare_zero_trust_tunnel_warp_connector/resource.tf
@@ -0,0 +1,4 @@
+resource "cloudflare_zero_trust_tunnel_warp_connector" "example_zero_trust_tunnel_warp_connector" {
+ account_id = "699d98642c564d2e855e9661899b7252"
+ name = "blog"
+}
diff --git a/internal/apijson/decoder.go b/internal/apijson/decoder.go
index 12ac2da30c..ca4124eaf3 100644
--- a/internal/apijson/decoder.go
+++ b/internal/apijson/decoder.go
@@ -758,9 +758,10 @@ func (d *decoderBuilder) newTerraformTypeDecoder(t reflect.Type) decoderFunc {
}
existingObjectListValue := value.Interface().(customfield.NestedObjectListLike)
if node.Type == gjson.Null {
- if b == Always {
+ if b == Always || existingObjectListValue.IsNullOrUnknown() {
nullValue := existingObjectListValue.NullValue(ctx)
value.Set(reflect.ValueOf(nullValue))
+ return nil
}
}
diff --git a/internal/apijson/json_test.go b/internal/apijson/json_test.go
index b6ccee27b7..7f2b58dece 100644
--- a/internal/apijson/json_test.go
+++ b/internal/apijson/json_test.go
@@ -1366,6 +1366,51 @@ func TestDecodeFromValue(t *testing.T) {
}
}
+var decode_unset_tests = map[string]struct {
+ buf string
+ val interface{}
+}{
+ "nested_object_list_is_omitted_null": {
+ `{}`,
+ ListWithNestedObj{
+ A: customfield.NullObjectList[Embedded2](ctx),
+ },
+ },
+ "nested_object_list_is_explicit_null": {
+ `{"a": null}`,
+ ListWithNestedObj{
+ A: customfield.NullObjectList[Embedded2](ctx),
+ },
+ },
+ "nested_object_list_is_empty": {
+ `{"a": []}`,
+ ListWithNestedObj{
+ A: customfield.NewObjectListMust(ctx, []Embedded2{}),
+ },
+ },
+}
+
+func TestDecodeUnsetBehaviour(t *testing.T) {
+ spew.Config.SortKeys = true
+ for name, test := range merge(decode_unset_tests) {
+ t.Run(name, func(t *testing.T) {
+ resultValue := reflect.New(reflect.TypeOf(test.val))
+ d := &decoderBuilder{
+ dateFormat: time.RFC3339,
+ unmarshalComputedOnly: false,
+ updateBehavior: IfUnset,
+ }
+ if err := d.unmarshal([]byte(test.buf), resultValue.Interface()); err != nil {
+ t.Fatalf("deserialization of %v failed with error %v", resultValue, err)
+ }
+ result := resultValue.Elem().Interface()
+ if !reflect.DeepEqual(result, test.val) {
+ t.Fatalf("incorrect deserialization for '%s':\nexpected:\n%s\nactual:\n%s\n", test.buf, spew.Sdump(test.val), spew.Sdump(result))
+ }
+ })
+ }
+}
+
type StructWithComputedFields struct {
RegStr types.String `tfsdk:"str" json:"str,optional"`
CompStr types.String `tfsdk:"comp_str" json:"comp_str,computed"`
diff --git a/internal/customfield/object_list.go b/internal/customfield/object_list.go
index 65f9852495..d27c02656c 100644
--- a/internal/customfield/object_list.go
+++ b/internal/customfield/object_list.go
@@ -108,6 +108,7 @@ type NestedObjectListLike interface {
NullValue(ctx context.Context) NestedObjectListLike
UnknownValue(ctx context.Context) NestedObjectListLike
KnownValue(ctx context.Context, T any) NestedObjectListLike
+ IsNullOrUnknown() bool
}
var _ NestedObjectListLike = (*NestedObjectList[basetypes.StringValue])(nil)
@@ -143,6 +144,10 @@ func (v NestedObjectList[T]) KnownValue(ctx context.Context, anyValues any) Nest
return r
}
+func (v NestedObjectList[T]) IsNullOrUnknown() bool {
+ return v.IsNull() || v.IsUnknown()
+}
+
func (v NestedObjectList[T]) Equal(o attr.Value) bool {
other, ok := o.(NestedObjectList[T])
if !ok {
diff --git a/internal/customfield/object_set.go b/internal/customfield/object_set.go
index d0a02dace6..f6d6e5a27f 100644
--- a/internal/customfield/object_set.go
+++ b/internal/customfield/object_set.go
@@ -131,6 +131,10 @@ func (v NestedObjectSet[T]) KnownValue(ctx context.Context, anyValues any) Neste
return r
}
+func (v NestedObjectSet[T]) IsNullOrUnknown() bool {
+ return v.IsNull() || v.IsUnknown()
+}
+
func (v NestedObjectSet[T]) Equal(o attr.Value) bool {
other, ok := o.(NestedObjectSet[T])
if !ok {
diff --git a/internal/customvalidator/object_size_at_most_validator.go b/internal/customvalidator/object_size_at_most_validator.go
new file mode 100644
index 0000000000..23eef366f5
--- /dev/null
+++ b/internal/customvalidator/object_size_at_most_validator.go
@@ -0,0 +1,53 @@
+package customvalidator
+
+import (
+ "context"
+ "fmt"
+ "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+)
+
+type objectSizeAtMostValidator struct {
+ max int
+}
+
+func (v objectSizeAtMostValidator) Description(_ context.Context) string {
+ return fmt.Sprintf("must contain at most %d elements", v.max)
+}
+
+func (v objectSizeAtMostValidator) MarkdownDescription(ctx context.Context) string {
+ return v.Description(ctx)
+}
+
+func (v objectSizeAtMostValidator) ValidateObject(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) {
+ if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
+ return
+ }
+
+ nonNullCount := 0
+ for _, attr := range req.ConfigValue.Attributes() {
+ if !attr.IsNull() && !attr.IsUnknown() {
+ nonNullCount++
+ }
+ }
+ if nonNullCount > v.max {
+ resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic(
+ req.Path,
+ v.Description(ctx),
+ fmt.Sprintf("%d", nonNullCount),
+ ))
+ }
+}
+
+// ObjectSizeAtMost returns an AttributeValidator which ensures that any configured
+// attribute or function parameter value:
+//
+// - Is an object.
+// - Contains at most max elements.
+//
+// Null (unconfigured) and unknown (known after apply) values are skipped.
+func ObjectSizeAtMost(maxVal int) objectSizeAtMostValidator {
+ return objectSizeAtMostValidator{
+ max: maxVal,
+ }
+}
diff --git a/internal/customvalidator/required_when_other_attribute_is_one_of_validator.go b/internal/customvalidator/required_when_other_attribute_is_one_of_validator.go
new file mode 100644
index 0000000000..4d1413001a
--- /dev/null
+++ b/internal/customvalidator/required_when_other_attribute_is_one_of_validator.go
@@ -0,0 +1,86 @@
+package customvalidator
+
+import (
+ "context"
+ "fmt"
+ "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/tfsdk"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "strings"
+)
+
+func RequiredWhenOtherStringIsOneOf(pathExpr path.Expression, wantStrValues ...string) requiredWhenOtherAttributeIsOneOfValidator {
+ var wantValues []attr.Value
+ for _, v := range wantStrValues {
+ wantValues = append(wantValues, types.StringValue(v))
+ }
+ return requiredWhenOtherAttributeIsOneOfValidator{
+ pathExpr,
+ wantValues,
+ }
+}
+
+type requiredWhenOtherAttributeIsOneOfValidator struct {
+ pathExpr path.Expression
+ wantValues []attr.Value
+}
+
+func (i requiredWhenOtherAttributeIsOneOfValidator) Description(ctx context.Context) string {
+ var wantValuesAsStrings []string
+ for _, v := range i.wantValues {
+ wantValuesAsStrings = append(wantValuesAsStrings, v.String())
+ }
+ return fmt.Sprintf("has to be set if %q is one of: %s", i.pathExpr, strings.Join(wantValuesAsStrings, ", "))
+}
+
+func (i requiredWhenOtherAttributeIsOneOfValidator) MarkdownDescription(ctx context.Context) string {
+ return i.Description(ctx)
+}
+
+func (i requiredWhenOtherAttributeIsOneOfValidator) validateAny(ctx context.Context, cfg *tfsdk.Config, attrPath path.Path, value attr.Value, resDiagnostics *diag.Diagnostics) {
+ if !value.IsNull() || value.IsUnknown() {
+ return
+ }
+
+ matchedPaths, diags := cfg.PathMatches(ctx, i.pathExpr)
+ resDiagnostics.Append(diags...)
+
+ for _, mp := range matchedPaths {
+ // If the user specifies the same attribute this validator is applied to,
+ // also as part of the input, skip it
+ if mp.Equal(attrPath) {
+ continue
+ }
+
+ var mpVal attr.Value
+ resDiagnostics.Append(cfg.GetAttribute(ctx, mp, &mpVal)...)
+
+ // Collect all errors
+ if diags.HasError() {
+ continue
+ }
+
+ // Delay validation until all involved attribute have a known value
+ if mpVal.IsUnknown() {
+ return
+ }
+
+ for _, wantValue := range i.wantValues {
+ if mpVal.Equal(wantValue) {
+ description := fmt.Sprintf("%q %s", attrPath, i.Description(ctx))
+ resDiagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic(
+ attrPath,
+ description,
+ ))
+ }
+ }
+ }
+}
+
+func (i requiredWhenOtherAttributeIsOneOfValidator) ValidateObject(ctx context.Context, req validator.ObjectRequest, res *validator.ObjectResponse) {
+ i.validateAny(ctx, &req.Config, req.Path, req.ConfigValue, &res.Diagnostics)
+}
diff --git a/internal/customvalidator/requires_other_attribute_to_be_one_of_validator.go b/internal/customvalidator/requires_other_attribute_to_be_one_of_validator.go
new file mode 100644
index 0000000000..bf9dbeae7d
--- /dev/null
+++ b/internal/customvalidator/requires_other_attribute_to_be_one_of_validator.go
@@ -0,0 +1,110 @@
+package customvalidator
+
+import (
+ "context"
+ "fmt"
+ "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/tfsdk"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "strings"
+)
+
+func RequiresOtherStringAttributeToBeOneOf(pathExpr path.Expression, wantStrValues ...string) requiresOtherAttributeToBeOneOfValidator {
+ var wantValues []attr.Value
+ for _, v := range wantStrValues {
+ wantValues = append(wantValues, types.StringValue(v))
+ }
+ return requiresOtherAttributeToBeOneOfValidator{
+ pathExpr,
+ wantValues,
+ }
+}
+
+func RequiresOtherStringAttributeToNullOrBeOneOf(pathExpr path.Expression, wantStrValues ...string) requiresOtherAttributeToBeOneOfValidator {
+ v := RequiresOtherStringAttributeToBeOneOf(pathExpr, wantStrValues...)
+ v.wantValues = append(v.wantValues, types.StringNull())
+ return v
+}
+
+type requiresOtherAttributeToBeOneOfValidator struct {
+ pathExpr path.Expression
+ wantValues []attr.Value
+}
+
+func (i requiresOtherAttributeToBeOneOfValidator) Description(ctx context.Context) string {
+ var wantValuesAsStrings []string
+ for _, v := range i.wantValues {
+ wantValuesAsStrings = append(wantValuesAsStrings, v.String())
+ }
+ return fmt.Sprintf("can only be set if %q is one of: %s", i.pathExpr, strings.Join(wantValuesAsStrings, ", "))
+}
+
+func (i requiresOtherAttributeToBeOneOfValidator) MarkdownDescription(ctx context.Context) string {
+ return i.Description(ctx)
+}
+
+func (i requiresOtherAttributeToBeOneOfValidator) validateAny(ctx context.Context, cfg *tfsdk.Config, attrPath path.Path, value attr.Value, resDiagnostics *diag.Diagnostics) {
+ if value.IsNull() || value.IsUnknown() {
+ return
+ }
+
+ matchedPaths, diags := cfg.PathMatches(ctx, i.pathExpr)
+ resDiagnostics.Append(diags...)
+
+ for _, mp := range matchedPaths {
+ // If the user specifies the same attribute this validator is applied to,
+ // also as part of the input, skip it
+ if mp.Equal(attrPath) {
+ continue
+ }
+
+ var mpVal attr.Value
+ resDiagnostics.Append(cfg.GetAttribute(ctx, mp, &mpVal)...)
+
+ // Collect all errors
+ if diags.HasError() {
+ continue
+ }
+
+ // Delay validation until all involved attribute have a known value
+ if mpVal.IsUnknown() {
+ return
+ }
+
+ foundMatch := false
+ for _, wantValue := range i.wantValues {
+ if mpVal.Equal(wantValue) {
+ foundMatch = true
+ break
+ }
+ }
+
+ if !foundMatch {
+ description := fmt.Sprintf("%q %s", attrPath, i.Description(ctx))
+ resDiagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic(
+ attrPath,
+ description,
+ ))
+ }
+ }
+}
+
+func (i requiresOtherAttributeToBeOneOfValidator) ValidateBool(ctx context.Context, req validator.BoolRequest, res *validator.BoolResponse) {
+ i.validateAny(ctx, &req.Config, req.Path, req.ConfigValue, &res.Diagnostics)
+}
+
+func (i requiresOtherAttributeToBeOneOfValidator) ValidateString(ctx context.Context, req validator.StringRequest, res *validator.StringResponse) {
+ i.validateAny(ctx, &req.Config, req.Path, req.ConfigValue, &res.Diagnostics)
+}
+
+func (i requiresOtherAttributeToBeOneOfValidator) ValidateObject(ctx context.Context, req validator.ObjectRequest, res *validator.ObjectResponse) {
+ i.validateAny(ctx, &req.Config, req.Path, req.ConfigValue, &res.Diagnostics)
+}
+
+func (i requiresOtherAttributeToBeOneOfValidator) ValidateList(ctx context.Context, req validator.ListRequest, res *validator.ListResponse) {
+ i.validateAny(ctx, &req.Config, req.Path, req.ConfigValue, &res.Diagnostics)
+}
diff --git a/internal/provider.go b/internal/provider.go
index 97267c1a87..08d1dd396d 100644
--- a/internal/provider.go
+++ b/internal/provider.go
@@ -207,6 +207,7 @@ import (
"github.com/cloudflare/terraform-provider-cloudflare/internal/services/zero_trust_tunnel_cloudflared_route"
"github.com/cloudflare/terraform-provider-cloudflare/internal/services/zero_trust_tunnel_cloudflared_token"
"github.com/cloudflare/terraform-provider-cloudflare/internal/services/zero_trust_tunnel_cloudflared_virtual_network"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/services/zero_trust_tunnel_warp_connector"
"github.com/cloudflare/terraform-provider-cloudflare/internal/services/zero_trust_tunnel_warp_connector_token"
"github.com/cloudflare/terraform-provider-cloudflare/internal/services/zone"
"github.com/cloudflare/terraform-provider-cloudflare/internal/services/zone_cache_reserve"
@@ -535,6 +536,7 @@ func (p *CloudflareProvider) Resources(ctx context.Context) []func() resource.Re
zero_trust_access_policy.NewResource,
zero_trust_tunnel_cloudflared.NewResource,
zero_trust_tunnel_cloudflared_config.NewResource,
+ zero_trust_tunnel_warp_connector.NewResource,
zero_trust_dlp_dataset.NewResource,
zero_trust_dlp_custom_profile.NewResource,
zero_trust_dlp_predefined_profile.NewResource,
@@ -824,6 +826,8 @@ func (p *CloudflareProvider) DataSources(ctx context.Context) []func() datasourc
zero_trust_tunnel_cloudflared.NewZeroTrustTunnelCloudflaredsDataSource,
zero_trust_tunnel_cloudflared_config.NewZeroTrustTunnelCloudflaredConfigDataSource,
zero_trust_tunnel_cloudflared_token.NewZeroTrustTunnelCloudflaredTokenDataSource,
+ zero_trust_tunnel_warp_connector.NewZeroTrustTunnelWARPConnectorDataSource,
+ zero_trust_tunnel_warp_connector.NewZeroTrustTunnelWARPConnectorsDataSource,
zero_trust_tunnel_warp_connector_token.NewZeroTrustTunnelWARPConnectorTokenDataSource,
zero_trust_dlp_dataset.NewZeroTrustDLPDatasetDataSource,
zero_trust_dlp_dataset.NewZeroTrustDLPDatasetsDataSource,
diff --git a/internal/services/account_token/resource_test.go b/internal/services/account_token/resource_test.go
new file mode 100644
index 0000000000..1e6c8c2382
--- /dev/null
+++ b/internal/services/account_token/resource_test.go
@@ -0,0 +1,208 @@
+package account_token_test
+
+import (
+ "os"
+ "testing"
+
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/acctest"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/utils"
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/hashicorp/terraform-plugin-testing/plancheck"
+)
+
+func TestAccAccountToken_Basic(t *testing.T) {
+ rnd := utils.GenerateRandomResourceName()
+ resourceID := "cloudflare_account_token." + rnd
+ accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")
+ permissionID := "82e64a83756745bbbb1c9c2701bf816b" // DNS read
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccCloudflareAccountTokenWithoutCondition(rnd, accountID, rnd, permissionID),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(resourceID, "name", rnd),
+ resource.TestCheckResourceAttr(resourceID, "policies.0.permission_groups.0.id", permissionID),
+ ),
+ },
+ {
+ Config: testAccCloudflareAccountTokenWithoutCondition(rnd, accountID, rnd+"-updated", permissionID),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(resourceID, "name", rnd+"-updated"),
+ resource.TestCheckResourceAttr(resourceID, "policies.0.permission_groups.0.id", permissionID),
+ ),
+ },
+ },
+ })
+}
+
+func TestAccAccountToken_DoesNotSetConditions(t *testing.T) {
+ rnd := utils.GenerateRandomResourceName()
+ name := "cloudflare_account_token." + rnd
+ accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")
+ permissionID := "82e64a83756745bbbb1c9c2701bf816b" // DNS read
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccCloudflareAccountTokenWithoutCondition(rnd, accountID, rnd, permissionID),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(name, "name", rnd),
+ resource.TestCheckNoResourceAttr(name, "condition.request_ip.0.in"),
+ resource.TestCheckNoResourceAttr(name, "condition.request_ip.0.not_in"),
+ ),
+ },
+ },
+ })
+}
+
+func testAccCloudflareAccountTokenWithoutCondition(resourceName, accountId, rnd, permissionID string) string {
+ return acctest.LoadTestCase("account_token-without-condition.tf", resourceName, accountId, rnd, permissionID)
+}
+
+func TestAccAccountToken_SetIndividualCondition(t *testing.T) {
+ rnd := utils.GenerateRandomResourceName()
+ name := "cloudflare_account_token." + rnd
+ accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")
+ permissionID := "82e64a83756745bbbb1c9c2701bf816b" // DNS read
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccCloudflareAccountTokenWithIndividualCondition(rnd, accountID, permissionID),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(name, "name", rnd),
+ resource.TestCheckResourceAttr(name, "condition.request_ip.in.0", "192.0.2.1/32"),
+ resource.TestCheckNoResourceAttr(name, "condition.request_ip.not_in"),
+ ),
+ },
+ },
+ })
+}
+
+func testAccCloudflareAccountTokenWithIndividualCondition(rnd, accountID, permissionID string) string {
+ return acctest.LoadTestCase("account_token-with-individual-condition.tf", rnd, accountID, permissionID)
+}
+
+func TestAccAccountToken_SetAllCondition(t *testing.T) {
+ rnd := utils.GenerateRandomResourceName()
+ name := "cloudflare_account_token." + rnd
+ accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")
+ permissionID := "82e64a83756745bbbb1c9c2701bf816b" // DNS read
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccCloudflareAccountTokenWithAllCondition(rnd, accountID, permissionID),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(name, "name", rnd),
+ resource.TestCheckResourceAttr(name, "condition.request_ip.in.0", "192.0.2.1/32"),
+ resource.TestCheckResourceAttr(name, "condition.request_ip.not_in.0", "198.51.100.1/32"),
+ ),
+ },
+ },
+ })
+}
+
+func testAccCloudflareAccountTokenWithAllCondition(rnd, accountID, permissionID string) string {
+ return acctest.LoadTestCase("account_token-with-all-condition.tf", rnd, accountID, permissionID)
+}
+
+func TestAccAccountToken_TokenTTL(t *testing.T) {
+ rnd := utils.GenerateRandomResourceName()
+ name := "cloudflare_account_token." + rnd
+ accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")
+ permissionID := "82e64a83756745bbbb1c9c2701bf816b" // DNS read
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccCloudflareAccountTokenWithTTL(rnd, accountID, permissionID),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(name, "name", rnd),
+ resource.TestCheckResourceAttr(name, "not_before", "2018-07-01T05:20:00Z"),
+ resource.TestCheckResourceAttr(name, "expires_on", "2032-01-01T00:00:00Z"),
+ ),
+ },
+ },
+ })
+}
+
+func testAccCloudflareAccountTokenWithTTL(rnd, accountID, permissionID string) string {
+ return acctest.LoadTestCase("account_token-with-ttl.tf", rnd, accountID, permissionID)
+}
+
+func TestAccAccountToken_PermissionGroupOrder(t *testing.T) {
+ rnd := utils.GenerateRandomResourceName()
+ name := "cloudflare_account_token." + rnd
+ accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")
+ permissionID1 := "82e64a83756745bbbb1c9c2701bf816b" // DNS read
+ permissionID2 := "e199d584e69344eba202452019deafe3" // Disable ESC read
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: acctest.LoadTestCase("account_token-permissiongroup-order.tf", rnd, accountID, permissionID1, permissionID2),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(name, "name", rnd),
+ resource.TestCheckResourceAttr(name, "policies.0.permission_groups.0.id", permissionID1),
+ resource.TestCheckResourceAttr(name, "policies.0.permission_groups.1.id", permissionID2),
+ ),
+ },
+ {
+ Config: acctest.LoadTestCase("account_token-permissiongroup-order.tf", rnd, accountID, permissionID2, permissionID1),
+ // changing the order of permission groups should not affect plan
+ ConfigPlanChecks: resource.ConfigPlanChecks{
+ PreApply: []plancheck.PlanCheck{
+ plancheck.ExpectEmptyPlan(),
+ },
+ },
+ },
+ },
+ })
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: acctest.LoadTestCase("account_token-permissiongroup-order.tf", rnd, accountID, permissionID2, permissionID1),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(name, "name", rnd),
+ resource.TestCheckResourceAttr(name, "policies.0.permission_groups.0.id", permissionID1),
+ resource.TestCheckResourceAttr(name, "policies.0.permission_groups.1.id", permissionID2),
+ ),
+ },
+ {
+ Config: acctest.LoadTestCase("account_token-permissiongroup-order.tf", rnd, accountID, permissionID2, permissionID1),
+ // re-applying same change does not produce drift
+ ConfigPlanChecks: resource.ConfigPlanChecks{
+ PreApply: []plancheck.PlanCheck{
+ plancheck.ExpectEmptyPlan(),
+ },
+ },
+ },
+ {
+ Config: acctest.LoadTestCase("account_token-permissiongroup-order.tf", rnd, accountID, permissionID1, permissionID2),
+ // changing the order of permission groups should not affect plan
+ ConfigPlanChecks: resource.ConfigPlanChecks{
+ PreApply: []plancheck.PlanCheck{
+ plancheck.ExpectEmptyPlan(),
+ },
+ },
+ },
+ },
+ })
+}
diff --git a/internal/services/account_token/schema.go b/internal/services/account_token/schema.go
index a765151b8b..9f33904c06 100644
--- a/internal/services/account_token/schema.go
+++ b/internal/services/account_token/schema.go
@@ -35,7 +35,7 @@ func ResourceSchema(ctx context.Context) schema.Schema {
Description: "Token name.",
Required: true,
},
- "policies": schema.SetNestedAttribute{
+ "policies": schema.ListNestedAttribute{
Description: "List of access policies assigned to the token.",
Required: true,
NestedObject: schema.NestedAttributeObject{
@@ -51,7 +51,7 @@ func ResourceSchema(ctx context.Context) schema.Schema {
stringvalidator.OneOfCaseInsensitive("allow", "deny"),
},
},
- "permission_groups": schema.ListNestedAttribute{
+ "permission_groups": schema.SetNestedAttribute{
Description: "A set of permission groups that are specified to the policy.",
Required: true,
NestedObject: schema.NestedAttributeObject{
diff --git a/internal/services/account_token/testdata/account_token-permissiongroup-order.tf b/internal/services/account_token/testdata/account_token-permissiongroup-order.tf
new file mode 100644
index 0000000000..2dc46b6590
--- /dev/null
+++ b/internal/services/account_token/testdata/account_token-permissiongroup-order.tf
@@ -0,0 +1,22 @@
+resource "cloudflare_account_token" "%[1]s" {
+ name = "%[1]s"
+ account_id = "%[2]s"
+
+ policies = [{
+ effect = "allow"
+ permission_groups = [{
+ id = "%[3]s"
+ },{
+ id = "%[4]s"
+ }]
+ resources = {
+ "com.cloudflare.api.account.%[2]s" = "*"
+ }
+ }]
+}
+
+data "cloudflare_account_token" "%[1]s" {
+ account_id = "%[2]s"
+ token_id = cloudflare_account_token.%[1]s.id
+ depends_on = [cloudflare_account_token.%[1]s]
+}
\ No newline at end of file
diff --git a/internal/services/account_token/testdata/account_token-with-all-condition.tf b/internal/services/account_token/testdata/account_token-with-all-condition.tf
new file mode 100644
index 0000000000..22a9cb57de
--- /dev/null
+++ b/internal/services/account_token/testdata/account_token-with-all-condition.tf
@@ -0,0 +1,22 @@
+
+resource "cloudflare_account_token" "%[1]s" {
+ name = "%[1]s"
+ account_id = "%[2]s"
+
+ policies = [{
+ effect = "allow"
+ permission_groups = [{
+ id = "%[3]s"
+ }]
+ resources = {
+ "com.cloudflare.api.account.%[2]s" = "*"
+ }
+ }]
+
+ condition = {
+ request_ip = {
+ in = ["192.0.2.1/32"]
+ not_in = ["198.51.100.1/32"]
+ }
+ }
+}
diff --git a/internal/services/account_token/testdata/account_token-with-individual-condition.tf b/internal/services/account_token/testdata/account_token-with-individual-condition.tf
new file mode 100644
index 0000000000..12a737dc3f
--- /dev/null
+++ b/internal/services/account_token/testdata/account_token-with-individual-condition.tf
@@ -0,0 +1,20 @@
+resource "cloudflare_account_token" "%[1]s" {
+ name = "%[1]s"
+ account_id = "%[2]s"
+
+ policies = [{
+ effect = "allow"
+ permission_groups = [{
+ id = "%[3]s"
+ }]
+ resources = {
+ "com.cloudflare.api.account.%[2]s" = "*"
+ }
+ }]
+
+ condition = {
+ request_ip = {
+ in = ["192.0.2.1/32"]
+ }
+ }
+}
diff --git a/internal/services/account_token/testdata/account_token-with-ttl.tf b/internal/services/account_token/testdata/account_token-with-ttl.tf
new file mode 100644
index 0000000000..91f3de5a62
--- /dev/null
+++ b/internal/services/account_token/testdata/account_token-with-ttl.tf
@@ -0,0 +1,15 @@
+resource "cloudflare_account_token" "%[1]s" {
+ name = "%[1]s"
+ account_id = "%[2]s"
+
+ policies = [{
+ effect = "allow"
+ permission_groups = [{ id = "%[3]s" }]
+ resources = {
+ "com.cloudflare.api.account.%[2]s" = "*"
+ }
+ }]
+
+ not_before = "2018-07-01T05:20:00Z"
+ expires_on = "2032-01-01T00:00:00Z"
+}
diff --git a/internal/services/account_token/testdata/account_token-without-condition.tf b/internal/services/account_token/testdata/account_token-without-condition.tf
new file mode 100644
index 0000000000..7e554f97e3
--- /dev/null
+++ b/internal/services/account_token/testdata/account_token-without-condition.tf
@@ -0,0 +1,12 @@
+resource "cloudflare_account_token" "%[1]s" {
+ name = "%[3]s"
+ account_id = "%[2]s"
+
+ policies = [{
+ effect = "allow"
+ permission_groups = [{ id = "%[4]s" }]
+ resources = {
+ "com.cloudflare.api.account.%[2]s" = "*"
+ }
+ }]
+}
diff --git a/internal/services/api_token/resource_test.go b/internal/services/api_token/resource_test.go
index ab302efde2..1c5b82ac84 100644
--- a/internal/services/api_token/resource_test.go
+++ b/internal/services/api_token/resource_test.go
@@ -1,7 +1,6 @@
package api_token_test
import (
- "os"
"testing"
"github.com/cloudflare/terraform-provider-cloudflare/internal/acctest"
@@ -10,12 +9,6 @@ import (
)
func TestAccAPIToken_Basic(t *testing.T) {
- // Temporarily unset CLOUDFLARE_API_TOKEN if it is set as the API token
- // endpoint does not yet support the API tokens without an explicit scope.
- if os.Getenv("CLOUDFLARE_API_TOKEN") != "" {
- t.Setenv("CLOUDFLARE_API_TOKEN", "")
- }
-
rnd := utils.GenerateRandomResourceName()
resourceID := "cloudflare_api_token." + rnd
permissionID := "82e64a83756745bbbb1c9c2701bf816b" // DNS read
@@ -40,41 +33,7 @@ func TestAccAPIToken_Basic(t *testing.T) {
})
}
-func TestAccAPIToken_AllowDeny(t *testing.T) {
- // Temporarily unset CLOUDFLARE_API_TOKEN if it is set as the API token
- // endpoint does not yet support the API tokens without an explicit scope.
- if os.Getenv("CLOUDFLARE_API_TOKEN") != "" {
- t.Setenv("CLOUDFLARE_API_TOKEN", "")
- }
-
- rnd := utils.GenerateRandomResourceName()
- zoneID := os.Getenv("CLOUDFLARE_ZONE_ID")
- permissionID := "82e64a83756745bbbb1c9c2701bf816b" // DNS read
-
- resource.Test(t, resource.TestCase{
- PreCheck: func() { acctest.TestAccPreCheck(t) },
- ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
- Steps: []resource.TestStep{
- {
- Config: testAPITokenConfigAllowDeny(rnd, permissionID, zoneID, false),
- },
- {
- Config: testAPITokenConfigAllowDeny(rnd, permissionID, zoneID, true),
- },
- {
- Config: testAPITokenConfigAllowDeny(rnd, permissionID, zoneID, false),
- },
- },
- })
-}
-
func TestAccAPIToken_DoesNotSetConditions(t *testing.T) {
- // Temporarily unset CLOUDFLARE_API_TOKEN if it is set as the API token
- // endpoint does not yet support the API tokens without an explicit scope.
- if os.Getenv("CLOUDFLARE_API_TOKEN") != "" {
- t.Setenv("CLOUDFLARE_API_TOKEN", "")
- }
-
rnd := utils.GenerateRandomResourceName()
name := "cloudflare_api_token." + rnd
permissionID := "82e64a83756745bbbb1c9c2701bf816b" // DNS read
@@ -87,8 +46,8 @@ func TestAccAPIToken_DoesNotSetConditions(t *testing.T) {
Config: testAccCloudflareAPITokenWithoutCondition(rnd, rnd, permissionID),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, "name", rnd),
- resource.TestCheckNoResourceAttr(name, "condition.0.request_ip.0.in"),
- resource.TestCheckNoResourceAttr(name, "condition.0.request_ip.0.not_in"),
+ resource.TestCheckNoResourceAttr(name, "condition.request_ip.0.in"),
+ resource.TestCheckNoResourceAttr(name, "condition.request_ip.0.not_in"),
),
},
},
@@ -100,14 +59,6 @@ func testAccCloudflareAPITokenWithoutCondition(resourceName, rnd, permissionID s
}
func TestAccAPIToken_SetIndividualCondition(t *testing.T) {
- acctest.TestAccSkipForDefaultZone(t, "Pending service fix to address nested object syntax as strings for `conditions`.")
-
- // Temporarily unset CLOUDFLARE_API_TOKEN if it is set as the API token
- // endpoint does not yet support the API tokens without an explicit scope.
- if os.Getenv("CLOUDFLARE_API_TOKEN") != "" {
- t.Setenv("CLOUDFLARE_API_TOKEN", "")
- }
-
rnd := utils.GenerateRandomResourceName()
name := "cloudflare_api_token." + rnd
permissionID := "82e64a83756745bbbb1c9c2701bf816b" // DNS read
@@ -120,8 +71,8 @@ func TestAccAPIToken_SetIndividualCondition(t *testing.T) {
Config: testAccCloudflareAPITokenWithIndividualCondition(rnd, permissionID),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, "name", rnd),
- resource.TestCheckResourceAttr(name, "condition.0.request_ip.0.in.0", "192.0.2.1/32"),
- resource.TestCheckNoResourceAttr(name, "condition.0.request_ip.0.not_in"),
+ resource.TestCheckResourceAttr(name, "condition.request_ip.in.0", "192.0.2.1/32"),
+ resource.TestCheckNoResourceAttr(name, "condition.request_ip.not_in"),
),
},
},
@@ -133,14 +84,6 @@ func testAccCloudflareAPITokenWithIndividualCondition(rnd string, permissionID s
}
func TestAccAPIToken_SetAllCondition(t *testing.T) {
- acctest.TestAccSkipForDefaultZone(t, "Pending service fix to address nested object syntax as strings for `conditions`.")
-
- // Temporarily unset CLOUDFLARE_API_TOKEN if it is set as the API token
- // endpoint does not yet support the API tokens without an explicit scope.
- if os.Getenv("CLOUDFLARE_API_TOKEN") != "" {
- t.Setenv("CLOUDFLARE_API_TOKEN", "")
- }
-
rnd := utils.GenerateRandomResourceName()
name := "cloudflare_api_token." + rnd
permissionID := "82e64a83756745bbbb1c9c2701bf816b" // DNS read
@@ -153,8 +96,8 @@ func TestAccAPIToken_SetAllCondition(t *testing.T) {
Config: testAccCloudflareAPITokenWithAllCondition(rnd, permissionID),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, "name", rnd),
- resource.TestCheckResourceAttr(name, "condition.0.request_ip.0.in.0", "192.0.2.1/32"),
- resource.TestCheckResourceAttr(name, "condition.0.request_ip.0.not_in.0", "198.51.100.1/32"),
+ resource.TestCheckResourceAttr(name, "condition.request_ip.in.0", "192.0.2.1/32"),
+ resource.TestCheckResourceAttr(name, "condition.request_ip.not_in.0", "198.51.100.1/32"),
),
},
},
@@ -165,22 +108,7 @@ func testAccCloudflareAPITokenWithAllCondition(rnd string, permissionID string)
return acctest.LoadTestCase("apitokenwithallcondition.tf", rnd, permissionID)
}
-func testAPITokenConfigAllowDeny(resourceID, permissionID, zoneID string, allowAllZonesExceptOne bool) string {
- var add string
- if allowAllZonesExceptOne {
- add = acctest.LoadTestCase("apitokenconfigallowdeny.tf", permissionID, zoneID)
- }
-
- return acctest.LoadTestCase("apitokenconfigallowdeny.tf", resourceID, permissionID, add)
-}
-
func TestAccAPIToken_TokenTTL(t *testing.T) {
- // Temporarily unset CLOUDFLARE_API_TOKEN if it is set as the API token
- // endpoint does not yet support the API tokens without an explicit scope.
- if os.Getenv("CLOUDFLARE_API_TOKEN") != "" {
- t.Setenv("CLOUDFLARE_API_TOKEN", "")
- }
-
rnd := utils.GenerateRandomResourceName()
name := "cloudflare_api_token." + rnd
permissionID := "82e64a83756745bbbb1c9c2701bf816b" // DNS read
diff --git a/internal/services/api_token/schema.go b/internal/services/api_token/schema.go
index 8ae1089fa1..b791033bd9 100644
--- a/internal/services/api_token/schema.go
+++ b/internal/services/api_token/schema.go
@@ -45,7 +45,7 @@ func ResourceSchema(ctx context.Context) schema.Schema {
stringvalidator.OneOfCaseInsensitive("allow", "deny"),
},
},
- "permission_groups": schema.ListNestedAttribute{
+ "permission_groups": schema.SetNestedAttribute{
Description: "A set of permission groups that are specified to the policy.",
Required: true,
NestedObject: schema.NestedAttributeObject{
diff --git a/internal/services/api_token/testdata/apitokenconfigallowdeny.tf b/internal/services/api_token/testdata/apitokenconfigallowdeny.tf
deleted file mode 100644
index 1614716f0a..0000000000
--- a/internal/services/api_token/testdata/apitokenconfigallowdeny.tf
+++ /dev/null
@@ -1,13 +0,0 @@
-resource "cloudflare_api_token" "%[1]s" {
- name = "%[1]s"
-
- policies = [{
- effect = "allow"
- permission_groups = [{
- id = "%[2]s"
- }]
- resources = {
- "com.cloudflare.api.account.zone.*" = "*"
- }
- }]
-}
diff --git a/internal/services/api_token/testdata/apitokendatasource.tf b/internal/services/api_token/testdata/apitokendatasource.tf
index 15d87c23ac..6f481dd8cf 100644
--- a/internal/services/api_token/testdata/apitokendatasource.tf
+++ b/internal/services/api_token/testdata/apitokendatasource.tf
@@ -1,5 +1,6 @@
resource "cloudflare_api_token" "%[1]s" {
name = "%[1]s"
+ status = "active"
policies = [{
effect = "allow"
diff --git a/internal/services/api_token/testdata/apitokenwithallcondition.tf b/internal/services/api_token/testdata/apitokenwithallcondition.tf
index 839a043ff4..3c0e1003ab 100644
--- a/internal/services/api_token/testdata/apitokenwithallcondition.tf
+++ b/internal/services/api_token/testdata/apitokenwithallcondition.tf
@@ -1,19 +1,20 @@
resource "cloudflare_api_token" "%[1]s" {
name = "%[1]s"
+ status = "active"
policies = [{
effect = "allow"
permission_groups = [{
- id = "%[2]s"
+ id = "%[2]s"
}]
resources = { "com.cloudflare.api.account.zone.*" = "*" }
}]
condition = {
- request_ip = {
- in = ["192.0.2.1/32"]
- not_in = ["198.51.100.1/32"]
- }
+ request_ip = {
+ in = ["192.0.2.1/32"]
+ not_in = ["198.51.100.1/32"]
+ }
}
}
diff --git a/internal/services/api_token/testdata/apitokenwithindividualcondition.tf b/internal/services/api_token/testdata/apitokenwithindividualcondition.tf
index 4807db2a97..304f6f3adb 100644
--- a/internal/services/api_token/testdata/apitokenwithindividualcondition.tf
+++ b/internal/services/api_token/testdata/apitokenwithindividualcondition.tf
@@ -1,5 +1,6 @@
resource "cloudflare_api_token" "%[1]s" {
name = "%[1]s"
+ status = "active"
policies = [{
effect = "allow"
@@ -10,8 +11,8 @@ resource "cloudflare_api_token" "%[1]s" {
}]
condition = {
- request_ip = {
- in = ["192.0.2.1/32"]
- }
+ request_ip = {
+ in = ["192.0.2.1/32"]
+ }
}
}
diff --git a/internal/services/api_token/testdata/apitokenwithoutcondition.tf b/internal/services/api_token/testdata/apitokenwithoutcondition.tf
index b64c34d059..083d1663f5 100644
--- a/internal/services/api_token/testdata/apitokenwithoutcondition.tf
+++ b/internal/services/api_token/testdata/apitokenwithoutcondition.tf
@@ -1,5 +1,6 @@
resource "cloudflare_api_token" "%[1]s" {
name = "%[2]s"
+ status = "active"
policies = [{
effect = "allow"
diff --git a/internal/services/api_token/testdata/apitokenwithttl.tf b/internal/services/api_token/testdata/apitokenwithttl.tf
index 829f99750e..4ae016b49f 100644
--- a/internal/services/api_token/testdata/apitokenwithttl.tf
+++ b/internal/services/api_token/testdata/apitokenwithttl.tf
@@ -1,5 +1,6 @@
resource "cloudflare_api_token" "%[1]s" {
name = "%[1]s"
+ status = "active"
policies = [{
effect = "allow"
diff --git a/internal/services/bot_management/data_source_schema.go b/internal/services/bot_management/data_source_schema.go
index 5276be083d..ecfc08233d 100644
--- a/internal/services/bot_management/data_source_schema.go
+++ b/internal/services/bot_management/data_source_schema.go
@@ -22,10 +22,14 @@ func DataSourceSchema(ctx context.Context) schema.Schema {
Required: true,
},
"ai_bots_protection": schema.StringAttribute{
- Description: "Enable rule to block AI Scrapers and Crawlers.\nAvailable values: \"block\", \"disabled\".",
+ Description: "Enable rule to block AI Scrapers and Crawlers.\nAvailable values: \"block\", \"disabled\", \"only_on_ad_pages\".",
Computed: true,
Validators: []validator.String{
- stringvalidator.OneOfCaseInsensitive("block", "disabled"),
+ stringvalidator.OneOfCaseInsensitive(
+ "block",
+ "disabled",
+ "only_on_ad_pages",
+ ),
},
},
"auto_update_model": schema.BoolAttribute{
diff --git a/internal/services/bot_management/model.go b/internal/services/bot_management/model.go
index 54cf1717e9..7c8e91795e 100644
--- a/internal/services/bot_management/model.go
+++ b/internal/services/bot_management/model.go
@@ -15,16 +15,16 @@ type BotManagementResultEnvelope struct {
type BotManagementModel struct {
ID types.String `tfsdk:"id" json:"-,computed"`
ZoneID types.String `tfsdk:"zone_id" path:"zone_id,required"`
- AIBotsProtection types.String `tfsdk:"ai_bots_protection" json:"ai_bots_protection,optional"`
- AutoUpdateModel types.Bool `tfsdk:"auto_update_model" json:"auto_update_model,optional"`
- CrawlerProtection types.String `tfsdk:"crawler_protection" json:"crawler_protection,optional"`
- EnableJS types.Bool `tfsdk:"enable_js" json:"enable_js,optional"`
- FightMode types.Bool `tfsdk:"fight_mode" json:"fight_mode,optional"`
- OptimizeWordpress types.Bool `tfsdk:"optimize_wordpress" json:"optimize_wordpress,optional"`
- SBFMDefinitelyAutomated types.String `tfsdk:"sbfm_definitely_automated" json:"sbfm_definitely_automated,optional"`
- SBFMLikelyAutomated types.String `tfsdk:"sbfm_likely_automated" json:"sbfm_likely_automated,optional"`
- SBFMStaticResourceProtection types.Bool `tfsdk:"sbfm_static_resource_protection" json:"sbfm_static_resource_protection,optional"`
- SBFMVerifiedBots types.String `tfsdk:"sbfm_verified_bots" json:"sbfm_verified_bots,optional"`
+ AIBotsProtection types.String `tfsdk:"ai_bots_protection" json:"ai_bots_protection,computed_optional"`
+ AutoUpdateModel types.Bool `tfsdk:"auto_update_model" json:"auto_update_model,computed_optional"`
+ CrawlerProtection types.String `tfsdk:"crawler_protection" json:"crawler_protection,computed_optional"`
+ EnableJS types.Bool `tfsdk:"enable_js" json:"enable_js,computed_optional"`
+ FightMode types.Bool `tfsdk:"fight_mode" json:"fight_mode,computed_optional"`
+ OptimizeWordpress types.Bool `tfsdk:"optimize_wordpress" json:"optimize_wordpress,computed_optional"`
+ SBFMDefinitelyAutomated types.String `tfsdk:"sbfm_definitely_automated" json:"sbfm_definitely_automated,computed_optional"`
+ SBFMLikelyAutomated types.String `tfsdk:"sbfm_likely_automated" json:"sbfm_likely_automated,computed_optional"`
+ SBFMStaticResourceProtection types.Bool `tfsdk:"sbfm_static_resource_protection" json:"sbfm_static_resource_protection,computed_optional"`
+ SBFMVerifiedBots types.String `tfsdk:"sbfm_verified_bots" json:"sbfm_verified_bots,computed_optional"`
SuppressSessionScore types.Bool `tfsdk:"suppress_session_score" json:"suppress_session_score,computed_optional"`
UsingLatestModel types.Bool `tfsdk:"using_latest_model" json:"using_latest_model,computed"`
StaleZoneConfiguration customfield.NestedObject[BotManagementStaleZoneConfigurationModel] `tfsdk:"stale_zone_configuration" json:"stale_zone_configuration,computed"`
diff --git a/internal/services/bot_management/schema.go b/internal/services/bot_management/schema.go
index 095b324ce4..a8d3b16a6a 100644
--- a/internal/services/bot_management/schema.go
+++ b/internal/services/bot_management/schema.go
@@ -31,18 +31,25 @@ func ResourceSchema(ctx context.Context) schema.Schema {
PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown(), stringplanmodifier.RequiresReplace()},
},
"ai_bots_protection": schema.StringAttribute{
- Description: "Enable rule to block AI Scrapers and Crawlers.\nAvailable values: \"block\", \"disabled\".",
+ Description: "Enable rule to block AI Scrapers and Crawlers.\nAvailable values: \"block\", \"disabled\", \"only_on_ad_pages\".",
+ Computed: true,
Optional: true,
Validators: []validator.String{
- stringvalidator.OneOfCaseInsensitive("block", "disabled"),
+ stringvalidator.OneOfCaseInsensitive(
+ "block",
+ "disabled",
+ "only_on_ad_pages",
+ ),
},
},
"auto_update_model": schema.BoolAttribute{
Description: "Automatically update to the newest bot detection models created by Cloudflare as they are released. [Learn more.](https://developers.cloudflare.com/bots/reference/machine-learning-models#model-versions-and-release-notes)",
+ Computed: true,
Optional: true,
},
"crawler_protection": schema.StringAttribute{
Description: "Enable rule to punish AI Scrapers and Crawlers via a link maze.\nAvailable values: \"enabled\", \"disabled\".",
+ Computed: true,
Optional: true,
Validators: []validator.String{
stringvalidator.OneOfCaseInsensitive("enabled", "disabled"),
@@ -50,18 +57,22 @@ func ResourceSchema(ctx context.Context) schema.Schema {
},
"enable_js": schema.BoolAttribute{
Description: "Use lightweight, invisible JavaScript detections to improve Bot Management. [Learn more about JavaScript Detections](https://developers.cloudflare.com/bots/reference/javascript-detections/).",
+ Computed: true,
Optional: true,
},
"fight_mode": schema.BoolAttribute{
Description: "Whether to enable Bot Fight Mode.",
+ Computed: true,
Optional: true,
},
"optimize_wordpress": schema.BoolAttribute{
Description: "Whether to optimize Super Bot Fight Mode protections for Wordpress.",
+ Computed: true,
Optional: true,
},
"sbfm_definitely_automated": schema.StringAttribute{
Description: "Super Bot Fight Mode (SBFM) action to take on definitely automated requests.\nAvailable values: \"allow\", \"block\", \"managed_challenge\".",
+ Computed: true,
Optional: true,
Validators: []validator.String{
stringvalidator.OneOfCaseInsensitive(
@@ -73,6 +84,7 @@ func ResourceSchema(ctx context.Context) schema.Schema {
},
"sbfm_likely_automated": schema.StringAttribute{
Description: "Super Bot Fight Mode (SBFM) action to take on likely automated requests.\nAvailable values: \"allow\", \"block\", \"managed_challenge\".",
+ Computed: true,
Optional: true,
Validators: []validator.String{
stringvalidator.OneOfCaseInsensitive(
@@ -84,10 +96,12 @@ func ResourceSchema(ctx context.Context) schema.Schema {
},
"sbfm_static_resource_protection": schema.BoolAttribute{
Description: "Super Bot Fight Mode (SBFM) to enable static resource protection.\nEnable if static resources on your application need bot protection.\nNote: Static resource protection can also result in legitimate traffic being blocked.",
+ Computed: true,
Optional: true,
},
"sbfm_verified_bots": schema.StringAttribute{
Description: "Super Bot Fight Mode (SBFM) action to take on verified bots requests.\nAvailable values: \"allow\", \"block\".",
+ Computed: true,
Optional: true,
Validators: []validator.String{
stringvalidator.OneOfCaseInsensitive("allow", "block"),
diff --git a/internal/services/image/model.go b/internal/services/image/model.go
index 34dbe274f7..f07927e69e 100644
--- a/internal/services/image/model.go
+++ b/internal/services/image/model.go
@@ -18,10 +18,10 @@ type ImageResultEnvelope struct {
}
type ImageModel struct {
- ID types.String `tfsdk:"id" json:"id,computed"`
+ ID types.String `tfsdk:"id" json:"id,required"`
AccountID types.String `tfsdk:"account_id" path:"account_id,required"`
+ File types.String `tfsdk:"file" json:"file,optional,no_refresh"`
URL types.String `tfsdk:"url" json:"url,optional,no_refresh"`
- File jsontypes.Normalized `tfsdk:"file" json:"file,optional,no_refresh"`
Metadata jsontypes.Normalized `tfsdk:"metadata" json:"metadata,optional,no_refresh"`
RequireSignedURLs types.Bool `tfsdk:"require_signed_urls" json:"requireSignedURLs,computed_optional"`
Filename types.String `tfsdk:"filename" json:"filename,computed"`
diff --git a/internal/services/image/schema.go b/internal/services/image/schema.go
index 6210176b6c..c474520b14 100644
--- a/internal/services/image/schema.go
+++ b/internal/services/image/schema.go
@@ -22,25 +22,25 @@ func ResourceSchema(ctx context.Context) schema.Schema {
return schema.Schema{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
- Description: "Image unique identifier.",
- Computed: true,
- PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
+ Description: "An optional custom unique identifier for your image.",
+ Required: true,
+ PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown(), stringplanmodifier.RequiresReplace()},
},
"account_id": schema.StringAttribute{
Description: "Account identifier tag.",
Required: true,
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
},
+ "file": schema.StringAttribute{
+ Description: "An image binary data. Only needed when type is uploading a file.",
+ Optional: true,
+ PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
+ },
"url": schema.StringAttribute{
Description: "A URL to fetch an image from origin. Only needed when type is uploading from a URL.",
Optional: true,
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
},
- "file": schema.StringAttribute{
- Description: "An image binary data. Only needed when type is uploading a file.",
- Optional: true,
- CustomType: jsontypes.NormalizedType{},
- },
"metadata": schema.StringAttribute{
Description: "User modifiable key-value store. Can use used for keeping references to another system of record for managing images.",
Optional: true,
diff --git a/internal/services/list_item/resource.go b/internal/services/list_item/resource.go
index 162fa2e13c..5dba9ba94e 100644
--- a/internal/services/list_item/resource.go
+++ b/internal/services/list_item/resource.go
@@ -148,86 +148,86 @@ func (r *ListItemResource) Create(ctx context.Context, req resource.CreateReques
func (r *ListItemResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data *ListItemModel
- // resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
-
- // if resp.Diagnostics.HasError() {
- // return
- // }
-
- // var state *ListItemModel
-
- // resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
-
- // if resp.Diagnostics.HasError() {
- // return
- // }
-
- // dataBytes, err := data.MarshalJSONForUpdate(*state)
- // if err != nil {
- // resp.Diagnostics.AddError("failed to serialize http request", err.Error())
- // return
- // }
- // res := new(http.Response)
- // env := ListItemResultEnvelope{*data}
- // _, err = r.client.Rules.Lists.Items.Update(
- // ctx,
- // data.ListID.ValueString(),
- // rules.ListItemUpdateParams{
- // AccountID: cloudflare.F(data.AccountID.ValueString()),
- // },
- // option.WithRequestBody("application/json", dataBytes),
- // option.WithResponseBodyInto(&res),
- // option.WithMiddleware(logging.Middleware(ctx)),
- // )
- // if err != nil {
- // resp.Diagnostics.AddError("failed to make http request", err.Error())
- // return
- // }
- // bytes, _ := io.ReadAll(res.Body)
- // err = apijson.UnmarshalComputed(bytes, &env)
- // if err != nil {
- // resp.Diagnostics.AddError("failed to deserialize http request", err.Error())
- // return
- // }
- // data = &env.Result
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ var state *ListItemModel
+
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ dataBytes, err := data.MarshalJSONForUpdate(*state)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to serialize http request", err.Error())
+ return
+ }
+ res := new(http.Response)
+ env := ListItemResultEnvelope{*data}
+ _, err = r.client.Rules.Lists.Items.Update(
+ ctx,
+ data.ListID.ValueString(),
+ rules.ListItemUpdateParams{
+ AccountID: cloudflare.F(data.AccountID.ValueString()),
+ },
+ option.WithRequestBody("application/json", dataBytes),
+ option.WithResponseBodyInto(&res),
+ option.WithMiddleware(logging.Middleware(ctx)),
+ )
+ if err != nil {
+ resp.Diagnostics.AddError("failed to make http request", err.Error())
+ return
+ }
+ bytes, _ := io.ReadAll(res.Body)
+ err = apijson.UnmarshalComputed(bytes, &env)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to deserialize http request", err.Error())
+ return
+ }
+ data = &env.Result
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *ListItemResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data *ListItemModel
- // resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
-
- // if resp.Diagnostics.HasError() {
- // return
- // }
-
- // res := new(http.Response)
- // env := ListItemResultEnvelope{*data}
- // _, err := r.client.Rules.Lists.Items.Get(
- // ctx,
- // data.AccountID.ValueString(),
- // data.ListID.ValueString(),
- // data.ID.ValueString(),
- // option.WithResponseBodyInto(&res),
- // option.WithMiddleware(logging.Middleware(ctx)),
- // )
- // if res != nil && res.StatusCode == 404 {
- // resp.Diagnostics.AddWarning("Resource not found", "The resource was not found on the server and will be removed from state.")
- // resp.State.RemoveResource(ctx)
- // return
- // }
- // if err != nil {
- // resp.Diagnostics.AddError("failed to make http request", err.Error())
- // return
- // }
- // bytes, _ := io.ReadAll(res.Body)
- // err = apijson.Unmarshal(bytes, &env)
- // if err != nil {
- // resp.Diagnostics.AddError("failed to deserialize http request", err.Error())
- // return
- // }
- // data = &env.Result
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ res := new(http.Response)
+ env := ListItemResultEnvelope{*data}
+ _, err := r.client.Rules.Lists.Items.Get(
+ ctx,
+ data.ListID.ValueString(),
+ data.ID.ValueString(),
+ rules.ListItemGetParams{AccountID: cloudflare.F(data.AccountID.ValueString())},
+ option.WithResponseBodyInto(&res),
+ option.WithMiddleware(logging.Middleware(ctx)),
+ )
+ if res != nil && res.StatusCode == 404 {
+ resp.Diagnostics.AddWarning("Resource not found", "The resource was not found on the server and will be removed from state.")
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ if err != nil {
+ resp.Diagnostics.AddError("failed to make http request", err.Error())
+ return
+ }
+ bytes, _ := io.ReadAll(res.Body)
+ err = apijson.Unmarshal(bytes, &env)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to deserialize http request", err.Error())
+ return
+ }
+ data = &env.Result
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
diff --git a/internal/services/logpull_retention/model.go b/internal/services/logpull_retention/model.go
index a3d7b837f7..4d26ca9574 100644
--- a/internal/services/logpull_retention/model.go
+++ b/internal/services/logpull_retention/model.go
@@ -12,6 +12,7 @@ type LogpullRetentionResultEnvelope struct {
}
type LogpullRetentionModel struct {
+ ID types.String `tfsdk:"id" json:"-,computed"`
ZoneID types.String `tfsdk:"zone_id" path:"zone_id,required"`
Flag types.Bool `tfsdk:"flag" json:"flag,optional"`
}
diff --git a/internal/services/logpull_retention/resource.go b/internal/services/logpull_retention/resource.go
index 64a91d775c..aa82c005d5 100644
--- a/internal/services/logpull_retention/resource.go
+++ b/internal/services/logpull_retention/resource.go
@@ -12,13 +12,16 @@ import (
"github.com/cloudflare/cloudflare-go/v4/logs"
"github.com/cloudflare/cloudflare-go/v4/option"
"github.com/cloudflare/terraform-provider-cloudflare/internal/apijson"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/importpath"
"github.com/cloudflare/terraform-provider-cloudflare/internal/logging"
"github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/types"
)
// Ensure provider defined types fully satisfy framework interfaces.
var _ resource.ResourceWithConfigure = (*LogpullRetentionResource)(nil)
var _ resource.ResourceWithModifyPlan = (*LogpullRetentionResource)(nil)
+var _ resource.ResourceWithImportState = (*LogpullRetentionResource)(nil)
func NewResource() resource.Resource {
return &LogpullRetentionResource{}
@@ -88,6 +91,7 @@ func (r *LogpullRetentionResource) Create(ctx context.Context, req resource.Crea
return
}
data = &env.Result
+ data.ID = data.ZoneID
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
@@ -131,6 +135,7 @@ func (r *LogpullRetentionResource) Read(ctx context.Context, req resource.ReadRe
return
}
data = &env.Result
+ data.ID = data.ZoneID
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
@@ -139,6 +144,48 @@ func (r *LogpullRetentionResource) Delete(ctx context.Context, req resource.Dele
}
+func (r *LogpullRetentionResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ var data *LogpullRetentionModel = new(LogpullRetentionModel)
+
+ path := ""
+ diags := importpath.ParseImportID(
+ req.ID,
+ "",
+ &path,
+ )
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ data.ZoneID = types.StringValue(path)
+
+ res := new(http.Response)
+ env := LogpullRetentionResultEnvelope{*data}
+ _, err := r.client.Logs.Control.Retention.Get(
+ ctx,
+ logs.ControlRetentionGetParams{
+ ZoneID: cloudflare.F(path),
+ },
+ option.WithResponseBodyInto(&res),
+ option.WithMiddleware(logging.Middleware(ctx)),
+ )
+ if err != nil {
+ resp.Diagnostics.AddError("failed to make http request", err.Error())
+ return
+ }
+ bytes, _ := io.ReadAll(res.Body)
+ err = apijson.Unmarshal(bytes, &env)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to deserialize http request", err.Error())
+ return
+ }
+ data = &env.Result
+ data.ID = data.ZoneID
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
func (r *LogpullRetentionResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
if req.State.Raw.IsNull() {
resp.Diagnostics.AddWarning(
diff --git a/internal/services/logpull_retention/schema.go b/internal/services/logpull_retention/schema.go
index 08ee07984a..44e2a97479 100644
--- a/internal/services/logpull_retention/schema.go
+++ b/internal/services/logpull_retention/schema.go
@@ -17,10 +17,15 @@ var _ resource.ResourceWithConfigValidators = (*LogpullRetentionResource)(nil)
func ResourceSchema(ctx context.Context) schema.Schema {
return schema.Schema{
Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Description: "Identifier.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown(), stringplanmodifier.RequiresReplace()},
+ },
"zone_id": schema.StringAttribute{
Description: "Identifier.",
Required: true,
- PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
+ PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown(), stringplanmodifier.RequiresReplace()},
},
"flag": schema.BoolAttribute{
Description: "The log retention flag for Logpull API.",
diff --git a/internal/services/logpush_dataset_job/data_source_model.go b/internal/services/logpush_dataset_job/data_source_model.go
index fab17e6add..78229e634f 100644
--- a/internal/services/logpush_dataset_job/data_source_model.go
+++ b/internal/services/logpush_dataset_job/data_source_model.go
@@ -31,9 +31,9 @@ type LogpushDatasetJobDataSourceModel struct {
LastComplete timetypes.RFC3339 `tfsdk:"last_complete" json:"last_complete,computed" format:"date-time"`
LastError timetypes.RFC3339 `tfsdk:"last_error" json:"last_error,computed" format:"date-time"`
LogpullOptions types.String `tfsdk:"logpull_options" json:"logpull_options,computed"`
- MaxUploadBytes types.Int64 `tfsdk:"max_upload_bytes" json:"max_upload_bytes,computed"`
- MaxUploadIntervalSeconds types.Int64 `tfsdk:"max_upload_interval_seconds" json:"max_upload_interval_seconds,computed"`
- MaxUploadRecords types.Int64 `tfsdk:"max_upload_records" json:"max_upload_records,computed"`
+ MaxUploadBytes types.Float64 `tfsdk:"max_upload_bytes" json:"max_upload_bytes,computed"`
+ MaxUploadIntervalSeconds types.Float64 `tfsdk:"max_upload_interval_seconds" json:"max_upload_interval_seconds,computed"`
+ MaxUploadRecords types.Float64 `tfsdk:"max_upload_records" json:"max_upload_records,computed"`
Name types.String `tfsdk:"name" json:"name,computed"`
OutputOptions customfield.NestedObject[LogpushDatasetJobOutputOptionsDataSourceModel] `tfsdk:"output_options" json:"output_options,computed"`
}
diff --git a/internal/services/logpush_dataset_job/data_source_schema.go b/internal/services/logpush_dataset_job/data_source_schema.go
index 42b4204d12..2a85d30bc5 100644
--- a/internal/services/logpush_dataset_job/data_source_schema.go
+++ b/internal/services/logpush_dataset_job/data_source_schema.go
@@ -124,10 +124,10 @@ func DataSourceSchema(ctx context.Context) schema.Schema {
},
},
"kind": schema.StringAttribute{
- Description: "The kind parameter (optional) is used to differentiate between Logpush and Edge Log Delivery jobs. Currently, Edge Log Delivery is only supported for the `http_requests` dataset.\nAvailable values: \"edge\".",
+ Description: "The kind parameter (optional) is used to differentiate between Logpush and Edge Log Delivery jobs (when supported by the dataset).\nAvailable values: \"\", \"edge\".",
Computed: true,
Validators: []validator.String{
- stringvalidator.OneOfCaseInsensitive("edge"),
+ stringvalidator.OneOfCaseInsensitive("", "edge"),
},
},
"last_complete": schema.StringAttribute{
@@ -145,25 +145,25 @@ func DataSourceSchema(ctx context.Context) schema.Schema {
Computed: true,
DeprecationMessage: "This attribute is deprecated.",
},
- "max_upload_bytes": schema.Int64Attribute{
- Description: "The maximum uncompressed file size of a batch of logs. This setting value must be between `5 MB` and `1 GB`, or `0` to disable it. Note that you cannot set a minimum file size; this means that log files may be much smaller than this batch size. This parameter is not available for jobs with `edge` as its kind.",
+ "max_upload_bytes": schema.Float64Attribute{
+ Description: "The maximum uncompressed file size of a batch of logs. This setting value must be between `5 MB` and `1 GB`, or `0` to disable it. Note that you cannot set a minimum file size; this means that log files may be much smaller than this batch size.\nAvailable values: 0.",
Computed: true,
- Validators: []validator.Int64{
- int64validator.Between(5000000, 1000000000),
+ Validators: []validator.Float64{
+ float64validator.OneOf(0),
},
},
- "max_upload_interval_seconds": schema.Int64Attribute{
- Description: "The maximum interval in seconds for log batches. This setting must be between 30 and 300 seconds (5 minutes), or `0` to disable it. Note that you cannot specify a minimum interval for log batches; this means that log files may be sent in shorter intervals than this. This parameter is only used for jobs with `edge` as its kind.",
+ "max_upload_interval_seconds": schema.Float64Attribute{
+ Description: "The maximum interval in seconds for log batches. This setting must be between 30 and 300 seconds (5 minutes), or `0` to disable it. Note that you cannot specify a minimum interval for log batches; this means that log files may be sent in shorter intervals than this.\nAvailable values: 0.",
Computed: true,
- Validators: []validator.Int64{
- int64validator.Between(30, 300),
+ Validators: []validator.Float64{
+ float64validator.OneOf(0),
},
},
- "max_upload_records": schema.Int64Attribute{
- Description: "The maximum number of log lines per batch. This setting must be between 1000 and 1,000,000 lines, or `0` to disable it. Note that you cannot specify a minimum number of log lines per batch; this means that log files may contain many fewer lines than this. This parameter is not available for jobs with `edge` as its kind.",
+ "max_upload_records": schema.Float64Attribute{
+ Description: "The maximum number of log lines per batch. This setting must be between 1000 and 1,000,000 lines, or `0` to disable it. Note that you cannot specify a minimum number of log lines per batch; this means that log files may contain many fewer lines than this.\nAvailable values: 0.",
Computed: true,
- Validators: []validator.Int64{
- int64validator.Between(1000, 1000000),
+ Validators: []validator.Float64{
+ float64validator.OneOf(0),
},
},
"name": schema.StringAttribute{
diff --git a/internal/services/logpush_job/data_source_model.go b/internal/services/logpush_job/data_source_model.go
index 2c9db3e04e..dab09c2f24 100644
--- a/internal/services/logpush_job/data_source_model.go
+++ b/internal/services/logpush_job/data_source_model.go
@@ -31,9 +31,9 @@ type LogpushJobDataSourceModel struct {
LastComplete timetypes.RFC3339 `tfsdk:"last_complete" json:"last_complete,computed" format:"date-time"`
LastError timetypes.RFC3339 `tfsdk:"last_error" json:"last_error,computed" format:"date-time"`
LogpullOptions types.String `tfsdk:"logpull_options" json:"logpull_options,computed"`
- MaxUploadBytes types.Int64 `tfsdk:"max_upload_bytes" json:"max_upload_bytes,computed"`
- MaxUploadIntervalSeconds types.Int64 `tfsdk:"max_upload_interval_seconds" json:"max_upload_interval_seconds,computed"`
- MaxUploadRecords types.Int64 `tfsdk:"max_upload_records" json:"max_upload_records,computed"`
+ MaxUploadBytes types.Float64 `tfsdk:"max_upload_bytes" json:"max_upload_bytes,computed"`
+ MaxUploadIntervalSeconds types.Float64 `tfsdk:"max_upload_interval_seconds" json:"max_upload_interval_seconds,computed"`
+ MaxUploadRecords types.Float64 `tfsdk:"max_upload_records" json:"max_upload_records,computed"`
Name types.String `tfsdk:"name" json:"name,computed"`
OutputOptions customfield.NestedObject[LogpushJobOutputOptionsDataSourceModel] `tfsdk:"output_options" json:"output_options,computed"`
}
diff --git a/internal/services/logpush_job/data_source_schema.go b/internal/services/logpush_job/data_source_schema.go
index 2fbf92ea21..4d6fde50cb 100644
--- a/internal/services/logpush_job/data_source_schema.go
+++ b/internal/services/logpush_job/data_source_schema.go
@@ -98,10 +98,10 @@ func DataSourceSchema(ctx context.Context) schema.Schema {
},
},
"kind": schema.StringAttribute{
- Description: "The kind parameter (optional) is used to differentiate between Logpush and Edge Log Delivery jobs. Currently, Edge Log Delivery is only supported for the `http_requests` dataset.\nAvailable values: \"edge\".",
+ Description: "The kind parameter (optional) is used to differentiate between Logpush and Edge Log Delivery jobs (when supported by the dataset).\nAvailable values: \"\", \"edge\".",
Computed: true,
Validators: []validator.String{
- stringvalidator.OneOfCaseInsensitive("edge"),
+ stringvalidator.OneOfCaseInsensitive("", "edge"),
},
},
"last_complete": schema.StringAttribute{
@@ -119,25 +119,25 @@ func DataSourceSchema(ctx context.Context) schema.Schema {
Computed: true,
DeprecationMessage: "This attribute is deprecated.",
},
- "max_upload_bytes": schema.Int64Attribute{
- Description: "The maximum uncompressed file size of a batch of logs. This setting value must be between `5 MB` and `1 GB`, or `0` to disable it. Note that you cannot set a minimum file size; this means that log files may be much smaller than this batch size. This parameter is not available for jobs with `edge` as its kind.",
+ "max_upload_bytes": schema.Float64Attribute{
+ Description: "The maximum uncompressed file size of a batch of logs. This setting value must be between `5 MB` and `1 GB`, or `0` to disable it. Note that you cannot set a minimum file size; this means that log files may be much smaller than this batch size.\nAvailable values: 0.",
Computed: true,
- Validators: []validator.Int64{
- int64validator.Between(5000000, 1000000000),
+ Validators: []validator.Float64{
+ float64validator.OneOf(0),
},
},
- "max_upload_interval_seconds": schema.Int64Attribute{
- Description: "The maximum interval in seconds for log batches. This setting must be between 30 and 300 seconds (5 minutes), or `0` to disable it. Note that you cannot specify a minimum interval for log batches; this means that log files may be sent in shorter intervals than this. This parameter is only used for jobs with `edge` as its kind.",
+ "max_upload_interval_seconds": schema.Float64Attribute{
+ Description: "The maximum interval in seconds for log batches. This setting must be between 30 and 300 seconds (5 minutes), or `0` to disable it. Note that you cannot specify a minimum interval for log batches; this means that log files may be sent in shorter intervals than this.\nAvailable values: 0.",
Computed: true,
- Validators: []validator.Int64{
- int64validator.Between(30, 300),
+ Validators: []validator.Float64{
+ float64validator.OneOf(0),
},
},
- "max_upload_records": schema.Int64Attribute{
- Description: "The maximum number of log lines per batch. This setting must be between 1000 and 1,000,000 lines, or `0` to disable it. Note that you cannot specify a minimum number of log lines per batch; this means that log files may contain many fewer lines than this. This parameter is not available for jobs with `edge` as its kind.",
+ "max_upload_records": schema.Float64Attribute{
+ Description: "The maximum number of log lines per batch. This setting must be between 1000 and 1,000,000 lines, or `0` to disable it. Note that you cannot specify a minimum number of log lines per batch; this means that log files may contain many fewer lines than this.\nAvailable values: 0.",
Computed: true,
- Validators: []validator.Int64{
- int64validator.Between(1000, 1000000),
+ Validators: []validator.Float64{
+ float64validator.OneOf(0),
},
},
"name": schema.StringAttribute{
diff --git a/internal/services/logpush_job/list_data_source_model.go b/internal/services/logpush_job/list_data_source_model.go
index 481f67b0c7..ad071eb5b7 100644
--- a/internal/services/logpush_job/list_data_source_model.go
+++ b/internal/services/logpush_job/list_data_source_model.go
@@ -47,9 +47,9 @@ type LogpushJobsResultDataSourceModel struct {
LastComplete timetypes.RFC3339 `tfsdk:"last_complete" json:"last_complete,computed" format:"date-time"`
LastError timetypes.RFC3339 `tfsdk:"last_error" json:"last_error,computed" format:"date-time"`
LogpullOptions types.String `tfsdk:"logpull_options" json:"logpull_options,computed"`
- MaxUploadBytes types.Int64 `tfsdk:"max_upload_bytes" json:"max_upload_bytes,computed"`
- MaxUploadIntervalSeconds types.Int64 `tfsdk:"max_upload_interval_seconds" json:"max_upload_interval_seconds,computed"`
- MaxUploadRecords types.Int64 `tfsdk:"max_upload_records" json:"max_upload_records,computed"`
+ MaxUploadBytes types.Float64 `tfsdk:"max_upload_bytes" json:"max_upload_bytes,computed"`
+ MaxUploadIntervalSeconds types.Float64 `tfsdk:"max_upload_interval_seconds" json:"max_upload_interval_seconds,computed"`
+ MaxUploadRecords types.Float64 `tfsdk:"max_upload_records" json:"max_upload_records,computed"`
Name types.String `tfsdk:"name" json:"name,computed"`
OutputOptions customfield.NestedObject[LogpushJobsOutputOptionsDataSourceModel] `tfsdk:"output_options" json:"output_options,computed"`
}
diff --git a/internal/services/logpush_job/list_data_source_schema.go b/internal/services/logpush_job/list_data_source_schema.go
index 94f743edb6..88d068e7d0 100644
--- a/internal/services/logpush_job/list_data_source_schema.go
+++ b/internal/services/logpush_job/list_data_source_schema.go
@@ -104,10 +104,10 @@ func ListDataSourceSchema(ctx context.Context) schema.Schema {
},
},
"kind": schema.StringAttribute{
- Description: "The kind parameter (optional) is used to differentiate between Logpush and Edge Log Delivery jobs. Currently, Edge Log Delivery is only supported for the `http_requests` dataset.\nAvailable values: \"edge\".",
+ Description: "The kind parameter (optional) is used to differentiate between Logpush and Edge Log Delivery jobs (when supported by the dataset).\nAvailable values: \"\", \"edge\".",
Computed: true,
Validators: []validator.String{
- stringvalidator.OneOfCaseInsensitive("edge"),
+ stringvalidator.OneOfCaseInsensitive("", "edge"),
},
},
"last_complete": schema.StringAttribute{
@@ -125,25 +125,25 @@ func ListDataSourceSchema(ctx context.Context) schema.Schema {
Computed: true,
DeprecationMessage: "This attribute is deprecated.",
},
- "max_upload_bytes": schema.Int64Attribute{
- Description: "The maximum uncompressed file size of a batch of logs. This setting value must be between `5 MB` and `1 GB`, or `0` to disable it. Note that you cannot set a minimum file size; this means that log files may be much smaller than this batch size. This parameter is not available for jobs with `edge` as its kind.",
+ "max_upload_bytes": schema.Float64Attribute{
+ Description: "The maximum uncompressed file size of a batch of logs. This setting value must be between `5 MB` and `1 GB`, or `0` to disable it. Note that you cannot set a minimum file size; this means that log files may be much smaller than this batch size.\nAvailable values: 0.",
Computed: true,
- Validators: []validator.Int64{
- int64validator.Between(5000000, 1000000000),
+ Validators: []validator.Float64{
+ float64validator.OneOf(0),
},
},
- "max_upload_interval_seconds": schema.Int64Attribute{
- Description: "The maximum interval in seconds for log batches. This setting must be between 30 and 300 seconds (5 minutes), or `0` to disable it. Note that you cannot specify a minimum interval for log batches; this means that log files may be sent in shorter intervals than this. This parameter is only used for jobs with `edge` as its kind.",
+ "max_upload_interval_seconds": schema.Float64Attribute{
+ Description: "The maximum interval in seconds for log batches. This setting must be between 30 and 300 seconds (5 minutes), or `0` to disable it. Note that you cannot specify a minimum interval for log batches; this means that log files may be sent in shorter intervals than this.\nAvailable values: 0.",
Computed: true,
- Validators: []validator.Int64{
- int64validator.Between(30, 300),
+ Validators: []validator.Float64{
+ float64validator.OneOf(0),
},
},
- "max_upload_records": schema.Int64Attribute{
- Description: "The maximum number of log lines per batch. This setting must be between 1000 and 1,000,000 lines, or `0` to disable it. Note that you cannot specify a minimum number of log lines per batch; this means that log files may contain many fewer lines than this. This parameter is not available for jobs with `edge` as its kind.",
+ "max_upload_records": schema.Float64Attribute{
+ Description: "The maximum number of log lines per batch. This setting must be between 1000 and 1,000,000 lines, or `0` to disable it. Note that you cannot specify a minimum number of log lines per batch; this means that log files may contain many fewer lines than this.\nAvailable values: 0.",
Computed: true,
- Validators: []validator.Int64{
- int64validator.Between(1000, 1000000),
+ Validators: []validator.Float64{
+ float64validator.OneOf(0),
},
},
"name": schema.StringAttribute{
diff --git a/internal/services/logpush_job/model.go b/internal/services/logpush_job/model.go
index f65ccb95d5..9990cd678e 100644
--- a/internal/services/logpush_job/model.go
+++ b/internal/services/logpush_job/model.go
@@ -22,13 +22,13 @@ type LogpushJobModel struct {
Enabled types.Bool `tfsdk:"enabled" json:"enabled,optional"`
Filter types.String `tfsdk:"filter" json:"filter,optional,no_refresh"`
LogpullOptions types.String `tfsdk:"logpull_options" json:"logpull_options,optional"`
- MaxUploadBytes types.Int64 `tfsdk:"max_upload_bytes" json:"max_upload_bytes,optional"`
+ MaxUploadBytes types.Float64 `tfsdk:"max_upload_bytes" json:"max_upload_bytes,optional"`
Name types.String `tfsdk:"name" json:"name,optional"`
OwnershipChallenge types.String `tfsdk:"ownership_challenge" json:"ownership_challenge,optional,no_refresh"`
Frequency types.String `tfsdk:"frequency" json:"frequency,computed_optional"`
Kind types.String `tfsdk:"kind" json:"kind,computed_optional"`
- MaxUploadIntervalSeconds types.Int64 `tfsdk:"max_upload_interval_seconds" json:"max_upload_interval_seconds,computed_optional"`
- MaxUploadRecords types.Int64 `tfsdk:"max_upload_records" json:"max_upload_records,computed_optional"`
+ MaxUploadIntervalSeconds types.Float64 `tfsdk:"max_upload_interval_seconds" json:"max_upload_interval_seconds,computed_optional"`
+ MaxUploadRecords types.Float64 `tfsdk:"max_upload_records" json:"max_upload_records,computed_optional"`
OutputOptions customfield.NestedObject[LogpushJobOutputOptionsModel] `tfsdk:"output_options" json:"output_options,computed_optional"`
ErrorMessage types.String `tfsdk:"error_message" json:"error_message,computed"`
LastComplete timetypes.RFC3339 `tfsdk:"last_complete" json:"last_complete,computed" format:"date-time"`
diff --git a/internal/services/logpush_job/schema.go b/internal/services/logpush_job/schema.go
index 77f35820ab..f97d5509d6 100644
--- a/internal/services/logpush_job/schema.go
+++ b/internal/services/logpush_job/schema.go
@@ -14,7 +14,6 @@ import (
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/float64default"
- "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
@@ -98,11 +97,11 @@ func ResourceSchema(ctx context.Context) schema.Schema {
Optional: true,
DeprecationMessage: "This attribute is deprecated.",
},
- "max_upload_bytes": schema.Int64Attribute{
- Description: "The maximum uncompressed file size of a batch of logs. This setting value must be between `5 MB` and `1 GB`, or `0` to disable it. Note that you cannot set a minimum file size; this means that log files may be much smaller than this batch size. This parameter is not available for jobs with `edge` as its kind.",
+ "max_upload_bytes": schema.Float64Attribute{
+ Description: "The maximum uncompressed file size of a batch of logs. This setting value must be between `5 MB` and `1 GB`, or `0` to disable it. Note that you cannot set a minimum file size; this means that log files may be much smaller than this batch size.\nAvailable values: 0.",
Optional: true,
- Validators: []validator.Int64{
- int64validator.Between(5000000, 1000000000),
+ Validators: []validator.Float64{
+ float64validator.OneOf(0),
},
},
"name": schema.StringAttribute{
@@ -125,30 +124,29 @@ func ResourceSchema(ctx context.Context) schema.Schema {
Default: stringdefault.StaticString("high"),
},
"kind": schema.StringAttribute{
- Description: "The kind parameter (optional) is used to differentiate between Logpush and Edge Log Delivery jobs. Currently, Edge Log Delivery is only supported for the `http_requests` dataset.\nAvailable values: \"edge\".",
+ Description: "The kind parameter (optional) is used to differentiate between Logpush and Edge Log Delivery jobs (when supported by the dataset).\nAvailable values: \"\", \"edge\".",
Computed: true,
Optional: true,
Validators: []validator.String{
- stringvalidator.OneOfCaseInsensitive("edge"),
+ stringvalidator.OneOfCaseInsensitive("", "edge"),
},
+ Default: stringdefault.StaticString(""),
},
- "max_upload_interval_seconds": schema.Int64Attribute{
- Description: "The maximum interval in seconds for log batches. This setting must be between 30 and 300 seconds (5 minutes), or `0` to disable it. Note that you cannot specify a minimum interval for log batches; this means that log files may be sent in shorter intervals than this. This parameter is only used for jobs with `edge` as its kind.",
+ "max_upload_interval_seconds": schema.Float64Attribute{
+ Description: "The maximum interval in seconds for log batches. This setting must be between 30 and 300 seconds (5 minutes), or `0` to disable it. Note that you cannot specify a minimum interval for log batches; this means that log files may be sent in shorter intervals than this.\nAvailable values: 0.",
Computed: true,
Optional: true,
- Validators: []validator.Int64{
- int64validator.Between(30, 300),
+ Validators: []validator.Float64{
+ float64validator.OneOf(0),
},
- Default: int64default.StaticInt64(30),
},
- "max_upload_records": schema.Int64Attribute{
- Description: "The maximum number of log lines per batch. This setting must be between 1000 and 1,000,000 lines, or `0` to disable it. Note that you cannot specify a minimum number of log lines per batch; this means that log files may contain many fewer lines than this. This parameter is not available for jobs with `edge` as its kind.",
+ "max_upload_records": schema.Float64Attribute{
+ Description: "The maximum number of log lines per batch. This setting must be between 1000 and 1,000,000 lines, or `0` to disable it. Note that you cannot specify a minimum number of log lines per batch; this means that log files may contain many fewer lines than this.\nAvailable values: 0.",
Computed: true,
Optional: true,
- Validators: []validator.Int64{
- int64validator.Between(1000, 1000000),
+ Validators: []validator.Float64{
+ float64validator.OneOf(0),
},
- Default: int64default.StaticInt64(100000),
},
"output_options": schema.SingleNestedAttribute{
Description: "The structured replacement for `logpull_options`. When including this field, the `logpull_option` field will be ignored.",
diff --git a/internal/services/rate_limit/data_source_schema.go b/internal/services/rate_limit/data_source_schema.go
index 7a03cc0f3c..1a44cf701c 100644
--- a/internal/services/rate_limit/data_source_schema.go
+++ b/internal/services/rate_limit/data_source_schema.go
@@ -34,7 +34,7 @@ func DataSourceSchema(ctx context.Context) schema.Schema {
Required: true,
},
"description": schema.StringAttribute{
- Description: "An informative summary of the rate limit. This value is sanitized and any tags will be removed.",
+ Description: "An informative summary of the rule. This value is sanitized and any tags will be removed.",
Computed: true,
},
"disabled": schema.BoolAttribute{
diff --git a/internal/services/rate_limit/list_data_source_schema.go b/internal/services/rate_limit/list_data_source_schema.go
index 1548890971..0d6cdb12a6 100644
--- a/internal/services/rate_limit/list_data_source_schema.go
+++ b/internal/services/rate_limit/list_data_source_schema.go
@@ -106,7 +106,7 @@ func ListDataSourceSchema(ctx context.Context) schema.Schema {
},
},
"description": schema.StringAttribute{
- Description: "An informative summary of the rate limit. This value is sanitized and any tags will be removed.",
+ Description: "An informative summary of the rule. This value is sanitized and any tags will be removed.",
Computed: true,
},
"disabled": schema.BoolAttribute{
diff --git a/internal/services/rate_limit/schema.go b/internal/services/rate_limit/schema.go
index faa3a417a9..70afb07e79 100644
--- a/internal/services/rate_limit/schema.go
+++ b/internal/services/rate_limit/schema.go
@@ -157,7 +157,7 @@ func ResourceSchema(ctx context.Context) schema.Schema {
},
},
"description": schema.StringAttribute{
- Description: "An informative summary of the rate limit. This value is sanitized and any tags will be removed.",
+ Description: "An informative summary of the rule. This value is sanitized and any tags will be removed.",
Computed: true,
},
"disabled": schema.BoolAttribute{
diff --git a/internal/services/user_agent_blocking_rule/data_source.go b/internal/services/user_agent_blocking_rule/data_source.go
index 79c45b7402..944a5034fd 100644
--- a/internal/services/user_agent_blocking_rule/data_source.go
+++ b/internal/services/user_agent_blocking_rule/data_source.go
@@ -57,6 +57,36 @@ func (d *UserAgentBlockingRuleDataSource) Read(ctx context.Context, req datasour
return
}
+ if data.Filter != nil {
+ params, diags := data.toListParams(ctx)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ env := UserAgentBlockingRulesResultListDataSourceEnvelope{}
+ page, err := d.client.Firewall.UARules.List(ctx, params)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to make http request", err.Error())
+ return
+ }
+
+ bytes := []byte(page.JSON.RawJSON())
+ err = apijson.UnmarshalComputed(bytes, &env)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to unmarshal http request", err.Error())
+ return
+ }
+
+ if count := len(env.Result.Elements()); count != 1 {
+ resp.Diagnostics.AddError("failed to find exactly one result", fmt.Sprint(count)+" found")
+ return
+ }
+ ts, diags := env.Result.AsStructSliceT(ctx)
+ resp.Diagnostics.Append(diags...)
+ data.UARuleID = ts[0].ID
+ }
+
params, diags := data.toReadParams(ctx)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
diff --git a/internal/services/user_agent_blocking_rule/data_source_model.go b/internal/services/user_agent_blocking_rule/data_source_model.go
index e7b35ed62d..7d24cf71af 100644
--- a/internal/services/user_agent_blocking_rule/data_source_model.go
+++ b/internal/services/user_agent_blocking_rule/data_source_model.go
@@ -7,6 +7,7 @@ import (
"github.com/cloudflare/cloudflare-go/v4"
"github.com/cloudflare/cloudflare-go/v4/firewall"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/customfield"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types"
)
@@ -16,8 +17,14 @@ type UserAgentBlockingRuleResultDataSourceEnvelope struct {
}
type UserAgentBlockingRuleDataSourceModel struct {
- UARuleID types.String `tfsdk:"ua_rule_id" path:"ua_rule_id,required"`
- ZoneID types.String `tfsdk:"zone_id" path:"zone_id,required"`
+ ID types.String `tfsdk:"id" path:"ua_rule_id,computed"`
+ UARuleID types.String `tfsdk:"ua_rule_id" path:"ua_rule_id,optional"`
+ ZoneID types.String `tfsdk:"zone_id" path:"zone_id,required"`
+ Description types.String `tfsdk:"description" json:"description,computed"`
+ Mode types.String `tfsdk:"mode" json:"mode,computed"`
+ Paused types.Bool `tfsdk:"paused" json:"paused,computed"`
+ Configuration customfield.NestedObject[UserAgentBlockingRuleConfigurationDataSourceModel] `tfsdk:"configuration" json:"configuration,computed"`
+ Filter *UserAgentBlockingRuleFindOneByDataSourceModel `tfsdk:"filter"`
}
func (m *UserAgentBlockingRuleDataSourceModel) toReadParams(_ context.Context) (params firewall.UARuleGetParams, diags diag.Diagnostics) {
@@ -27,3 +34,32 @@ func (m *UserAgentBlockingRuleDataSourceModel) toReadParams(_ context.Context) (
return
}
+
+func (m *UserAgentBlockingRuleDataSourceModel) toListParams(_ context.Context) (params firewall.UARuleListParams, diags diag.Diagnostics) {
+ params = firewall.UARuleListParams{
+ ZoneID: cloudflare.F(m.ZoneID.ValueString()),
+ }
+
+ if !m.Filter.Description.IsNull() {
+ params.Description = cloudflare.F(m.Filter.Description.ValueString())
+ }
+ if !m.Filter.Paused.IsNull() {
+ params.Paused = cloudflare.F(m.Filter.Paused.ValueBool())
+ }
+ if !m.Filter.UserAgent.IsNull() {
+ params.UserAgent = cloudflare.F(m.Filter.UserAgent.ValueString())
+ }
+
+ return
+}
+
+type UserAgentBlockingRuleConfigurationDataSourceModel struct {
+ Target types.String `tfsdk:"target" json:"target,computed"`
+ Value types.String `tfsdk:"value" json:"value,computed"`
+}
+
+type UserAgentBlockingRuleFindOneByDataSourceModel struct {
+ Description types.String `tfsdk:"description" query:"description,optional"`
+ Paused types.Bool `tfsdk:"paused" query:"paused,optional"`
+ UserAgent types.String `tfsdk:"user_agent" query:"user_agent,optional"`
+}
diff --git a/internal/services/user_agent_blocking_rule/data_source_schema.go b/internal/services/user_agent_blocking_rule/data_source_schema.go
index a045f7088c..6693ab9ecd 100644
--- a/internal/services/user_agent_blocking_rule/data_source_schema.go
+++ b/internal/services/user_agent_blocking_rule/data_source_schema.go
@@ -5,8 +5,13 @@ package user_agent_blocking_rule
import (
"context"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/customfield"
+ "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator"
+ "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
)
var _ datasource.DataSourceWithConfigValidators = (*UserAgentBlockingRuleDataSource)(nil)
@@ -14,14 +19,70 @@ var _ datasource.DataSourceWithConfigValidators = (*UserAgentBlockingRuleDataSou
func DataSourceSchema(ctx context.Context) schema.Schema {
return schema.Schema{
Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Description: "The unique identifier of the User Agent Blocking rule.",
+ Computed: true,
+ },
"ua_rule_id": schema.StringAttribute{
Description: "The unique identifier of the User Agent Blocking rule.",
- Required: true,
+ Optional: true,
},
"zone_id": schema.StringAttribute{
Description: "Defines an identifier.",
Required: true,
},
+ "description": schema.StringAttribute{
+ Description: "An informative summary of the rule.",
+ Computed: true,
+ },
+ "mode": schema.StringAttribute{
+ Description: "The action to apply to a matched request.\nAvailable values: \"block\", \"challenge\", \"js_challenge\", \"managed_challenge\".",
+ Computed: true,
+ Validators: []validator.String{
+ stringvalidator.OneOfCaseInsensitive(
+ "block",
+ "challenge",
+ "js_challenge",
+ "managed_challenge",
+ ),
+ },
+ },
+ "paused": schema.BoolAttribute{
+ Description: "When true, indicates that the rule is currently paused.",
+ Computed: true,
+ },
+ "configuration": schema.SingleNestedAttribute{
+ Description: "The configuration object for the current rule.",
+ Computed: true,
+ CustomType: customfield.NewNestedObjectType[UserAgentBlockingRuleConfigurationDataSourceModel](ctx),
+ Attributes: map[string]schema.Attribute{
+ "target": schema.StringAttribute{
+ Description: "The configuration target for this rule. You must set the target to `ua` for User Agent Blocking rules.",
+ Computed: true,
+ },
+ "value": schema.StringAttribute{
+ Description: "The exact user agent string to match. This value will be compared to the received `User-Agent` HTTP header value.",
+ Computed: true,
+ },
+ },
+ },
+ "filter": schema.SingleNestedAttribute{
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "description": schema.StringAttribute{
+ Description: "A string to search for in the description of existing rules.",
+ Optional: true,
+ },
+ "paused": schema.BoolAttribute{
+ Description: "When true, indicates that the rule is currently paused.",
+ Optional: true,
+ },
+ "user_agent": schema.StringAttribute{
+ Description: "A string to search for in the user agent values of existing rules.",
+ Optional: true,
+ },
+ },
+ },
},
}
}
@@ -31,5 +92,7 @@ func (d *UserAgentBlockingRuleDataSource) Schema(ctx context.Context, req dataso
}
func (d *UserAgentBlockingRuleDataSource) ConfigValidators(_ context.Context) []datasource.ConfigValidator {
- return []datasource.ConfigValidator{}
+ return []datasource.ConfigValidator{
+ datasourcevalidator.ExactlyOneOf(path.MatchRoot("ua_rule_id"), path.MatchRoot("filter")),
+ }
}
diff --git a/internal/services/user_agent_blocking_rule/list_data_source_model.go b/internal/services/user_agent_blocking_rule/list_data_source_model.go
index f859070d69..da4b6cae41 100644
--- a/internal/services/user_agent_blocking_rule/list_data_source_model.go
+++ b/internal/services/user_agent_blocking_rule/list_data_source_model.go
@@ -17,12 +17,12 @@ type UserAgentBlockingRulesResultListDataSourceEnvelope struct {
}
type UserAgentBlockingRulesDataSourceModel struct {
- ZoneID types.String `tfsdk:"zone_id" path:"zone_id,required"`
- Description types.String `tfsdk:"description" query:"description,optional"`
- DescriptionSearch types.String `tfsdk:"description_search" query:"description_search,optional"`
- UASearch types.String `tfsdk:"ua_search" query:"ua_search,optional"`
- MaxItems types.Int64 `tfsdk:"max_items"`
- Result customfield.NestedObjectList[UserAgentBlockingRulesResultDataSourceModel] `tfsdk:"result"`
+ ZoneID types.String `tfsdk:"zone_id" path:"zone_id,required"`
+ Description types.String `tfsdk:"description" query:"description,optional"`
+ Paused types.Bool `tfsdk:"paused" query:"paused,optional"`
+ UserAgent types.String `tfsdk:"user_agent" query:"user_agent,optional"`
+ MaxItems types.Int64 `tfsdk:"max_items"`
+ Result customfield.NestedObjectList[UserAgentBlockingRulesResultDataSourceModel] `tfsdk:"result"`
}
func (m *UserAgentBlockingRulesDataSourceModel) toListParams(_ context.Context) (params firewall.UARuleListParams, diags diag.Diagnostics) {
@@ -33,11 +33,11 @@ func (m *UserAgentBlockingRulesDataSourceModel) toListParams(_ context.Context)
if !m.Description.IsNull() {
params.Description = cloudflare.F(m.Description.ValueString())
}
- if !m.DescriptionSearch.IsNull() {
- params.DescriptionSearch = cloudflare.F(m.DescriptionSearch.ValueString())
+ if !m.Paused.IsNull() {
+ params.Paused = cloudflare.F(m.Paused.ValueBool())
}
- if !m.UASearch.IsNull() {
- params.UASearch = cloudflare.F(m.UASearch.ValueString())
+ if !m.UserAgent.IsNull() {
+ params.UserAgent = cloudflare.F(m.UserAgent.ValueString())
}
return
diff --git a/internal/services/user_agent_blocking_rule/list_data_source_schema.go b/internal/services/user_agent_blocking_rule/list_data_source_schema.go
index fbbb6b9705..f87e2df07f 100644
--- a/internal/services/user_agent_blocking_rule/list_data_source_schema.go
+++ b/internal/services/user_agent_blocking_rule/list_data_source_schema.go
@@ -26,11 +26,11 @@ func ListDataSourceSchema(ctx context.Context) schema.Schema {
Description: "A string to search for in the description of existing rules.",
Optional: true,
},
- "description_search": schema.StringAttribute{
- Description: "A string to search for in the description of existing rules.",
+ "paused": schema.BoolAttribute{
+ Description: "When true, indicates that the rule is currently paused.",
Optional: true,
},
- "ua_search": schema.StringAttribute{
+ "user_agent": schema.StringAttribute{
Description: "A string to search for in the user agent values of existing rules.",
Optional: true,
},
diff --git a/internal/services/user_agent_blocking_rule/model.go b/internal/services/user_agent_blocking_rule/model.go
index 9b6eb6b86a..3658285a71 100644
--- a/internal/services/user_agent_blocking_rule/model.go
+++ b/internal/services/user_agent_blocking_rule/model.go
@@ -12,10 +12,12 @@ type UserAgentBlockingRuleResultEnvelope struct {
}
type UserAgentBlockingRuleModel struct {
+ ID types.String `tfsdk:"id" json:"id,computed"`
ZoneID types.String `tfsdk:"zone_id" path:"zone_id,required"`
- UARuleID types.String `tfsdk:"ua_rule_id" path:"ua_rule_id,optional"`
- Mode types.String `tfsdk:"mode" json:"mode,required,no_refresh"`
- Configuration *UserAgentBlockingRuleConfigurationModel `tfsdk:"configuration" json:"configuration,required,no_refresh"`
+ Mode types.String `tfsdk:"mode" json:"mode,required"`
+ Configuration *UserAgentBlockingRuleConfigurationModel `tfsdk:"configuration" json:"configuration,required"`
+ Description types.String `tfsdk:"description" json:"description,optional"`
+ Paused types.Bool `tfsdk:"paused" json:"paused,optional"`
}
func (m UserAgentBlockingRuleModel) MarshalJSON() (data []byte, err error) {
diff --git a/internal/services/user_agent_blocking_rule/resource.go b/internal/services/user_agent_blocking_rule/resource.go
index ff013ef572..0f7c4e4343 100644
--- a/internal/services/user_agent_blocking_rule/resource.go
+++ b/internal/services/user_agent_blocking_rule/resource.go
@@ -12,13 +12,16 @@ import (
"github.com/cloudflare/cloudflare-go/v4/firewall"
"github.com/cloudflare/cloudflare-go/v4/option"
"github.com/cloudflare/terraform-provider-cloudflare/internal/apijson"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/importpath"
"github.com/cloudflare/terraform-provider-cloudflare/internal/logging"
"github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/types"
)
// Ensure provider defined types fully satisfy framework interfaces.
var _ resource.ResourceWithConfigure = (*UserAgentBlockingRuleResource)(nil)
var _ resource.ResourceWithModifyPlan = (*UserAgentBlockingRuleResource)(nil)
+var _ resource.ResourceWithImportState = (*UserAgentBlockingRuleResource)(nil)
func NewResource() resource.Resource {
return &UserAgentBlockingRuleResource{}
@@ -118,7 +121,7 @@ func (r *UserAgentBlockingRuleResource) Update(ctx context.Context, req resource
env := UserAgentBlockingRuleResultEnvelope{*data}
_, err = r.client.Firewall.UARules.Update(
ctx,
- data.UARuleID.ValueString(),
+ data.ID.ValueString(),
firewall.UARuleUpdateParams{
ZoneID: cloudflare.F(data.ZoneID.ValueString()),
},
@@ -154,7 +157,7 @@ func (r *UserAgentBlockingRuleResource) Read(ctx context.Context, req resource.R
env := UserAgentBlockingRuleResultEnvelope{*data}
_, err := r.client.Firewall.UARules.Get(
ctx,
- data.UARuleID.ValueString(),
+ data.ID.ValueString(),
firewall.UARuleGetParams{
ZoneID: cloudflare.F(data.ZoneID.ValueString()),
},
@@ -192,7 +195,7 @@ func (r *UserAgentBlockingRuleResource) Delete(ctx context.Context, req resource
_, err := r.client.Firewall.UARules.Delete(
ctx,
- data.UARuleID.ValueString(),
+ data.ID.ValueString(),
firewall.UARuleDeleteParams{
ZoneID: cloudflare.F(data.ZoneID.ValueString()),
},
@@ -206,6 +209,51 @@ func (r *UserAgentBlockingRuleResource) Delete(ctx context.Context, req resource
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
+func (r *UserAgentBlockingRuleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ var data *UserAgentBlockingRuleModel = new(UserAgentBlockingRuleModel)
+
+ path_zone_id := ""
+ path_ua_rule_id := ""
+ diags := importpath.ParseImportID(
+ req.ID,
+ "/",
+ &path_zone_id,
+ &path_ua_rule_id,
+ )
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ data.ZoneID = types.StringValue(path_zone_id)
+ data.ID = types.StringValue(path_ua_rule_id)
+
+ res := new(http.Response)
+ env := UserAgentBlockingRuleResultEnvelope{*data}
+ _, err := r.client.Firewall.UARules.Get(
+ ctx,
+ path_ua_rule_id,
+ firewall.UARuleGetParams{
+ ZoneID: cloudflare.F(path_zone_id),
+ },
+ option.WithResponseBodyInto(&res),
+ option.WithMiddleware(logging.Middleware(ctx)),
+ )
+ if err != nil {
+ resp.Diagnostics.AddError("failed to make http request", err.Error())
+ return
+ }
+ bytes, _ := io.ReadAll(res.Body)
+ err = apijson.Unmarshal(bytes, &env)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to deserialize http request", err.Error())
+ return
+ }
+ data = &env.Result
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
func (r *UserAgentBlockingRuleResource) ModifyPlan(_ context.Context, _ resource.ModifyPlanRequest, _ *resource.ModifyPlanResponse) {
}
diff --git a/internal/services/user_agent_blocking_rule/schema.go b/internal/services/user_agent_blocking_rule/schema.go
index 1e147f6783..6d8de34c54 100644
--- a/internal/services/user_agent_blocking_rule/schema.go
+++ b/internal/services/user_agent_blocking_rule/schema.go
@@ -18,16 +18,16 @@ var _ resource.ResourceWithConfigValidators = (*UserAgentBlockingRuleResource)(n
func ResourceSchema(ctx context.Context) schema.Schema {
return schema.Schema{
Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Description: "The unique identifier of the User Agent Blocking rule.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
+ },
"zone_id": schema.StringAttribute{
Description: "Defines an identifier.",
Required: true,
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
},
- "ua_rule_id": schema.StringAttribute{
- Description: "The unique identifier of the User Agent Blocking rule.",
- Optional: true,
- PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
- },
"mode": schema.StringAttribute{
Description: "The action to apply to a matched request.\nAvailable values: \"block\", \"challenge\", \"whitelist\", \"js_challenge\", \"managed_challenge\".",
Required: true,
@@ -57,6 +57,14 @@ func ResourceSchema(ctx context.Context) schema.Schema {
},
},
},
+ "description": schema.StringAttribute{
+ Description: "An informative summary of the rule. This value is sanitized and any tags will be removed.",
+ Optional: true,
+ },
+ "paused": schema.BoolAttribute{
+ Description: "When true, indicates that the rule is currently paused.",
+ Optional: true,
+ },
},
}
}
diff --git a/internal/services/workers_kv/data_source_schema.go b/internal/services/workers_kv/data_source_schema.go
index 229862c987..0f33e89d6a 100644
--- a/internal/services/workers_kv/data_source_schema.go
+++ b/internal/services/workers_kv/data_source_schema.go
@@ -15,7 +15,7 @@ func DataSourceSchema(ctx context.Context) schema.Schema {
return schema.Schema{
Attributes: map[string]schema.Attribute{
"account_id": schema.StringAttribute{
- Description: "Identifier",
+ Description: "Identifier.",
Required: true,
},
"key_name": schema.StringAttribute{
diff --git a/internal/services/workers_kv/model.go b/internal/services/workers_kv/model.go
index 13817426ea..d0cbf4b0c1 100644
--- a/internal/services/workers_kv/model.go
+++ b/internal/services/workers_kv/model.go
@@ -7,6 +7,7 @@ import (
"mime/multipart"
"github.com/cloudflare/terraform-provider-cloudflare/internal/apiform"
+ "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
"github.com/hashicorp/terraform-plugin-framework/types"
)
@@ -15,12 +16,12 @@ type WorkersKVResultEnvelope struct {
}
type WorkersKVModel struct {
- ID types.String `tfsdk:"id" json:"-,computed"`
- KeyName types.String `tfsdk:"key_name" path:"key_name,required"`
- AccountID types.String `tfsdk:"account_id" path:"account_id,required"`
- NamespaceID types.String `tfsdk:"namespace_id" path:"namespace_id,required"`
- Metadata types.String `tfsdk:"metadata" json:"metadata,optional,no_refresh"`
- Value types.String `tfsdk:"value" json:"value,required,no_refresh"`
+ ID types.String `tfsdk:"id" json:"-,computed"`
+ KeyName types.String `tfsdk:"key_name" path:"key_name,required"`
+ AccountID types.String `tfsdk:"account_id" path:"account_id,required"`
+ NamespaceID types.String `tfsdk:"namespace_id" path:"namespace_id,required"`
+ Value types.String `tfsdk:"value" json:"value,required,no_refresh"`
+ Metadata jsontypes.Normalized `tfsdk:"metadata" json:"metadata,optional,no_refresh"`
}
func (r WorkersKVModel) MarshalMultipart() (data []byte, contentType string, err error) {
diff --git a/internal/services/workers_kv/schema.go b/internal/services/workers_kv/schema.go
index 7aa341cbfe..5cda5f704a 100644
--- a/internal/services/workers_kv/schema.go
+++ b/internal/services/workers_kv/schema.go
@@ -5,6 +5,7 @@ package workers_kv
import (
"context"
+ "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
@@ -27,7 +28,7 @@ func ResourceSchema(ctx context.Context) schema.Schema {
PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown(), stringplanmodifier.RequiresReplace()},
},
"account_id": schema.StringAttribute{
- Description: "Identifier",
+ Description: "Identifier.",
Required: true,
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
},
@@ -36,14 +37,14 @@ func ResourceSchema(ctx context.Context) schema.Schema {
Required: true,
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
},
- "metadata": schema.StringAttribute{
- Description: "Arbitrary JSON to be associated with a key/value pair.",
- Optional: true,
- },
"value": schema.StringAttribute{
Description: "A byte sequence to be stored, up to 25 MiB in length.",
Required: true,
},
+ "metadata": schema.StringAttribute{
+ Optional: true,
+ CustomType: jsontypes.NormalizedType{},
+ },
},
}
}
diff --git a/internal/services/workers_kv_namespace/data_source_schema.go b/internal/services/workers_kv_namespace/data_source_schema.go
index 4334a2b91f..7732ca5276 100644
--- a/internal/services/workers_kv_namespace/data_source_schema.go
+++ b/internal/services/workers_kv_namespace/data_source_schema.go
@@ -27,7 +27,7 @@ func DataSourceSchema(ctx context.Context) schema.Schema {
Optional: true,
},
"account_id": schema.StringAttribute{
- Description: "Identifier",
+ Description: "Identifier.",
Required: true,
},
"beta": schema.BoolAttribute{
diff --git a/internal/services/workers_kv_namespace/list_data_source_schema.go b/internal/services/workers_kv_namespace/list_data_source_schema.go
index d574842856..7dbe18dadb 100644
--- a/internal/services/workers_kv_namespace/list_data_source_schema.go
+++ b/internal/services/workers_kv_namespace/list_data_source_schema.go
@@ -19,7 +19,7 @@ func ListDataSourceSchema(ctx context.Context) schema.Schema {
return schema.Schema{
Attributes: map[string]schema.Attribute{
"account_id": schema.StringAttribute{
- Description: "Identifier",
+ Description: "Identifier.",
Required: true,
},
"direction": schema.StringAttribute{
diff --git a/internal/services/workers_kv_namespace/schema.go b/internal/services/workers_kv_namespace/schema.go
index 800b1b61f4..97fd9c74b6 100644
--- a/internal/services/workers_kv_namespace/schema.go
+++ b/internal/services/workers_kv_namespace/schema.go
@@ -22,7 +22,7 @@ func ResourceSchema(ctx context.Context) schema.Schema {
PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
},
"account_id": schema.StringAttribute{
- Description: "Identifier",
+ Description: "Identifier.",
Required: true,
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
},
diff --git a/internal/services/zero_trust_access_application/model.go b/internal/services/zero_trust_access_application/model.go
index d2099eb85b..0206f57f8a 100644
--- a/internal/services/zero_trust_access_application/model.go
+++ b/internal/services/zero_trust_access_application/model.go
@@ -24,10 +24,10 @@ type ZeroTrustAccessApplicationModel struct {
CustomDenyMessage types.String `tfsdk:"custom_deny_message" json:"custom_deny_message,optional"`
CustomDenyURL types.String `tfsdk:"custom_deny_url" json:"custom_deny_url,optional"`
CustomNonIdentityDenyURL types.String `tfsdk:"custom_non_identity_deny_url" json:"custom_non_identity_deny_url,optional"`
- Domain types.String `tfsdk:"domain" json:"domain,optional"`
+ Domain types.String `tfsdk:"domain" json:"domain,computed_optional"`
HeaderBgColor types.String `tfsdk:"header_bg_color" json:"header_bg_color,optional"`
LogoURL types.String `tfsdk:"logo_url" json:"logo_url,optional"`
- Name types.String `tfsdk:"name" json:"name,optional"`
+ Name types.String `tfsdk:"name" json:"name,computed_optional"`
OptionsPreflightBypass types.Bool `tfsdk:"options_preflight_bypass" json:"options_preflight_bypass,optional"`
ReadServiceTokensFromHeader types.String `tfsdk:"read_service_tokens_from_header" json:"read_service_tokens_from_header,optional"`
SameSiteCookieAttribute types.String `tfsdk:"same_site_cookie_attribute" json:"same_site_cookie_attribute,optional"`
@@ -41,21 +41,19 @@ type ZeroTrustAccessApplicationModel struct {
SCIMConfig *ZeroTrustAccessApplicationSCIMConfigModel `tfsdk:"scim_config" json:"scim_config,optional"`
TargetCriteria *[]*ZeroTrustAccessApplicationTargetCriteriaModel `tfsdk:"target_criteria" json:"target_criteria,optional"`
AppLauncherVisible types.Bool `tfsdk:"app_launcher_visible" json:"app_launcher_visible,computed_optional"`
- AutoRedirectToIdentity types.Bool `tfsdk:"auto_redirect_to_identity" json:"auto_redirect_to_identity,computed_optional"`
- EnableBindingCookie types.Bool `tfsdk:"enable_binding_cookie" json:"enable_binding_cookie,computed_optional"`
+ AutoRedirectToIdentity types.Bool `tfsdk:"auto_redirect_to_identity" json:"auto_redirect_to_identity,optional"`
+ EnableBindingCookie types.Bool `tfsdk:"enable_binding_cookie" json:"enable_binding_cookie,optional"`
HTTPOnlyCookieAttribute types.Bool `tfsdk:"http_only_cookie_attribute" json:"http_only_cookie_attribute,computed_optional"`
- PathCookieAttribute types.Bool `tfsdk:"path_cookie_attribute" json:"path_cookie_attribute,computed_optional"`
+ PathCookieAttribute types.Bool `tfsdk:"path_cookie_attribute" json:"path_cookie_attribute,optional"`
SessionDuration types.String `tfsdk:"session_duration" json:"session_duration,computed_optional"`
SkipAppLauncherLoginPage types.Bool `tfsdk:"skip_app_launcher_login_page" json:"skip_app_launcher_login_page,computed_optional"`
SelfHostedDomains customfield.List[types.String] `tfsdk:"self_hosted_domains" json:"self_hosted_domains,computed_optional"`
- Tags customfield.List[types.String] `tfsdk:"tags" json:"tags,computed_optional"`
+ Tags customfield.List[types.String] `tfsdk:"tags" json:"tags,optional"`
Destinations customfield.NestedObjectList[ZeroTrustAccessApplicationDestinationsModel] `tfsdk:"destinations" json:"destinations,computed_optional"`
- LandingPageDesign customfield.NestedObject[ZeroTrustAccessApplicationLandingPageDesignModel] `tfsdk:"landing_page_design" json:"landing_page_design,computed_optional"`
- Policies customfield.NestedObjectList[ZeroTrustAccessApplicationPoliciesModel] `tfsdk:"policies" json:"policies,computed_optional"`
- SaaSApp customfield.NestedObject[ZeroTrustAccessApplicationSaaSAppModel] `tfsdk:"saas_app" json:"saas_app,computed_optional"`
+ LandingPageDesign customfield.NestedObject[ZeroTrustAccessApplicationLandingPageDesignModel] `tfsdk:"landing_page_design" json:"landing_page_design,optional"`
+ Policies *[]ZeroTrustAccessApplicationPoliciesModel `tfsdk:"policies" json:"policies,optional"`
+ SaaSApp customfield.NestedObject[ZeroTrustAccessApplicationSaaSAppModel] `tfsdk:"saas_app" json:"saas_app,optional"`
AUD types.String `tfsdk:"aud" json:"aud,computed"`
- CreatedAt timetypes.RFC3339 `tfsdk:"created_at" json:"created_at,computed" format:"date-time"`
- UpdatedAt timetypes.RFC3339 `tfsdk:"updated_at" json:"updated_at,computed" format:"date-time"`
}
func (m ZeroTrustAccessApplicationModel) MarshalJSON() (data []byte, err error) {
@@ -144,13 +142,13 @@ type ZeroTrustAccessApplicationLandingPageDesignModel struct {
type ZeroTrustAccessApplicationPoliciesModel struct {
ID types.String `tfsdk:"id" json:"id,optional"`
- Precedence types.Int64 `tfsdk:"precedence" json:"precedence,optional"`
+ Precedence types.Int64 `tfsdk:"precedence" json:"precedence,computed_optional"`
Decision types.String `tfsdk:"decision" json:"decision,optional"`
- Include customfield.NestedObjectList[ZeroTrustAccessApplicationPoliciesIncludeModel] `tfsdk:"include" json:"include,computed_optional"`
+ Include customfield.NestedObjectList[ZeroTrustAccessApplicationPoliciesIncludeModel] `tfsdk:"include" json:"include,optional"`
Name types.String `tfsdk:"name" json:"name,optional"`
ConnectionRules *ZeroTrustAccessApplicationPoliciesConnectionRulesModel `tfsdk:"connection_rules" json:"connection_rules,optional"`
- Exclude customfield.NestedObjectList[ZeroTrustAccessApplicationPoliciesExcludeModel] `tfsdk:"exclude" json:"exclude,computed_optional"`
- Require customfield.NestedObjectList[ZeroTrustAccessApplicationPoliciesRequireModel] `tfsdk:"require" json:"require,computed_optional"`
+ Exclude customfield.NestedObjectList[ZeroTrustAccessApplicationPoliciesExcludeModel] `tfsdk:"exclude" json:"exclude,optional"`
+ Require customfield.NestedObjectList[ZeroTrustAccessApplicationPoliciesRequireModel] `tfsdk:"require" json:"require,optional"`
}
type ZeroTrustAccessApplicationPoliciesIncludeModel struct {
@@ -529,18 +527,18 @@ type ZeroTrustAccessApplicationSaaSAppModel struct {
CustomAttributes *[]*ZeroTrustAccessApplicationSaaSAppCustomAttributesModel `tfsdk:"custom_attributes" json:"custom_attributes,optional"`
DefaultRelayState types.String `tfsdk:"default_relay_state" json:"default_relay_state,optional"`
IdPEntityID types.String `tfsdk:"idp_entity_id" json:"idp_entity_id,computed_optional"`
- NameIDFormat types.String `tfsdk:"name_id_format" json:"name_id_format,optional"`
+ NameIDFormat types.String `tfsdk:"name_id_format" json:"name_id_format,computed_optional"`
NameIDTransformJsonata types.String `tfsdk:"name_id_transform_jsonata" json:"name_id_transform_jsonata,optional"`
- PublicKey types.String `tfsdk:"public_key" json:"public_key,computed_optional"`
+ PublicKey types.String `tfsdk:"public_key" json:"public_key,computed"`
SAMLAttributeTransformJsonata types.String `tfsdk:"saml_attribute_transform_jsonata" json:"saml_attribute_transform_jsonata,optional"`
SPEntityID types.String `tfsdk:"sp_entity_id" json:"sp_entity_id,optional"`
SSOEndpoint types.String `tfsdk:"sso_endpoint" json:"sso_endpoint,computed_optional"`
UpdatedAt timetypes.RFC3339 `tfsdk:"updated_at" json:"updated_at,computed" format:"date-time"`
- AccessTokenLifetime types.String `tfsdk:"access_token_lifetime" json:"access_token_lifetime,optional"`
+ AccessTokenLifetime types.String `tfsdk:"access_token_lifetime" json:"access_token_lifetime,computed_optional"`
AllowPKCEWithoutClientSecret types.Bool `tfsdk:"allow_pkce_without_client_secret" json:"allow_pkce_without_client_secret,optional"`
AppLauncherURL types.String `tfsdk:"app_launcher_url" json:"app_launcher_url,optional"`
- ClientID types.String `tfsdk:"client_id" json:"client_id,computed_optional"`
- ClientSecret types.String `tfsdk:"client_secret" json:"client_secret,computed_optional"`
+ ClientID types.String `tfsdk:"client_id" json:"client_id,computed"`
+ ClientSecret types.String `tfsdk:"client_secret" json:"client_secret,computed"`
CustomClaims *[]*ZeroTrustAccessApplicationSaaSAppCustomClaimsModel `tfsdk:"custom_claims" json:"custom_claims,optional"`
GrantTypes *[]types.String `tfsdk:"grant_types" json:"grant_types,optional"`
GroupFilterRegex types.String `tfsdk:"group_filter_regex" json:"group_filter_regex,optional"`
diff --git a/internal/services/zero_trust_access_application/normalizations.go b/internal/services/zero_trust_access_application/normalizations.go
new file mode 100644
index 0000000000..36cab24f7d
--- /dev/null
+++ b/internal/services/zero_trust_access_application/normalizations.go
@@ -0,0 +1,178 @@
+package zero_trust_access_application
+
+import (
+ "context"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/customfield"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/tfsdk"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+ "slices"
+)
+
+func normalizeEmptyAndNullString(data *basetypes.StringValue, stateData basetypes.StringValue) {
+ if data.ValueString() != "" || stateData.ValueString() != "" {
+ return
+ }
+ *data = stateData
+}
+
+func normalizeFalseAndNullBool(data *basetypes.BoolValue, stateData basetypes.BoolValue) {
+ if data.ValueBool() || stateData.ValueBool() {
+ return
+ }
+ *data = stateData
+}
+
+func normalizeTrueAndNullBool(data *basetypes.BoolValue, stateData basetypes.BoolValue) {
+ if (!data.IsNull() && !data.ValueBool()) || (!stateData.IsNull() && !stateData.ValueBool()) {
+ return
+ }
+ if stateData.IsUnknown() {
+ return
+ }
+ *data = stateData
+}
+
+type ListField interface {
+ Elements() []attr.Value
+}
+
+func normalizeEmptyAndNullList[T ListField](data *T, stateData T) {
+ if len((*data).Elements()) != 0 || len(stateData.Elements()) != 0 {
+ return
+ }
+ *data = stateData
+}
+
+func normalizeEmptyAndNullSlice[T any](data **[]T, stateData *[]T) {
+ if (*data != nil && len(**data) != 0) || (stateData != nil && len(*stateData) != 0) {
+ return
+ }
+ *data = stateData
+}
+
+type IsNull interface {
+ IsNull() bool
+}
+
+func persistNullFromState[T IsNull](data *T, stateData T) {
+ if stateData.IsNull() {
+ *data = stateData
+ }
+}
+
+func normalizeReadZeroTrustApplicationSamlAppData(data *ZeroTrustAccessApplicationSaaSAppModel, stateData ZeroTrustAccessApplicationSaaSAppModel) {
+ normalizeEmptyAndNullString(&data.SPEntityID, stateData.SPEntityID)
+ normalizeEmptyAndNullString(&data.ConsumerServiceURL, stateData.ConsumerServiceURL)
+}
+
+func normalizeReadZeroTrustApplicationOidcAppData(data *ZeroTrustAccessApplicationSaaSAppModel, stateData ZeroTrustAccessApplicationSaaSAppModel) {
+ // Prevent diffs on the default access_token_lifetime
+ if data.AccessTokenLifetime.ValueString() == "5m" && stateData.AccessTokenLifetime == types.StringNull() {
+ data.AccessTokenLifetime = stateData.AccessTokenLifetime
+ }
+
+ // client_secret is only returned when the app is first created, assigning here from the state
+ // to prevent a diff when the app is updated
+ if !stateData.ClientSecret.IsUnknown() && !stateData.ClientSecret.IsNull() {
+ data.ClientSecret = stateData.ClientSecret
+ }
+
+ normalizeFalseAndNullBool(&data.AllowPKCEWithoutClientSecret, stateData.AllowPKCEWithoutClientSecret)
+}
+
+// Normalizing function to ensure consistency between the state and the meaning of the API response.
+// Alters the API response before applying it to the state by laxing equalities between null & zero-value
+// for some attributes, and nullifies fields that terraform should not be saving in the state.
+func normalizeReadZeroTrustApplicationAPIData(ctx context.Context, data, stateData *ZeroTrustAccessApplicationModel) diag.Diagnostics {
+ diags := make(diag.Diagnostics, 0)
+
+ // Empty `allowed_idps` is the same as a null value. The API might return an empty array, so we need to normalize it
+ // here to avoid a diff
+ normalizeEmptyAndNullSlice(&data.AllowedIdPs, stateData.AllowedIdPs)
+ // `policies` might not be in the configuration, so we need to normalize it here to avoid a diff
+ normalizeEmptyAndNullSlice(&data.Policies, stateData.Policies)
+ // `tags` might not be in the configuration, so we need to normalize it here to avoid a diff
+ normalizeEmptyAndNullList(&data.Tags, stateData.Tags)
+
+ normalizeFalseAndNullBool(&data.EnableBindingCookie, stateData.EnableBindingCookie)
+ normalizeFalseAndNullBool(&data.OptionsPreflightBypass, stateData.OptionsPreflightBypass)
+ normalizeFalseAndNullBool(&data.AutoRedirectToIdentity, stateData.AutoRedirectToIdentity)
+ if slices.Contains(selfHostedAppTypes, data.Type.String()) {
+ normalizeTrueAndNullBool(&data.HTTPOnlyCookieAttribute, stateData.HTTPOnlyCookieAttribute)
+ }
+
+ if !data.SaaSApp.IsNull() && !stateData.SaaSApp.IsNull() {
+ var dataSaasApp, stateDataSaasApp ZeroTrustAccessApplicationSaaSAppModel
+ diags.Append(data.SaaSApp.As(ctx, &dataSaasApp, basetypes.ObjectAsOptions{})...)
+ diags.Append(stateData.SaaSApp.As(ctx, &stateDataSaasApp, basetypes.ObjectAsOptions{})...)
+ if diags.HasError() {
+ return diags
+ }
+
+ switch dataSaasApp.AuthType.ValueString() {
+ case "saml":
+ normalizeReadZeroTrustApplicationSamlAppData(&dataSaasApp, stateDataSaasApp)
+ case "oidc":
+ normalizeReadZeroTrustApplicationOidcAppData(&dataSaasApp, stateDataSaasApp)
+ }
+
+ var saasDiags diag.Diagnostics
+ data.SaaSApp, saasDiags = customfield.NewObject[ZeroTrustAccessApplicationSaaSAppModel](ctx, &dataSaasApp)
+ diags.Append(saasDiags...)
+ if diags.HasError() {
+ return diags
+ }
+ }
+
+ if data.Policies != nil && stateData.Policies != nil {
+ for i := range *data.Policies {
+ // Preserve null values from the Terraform state, even if the API response returns actual values.
+ // This is important because the API may populate these fields when it expands the attached reusable policy
+ // from its given ID.
+ //
+ // However, we intentionally avoid storing the full expanded policy inside the application resource's
+ // nested block, as its source of truth is the reusable policy resource itself.
+ // Only the policy ID should be persisted in state for reusable policies.
+ // For legacy policies, the ID should be ignored as they are not a standalone resource, but rather
+ // live as a nested object owned by the application.
+ persistNullFromState(&(*data.Policies)[i].ID, (*stateData.Policies)[i].ID)
+ persistNullFromState(&(*data.Policies)[i].Decision, (*stateData.Policies)[i].Decision)
+ persistNullFromState(&(*data.Policies)[i].Name, (*stateData.Policies)[i].Name)
+ persistNullFromState(&(*data.Policies)[i].Include, (*stateData.Policies)[i].Include)
+ persistNullFromState(&(*data.Policies)[i].Require, (*stateData.Policies)[i].Require)
+ persistNullFromState(&(*data.Policies)[i].Exclude, (*stateData.Policies)[i].Exclude)
+ }
+ }
+
+ if data.SCIMConfig != nil && stateData.SCIMConfig != nil {
+ if data.SCIMConfig.Authentication != nil && stateData.SCIMConfig.Authentication != nil {
+ data.SCIMConfig.Authentication.Password = stateData.SCIMConfig.Authentication.Password
+ data.SCIMConfig.Authentication.Token = stateData.SCIMConfig.Authentication.Token
+ data.SCIMConfig.Authentication.ClientSecret = stateData.SCIMConfig.Authentication.ClientSecret
+ }
+ }
+
+ return diags
+}
+
+// Some fields are write-only sensitive and should not be stored in the state.
+// Usually these secrets are injected in the config from a secret store.
+func loadConfigSensitiveValuesForWriting(ctx context.Context, data *ZeroTrustAccessApplicationModel, cfg *tfsdk.Config) diag.Diagnostics {
+ var (
+ diags = make(diag.Diagnostics, 0)
+ cfgData *ZeroTrustAccessApplicationModel
+ )
+ diags.Append(cfg.Get(ctx, &cfgData)...)
+
+ if data.SCIMConfig != nil && cfgData.SCIMConfig != nil {
+ if data.SCIMConfig.Authentication != nil && cfgData.SCIMConfig.Authentication != nil {
+ data.SCIMConfig.Authentication.Password = cfgData.SCIMConfig.Authentication.Password
+ data.SCIMConfig.Authentication.Token = cfgData.SCIMConfig.Authentication.Token
+ data.SCIMConfig.Authentication.ClientSecret = cfgData.SCIMConfig.Authentication.ClientSecret
+ }
+ }
+ return diags
+}
diff --git a/internal/services/zero_trust_access_application/plan_modifiers.go b/internal/services/zero_trust_access_application/plan_modifiers.go
new file mode 100644
index 0000000000..3e0592931c
--- /dev/null
+++ b/internal/services/zero_trust_access_application/plan_modifiers.go
@@ -0,0 +1,137 @@
+package zero_trust_access_application
+
+import (
+ "context"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/customfield"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+ "regexp"
+ "slices"
+)
+
+var (
+ selfHostedAppTypes = []string{"self_hosted", "ssh", "vnc", "rdp"}
+ saasAppTypes = []string{"saas", "dash_sso"}
+ appLauncherVisibleAppTypes = []string{"self_hosted", "ssh", "vnc", "rdp", "saas", "bookmark", "infrastructure"}
+ targetCompatibleAppTypes = []string{"rdp", "infrastructure"}
+ durationRegex = regexp.MustCompile(`^(?:0|[-+]?(\d+(?:\.\d*)?|\.\d+)(?:ns|us|µs|ms|s|m|h)(?:(\d+(?:\.\d*)?|\.\d+)(?:ns|us|µs|ms|s|m|h))*)$`)
+)
+
+// Sets a specific default value for a computed attribute specific to a set of app types, in case the attribute is unknown.
+// If the app type is not in the list, it sets the second default value.
+func setDefaultAccordingToAppTypes[T attr.Value](wantAppTypes []string, gotAppType string, planAttribute *T, default1, default2 T) {
+ if planAttribute == nil || !(*planAttribute).IsUnknown() {
+ return
+ }
+ if slices.Contains(wantAppTypes, gotAppType) {
+ *planAttribute = default1
+ } else {
+ *planAttribute = default2
+ }
+}
+
+// Sets a specific default value for a computed attribute specific to a given app type, in case the attribute is unknown.
+// If the app type does not match, it sets the second default value.
+func setDefaultAccordingToAppType[T attr.Value](wantAppType string, gotAppType string, planAttribute *T, default1, default2 T) {
+ setDefaultAccordingToAppTypes([]string{wantAppType}, gotAppType, planAttribute, default1, default2)
+}
+
+func modifyPlanForDomains(ctx context.Context, planApp, stateApp *ZeroTrustAccessApplicationModel) {
+ appType := planApp.Type.ValueString()
+
+ setDefaultAccordingToAppTypes(selfHostedAppTypes, appType, &planApp.SelfHostedDomains, customfield.UnknownList[types.String](ctx), customfield.NullList[types.String](ctx))
+ setDefaultAccordingToAppTypes(selfHostedAppTypes, appType, &planApp.Destinations, customfield.UnknownObjectList[ZeroTrustAccessApplicationDestinationsModel](ctx), customfield.NullObjectList[ZeroTrustAccessApplicationDestinationsModel](ctx))
+ setDefaultAccordingToAppTypes(selfHostedAppTypes, appType, &planApp.HTTPOnlyCookieAttribute, types.BoolUnknown(), types.BoolNull())
+
+ // A self_hosted_app's 'domain', 'self_hosted_domains', and 'destinations' are all tied together in the API.
+ // changing one, causes the others to change. So we need to tell TF to set the other two to unknown if any of them
+ // changes from the previous state.
+ if stateApp == nil ||
+ (!planApp.Domain.IsUnknown() && !planApp.Domain.Equal(stateApp.Domain)) ||
+ (!planApp.SelfHostedDomains.IsUnknown() && !planApp.SelfHostedDomains.Equal(stateApp.SelfHostedDomains)) ||
+ (!planApp.Destinations.IsUnknown() && !planApp.Destinations.Equal(stateApp.Destinations)) {
+
+ if planApp.Domain.IsNull() {
+ planApp.Domain = types.StringUnknown()
+ }
+ if planApp.SelfHostedDomains.IsNull() {
+ planApp.SelfHostedDomains = customfield.UnknownList[types.String](ctx)
+ }
+ if planApp.Destinations.IsNull() {
+ planApp.Destinations = customfield.UnknownObjectList[ZeroTrustAccessApplicationDestinationsModel](ctx)
+ }
+ } else {
+ // If the domain, self_hosted_domains, and destinations have not changed, we can copy all of them from the state.
+ planApp.Domain = stateApp.Domain
+ planApp.SelfHostedDomains = stateApp.SelfHostedDomains
+ planApp.Destinations = stateApp.Destinations
+ }
+}
+
+func modifySaasAppNestedObjectPlan(ctx context.Context, planApp *ZeroTrustAccessApplicationModel) diag.Diagnostics {
+ diags := diag.Diagnostics{}
+ if planApp.SaaSApp.IsNull() {
+ return diags
+ }
+ var planSaasApp ZeroTrustAccessApplicationSaaSAppModel
+ diags.Append(planApp.SaaSApp.As(ctx, &planSaasApp, basetypes.ObjectAsOptions{})...)
+
+ oidcType, samlType, currentType := "oidc", "saml", planSaasApp.AuthType.ValueString()
+
+ // These fields are non-existent for non-oidc saas_apps. So we can set them to null and avoid the recurring
+ // diffs due to unknown computed values.
+ setDefaultAccordingToAppType(oidcType, currentType, &planSaasApp.ClientID, types.StringUnknown(), types.StringNull())
+ setDefaultAccordingToAppType(oidcType, currentType, &planSaasApp.ClientSecret, types.StringUnknown(), types.StringNull())
+ setDefaultAccordingToAppType(oidcType, currentType, &planSaasApp.AllowPKCEWithoutClientSecret, types.BoolValue(false), types.BoolNull())
+ setDefaultAccordingToAppType(oidcType, currentType, &planSaasApp.AccessTokenLifetime, types.StringValue("5m"), types.StringNull())
+
+ // These fields are non-existent for non-saml saas_apps. So we can set them to null and avoid the recurring
+ // diffs due to unknown computed values.
+ setDefaultAccordingToAppType(samlType, currentType, &planSaasApp.IdPEntityID, types.StringUnknown(), types.StringNull())
+ setDefaultAccordingToAppType(samlType, currentType, &planSaasApp.NameIDFormat, types.StringUnknown(), types.StringNull())
+ setDefaultAccordingToAppType(samlType, currentType, &planSaasApp.SSOEndpoint, types.StringUnknown(), types.StringNull())
+
+ planApp.SaaSApp, _ = customfield.NewObject[ZeroTrustAccessApplicationSaaSAppModel](ctx, &planSaasApp)
+ return diags
+}
+
+func modifyNestedPoliciesPlan(_ context.Context, planApp *ZeroTrustAccessApplicationModel) {
+ lastKnownPrecedence := 0
+ for i := range *planApp.Policies {
+ if (*planApp.Policies)[i].Precedence.IsUnknown() {
+ (*planApp.Policies)[i].Precedence = types.Int64Value(int64(lastKnownPrecedence + 1))
+ }
+ lastKnownPrecedence = int((*planApp.Policies)[i].Precedence.ValueInt64())
+ }
+}
+
+func modifyPlan(ctx context.Context, req resource.ModifyPlanRequest, res *resource.ModifyPlanResponse) {
+ var planApp, stateApp *ZeroTrustAccessApplicationModel
+ res.Diagnostics.Append(req.Plan.Get(ctx, &planApp)...)
+ res.Diagnostics.Append(req.State.Get(ctx, &stateApp)...)
+ if res.Diagnostics.HasError() || planApp == nil {
+ return
+ }
+
+ modifyPlanForDomains(ctx, planApp, stateApp)
+
+ appType := planApp.Type.ValueString()
+
+ // Add default values for some app type specific attributes
+ setDefaultAccordingToAppTypes(selfHostedAppTypes, appType, &planApp.HTTPOnlyCookieAttribute, types.BoolValue(true), types.BoolNull())
+ setDefaultAccordingToAppTypes(appLauncherVisibleAppTypes, appType, &planApp.AppLauncherVisible, types.BoolValue(true), types.BoolNull())
+ setDefaultAccordingToAppType("app_launcher", appType, &planApp.SkipAppLauncherLoginPage, types.BoolValue(false), types.BoolNull())
+
+ if appType == "saas" {
+ res.Diagnostics.Append(modifySaasAppNestedObjectPlan(ctx, planApp)...)
+ }
+
+ if planApp.Policies != nil {
+ modifyNestedPoliciesPlan(ctx, planApp)
+ }
+
+ res.Plan.Set(ctx, &planApp)
+}
diff --git a/internal/services/zero_trust_access_application/resource.go b/internal/services/zero_trust_access_application/resource.go
index da74f2fb27..62dd826e5c 100644
--- a/internal/services/zero_trust_access_application/resource.go
+++ b/internal/services/zero_trust_access_application/resource.go
@@ -5,9 +5,6 @@ package zero_trust_access_application
import (
"context"
"fmt"
- "io"
- "net/http"
-
"github.com/cloudflare/cloudflare-go/v4"
"github.com/cloudflare/cloudflare-go/v4/option"
"github.com/cloudflare/cloudflare-go/v4/zero_trust"
@@ -16,6 +13,8 @@ import (
"github.com/cloudflare/terraform-provider-cloudflare/internal/logging"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
+ "io"
+ "net/http"
)
// Ensure provider defined types fully satisfy framework interfaces.
@@ -64,11 +63,17 @@ func (r *ZeroTrustAccessApplicationResource) Create(ctx context.Context, req res
return
}
+ resp.Diagnostics.Append(loadConfigSensitiveValuesForWriting(ctx, data, &req.Config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
dataBytes, err := data.MarshalJSON()
if err != nil {
resp.Diagnostics.AddError("failed to serialize http request", err.Error())
return
}
+
res := new(http.Response)
env := ZeroTrustAccessApplicationResultEnvelope{*data}
params := zero_trust.AccessApplicationNewParams{}
@@ -118,6 +123,11 @@ func (r *ZeroTrustAccessApplicationResource) Update(ctx context.Context, req res
return
}
+ resp.Diagnostics.Append(loadConfigSensitiveValuesForWriting(ctx, data, &req.Config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
dataBytes, err := data.MarshalJSONForUpdate(*state)
if err != nil {
resp.Diagnostics.AddError("failed to serialize http request", err.Error())
@@ -153,6 +163,14 @@ func (r *ZeroTrustAccessApplicationResource) Update(ctx context.Context, req res
}
data = &env.Result
+ var planData *ZeroTrustAccessApplicationModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &planData)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ normalizeReadZeroTrustApplicationAPIData(ctx, data, planData)
+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
@@ -199,6 +217,14 @@ func (r *ZeroTrustAccessApplicationResource) Read(ctx context.Context, req resou
}
data = &env.Result
+ var stateData *ZeroTrustAccessApplicationModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &stateData)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(normalizeReadZeroTrustApplicationAPIData(ctx, data, stateData)...)
+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
@@ -287,6 +313,6 @@ func (r *ZeroTrustAccessApplicationResource) ImportState(ctx context.Context, re
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
-func (r *ZeroTrustAccessApplicationResource) ModifyPlan(_ context.Context, _ resource.ModifyPlanRequest, _ *resource.ModifyPlanResponse) {
-
+func (r *ZeroTrustAccessApplicationResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, res *resource.ModifyPlanResponse) {
+ modifyPlan(ctx, req, res)
}
diff --git a/internal/services/zero_trust_access_application/resource_test.go b/internal/services/zero_trust_access_application/resource_test.go
index 775f5a4060..95673fe179 100644
--- a/internal/services/zero_trust_access_application/resource_test.go
+++ b/internal/services/zero_trust_access_application/resource_test.go
@@ -102,6 +102,11 @@ func TestAccCloudflareAccessApplication_BasicZone(t *testing.T) {
resource.TestCheckResourceAttr(name, "auto_redirect_to_identity", "false"),
),
},
+ {
+ // Ensures no diff on second plan
+ Config: testAccCloudflareAccessApplicationConfigBasic(rnd, domain, cloudflare.ZoneIdentifier(zoneID)),
+ PlanOnly: true,
+ },
},
})
}
@@ -131,6 +136,11 @@ func TestAccCloudflareAccessApplication_BasicAccount(t *testing.T) {
resource.TestCheckResourceAttr(name, "auto_redirect_to_identity", "false"),
),
},
+ {
+ // Ensures no diff on second plan
+ Config: testAccCloudflareAccessApplicationConfigBasic(rnd, domain, cloudflare.AccountIdentifier(accountID)),
+ PlanOnly: true,
+ },
},
})
}
@@ -171,6 +181,11 @@ func TestAccCloudflareAccessApplication_WithSCIMConfigHttpBasic(t *testing.T) {
resource.TestCheckResourceAttr(name, "scim_config.mappings.0.operations.delete", "true"),
),
},
+ {
+ // Ensures no diff on second plan
+ Config: testAccCloudflareAccessApplicationSCIMConfigValidHttpBasic(rnd, accountID, domain),
+ PlanOnly: true,
+ },
},
})
}
@@ -211,6 +226,11 @@ func TestAccCloudflareAccessApplication_UpdateSCIMConfig(t *testing.T) {
resource.TestCheckResourceAttr(name, "scim_config.mappings.0.operations.delete", "true"),
),
},
+ {
+ // Ensures no diff on second plan
+ Config: testAccCloudflareAccessApplicationSCIMConfigValidHttpBasic(rnd, accountID, domain),
+ PlanOnly: true,
+ },
{
Config: testAccCloudflareAccessApplicationSCIMConfigValidOAuthBearerTokenNoMappings(rnd, accountID, domain),
Check: resource.ComposeTestCheckFunc(
@@ -228,6 +248,11 @@ func TestAccCloudflareAccessApplication_UpdateSCIMConfig(t *testing.T) {
resource.TestCheckResourceAttr(name, "scim_config.mappings.#", "0"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessApplicationSCIMConfigValidOAuthBearerTokenNoMappings(rnd, accountID, domain),
+ PlanOnly: true,
+ },
},
})
}
@@ -303,6 +328,11 @@ func TestAccCloudflareAccessApplication_WithSCIMConfigOAuthBearerToken(t *testin
resource.TestCheckResourceAttr(name, "scim_config.mappings.0.operations.delete", "true"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessApplicationSCIMConfigValidOAuthBearerToken(rnd, accountID, domain),
+ PlanOnly: true,
+ },
},
})
}
@@ -347,6 +377,11 @@ func TestAccCloudflareAccessApplication_WithSCIMConfigOAuth2(t *testing.T) {
resource.TestCheckResourceAttr(name, "scim_config.mappings.0.operations.delete", "true"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessApplicationSCIMConfigValidOAuth2(rnd, accountID, domain),
+ PlanOnly: true,
+ },
},
})
}
@@ -394,6 +429,11 @@ func TestAccCloudflareAccessApplication_WithCORS(t *testing.T) {
resource.TestCheckResourceAttr(name, "auto_redirect_to_identity", "false"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessApplicationConfigWithCORS(rnd, zoneID, domain),
+ PlanOnly: true,
+ },
},
})
}
@@ -438,6 +478,11 @@ func TestAccCloudflareAccessApplication_WithSAMLSaas(t *testing.T) {
resource.TestCheckResourceAttr(name, "saas_app.custom_attributes.1.required", "true"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessApplicationConfigWithSAMLSaas(rnd, accountID),
+ PlanOnly: true,
+ },
},
})
}
@@ -481,13 +526,18 @@ func TestAccCloudflareAccessApplication_WithSAMLSaas_Import(t *testing.T) {
Config: testAccCloudflareAccessApplicationConfigWithSAMLSaas(rnd, accountID),
Check: checkFn,
},
- // {
- // ImportState: true,
- // ImportStateVerify: true,
- // ResourceName: name,
- // ImportStateIdPrefix: fmt.Sprintf("%s/", accountID),
- // Check: checkFn,
- // },
+ {
+ ImportState: true,
+ ImportStateVerify: true,
+ ResourceName: name,
+ ImportStateIdPrefix: fmt.Sprintf("accounts/%s/", accountID),
+ Check: checkFn,
+ },
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessApplicationConfigWithSAMLSaas(rnd, accountID),
+ PlanOnly: true,
+ },
},
})
}
@@ -536,6 +586,11 @@ func TestAccCloudflareAccessApplication_WithOIDCSaas(t *testing.T) {
resource.TestCheckResourceAttrSet(name, "saas_app.public_key"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessApplicationConfigWithOIDCSaas(rnd, accountID),
+ PlanOnly: true,
+ },
},
})
}
@@ -585,14 +640,19 @@ func TestAccCloudflareAccessApplication_WithOIDCSaas_Import(t *testing.T) {
Config: testAccCloudflareAccessApplicationConfigWithOIDCSaas(rnd, accountID),
Check: checkFn,
},
- // {
- // ImportState: true,
- // ImportStateVerify: true,
- // ImportStateVerifyIgnore: []string{"saas_app.client_secret"},
- // ResourceName: name,
- // ImportStateIdPrefix: fmt.Sprintf("%s/", accountID),
- // Check: checkFn,
- // },
+ {
+ ImportState: true,
+ ImportStateVerify: true,
+ ImportStateVerifyIgnore: []string{"saas_app.client_secret", "saas_app.allow_pkce_without_client_secret"},
+ ResourceName: name,
+ ImportStateIdPrefix: fmt.Sprintf("accounts/%s/", accountID),
+ Check: checkFn,
+ },
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessApplicationConfigWithOIDCSaas(rnd, accountID),
+ PlanOnly: true,
+ },
},
})
}
@@ -620,6 +680,11 @@ func TestAccCloudflareAccessApplication_WithAutoRedirectToIdentity(t *testing.T)
resource.TestCheckResourceAttr(name, "allowed_idps.#", "1"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessApplicationConfigWithAutoRedirectToIdentity(rnd, zoneID, domain),
+ PlanOnly: true,
+ },
},
})
}
@@ -646,6 +711,11 @@ func TestAccCloudflareAccessApplication_WithEnableBindingCookie(t *testing.T) {
resource.TestCheckResourceAttr(name, "enable_binding_cookie", "true"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessApplicationConfigWithEnableBindingCookie(rnd, zoneID, domain),
+ PlanOnly: true,
+ },
},
})
}
@@ -674,11 +744,16 @@ func TestAccCloudflareAccessApplication_WithCustomDenyFields(t *testing.T) {
resource.TestCheckResourceAttr(name, "custom_non_identity_deny_url", "https://www.blocked.com"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessApplicationConfigWithCustomDenyFields(rnd, zoneID, domain),
+ PlanOnly: true,
+ },
},
})
}
-func TestAccCloudflareAccessApplication_WithADefinedIdps(t *testing.T) {
+func TestAccCloudflareAccessApplication_WithADefinedIdp(t *testing.T) {
rnd := utils.GenerateRandomResourceName()
name := fmt.Sprintf("cloudflare_zero_trust_access_application.%s", rnd)
@@ -701,6 +776,11 @@ func TestAccCloudflareAccessApplication_WithADefinedIdps(t *testing.T) {
resource.TestCheckResourceAttr(name, "allowed_idps.#", "1"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessApplicationConfigWithADefinedIdp(rnd, zoneID, domain, accountID),
+ PlanOnly: true,
+ },
},
})
}
@@ -723,6 +803,11 @@ func TestAccCloudflareAccessApplication_WithMultipleIdpsReordered(t *testing.T)
{
Config: testAccCloudflareAccessApplicationConfigWithMultipleIdps(rnd, zoneID, domain, accountID, idp2, idp1),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessApplicationConfigWithMultipleIdps(rnd, zoneID, domain, accountID, idp2, idp1),
+ PlanOnly: true,
+ },
},
})
}
@@ -749,6 +834,11 @@ func TestAccCloudflareAccessApplication_WithHttpOnlyCookieAttribute(t *testing.T
resource.TestCheckResourceAttr(name, "http_only_cookie_attribute", "true"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessApplicationConfigWithHTTPOnlyCookieAttribute(rnd, zoneID, domain),
+ PlanOnly: true,
+ },
},
})
}
@@ -775,6 +865,11 @@ func TestAccCloudflareAccessApplication_WithHTTPOnlyCookieAttributeSetToFalse(t
resource.TestCheckResourceAttr(name, "http_only_cookie_attribute", "false"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessApplicationConfigWithHTTPOnlyCookieAttributeSetToFalse(rnd, zoneID, domain),
+ PlanOnly: true,
+ },
},
})
}
@@ -801,6 +896,11 @@ func TestAccCloudflareAccessApplication_WithSameSiteCookieAttribute(t *testing.T
resource.TestCheckResourceAttr(name, "same_site_cookie_attribute", "strict"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessApplicationConfigSameSiteCookieAttribute(rnd, zoneID, domain),
+ PlanOnly: true,
+ },
},
})
}
@@ -827,6 +927,11 @@ func TestAccCloudflareAccessApplication_WithLogoURL(t *testing.T) {
resource.TestCheckResourceAttr(name, "logo_url", "https://www.cloudflare.com/img/logo-web-badges/cf-logo-on-white-bg.svg"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessApplicationConfigLogoURL(rnd, zoneID, domain),
+ PlanOnly: true,
+ },
},
})
}
@@ -853,6 +958,11 @@ func TestAccCloudflareAccessApplication_WithSkipInterstitial(t *testing.T) {
resource.TestCheckResourceAttr(name, "skip_interstitial", "true"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessApplicationConfigSkipInterstitial(rnd, zoneID, domain),
+ PlanOnly: true,
+ },
},
})
}
@@ -879,6 +989,11 @@ func TestAccCloudflareAccessApplication_WithAppLauncherVisible(t *testing.T) {
resource.TestCheckResourceAttr(name, "app_launcher_visible", "true"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessApplicationConfigWithAppLauncherVisible(rnd, zoneID, domain),
+ PlanOnly: true,
+ },
},
})
}
@@ -908,6 +1023,11 @@ func TestAccCloudflareAccessApplication_WithSelfHostedDomains(t *testing.T) {
resource.TestCheckResourceAttr(name, "auto_redirect_to_identity", "false"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessApplicationWithSelfHostedDomains(rnd, domain, cloudflare.AccountIdentifier(accountID)),
+ PlanOnly: true,
+ },
},
})
}
@@ -935,6 +1055,41 @@ func TestAccCloudflareAccessApplication_WithDefinedTags(t *testing.T) {
resource.TestCheckResourceAttr(name, "tags.#", "1"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessApplicationConfigWithADefinedTag(rnd, zoneID, domain, accountID),
+ PlanOnly: true,
+ },
+ },
+ })
+}
+
+func TestAccCloudflareAccessApplication_WithLegacyPolicies(t *testing.T) {
+ rnd := utils.GenerateRandomResourceName()
+ name := fmt.Sprintf("cloudflare_zero_trust_access_application.%s", rnd)
+ accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() {
+ acctest.TestAccPreCheck(t)
+ acctest.TestAccPreCheck_AccountID(t)
+ },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+ CheckDestroy: testAccCheckCloudflareAccessApplicationDestroy,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccCloudflareAccessApplicationConfigWithLegacyPolicies(rnd, domain, accountID),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(name, "name", rnd),
+ resource.TestCheckResourceAttr(name, "domain", fmt.Sprintf("%s.%s", rnd, domain)),
+ resource.TestCheckResourceAttr(name, "type", "self_hosted"),
+ resource.TestCheckResourceAttr(name, "policies.#", "3"),
+ ),
+ },
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessApplicationConfigWithLegacyPolicies(rnd, domain, accountID),
+ PlanOnly: true,
+ },
},
})
}
@@ -960,6 +1115,11 @@ func TestAccCloudflareAccessApplication_WithReusablePolicies(t *testing.T) {
resource.TestCheckResourceAttr(name, "policies.#", "2"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessApplicationConfigWithReusablePolicies(rnd, domain, accountID),
+ PlanOnly: true,
+ },
},
})
}
@@ -976,7 +1136,8 @@ func TestAccCloudflareAccessApplication_WithAppLauncherCustomization(t *testing.
ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
CheckDestroy: testAccCheckCloudflareAccessApplicationDestroy,
Steps: []resource.TestStep{
- {Config: testAccessApplicationWithAppLauncherCustomizationFields(rnd, accountID),
+ {
+ Config: testAccessApplicationWithAppLauncherCustomizationFields(rnd, accountID),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, consts.AccountIDSchemaKey, accountID),
resource.TestCheckResourceAttr(name, "type", "app_launcher"),
@@ -993,6 +1154,11 @@ func TestAccCloudflareAccessApplication_WithAppLauncherCustomization(t *testing.
resource.TestCheckResourceAttr(name, "footer_links.0.url", "https://www.cloudflare.com"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccessApplicationWithAppLauncherCustomizationFields(rnd, accountID),
+ PlanOnly: true,
+ },
},
})
}
@@ -1068,7 +1234,8 @@ func testAccCloudflareAccessApplicationWithSelfHostedDomains(rnd string, domain
func testAccCheckCloudflareAccessApplicationDestroy(s *terraform.State) error {
client, clientErr := acctest.SharedV1Client() // TODO(terraform): replace with SharedV2Clent
if clientErr != nil {
- tflog.Error(context.TODO(), fmt.Sprintf("failed to create Cloudflare client: %s", clientErr))
+ tflog.Error(context.TODO(), fmt.Sprintf("failed to create Cloudflare client for destroy check: %s", clientErr))
+ return clientErr
}
for _, rs := range s.RootModule().Resources {
@@ -1124,111 +1291,113 @@ func TestAccCloudflareAccessApplicationWithZoneID(t *testing.T) {
resource.TestCheckResourceAttr(name, consts.ZoneIDSchemaKey, zoneID),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccessApplicationWithZoneIDUpdated(rnd, zone, zoneID),
+ PlanOnly: true,
+ },
},
})
}
-// TODO: tighten up validation here and re-enable.
-//
-// func TestAccCloudflareAccessApplicationWithMissingCORSMethods(t *testing.T) {
-// rnd := utils.GenerateRandomResourceName()
-// zone := os.Getenv("CLOUDFLARE_DOMAIN")
-// zoneID := os.Getenv("CLOUDFLARE_ZONE_ID")
-
-// resource.Test(t, resource.TestCase{
-// PreCheck: func() {
-// acctest.TestAccPreCheck(t)
-// acctest.TestAccPreCheck_AccountID(t)
-// },
-// ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
-// Steps: []resource.TestStep{
-// {
-// Config: testAccessApplicationWithMissingCORSMethods(rnd, zone, zoneID),
-// ExpectError: regexp.MustCompile("must set allowed_methods or allow_all_methods"),
-// },
-// },
-// })
-// }
-
-// func TestAccCloudflareAccessApplicationWithMissingCORSOrigins(t *testing.T) {
-// rnd := utils.GenerateRandomResourceName()
-// zone := os.Getenv("CLOUDFLARE_DOMAIN")
-// zoneID := os.Getenv("CLOUDFLARE_ZONE_ID")
-
-// resource.Test(t, resource.TestCase{
-// PreCheck: func() {
-// acctest.TestAccPreCheck(t)
-// acctest.TestAccPreCheck_AccountID(t)
-// },
-// ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
-// Steps: []resource.TestStep{
-// {
-// Config: testAccessApplicationWithMissingCORSOrigins(rnd, zone, zoneID),
-// ExpectError: regexp.MustCompile("must set allowed_origins or allow_all_origins"),
-// },
-// },
-// })
-// }
-
-// func TestAccCloudflareAccessApplicationWithInvalidSessionDuration(t *testing.T) {
-// rnd := utils.GenerateRandomResourceName()
-// zone := os.Getenv("CLOUDFLARE_DOMAIN")
-// zoneID := os.Getenv("CLOUDFLARE_ZONE_ID")
-
-// resource.Test(t, resource.TestCase{
-// PreCheck: func() {
-// acctest.TestAccPreCheck(t)
-// acctest.TestAccPreCheck_AccountID(t)
-// },
-// ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
-// Steps: []resource.TestStep{
-// {
-// Config: testAccessApplicationWithInvalidSessionDuration(rnd, zone, zoneID),
-// ExpectError: regexp.MustCompile(regexp.QuoteMeta(`"session_duration" only supports "ns", "us" (or "µs"), "ms", "s", "m", or "h" as valid units`)),
-// },
-// },
-// })
-// }
-
-// func TestAccCloudflareAccessApplicationMisconfiguredCORSCredentialsAllowingAllOrigins(t *testing.T) {
-// rnd := utils.GenerateRandomResourceName()
-// zone := os.Getenv("CLOUDFLARE_DOMAIN")
-// zoneID := os.Getenv("CLOUDFLARE_ZONE_ID")
-
-// resource.Test(t, resource.TestCase{
-// PreCheck: func() {
-// acctest.TestAccPreCheck(t)
-// acctest.TestAccPreCheck_AccountID(t)
-// },
-// ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
-// Steps: []resource.TestStep{
-// {
-// Config: testAccessApplicationMisconfiguredCORSAllowAllOriginsWithCredentials(rnd, zone, zoneID),
-// ExpectError: regexp.MustCompile(regexp.QuoteMeta(`CORS credentials are not permitted when all origins are allowed`)),
-// },
-// },
-// })
-// }
-
-// func TestAccCloudflareAccessApplicationMisconfiguredCORSCredentialsAllowingWildcardOrigins(t *testing.T) {
-// rnd := utils.GenerateRandomResourceName()
-// zone := os.Getenv("CLOUDFLARE_DOMAIN")
-// zoneID := os.Getenv("CLOUDFLARE_ZONE_ID")
-
-// resource.Test(t, resource.TestCase{
-// PreCheck: func() {
-// acctest.TestAccPreCheck(t)
-// acctest.TestAccPreCheck_AccountID(t)
-// },
-// ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
-// Steps: []resource.TestStep{
-// {
-// Config: testAccessApplicationMisconfiguredCORSAllowWildcardOriginWithCredentials(rnd, zone, zoneID),
-// ExpectError: regexp.MustCompile(regexp.QuoteMeta(`CORS credentials are not permitted when all origins are allowed`)),
-// },
-// },
-// })
-// }
+func TestAccCloudflareAccessApplicationWithMissingCORSMethods(t *testing.T) {
+ rnd := utils.GenerateRandomResourceName()
+ zone := os.Getenv("CLOUDFLARE_DOMAIN")
+ zoneID := os.Getenv("CLOUDFLARE_ZONE_ID")
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() {
+ acctest.TestAccPreCheck(t)
+ acctest.TestAccPreCheck_AccountID(t)
+ },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccessApplicationWithMissingCORSMethods(rnd, zone, zoneID),
+ ExpectError: regexp.MustCompile(`No attribute specified when one \(and only one\) of\s+\[cors_headers\.allow_all_methods\.<\.allowed_methods\] is required`),
+ },
+ },
+ })
+}
+
+func TestAccCloudflareAccessApplicationWithMissingCORSOrigins(t *testing.T) {
+ rnd := utils.GenerateRandomResourceName()
+ zone := os.Getenv("CLOUDFLARE_DOMAIN")
+ zoneID := os.Getenv("CLOUDFLARE_ZONE_ID")
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() {
+ acctest.TestAccPreCheck(t)
+ acctest.TestAccPreCheck_AccountID(t)
+ },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccessApplicationWithMissingCORSOrigins(rnd, zone, zoneID),
+ ExpectError: regexp.MustCompile(`No attribute specified when one \(and only one\) of\s+\[cors_headers\.allow_all_origins\.<\.allowed_origins\] is required`),
+ },
+ },
+ })
+}
+
+func TestAccCloudflareAccessApplicationWithInvalidSessionDuration(t *testing.T) {
+ rnd := utils.GenerateRandomResourceName()
+ zone := os.Getenv("CLOUDFLARE_DOMAIN")
+ zoneID := os.Getenv("CLOUDFLARE_ZONE_ID")
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() {
+ acctest.TestAccPreCheck(t)
+ acctest.TestAccPreCheck_AccountID(t)
+ },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccessApplicationWithInvalidSessionDuration(rnd, zone, zoneID),
+ ExpectError: regexp.MustCompile(`"session_duration" only supports .*`),
+ },
+ },
+ })
+}
+
+func TestAccCloudflareAccessApplicationMisconfiguredCORSCredentialsAllowingAllOrigins(t *testing.T) {
+ rnd := utils.GenerateRandomResourceName()
+ zone := os.Getenv("CLOUDFLARE_DOMAIN")
+ zoneID := os.Getenv("CLOUDFLARE_ZONE_ID")
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() {
+ acctest.TestAccPreCheck(t)
+ acctest.TestAccPreCheck_AccountID(t)
+ },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccessApplicationMisconfiguredCORSAllowAllOriginsWithCredentials(rnd, zone, zoneID),
+ ExpectError: regexp.MustCompile(`Attribute "cors_headers.allow_all_origins" cannot be specified when\s+"cors_headers.allow_credentials" is specified`),
+ },
+ },
+ })
+}
+
+func TestAccCloudflareAccessApplicationWithInvalidSaas(t *testing.T) {
+ rnd := utils.GenerateRandomResourceName()
+ accoundID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() {
+ acctest.TestAccPreCheck(t)
+ acctest.TestAccPreCheck_AccountID(t)
+ },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccessApplicationWithInvalidSaas(rnd, accoundID),
+ ExpectError: regexp.MustCompile("\"saas_app\" has to be set if \"type\" is one of: \"saas\", \"dash_sso\""),
+ },
+ },
+ })
+}
func testAccessApplicationWithZoneID(resourceID, zone, zoneID string) string {
return acctest.LoadTestCase("accessapplicationwithzoneid.tf", resourceID, zone, zoneID)
@@ -1254,10 +1423,6 @@ func testAccessApplicationMisconfiguredCORSAllowAllOriginsWithCredentials(resour
return acctest.LoadTestCase("accessapplicationmisconfiguredcorsallowalloriginswithcredentials.tf", resourceID, zone, zoneID)
}
-func testAccessApplicationMisconfiguredCORSAllowWildcardOriginWithCredentials(resourceID, zone, zoneID string) string {
- return acctest.LoadTestCase("accessapplicationmisconfiguredcorsallowwildcardoriginwithcredentials.tf", resourceID, zone, zoneID)
-}
-
func testAccCloudflareAccessApplicationConfigWithADefinedTag(rnd, zoneID, domain string, accountID string) string {
return acctest.LoadTestCase("accessapplicationconfigwithadefinedtag.tf", rnd, zoneID, domain, accountID)
}
@@ -1282,10 +1447,6 @@ func testAccCloudflareAccessApplicationSCIMConfigOAuth2MissingRequired(rnd, acco
return acctest.LoadTestCase("accessapplicationscimconfigoauth2missingrequired.tf", rnd, accountID, domain)
}
-func testAccCloudflareAccessApplicationSCIMConfigAuthenticationInvalid(rnd, accountID, domain string) string {
- return acctest.LoadTestCase("accessapplicationscimconfigauthenticationinvalid.tf", rnd, accountID, domain)
-}
-
func testAccCloudflareAccessApplicationSCIMConfigHttpBasicMissingRequired(rnd, accountID, domain string) string {
return acctest.LoadTestCase("accessapplicationscimconfighttpbasicmissingrequired.tf", rnd, accountID, domain)
}
@@ -1294,6 +1455,14 @@ func testAccCloudflareAccessApplicationSCIMConfigInvalidMappingSchema(rnd, accou
return acctest.LoadTestCase("accessapplicationscimconfiginvalidmappingschema.tf", rnd, accountID, domain)
}
+func testAccCloudflareAccessApplicationConfigWithLegacyPolicies(rnd, domain string, accountID string) string {
+ return acctest.LoadTestCase("accessapplicationconfigwithlegacypolicies.tf", rnd, domain, accountID)
+}
+
func testAccCloudflareAccessApplicationConfigWithReusablePolicies(rnd, domain string, accountID string) string {
return acctest.LoadTestCase("accessapplicationconfigwithreusablepolicies.tf", rnd, domain, accountID)
}
+
+func testAccessApplicationWithInvalidSaas(resourceID, accountID string) string {
+ return acctest.LoadTestCase("accessapplicationconfigwithinvalidsaas.tf", resourceID, accountID)
+}
diff --git a/internal/services/zero_trust_access_application/schema.go b/internal/services/zero_trust_access_application/schema.go
index a9fbd4cb81..c0bad05762 100644
--- a/internal/services/zero_trust_access_application/schema.go
+++ b/internal/services/zero_trust_access_application/schema.go
@@ -4,15 +4,18 @@ package zero_trust_access_application
import (
"context"
-
"github.com/cloudflare/terraform-provider-cloudflare/internal/customfield"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/customvalidator"
"github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes"
+ "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/float64validator"
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
+ "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator"
+ "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ "github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
- "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
@@ -47,14 +50,23 @@ func ResourceSchema(ctx context.Context) schema.Schema {
"allow_iframe": schema.BoolAttribute{
Description: "Enables loading application content in an iFrame.",
Optional: true,
+ Validators: []validator.Bool{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), selfHostedAppTypes...),
+ },
},
"app_launcher_logo_url": schema.StringAttribute{
Description: "The image URL of the logo shown in the App Launcher header.",
Optional: true,
+ Validators: []validator.String{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), "app_launcher"),
+ },
},
"bg_color": schema.StringAttribute{
Description: "The background color of the App Launcher page.",
Optional: true,
+ Validators: []validator.String{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), "app_launcher"),
+ },
},
"custom_deny_message": schema.StringAttribute{
Description: "The custom error message shown to a user when they are denied access to the application.",
@@ -71,38 +83,59 @@ func ResourceSchema(ctx context.Context) schema.Schema {
"domain": schema.StringAttribute{
Description: "The primary hostname and path secured by Access. This domain will be displayed if the app is visible in the App Launcher.",
Optional: true,
+ Computed: true,
},
"header_bg_color": schema.StringAttribute{
Description: "The background color of the App Launcher header.",
Optional: true,
+ Validators: []validator.String{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), "app_launcher"),
+ },
},
"logo_url": schema.StringAttribute{
Description: "The image URL for the logo shown in the App Launcher dashboard.",
Optional: true,
},
"name": schema.StringAttribute{
- Description: "The name of the application.",
- Optional: true,
+ Description: "The name of the application.",
+ Optional: true,
+ Computed: true,
+ PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
},
"options_preflight_bypass": schema.BoolAttribute{
Description: "Allows options preflight requests to bypass Access authentication and go directly to the origin. Cannot turn on if cors_headers is set.",
Optional: true,
+ Validators: []validator.Bool{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), selfHostedAppTypes...),
+ },
},
"read_service_tokens_from_header": schema.StringAttribute{
Description: "Allows matching Access Service Tokens passed HTTP in a single header with this name.\nThis works as an alternative to the (CF-Access-Client-Id, CF-Access-Client-Secret) pair of headers.\nThe header value will be interpreted as a json object similar to: \n {\n \"cf-access-client-id\": \"88bf3b6d86161464f6509f7219099e57.access.example.com\",\n \"cf-access-client-secret\": \"bdd31cbc4dec990953e39163fbbb194c93313ca9f0a6e420346af9d326b1d2a5\"\n }",
Optional: true,
+ Validators: []validator.String{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), selfHostedAppTypes...),
+ },
},
"same_site_cookie_attribute": schema.StringAttribute{
Description: "Sets the SameSite cookie setting, which provides increased security against CSRF attacks.",
Optional: true,
+ Validators: []validator.String{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), selfHostedAppTypes...),
+ },
},
"service_auth_401_redirect": schema.BoolAttribute{
Description: "Returns a 401 status code when the request is blocked by a Service Auth policy.",
Optional: true,
+ Validators: []validator.Bool{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), selfHostedAppTypes...),
+ },
},
"skip_interstitial": schema.BoolAttribute{
Description: "Enables automatic authentication through cloudflared.",
Optional: true,
+ Validators: []validator.Bool{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), selfHostedAppTypes...),
+ },
},
"type": schema.StringAttribute{
Description: "The application type.\nAvailable values: \"self_hosted\", \"saas\", \"ssh\", \"vnc\", \"app_launcher\", \"warp\", \"biso\", \"bookmark\", \"dash_sso\", \"infrastructure\", \"rdp\".",
@@ -123,7 +156,7 @@ func ResourceSchema(ctx context.Context) schema.Schema {
),
},
},
- "allowed_idps": schema.ListAttribute{
+ "allowed_idps": schema.SetAttribute{
Description: "The identity providers your users can select when connecting to this application. Defaults to all IdPs configured in your account.",
Optional: true,
ElementType: types.StringType,
@@ -135,33 +168,48 @@ func ResourceSchema(ctx context.Context) schema.Schema {
},
"cors_headers": schema.SingleNestedAttribute{
Optional: true,
+ Validators: []validator.Object{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), selfHostedAppTypes...),
+ },
Attributes: map[string]schema.Attribute{
"allow_all_headers": schema.BoolAttribute{
Description: "Allows all HTTP request headers.",
Optional: true,
+ Validators: []validator.Bool{
+ boolvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("allowed_headers")),
+ },
},
"allow_all_methods": schema.BoolAttribute{
Description: "Allows all HTTP request methods.",
Optional: true,
+ Validators: []validator.Bool{
+ boolvalidator.ExactlyOneOf(path.MatchRelative().AtParent().AtName("allowed_methods")),
+ },
},
"allow_all_origins": schema.BoolAttribute{
Description: "Allows all origins.",
Optional: true,
+ Validators: []validator.Bool{
+ boolvalidator.ExactlyOneOf(path.MatchRelative().AtParent().AtName("allowed_origins")),
+ },
},
"allow_credentials": schema.BoolAttribute{
Description: "When set to `true`, includes credentials (cookies, authorization headers, or TLS client certificates) with requests.",
Optional: true,
+ Validators: []validator.Bool{
+ boolvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("allow_all_origins")),
+ },
},
- "allowed_headers": schema.ListAttribute{
+ "allowed_headers": schema.SetAttribute{
Description: "Allowed HTTP request headers.",
Optional: true,
ElementType: types.StringType,
},
- "allowed_methods": schema.ListAttribute{
+ "allowed_methods": schema.SetAttribute{
Description: "Allowed HTTP request methods.",
Optional: true,
- Validators: []validator.List{
- listvalidator.ValueStringsAre(
+ Validators: []validator.Set{
+ setvalidator.ValueStringsAre(
stringvalidator.OneOfCaseInsensitive(
"GET",
"POST",
@@ -177,7 +225,7 @@ func ResourceSchema(ctx context.Context) schema.Schema {
},
ElementType: types.StringType,
},
- "allowed_origins": schema.ListAttribute{
+ "allowed_origins": schema.SetAttribute{
Description: "Allowed origins.",
Optional: true,
ElementType: types.StringType,
@@ -194,6 +242,9 @@ func ResourceSchema(ctx context.Context) schema.Schema {
"footer_links": schema.ListNestedAttribute{
Description: "The links in the App Launcher footer.",
Optional: true,
+ Validators: []validator.List{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), "app_launcher"),
+ },
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
@@ -226,6 +277,7 @@ func ResourceSchema(ctx context.Context) schema.Schema {
"password": schema.StringAttribute{
Description: "Password used to authenticate with the remote SCIM service.",
Optional: true,
+ Sensitive: true,
},
"scheme": schema.StringAttribute{
Description: "The authentication scheme to use when making SCIM requests to this application.\nAvailable values: \"httpbasic\", \"oauthbearertoken\", \"oauth2\", \"access_service_token\".",
@@ -333,6 +385,9 @@ func ResourceSchema(ctx context.Context) schema.Schema {
},
"target_criteria": schema.ListNestedAttribute{
Optional: true,
+ Validators: []validator.List{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), targetCompatibleAppTypes...),
+ },
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"port": schema.Int64Attribute{
@@ -360,64 +415,85 @@ func ResourceSchema(ctx context.Context) schema.Schema {
Description: "Displays the application in the App Launcher.",
Computed: true,
Optional: true,
- Default: booldefault.StaticBool(true),
+ Validators: []validator.Bool{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), appLauncherVisibleAppTypes...),
+ },
},
"auto_redirect_to_identity": schema.BoolAttribute{
Description: "When set to `true`, users skip the identity provider selection step during login. You must specify only one identity provider in allowed_idps.",
- Computed: true,
Optional: true,
- Default: booldefault.StaticBool(false),
},
"enable_binding_cookie": schema.BoolAttribute{
Description: "Enables the binding cookie, which increases security against compromised authorization tokens and CSRF attacks.",
- Computed: true,
Optional: true,
- Default: booldefault.StaticBool(false),
+ Validators: []validator.Bool{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), selfHostedAppTypes...),
+ },
},
"http_only_cookie_attribute": schema.BoolAttribute{
Description: "Enables the HttpOnly cookie attribute, which increases security against XSS attacks.",
Computed: true,
Optional: true,
- Default: booldefault.StaticBool(true),
+ Validators: []validator.Bool{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), selfHostedAppTypes...),
+ },
},
"path_cookie_attribute": schema.BoolAttribute{
Description: "Enables cookie paths to scope an application's JWT to the application path. If disabled, the JWT will scope to the hostname by default",
- Computed: true,
Optional: true,
- Default: booldefault.StaticBool(false),
+ Validators: []validator.Bool{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), selfHostedAppTypes...),
+ },
},
"session_duration": schema.StringAttribute{
Description: "The amount of time that tokens issued for this application will be valid. Must be in the format `300ms` or `2h45m`. Valid time units are: ns, us (or µs), ms, s, m, h. Note: unsupported for infrastructure type applications.",
Computed: true,
Optional: true,
Default: stringdefault.StaticString("24h"),
+ Validators: []validator.String{
+ stringvalidator.RegexMatches(durationRegex, `"session_duration" only supports "ns", "us" (or "µs"), "ms", "s", "m", or "h" as valid units`),
+ },
},
"skip_app_launcher_login_page": schema.BoolAttribute{
Description: "Determines when to skip the App Launcher landing page.",
- Computed: true,
Optional: true,
- Default: booldefault.StaticBool(false),
+ Computed: true,
+ Validators: []validator.Bool{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), "app_launcher"),
+ },
},
"self_hosted_domains": schema.ListAttribute{
Description: "List of public domains that Access will secure. This field is deprecated in favor of `destinations` and will be supported until **November 21, 2025.** If `destinations` are provided, then `self_hosted_domains` will be ignored.",
- Computed: true,
Optional: true,
+ Computed: true,
DeprecationMessage: "This attribute is deprecated.",
CustomType: customfield.NewListType[types.String](ctx),
ElementType: types.StringType,
+
+ Validators: []validator.List{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), selfHostedAppTypes...),
+ listvalidator.ConflictsWith(path.Expressions{
+ path.MatchRoot("destinations"),
+ }...),
+ },
},
"tags": schema.ListAttribute{
Description: "The tags you want assigned to an application. Tags are used to filter applications in the App Launcher dashboard.",
- Computed: true,
- Optional: true,
CustomType: customfield.NewListType[types.String](ctx),
+ Optional: true,
ElementType: types.StringType,
},
"destinations": schema.ListNestedAttribute{
Description: "List of destinations secured by Access. This supersedes `self_hosted_domains` to allow for more flexibility in defining different types of domains. If `destinations` are provided, then `self_hosted_domains` will be ignored.",
- Computed: true,
Optional: true,
+ Computed: true,
CustomType: customfield.NewNestedObjectListType[ZeroTrustAccessApplicationDestinationsModel](ctx),
+ Validators: []validator.List{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), selfHostedAppTypes...),
+ listvalidator.ConflictsWith(path.Expressions{
+ path.MatchRoot("self_hosted_domains"),
+ }...),
+ },
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"type": schema.StringAttribute{
@@ -459,9 +535,11 @@ func ResourceSchema(ctx context.Context) schema.Schema {
},
"landing_page_design": schema.SingleNestedAttribute{
Description: "The design of the App Launcher landing page shown to users when they log in.",
- Computed: true,
Optional: true,
CustomType: customfield.NewNestedObjectType[ZeroTrustAccessApplicationLandingPageDesignModel](ctx),
+ Validators: []validator.Object{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), "app_launcher"),
+ },
Attributes: map[string]schema.Attribute{
"button_color": schema.StringAttribute{
Description: "The background color of the log in button on the landing page.",
@@ -489,18 +567,20 @@ func ResourceSchema(ctx context.Context) schema.Schema {
},
"policies": schema.ListNestedAttribute{
Description: "The policies that Access applies to the application, in ascending order of precedence. Items can reference existing policies or create new policies exclusive to the application.",
- Computed: true,
Optional: true,
- CustomType: customfield.NewNestedObjectListType[ZeroTrustAccessApplicationPoliciesModel](ctx),
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "The UUID of the policy",
Optional: true,
+ Validators: []validator.String{
+ stringvalidator.ExactlyOneOf(path.MatchRelative().AtParent().AtName("include")),
+ },
},
"precedence": schema.Int64Attribute{
Description: "The order of execution for this policy. Must be unique for each policy within an app.",
Optional: true,
+ Computed: true,
},
"decision": schema.StringAttribute{
Description: "The action Access will take if a user matches this policy. Infrastructure application policies can only use the Allow action.\nAvailable values: \"allow\", \"deny\", \"non_identity\", \"bypass\".",
@@ -512,14 +592,20 @@ func ResourceSchema(ctx context.Context) schema.Schema {
"non_identity",
"bypass",
),
+ stringvalidator.AlsoRequires(path.MatchRelative().AtParent().AtName("include")),
},
},
"include": schema.ListNestedAttribute{
Description: "Rules evaluated with an OR logical operator. A user needs to meet only one of the Include rules.",
- Computed: true,
Optional: true,
- CustomType: customfield.NewNestedObjectListType[ZeroTrustAccessApplicationPoliciesIncludeModel](ctx),
+ Validators: []validator.List{
+ listvalidator.AlsoRequires(path.MatchRelative().AtParent().AtName("decision")),
+ },
+ CustomType: customfield.NewNestedObjectListType[ZeroTrustAccessApplicationPoliciesIncludeModel](ctx),
NestedObject: schema.NestedAttributeObject{
+ Validators: []validator.Object{
+ customvalidator.ObjectSizeAtMost(1),
+ },
Attributes: map[string]schema.Attribute{
"group": schema.SingleNestedAttribute{
Optional: true,
@@ -752,10 +838,16 @@ func ResourceSchema(ctx context.Context) schema.Schema {
"name": schema.StringAttribute{
Description: "The name of the Access policy.",
Optional: true,
+ Validators: []validator.String{
+ stringvalidator.AlsoRequires(path.MatchRelative().AtParent().AtName("include")),
+ },
},
"connection_rules": schema.SingleNestedAttribute{
Description: "The rules that define how users may connect to the targets secured by your application.",
Optional: true,
+ Validators: []validator.Object{
+ objectvalidator.AlsoRequires(path.MatchRelative().AtParent().AtName("include")),
+ },
Attributes: map[string]schema.Attribute{
"ssh": schema.SingleNestedAttribute{
Description: "The SSH-specific rules that define how users may connect to the targets secured by your application.",
@@ -776,10 +868,15 @@ func ResourceSchema(ctx context.Context) schema.Schema {
},
"exclude": schema.ListNestedAttribute{
Description: "Rules evaluated with a NOT logical operator. To match the policy, a user cannot meet any of the Exclude rules.",
- Computed: true,
Optional: true,
- CustomType: customfield.NewNestedObjectListType[ZeroTrustAccessApplicationPoliciesExcludeModel](ctx),
+ Validators: []validator.List{
+ listvalidator.AlsoRequires(path.MatchRelative().AtParent().AtName("include")),
+ },
+ CustomType: customfield.NewNestedObjectListType[ZeroTrustAccessApplicationPoliciesExcludeModel](ctx),
NestedObject: schema.NestedAttributeObject{
+ Validators: []validator.Object{
+ customvalidator.ObjectSizeAtMost(1),
+ },
Attributes: map[string]schema.Attribute{
"group": schema.SingleNestedAttribute{
Optional: true,
@@ -1011,10 +1108,15 @@ func ResourceSchema(ctx context.Context) schema.Schema {
},
"require": schema.ListNestedAttribute{
Description: "Rules evaluated with an AND logical operator. To match the policy, a user must meet all of the Require rules.",
- Computed: true,
Optional: true,
- CustomType: customfield.NewNestedObjectListType[ZeroTrustAccessApplicationPoliciesRequireModel](ctx),
+ Validators: []validator.List{
+ listvalidator.AlsoRequires(path.MatchRelative().AtParent().AtName("include")),
+ },
+ CustomType: customfield.NewNestedObjectListType[ZeroTrustAccessApplicationPoliciesRequireModel](ctx),
NestedObject: schema.NestedAttributeObject{
+ Validators: []validator.Object{
+ customvalidator.ObjectSizeAtMost(1),
+ },
Attributes: map[string]schema.Attribute{
"group": schema.SingleNestedAttribute{
Optional: true,
@@ -1248,14 +1350,18 @@ func ResourceSchema(ctx context.Context) schema.Schema {
},
},
"saas_app": schema.SingleNestedAttribute{
- Computed: true,
Optional: true,
CustomType: customfield.NewNestedObjectType[ZeroTrustAccessApplicationSaaSAppModel](ctx),
+ Validators: []validator.Object{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), saasAppTypes...),
+ customvalidator.RequiredWhenOtherStringIsOneOf(path.MatchRoot("type"), saasAppTypes...),
+ },
Attributes: map[string]schema.Attribute{
"auth_type": schema.StringAttribute{
- Computed: true,
Description: "Optional identifier indicating the authentication protocol used for the saas app. Required for OIDC. Default if unset is \"saml\"\nAvailable values: \"saml\", \"oidc\".",
Optional: true,
+ Computed: true,
+ Default: stringdefault.StaticString("saml"),
Validators: []validator.String{
stringvalidator.OneOfCaseInsensitive("saml", "oidc"),
},
@@ -1263,13 +1369,22 @@ func ResourceSchema(ctx context.Context) schema.Schema {
"consumer_service_url": schema.StringAttribute{
Description: "The service provider's endpoint that is responsible for receiving and parsing a SAML assertion.",
Optional: true,
+ Validators: []validator.String{
+ customvalidator.RequiresOtherStringAttributeToNullOrBeOneOf(path.MatchRoot("saas_app").AtName("auth_type"), "saml"),
+ },
},
"created_at": schema.StringAttribute{
Computed: true,
CustomType: timetypes.RFC3339Type{},
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
},
"custom_attributes": schema.ListNestedAttribute{
Optional: true,
+ Validators: []validator.List{
+ customvalidator.RequiresOtherStringAttributeToNullOrBeOneOf(path.MatchRoot("saas_app").AtName("auth_type"), "saml"),
+ },
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"friendly_name": schema.StringAttribute{
@@ -1326,40 +1441,71 @@ func ResourceSchema(ctx context.Context) schema.Schema {
"default_relay_state": schema.StringAttribute{
Description: "The URL that the user will be redirected to after a successful login for IDP initiated logins.",
Optional: true,
+ Validators: []validator.String{
+ customvalidator.RequiresOtherStringAttributeToNullOrBeOneOf(path.MatchRoot("saas_app").AtName("auth_type"), "saml"),
+ },
},
"idp_entity_id": schema.StringAttribute{
Description: "The unique identifier for your SaaS application.",
Computed: true,
Optional: true,
+ Validators: []validator.String{
+ customvalidator.RequiresOtherStringAttributeToNullOrBeOneOf(path.MatchRoot("saas_app").AtName("auth_type"), "saml"),
+ },
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
},
"name_id_format": schema.StringAttribute{
Description: "The format of the name identifier sent to the SaaS application.\nAvailable values: \"id\", \"email\".",
Optional: true,
+ Computed: true,
Validators: []validator.String{
stringvalidator.OneOfCaseInsensitive("id", "email"),
+ customvalidator.RequiresOtherStringAttributeToNullOrBeOneOf(path.MatchRoot("saas_app").AtName("auth_type"), "saml"),
+ },
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
},
},
"name_id_transform_jsonata": schema.StringAttribute{
Description: "A [JSONata](https://jsonata.org/) expression that transforms an application's user identities into a NameID value for its SAML assertion. This expression should evaluate to a singular string. The output of this expression can override the `name_id_format` setting.",
Optional: true,
+ Validators: []validator.String{
+ customvalidator.RequiresOtherStringAttributeToNullOrBeOneOf(path.MatchRoot("saas_app").AtName("auth_type"), "saml"),
+ },
},
"public_key": schema.StringAttribute{
Description: "The Access public certificate that will be used to verify your identity.",
Computed: true,
- Optional: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
},
"saml_attribute_transform_jsonata": schema.StringAttribute{
Description: "A [JSONata] (https://jsonata.org/) expression that transforms an application's user identities into attribute assertions in the SAML response. The expression can transform id, email, name, and groups values. It can also transform fields listed in the saml_attributes or oidc_fields of the identity provider used to authenticate. The output of this expression must be a JSON object.",
Optional: true,
+ Validators: []validator.String{
+ customvalidator.RequiresOtherStringAttributeToNullOrBeOneOf(path.MatchRoot("saas_app").AtName("auth_type"), "saml"),
+ },
},
"sp_entity_id": schema.StringAttribute{
Description: "A globally unique name for an identity or service provider.",
Optional: true,
+ Validators: []validator.String{
+ customvalidator.RequiresOtherStringAttributeToNullOrBeOneOf(path.MatchRoot("saas_app").AtName("auth_type"), "saml"),
+ },
},
"sso_endpoint": schema.StringAttribute{
Description: "The endpoint where your SaaS application will send login requests.",
Computed: true,
Optional: true,
+ Validators: []validator.String{
+ customvalidator.RequiresOtherStringAttributeToNullOrBeOneOf(path.MatchRoot("saas_app").AtName("auth_type"), "saml"),
+ },
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
},
"updated_at": schema.StringAttribute{
Computed: true,
@@ -1368,10 +1514,17 @@ func ResourceSchema(ctx context.Context) schema.Schema {
"access_token_lifetime": schema.StringAttribute{
Description: "The lifetime of the OIDC Access Token after creation. Valid units are m,h. Must be greater than or equal to 1m and less than or equal to 24h.",
Optional: true,
+ Computed: true,
+ Validators: []validator.String{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("saas_app").AtName("auth_type"), "oidc"),
+ },
},
"allow_pkce_without_client_secret": schema.BoolAttribute{
Description: "If client secret should be required on the token endpoint when authorization_code_with_pkce grant is used.",
Optional: true,
+ Validators: []validator.Bool{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("saas_app").AtName("auth_type"), "oidc"),
+ },
},
"app_launcher_url": schema.StringAttribute{
Description: "The URL where this applications tile redirects users",
@@ -1380,16 +1533,23 @@ func ResourceSchema(ctx context.Context) schema.Schema {
"client_id": schema.StringAttribute{
Description: "The application client id",
Computed: true,
- Optional: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
},
"client_secret": schema.StringAttribute{
Description: "The application client secret, only returned on POST request.",
Computed: true,
- Optional: true,
Sensitive: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
},
"custom_claims": schema.ListNestedAttribute{
Optional: true,
+ Validators: []validator.List{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("saas_app").AtName("auth_type"), "oidc"),
+ },
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
@@ -1433,6 +1593,7 @@ func ResourceSchema(ctx context.Context) schema.Schema {
Description: "The OIDC flows supported by this application",
Optional: true,
Validators: []validator.List{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("saas_app").AtName("auth_type"), "oidc"),
listvalidator.ValueStringsAre(
stringvalidator.OneOfCaseInsensitive(
"authorization_code",
@@ -1448,9 +1609,15 @@ func ResourceSchema(ctx context.Context) schema.Schema {
"group_filter_regex": schema.StringAttribute{
Description: "A regex to filter Cloudflare groups returned in ID token and userinfo endpoint",
Optional: true,
+ Validators: []validator.String{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("saas_app").AtName("auth_type"), "oidc"),
+ },
},
"hybrid_and_implicit_options": schema.SingleNestedAttribute{
Optional: true,
+ Validators: []validator.Object{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("saas_app").AtName("auth_type"), "oidc"),
+ },
Attributes: map[string]schema.Attribute{
"return_access_token_from_authorization_endpoint": schema.BoolAttribute{
Description: "If an Access Token should be returned from the OIDC Authorization endpoint",
@@ -1466,9 +1633,15 @@ func ResourceSchema(ctx context.Context) schema.Schema {
Description: "The permitted URL's for Cloudflare to return Authorization codes and Access/ID tokens",
Optional: true,
ElementType: types.StringType,
+ Validators: []validator.List{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("saas_app").AtName("auth_type"), "oidc"),
+ },
},
"refresh_token_options": schema.SingleNestedAttribute{
Optional: true,
+ Validators: []validator.Object{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("saas_app").AtName("auth_type"), "oidc"),
+ },
Attributes: map[string]schema.Attribute{
"lifetime": schema.StringAttribute{
Description: "How long a refresh token will be valid for after creation. Valid units are m,h,d. Must be longer than 1m.",
@@ -1480,6 +1653,7 @@ func ResourceSchema(ctx context.Context) schema.Schema {
Description: `Define the user information shared with access, "offline_access" scope will be automatically enabled if refresh tokens are enabled`,
Optional: true,
Validators: []validator.List{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("saas_app").AtName("auth_type"), "oidc"),
listvalidator.ValueStringsAre(
stringvalidator.OneOfCaseInsensitive(
"openid",
@@ -1496,14 +1670,9 @@ func ResourceSchema(ctx context.Context) schema.Schema {
"aud": schema.StringAttribute{
Description: "Audience tag.",
Computed: true,
- },
- "created_at": schema.StringAttribute{
- Computed: true,
- CustomType: timetypes.RFC3339Type{},
- },
- "updated_at": schema.StringAttribute{
- Computed: true,
- CustomType: timetypes.RFC3339Type{},
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
},
},
}
diff --git a/internal/services/zero_trust_access_application/testdata/accessapplicationconfigwithadefinedidp.tf b/internal/services/zero_trust_access_application/testdata/accessapplicationconfigwithadefinedidp.tf
index 38e8348191..90d485adc8 100644
--- a/internal/services/zero_trust_access_application/testdata/accessapplicationconfigwithadefinedidp.tf
+++ b/internal/services/zero_trust_access_application/testdata/accessapplicationconfigwithadefinedidp.tf
@@ -1,12 +1,10 @@
resource "cloudflare_zero_trust_access_identity_provider" "%[1]s" {
account_id = "%[4]s"
- name = "%[1]s"
- type = "onetimepin"
- config = {
- client_id = "test"
- client_secret = "test"
- }
+ name = "%[1]s"
+ type = "onetimepin"
+ config = {}
}
+
resource "cloudflare_zero_trust_access_application" "%[1]s" {
zone_id = "%[2]s"
name = "%[1]s"
@@ -14,5 +12,5 @@ resource "cloudflare_zero_trust_access_application" "%[1]s" {
type = "self_hosted"
session_duration = "24h"
auto_redirect_to_identity = true
- allowed_idps = [cloudflare_zero_trust_access_identity_provider.%[1]s.id]
+ allowed_idps = [cloudflare_zero_trust_access_identity_provider.%[1]s.id]
}
diff --git a/internal/services/zero_trust_access_application/testdata/accessapplicationconfigwithinvalidsaas.tf b/internal/services/zero_trust_access_application/testdata/accessapplicationconfigwithinvalidsaas.tf
new file mode 100644
index 0000000000..a16f9677be
--- /dev/null
+++ b/internal/services/zero_trust_access_application/testdata/accessapplicationconfigwithinvalidsaas.tf
@@ -0,0 +1,7 @@
+resource "cloudflare_zero_trust_access_application" "%[1]s" {
+ account_id = "%[2]s"
+ name = "%[1]s"
+ type = "saas"
+ session_duration = "24h"
+ auto_redirect_to_identity = false
+}
diff --git a/internal/services/zero_trust_access_application/testdata/accessapplicationconfigwithlegacypolicies.tf b/internal/services/zero_trust_access_application/testdata/accessapplicationconfigwithlegacypolicies.tf
new file mode 100644
index 0000000000..e95760f837
--- /dev/null
+++ b/internal/services/zero_trust_access_application/testdata/accessapplicationconfigwithlegacypolicies.tf
@@ -0,0 +1,42 @@
+resource "cloudflare_zero_trust_access_policy" "%[1]s_reusable_p1" {
+ account_id = "%[3]s"
+ name = "%[1]s"
+ decision = "non_identity"
+ include = [
+ {
+ ip = { ip = "127.0.0.1/32" }
+ }
+ ]
+}
+
+resource "cloudflare_zero_trust_access_application" "%[1]s" {
+ account_id = "%[3]s"
+ name = "%[1]s"
+ domain = "%[1]s.%[2]s"
+ type = "self_hosted"
+ policies = [
+ {
+ name = "%[1]s.legacy_p1"
+ decision = "allow"
+ include = [
+ {
+ email = { email = "a@example.com" }
+ }
+ ]
+ precedence = 2
+ },
+ {
+ id = cloudflare_zero_trust_access_policy.%[1]s_reusable_p1.id
+ precedence = 4
+ },
+ {
+ name = "%[1]s.legacy_p2"
+ decision = "non_identity"
+ include = [
+ {
+ ip = { ip = "127.0.0.1/32" }
+ }
+ ]
+ }
+ ]
+}
diff --git a/internal/services/zero_trust_access_application/testdata/accessapplicationconfigwithreusablepolicies.tf b/internal/services/zero_trust_access_application/testdata/accessapplicationconfigwithreusablepolicies.tf
index ff022ad272..b41dbcb5d5 100644
--- a/internal/services/zero_trust_access_application/testdata/accessapplicationconfigwithreusablepolicies.tf
+++ b/internal/services/zero_trust_access_application/testdata/accessapplicationconfigwithreusablepolicies.tf
@@ -1,42 +1,37 @@
resource "cloudflare_zero_trust_access_policy" "%[1]s_p1" {
- account_id = "%[3]s"
- name = "%[1]s"
- decision = "allow"
- include = [{
- email = { email = "a@example.com" }
- }]
+ account_id = "%[3]s"
+ name = "%[1]s"
+ decision = "allow"
+ include = [
+ {
+ email = { email = "a@example.com" }
+ }
+ ]
}
resource "cloudflare_zero_trust_access_policy" "%[1]s_p2" {
- account_id = "%[3]s"
- name = "%[1]s"
- decision = "non_identity"
- include = [{
- ip = { ip = "127.0.0.1/32" }
- }]
+ account_id = "%[3]s"
+ name = "%[1]s"
+ decision = "non_identity"
+ include = [
+ {
+ ip = { ip = "127.0.0.1/32" }
+ }
+ ]
}
resource "cloudflare_zero_trust_access_application" "%[1]s" {
- account_id = "%[3]s"
- name = "%[1]s"
- domain = "%[1]s.%[2]s"
- type = "self_hosted"
- policies = [
- {
- id = cloudflare_zero_trust_access_policy.%[1]s_p1.id
- decision = cloudflare_zero_trust_access_policy.%[1]s_p1.decision
- name = cloudflare_zero_trust_access_policy.%[1]s_p1.name
- include = [{
- email = { email = cloudflare_zero_trust_access_policy.%[1]s_p1.include.0.email.email }
- }]
+ account_id = "%[3]s"
+ name = "%[1]s"
+ domain = "%[1]s.%[2]s"
+ type = "self_hosted"
+ policies = [
+ {
+ id = cloudflare_zero_trust_access_policy.%[1]s_p1.id
+ precedence = 4
},
{
id = cloudflare_zero_trust_access_policy.%[1]s_p2.id
- decision = cloudflare_zero_trust_access_policy.%[1]s_p2.decision
- name = cloudflare_zero_trust_access_policy.%[1]s_p2.name
- include = [{
- ip = { ip = cloudflare_zero_trust_access_policy.%[1]s_p2.include.0.ip.ip }
- }]
}
]
}
diff --git a/internal/services/zero_trust_access_application/testdata/accessapplicationconfigwithsamlsaas.tf b/internal/services/zero_trust_access_application/testdata/accessapplicationconfigwithsamlsaas.tf
index 29485a75d1..15378c9e85 100644
--- a/internal/services/zero_trust_access_application/testdata/accessapplicationconfigwithsamlsaas.tf
+++ b/internal/services/zero_trust_access_application/testdata/accessapplicationconfigwithsamlsaas.tf
@@ -4,23 +4,23 @@ resource "cloudflare_zero_trust_access_application" "%[1]s" {
type = "saas"
session_duration = "24h"
saas_app = {
- consumer_service_url = "https://saas-app.example/sso/saml/consume"
- sp_entity_id = "saas-app.example"
- name_id_format = "email"
- default_relay_state = "https://saas-app.example"
- name_id_transform_jsonata = "$substringBefore(email, '@') & '+sandbox@' & $substringAfter(email, '@')"
+ consumer_service_url = "https://saas-app.example/sso/saml/consume"
+ sp_entity_id = "saas-app.example"
+ name_id_format = "email"
+ default_relay_state = "https://saas-app.example"
+ name_id_transform_jsonata = "$substringBefore(email, '@') & '+sandbox@' & $substringAfter(email, '@')"
saml_attribute_transform_jsonata = "$ ~>| groups | {'group_name': name} |"
custom_attributes = [
{
- name = "email"
- name_format = "urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
- source = { name = "user_email" }
+ name = "email"
+ name_format = "urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
+ source = { name = "user_email" }
},
{
- name = "rank"
- required = true
- friendly_name = "Rank"
- source = { name = "rank" }
+ name = "rank"
+ required = true
+ friendly_name = "Rank"
+ source = { name = "rank" }
}
]
}
diff --git a/internal/services/zero_trust_access_application/testdata/accessapplicationwithapplaunchercustomizationfields.tf b/internal/services/zero_trust_access_application/testdata/accessapplicationwithapplaunchercustomizationfields.tf
index 81b0a4625a..be442cd995 100644
--- a/internal/services/zero_trust_access_application/testdata/accessapplicationwithapplaunchercustomizationfields.tf
+++ b/internal/services/zero_trust_access_application/testdata/accessapplicationwithapplaunchercustomizationfields.tf
@@ -1,22 +1,23 @@
resource "cloudflare_zero_trust_access_application" "%[1]s" {
- account_id = "%[2]s"
- type = "app_launcher"
- session_duration = "24h"
- app_launcher_visible = false
- app_launcher_logo_url = "https://www.cloudflare.com/img/logo-web-badges/cf-logo-on-white-bg.svg"
- bg_color = "#000000"
- header_bg_color = "#000000"
+ account_id = "%[2]s"
+ type = "app_launcher"
+ session_duration = "24h"
+ app_launcher_logo_url = "https://www.cloudflare.com/img/logo-web-badges/cf-logo-on-white-bg.svg"
+ bg_color = "#000000"
+ header_bg_color = "#000000"
- footer_links = [{
- name = "footer link"
- url = "https://www.cloudflare.com"
- }]
+ footer_links = [
+ {
+ name = "footer link"
+ url = "https://www.cloudflare.com"
+ }
+ ]
- landing_page_design = {
- title = "title"
- message = "message"
- button_color = "#000000"
- image_url = "https://www.cloudflare.com/img/logo-web-badges/cf-logo-on-white-bg.svg"
- button_text_color = "#000000"
- }
+ landing_page_design = {
+ title = "title"
+ message = "message"
+ button_color = "#000000"
+ image_url = "https://www.cloudflare.com/img/logo-web-badges/cf-logo-on-white-bg.svg"
+ button_text_color = "#000000"
+ }
}
diff --git a/internal/services/zero_trust_access_group/model.go b/internal/services/zero_trust_access_group/model.go
index a5cf659a5e..85db544664 100644
--- a/internal/services/zero_trust_access_group/model.go
+++ b/internal/services/zero_trust_access_group/model.go
@@ -4,7 +4,6 @@ package zero_trust_access_group
import (
"github.com/cloudflare/terraform-provider-cloudflare/internal/apijson"
- "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes"
"github.com/hashicorp/terraform-plugin-framework/types"
)
@@ -21,8 +20,6 @@ type ZeroTrustAccessGroupModel struct {
IsDefault types.Bool `tfsdk:"is_default" json:"is_default,optional,no_refresh"`
Exclude *[]*ZeroTrustAccessGroupExcludeModel `tfsdk:"exclude" json:"exclude,optional"`
Require *[]*ZeroTrustAccessGroupRequireModel `tfsdk:"require" json:"require,optional"`
- CreatedAt timetypes.RFC3339 `tfsdk:"created_at" json:"created_at,computed" format:"date-time"`
- UpdatedAt timetypes.RFC3339 `tfsdk:"updated_at" json:"updated_at,computed" format:"date-time"`
}
func (m ZeroTrustAccessGroupModel) MarshalJSON() (data []byte, err error) {
diff --git a/internal/services/zero_trust_access_group/normalizations.go b/internal/services/zero_trust_access_group/normalizations.go
new file mode 100644
index 0000000000..b15db1c1d0
--- /dev/null
+++ b/internal/services/zero_trust_access_group/normalizations.go
@@ -0,0 +1,30 @@
+package zero_trust_access_group
+
+import (
+ "context"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+)
+
+type IsNull interface {
+ IsNull() bool
+}
+
+func normalizeEmptyAndNullSlice[T any](data **[]T, stateData *[]T) {
+ if (*data != nil && len(**data) != 0) || (stateData != nil && len(*stateData) != 0) {
+ return
+ }
+ *data = stateData
+}
+
+// Normalizing function to ensure consistency between the state/plan and the meaning of the API response.
+// Alters the API response before applying it to the state by laxing equalities between null & zero-value
+// for some attributes, and nullifies fields that terraform should not be saving in the state.
+func normalizeReadZeroTrustAccessGroupAPIData(ctx context.Context, data, sourceData *ZeroTrustAccessGroupModel) diag.Diagnostics {
+ diags := make(diag.Diagnostics, 0)
+
+ normalizeEmptyAndNullSlice(&data.Include, sourceData.Include)
+ normalizeEmptyAndNullSlice(&data.Require, sourceData.Require)
+ normalizeEmptyAndNullSlice(&data.Exclude, sourceData.Exclude)
+
+ return diags
+}
diff --git a/internal/services/zero_trust_access_group/resource.go b/internal/services/zero_trust_access_group/resource.go
index 38437608d8..d9ea814c8b 100644
--- a/internal/services/zero_trust_access_group/resource.go
+++ b/internal/services/zero_trust_access_group/resource.go
@@ -153,6 +153,14 @@ func (r *ZeroTrustAccessGroupResource) Update(ctx context.Context, req resource.
}
data = &env.Result
+ var planData *ZeroTrustAccessGroupModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &planData)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(normalizeReadZeroTrustAccessGroupAPIData(ctx, data, planData)...)
+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
@@ -199,6 +207,14 @@ func (r *ZeroTrustAccessGroupResource) Read(ctx context.Context, req resource.Re
}
data = &env.Result
+ var stateData *ZeroTrustAccessGroupModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &stateData)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(normalizeReadZeroTrustAccessGroupAPIData(ctx, data, stateData)...)
+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
diff --git a/internal/services/zero_trust_access_group/resource_test.go b/internal/services/zero_trust_access_group/resource_test.go
index 2654211ed1..17b594b893 100644
--- a/internal/services/zero_trust_access_group/resource_test.go
+++ b/internal/services/zero_trust_access_group/resource_test.go
@@ -95,30 +95,60 @@ func TestAccCloudflareAccessGroup_ConfigBasicAccount(t *testing.T) {
testAccCheckCloudflareAccessGroupExists(name, cloudflare.AccountIdentifier(accountID), &accessGroup),
resource.TestCheckResourceAttr(name, consts.AccountIDSchemaKey, accountID),
resource.TestCheckResourceAttr(name, "name", rnd),
- resource.TestCheckResourceAttr(name, "include.0.email.email", email),
- resource.TestCheckResourceAttr(name, "include.0.email_domain.domain", "example.com"),
- resource.TestCheckResourceAttrSet(name, "include.0.any_valid_service_token.%"),
- resource.TestCheckResourceAttr(name, "include.0.ip.ip", "192.0.2.1/32"),
- resource.TestCheckResourceAttr(name, "include.1.ip.ip", "192.0.2.2/32"),
- resource.TestCheckResourceAttr(name, "include.0.ip_list.id", "e3a0f205-c525-4e48-a293-ba5d1f00e638"),
- resource.TestCheckResourceAttr(name, "include.1.ip_list.id", "5d54cd30-ce52-46e4-9a46-a47887e1a167"),
+ resource.TestCheckResourceAttr(name, "include.0.any_valid_service_token.%", "0"),
+ resource.TestCheckResourceAttr(name, "include.1.email.email", email),
+ resource.TestCheckResourceAttr(name, "include.2.email_domain.domain", "example.com"),
+ resource.TestCheckResourceAttr(name, "include.3.ip.ip", "192.0.2.1/32"),
+ resource.TestCheckResourceAttr(name, "include.4.ip_list.id", "e3a0f205-c525-4e48-a293-ba5d1f00e638"),
+ resource.TestCheckResourceAttr(name, "include.5.saml.attribute_name", "Name1"),
+ resource.TestCheckResourceAttr(name, "include.5.saml.attribute_value", "Value1"),
+ resource.TestCheckResourceAttr(name, "include.5.saml.identity_provider_id", "1234"),
+ resource.TestCheckResourceAttr(name, "include.6.azure_ad.id", "group1"),
+ resource.TestCheckResourceAttr(name, "include.6.azure_ad.identity_provider_id", "1234"),
+ resource.TestCheckResourceAttr(name, "include.7.ip.ip", "192.0.2.2/32"),
+ resource.TestCheckResourceAttr(name, "include.8.ip_list.id", "5d54cd30-ce52-46e4-9a46-a47887e1a167"),
+ resource.TestCheckResourceAttr(name, "include.9.saml.attribute_name", "Name2"),
+ resource.TestCheckResourceAttr(name, "include.9.saml.attribute_value", "Value2"),
+ resource.TestCheckResourceAttr(name, "include.9.saml.identity_provider_id", "1234"),
+ resource.TestCheckResourceAttr(name, "include.10.azure_ad.id", "group2"),
+ resource.TestCheckResourceAttr(name, "include.10.azure_ad.identity_provider_id", "5678"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessGroupConfigBasic(rnd, email, cloudflare.AccountIdentifier(accountID)),
+ PlanOnly: true,
+ },
{
Config: testAccCloudflareAccessGroupConfigBasic(rnd, email, cloudflare.AccountIdentifier(accountID)),
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudflareAccessGroupExists(name, cloudflare.AccountIdentifier(accountID), &accessGroup),
resource.TestCheckResourceAttr(name, consts.AccountIDSchemaKey, accountID),
resource.TestCheckResourceAttr(name, "name", rnd),
- resource.TestCheckResourceAttr(name, "include.0.email.email", email),
- resource.TestCheckResourceAttr(name, "include.0.email_domain.domain", "example.com"),
- resource.TestCheckResourceAttrSet(name, "include.0.any_valid_service_token.%"),
- resource.TestCheckResourceAttr(name, "include.0.ip.ip", "192.0.2.1/32"),
- resource.TestCheckResourceAttr(name, "include.1.ip.ip", "192.0.2.2/32"),
- resource.TestCheckResourceAttr(name, "include.0.ip_list.id", "e3a0f205-c525-4e48-a293-ba5d1f00e638"),
- resource.TestCheckResourceAttr(name, "include.1.ip_list.id", "5d54cd30-ce52-46e4-9a46-a47887e1a167"),
+ resource.TestCheckResourceAttr(name, "include.0.any_valid_service_token.%", "0"),
+ resource.TestCheckResourceAttr(name, "include.1.email.email", email),
+ resource.TestCheckResourceAttr(name, "include.2.email_domain.domain", "example.com"),
+ resource.TestCheckResourceAttr(name, "include.3.ip.ip", "192.0.2.1/32"),
+ resource.TestCheckResourceAttr(name, "include.4.ip_list.id", "e3a0f205-c525-4e48-a293-ba5d1f00e638"),
+ resource.TestCheckResourceAttr(name, "include.5.saml.attribute_name", "Name1"),
+ resource.TestCheckResourceAttr(name, "include.5.saml.attribute_value", "Value1"),
+ resource.TestCheckResourceAttr(name, "include.5.saml.identity_provider_id", "1234"),
+ resource.TestCheckResourceAttr(name, "include.6.azure_ad.id", "group1"),
+ resource.TestCheckResourceAttr(name, "include.6.azure_ad.identity_provider_id", "1234"),
+ resource.TestCheckResourceAttr(name, "include.7.ip.ip", "192.0.2.2/32"),
+ resource.TestCheckResourceAttr(name, "include.8.ip_list.id", "5d54cd30-ce52-46e4-9a46-a47887e1a167"),
+ resource.TestCheckResourceAttr(name, "include.9.saml.attribute_name", "Name2"),
+ resource.TestCheckResourceAttr(name, "include.9.saml.attribute_value", "Value2"),
+ resource.TestCheckResourceAttr(name, "include.9.saml.identity_provider_id", "1234"),
+ resource.TestCheckResourceAttr(name, "include.10.azure_ad.id", "group2"),
+ resource.TestCheckResourceAttr(name, "include.10.azure_ad.identity_provider_id", "5678"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessGroupConfigBasic(rnd, email, cloudflare.AccountIdentifier(accountID)),
+ PlanOnly: true,
+ },
},
})
}
@@ -140,46 +170,60 @@ func TestAccCloudflareAccessGroup_ConfigBasicZone(t *testing.T) {
testAccCheckCloudflareAccessGroupExists(name, cloudflare.ZoneIdentifier(zoneID), &accessGroup),
resource.TestCheckResourceAttr(name, consts.ZoneIDSchemaKey, zoneID),
resource.TestCheckResourceAttr(name, "name", rnd),
- resource.TestCheckResourceAttr(name, "include.0.email.email", email),
- resource.TestCheckResourceAttr(name, "include.0.email_domain.domain", "example.com"),
- resource.TestCheckResourceAttrSet(name, "include.0.any_valid_service_token.%"),
- resource.TestCheckResourceAttr(name, "include.0.ip.ip", "192.0.2.1/32"),
- resource.TestCheckResourceAttr(name, "include.1.ip.ip", "192.0.2.2/32"),
- resource.TestCheckResourceAttr(name, "include.0.ip_list.id", "e3a0f205-c525-4e48-a293-ba5d1f00e638"),
- resource.TestCheckResourceAttr(name, "include.1.ip_list.id", "5d54cd30-ce52-46e4-9a46-a47887e1a167"),
- resource.TestCheckResourceAttr(name, "include.0.saml.attribute_name", "Name1"),
- resource.TestCheckResourceAttr(name, "include.0.saml.attribute_value", "Value1"),
- resource.TestCheckResourceAttr(name, "include.1.saml.attribute_name", "Name2"),
- resource.TestCheckResourceAttr(name, "include.1.saml.attribute_value", "Value2"),
- resource.TestCheckResourceAttr(name, "include.0.azure_ad.id", "group1"),
- resource.TestCheckResourceAttr(name, "include.0.azure_ad.identity_provider_id", "1234"),
- resource.TestCheckResourceAttr(name, "include.1.azure_ad.id", "group2"),
- resource.TestCheckResourceAttr(name, "include.1.azure_ad.identity_provider_id", "5678"),
+ resource.TestCheckResourceAttr(name, "include.0.any_valid_service_token.%", "0"),
+ resource.TestCheckResourceAttr(name, "include.1.email.email", email),
+ resource.TestCheckResourceAttr(name, "include.2.email_domain.domain", "example.com"),
+ resource.TestCheckResourceAttr(name, "include.3.ip.ip", "192.0.2.1/32"),
+ resource.TestCheckResourceAttr(name, "include.4.ip_list.id", "e3a0f205-c525-4e48-a293-ba5d1f00e638"),
+ resource.TestCheckResourceAttr(name, "include.5.saml.attribute_name", "Name1"),
+ resource.TestCheckResourceAttr(name, "include.5.saml.attribute_value", "Value1"),
+ resource.TestCheckResourceAttr(name, "include.5.saml.identity_provider_id", "1234"),
+ resource.TestCheckResourceAttr(name, "include.6.azure_ad.id", "group1"),
+ resource.TestCheckResourceAttr(name, "include.6.azure_ad.identity_provider_id", "1234"),
+ resource.TestCheckResourceAttr(name, "include.7.ip.ip", "192.0.2.2/32"),
+ resource.TestCheckResourceAttr(name, "include.8.ip_list.id", "5d54cd30-ce52-46e4-9a46-a47887e1a167"),
+ resource.TestCheckResourceAttr(name, "include.9.saml.attribute_name", "Name2"),
+ resource.TestCheckResourceAttr(name, "include.9.saml.attribute_value", "Value2"),
+ resource.TestCheckResourceAttr(name, "include.9.saml.identity_provider_id", "1234"),
+ resource.TestCheckResourceAttr(name, "include.10.azure_ad.id", "group2"),
+ resource.TestCheckResourceAttr(name, "include.10.azure_ad.identity_provider_id", "5678"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessGroupConfigBasic(rnd, email, cloudflare.ZoneIdentifier(zoneID)),
+ PlanOnly: true,
+ },
{
Config: testAccCloudflareAccessGroupConfigBasic(rnd, email, cloudflare.ZoneIdentifier(zoneID)),
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudflareAccessGroupExists(name, cloudflare.ZoneIdentifier(zoneID), &accessGroup),
resource.TestCheckResourceAttr(name, consts.ZoneIDSchemaKey, zoneID),
resource.TestCheckResourceAttr(name, "name", rnd),
- resource.TestCheckResourceAttr(name, "include.0.email.email", email),
- resource.TestCheckResourceAttr(name, "include.0.email_domain.domain", "example.com"),
- resource.TestCheckResourceAttrSet(name, "include.0.any_valid_service_token.%"),
- resource.TestCheckResourceAttr(name, "include.0.ip.ip", "192.0.2.1/32"),
- resource.TestCheckResourceAttr(name, "include.1.ip.ip", "192.0.2.2/32"),
- resource.TestCheckResourceAttr(name, "include.0.ip_list.id", "e3a0f205-c525-4e48-a293-ba5d1f00e638"),
- resource.TestCheckResourceAttr(name, "include.1.ip_list.id", "5d54cd30-ce52-46e4-9a46-a47887e1a167"),
- resource.TestCheckResourceAttr(name, "include.0.saml.attribute_name", "Name1"),
- resource.TestCheckResourceAttr(name, "include.0.saml.attribute_value", "Value1"),
- resource.TestCheckResourceAttr(name, "include.1.saml.attribute_name", "Name2"),
- resource.TestCheckResourceAttr(name, "include.1.saml.attribute_value", "Value2"),
- resource.TestCheckResourceAttr(name, "include.0.azure_ad.id", "group1"),
- resource.TestCheckResourceAttr(name, "include.0.azure_ad.identity_provider_id", "1234"),
- resource.TestCheckResourceAttr(name, "include.1.azure_ad.id", "group2"),
- resource.TestCheckResourceAttr(name, "include.1.azure_ad.identity_provider_id", "5678"),
+ resource.TestCheckResourceAttr(name, "include.0.any_valid_service_token.%", "0"),
+ resource.TestCheckResourceAttr(name, "include.1.email.email", email),
+ resource.TestCheckResourceAttr(name, "include.2.email_domain.domain", "example.com"),
+ resource.TestCheckResourceAttr(name, "include.3.ip.ip", "192.0.2.1/32"),
+ resource.TestCheckResourceAttr(name, "include.4.ip_list.id", "e3a0f205-c525-4e48-a293-ba5d1f00e638"),
+ resource.TestCheckResourceAttr(name, "include.5.saml.attribute_name", "Name1"),
+ resource.TestCheckResourceAttr(name, "include.5.saml.attribute_value", "Value1"),
+ resource.TestCheckResourceAttr(name, "include.5.saml.identity_provider_id", "1234"),
+ resource.TestCheckResourceAttr(name, "include.6.azure_ad.id", "group1"),
+ resource.TestCheckResourceAttr(name, "include.6.azure_ad.identity_provider_id", "1234"),
+ resource.TestCheckResourceAttr(name, "include.7.ip.ip", "192.0.2.2/32"),
+ resource.TestCheckResourceAttr(name, "include.8.ip_list.id", "5d54cd30-ce52-46e4-9a46-a47887e1a167"),
+ resource.TestCheckResourceAttr(name, "include.9.saml.attribute_name", "Name2"),
+ resource.TestCheckResourceAttr(name, "include.9.saml.attribute_value", "Value2"),
+ resource.TestCheckResourceAttr(name, "include.9.saml.identity_provider_id", "1234"),
+ resource.TestCheckResourceAttr(name, "include.10.azure_ad.id", "group2"),
+ resource.TestCheckResourceAttr(name, "include.10.azure_ad.identity_provider_id", "5678"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessGroupConfigBasic(rnd, email, cloudflare.ZoneIdentifier(zoneID)),
+ PlanOnly: true,
+ },
},
})
}
@@ -211,6 +255,11 @@ func TestAccCloudflareAccessGroup_ConfigEmailList(t *testing.T) {
resource.TestCheckResourceAttr(emailListName, "items.0.value", "test@example.com"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessGroupConfigEmailList(rnd, rnd2, cloudflare.AccountIdentifier(accountID)),
+ PlanOnly: true,
+ },
},
})
}
@@ -234,10 +283,15 @@ func TestAccCloudflareAccessGroup_Exclude(t *testing.T) {
resource.TestCheckResourceAttr(name, consts.AccountIDSchemaKey, accountID),
resource.TestCheckResourceAttr(name, "name", rnd),
resource.TestCheckResourceAttr(name, "include.0.email.email", email),
- resource.TestCheckResourceAttr(name, "include.0.email_domain.domain", "example.com"),
+ resource.TestCheckResourceAttr(name, "include.1.email_domain.domain", "example.com"),
resource.TestCheckResourceAttr(name, "exclude.0.email.email", email),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccessGroupConfigExclude(rnd, accountID, email),
+ PlanOnly: true,
+ },
},
})
}
@@ -261,10 +315,15 @@ func TestAccCloudflareAccessGroup_Require(t *testing.T) {
resource.TestCheckResourceAttr(name, consts.AccountIDSchemaKey, accountID),
resource.TestCheckResourceAttr(name, "name", rnd),
resource.TestCheckResourceAttr(name, "include.0.email.email", email),
- resource.TestCheckResourceAttr(name, "include.0.email_domain.domain", "example.com"),
+ resource.TestCheckResourceAttr(name, "include.1.email_domain.domain", "example.com"),
resource.TestCheckResourceAttr(name, "require.0.email.email", email),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccessGroupConfigRequire(rnd, accountID, email),
+ PlanOnly: true,
+ },
},
})
}
@@ -288,13 +347,18 @@ func TestAccCloudflareAccessGroup_FullConfig(t *testing.T) {
resource.TestCheckResourceAttr(name, consts.AccountIDSchemaKey, accountID),
resource.TestCheckResourceAttr(name, "name", rnd),
resource.TestCheckResourceAttr(name, "include.0.email.email", email),
- resource.TestCheckResourceAttr(name, "include.0.email_domain.domain", "example.com"),
+ resource.TestCheckResourceAttr(name, "include.1.email_domain.domain", "example.com"),
+ resource.TestCheckResourceAttr(name, "include.2.common_name.common_name", "common"),
+ resource.TestCheckResourceAttr(name, "include.3.common_name.common_name", "name"),
resource.TestCheckResourceAttr(name, "exclude.0.email.email", email),
resource.TestCheckResourceAttr(name, "require.0.email.email", email),
- resource.TestCheckResourceAttr(name, "include.0.common_name.common_name", "common"),
- resource.TestCheckResourceAttr(name, "include.1.common_name.common_name", "name"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccessGroupConfigFullConfig(rnd, accountID, email),
+ PlanOnly: true,
+ },
},
})
}
@@ -324,6 +388,11 @@ func TestAccCloudflareAccessGroup_WithIDP(t *testing.T) {
resource.TestCheckResourceAttr(groupName, "include.0.github_organization.team", team),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessGroupWithIDP(accountID, rnd, githubOrg, team),
+ PlanOnly: true,
+ },
},
})
}
@@ -353,6 +422,11 @@ func TestAccCloudflareAccessGroup_WithIDPAuthContext(t *testing.T) {
resource.TestCheckResourceAttr(groupName, "require.0.auth_context.ac_id", ctxACID),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessGroupWithIDPAuthContext(accountID, rnd, ctxID, ctxACID),
+ PlanOnly: true,
+ },
},
})
}
@@ -376,14 +450,24 @@ func TestAccCloudflareAccessGroup_Updated(t *testing.T) {
testAccCheckCloudflareAccessGroupExists(name, cloudflare.AccountIdentifier(accountID), &before),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessGroupConfigBasic(rnd, email, cloudflare.AccountIdentifier(accountID)),
+ PlanOnly: true,
+ },
{
Config: testAccCloudflareAccessGroupConfigBasic(rnd, "test-changed@example.com", cloudflare.AccountIdentifier(accountID)),
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudflareAccessGroupExists(name, cloudflare.AccountIdentifier(accountID), &after),
testAccCheckCloudflareAccessGroupIDUnchanged(&before, &after),
- resource.TestCheckResourceAttr(name, "include.0.email.email", "test-changed@example.com"),
+ resource.TestCheckResourceAttr(name, "include.1.email.email", "test-changed@example.com"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessGroupConfigBasic(rnd, "test-changed@example.com", cloudflare.AccountIdentifier(accountID)),
+ PlanOnly: true,
+ },
},
})
}
@@ -407,6 +491,11 @@ func TestAccCloudflareAccessGroup_UpdatedFromCommonNameToCommonNames(t *testing.
testAccCheckCloudflareAccessGroupExists(name, cloudflare.AccountIdentifier(accountID), &before),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessGroupConfigBasicWithCommonName(rnd, cloudflare.AccountIdentifier(accountID)),
+ PlanOnly: true,
+ },
{
Config: testAccCloudflareAccessGroupConfigBasicWithCommonNames(rnd, cloudflare.AccountIdentifier(accountID)),
Check: resource.ComposeTestCheckFunc(
@@ -416,6 +505,11 @@ func TestAccCloudflareAccessGroup_UpdatedFromCommonNameToCommonNames(t *testing.
resource.TestCheckResourceAttr(name, "include.1.common_name.common_name", "name"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCloudflareAccessGroupConfigBasicWithCommonNames(rnd, cloudflare.AccountIdentifier(accountID)),
+ PlanOnly: true,
+ },
},
})
}
diff --git a/internal/services/zero_trust_access_group/schema.go b/internal/services/zero_trust_access_group/schema.go
index 0a6b8d14f9..712de09130 100644
--- a/internal/services/zero_trust_access_group/schema.go
+++ b/internal/services/zero_trust_access_group/schema.go
@@ -4,8 +4,9 @@ package zero_trust_access_group
import (
"context"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/customvalidator"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
- "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
@@ -40,6 +41,9 @@ func ResourceSchema(ctx context.Context) schema.Schema {
Description: "Rules evaluated with an OR logical operator. A user needs to meet only one of the Include rules.",
Required: true,
NestedObject: schema.NestedAttributeObject{
+ Validators: []validator.Object{
+ customvalidator.ObjectSizeAtMost(1),
+ },
Attributes: map[string]schema.Attribute{
"group": schema.SingleNestedAttribute{
Optional: true,
@@ -277,6 +281,9 @@ func ResourceSchema(ctx context.Context) schema.Schema {
Description: "Rules evaluated with a NOT logical operator. To match a policy, a user cannot meet any of the Exclude rules.",
Optional: true,
NestedObject: schema.NestedAttributeObject{
+ Validators: []validator.Object{
+ customvalidator.ObjectSizeAtMost(1),
+ },
Attributes: map[string]schema.Attribute{
"group": schema.SingleNestedAttribute{
Optional: true,
@@ -510,6 +517,9 @@ func ResourceSchema(ctx context.Context) schema.Schema {
Description: "Rules evaluated with an AND logical operator. To match a policy, a user must meet all of the Require rules.",
Optional: true,
NestedObject: schema.NestedAttributeObject{
+ Validators: []validator.Object{
+ customvalidator.ObjectSizeAtMost(1),
+ },
Attributes: map[string]schema.Attribute{
"group": schema.SingleNestedAttribute{
Optional: true,
@@ -739,14 +749,6 @@ func ResourceSchema(ctx context.Context) schema.Schema {
},
},
},
- "created_at": schema.StringAttribute{
- Computed: true,
- CustomType: timetypes.RFC3339Type{},
- },
- "updated_at": schema.StringAttribute{
- Computed: true,
- CustomType: timetypes.RFC3339Type{},
- },
},
}
}
diff --git a/internal/services/zero_trust_access_group/testdata/accessgroupconfigbasic.tf b/internal/services/zero_trust_access_group/testdata/accessgroupconfigbasic.tf
index 6f4353b640..89b2a7552d 100644
--- a/internal/services/zero_trust_access_group/testdata/accessgroupconfigbasic.tf
+++ b/internal/services/zero_trust_access_group/testdata/accessgroupconfigbasic.tf
@@ -1,38 +1,51 @@
resource "cloudflare_zero_trust_access_group" "%[1]s" {
%[3]s_id = "%[4]s"
- name = "%[1]s"
-
- include = [{
- any_valid_service_token = {}
- email = { email = "%[2]s" }
- email_domain = { domain = "example.com" }
- ip = { ip = "192.0.2.1/32" }
- ip_list = {
- id = "e3a0f205-c525-4e48-a293-ba5d1f00e638",
- }
- saml = {
- attribute_name = "Name1"
- attribute_value = "Value1"
- identity_provider_id = "1234"
- }
- azure_ad = {
- id = "group1"
- identity_provider_id = "1234"
- },
- },
- {
- ip = { ip = "192.0.2.2/32" }
- ip_list = {
- id = "5d54cd30-ce52-46e4-9a46-a47887e1a167",
- }
- saml = {
- attribute_name = "Name2"
- attribute_value = "Value2"
- identity_provider_id = "1234"
- }
- azure_ad = {
- id = "group2"
- identity_provider_id = "5678"
- }
- }]
+ name = "%[1]s"
+ include = [
+ {
+ any_valid_service_token = {}
+ },
+ { email = { email = "%[2]s" } },
+ { email_domain = { domain = "example.com" } },
+ { ip = { ip = "192.0.2.1/32" } },
+ {
+ ip_list = {
+ id = "e3a0f205-c525-4e48-a293-ba5d1f00e638",
+ }
+ },
+ {
+ saml = {
+ attribute_name = "Name1"
+ attribute_value = "Value1"
+ identity_provider_id = "1234"
+ }
+ },
+ {
+ azure_ad = {
+ id = "group1"
+ identity_provider_id = "1234"
+ }
+ },
+ {
+ ip = { ip = "192.0.2.2/32" }
+ },
+ {
+ ip_list = {
+ id = "5d54cd30-ce52-46e4-9a46-a47887e1a167",
+ }
+ },
+ {
+ saml = {
+ attribute_name = "Name2"
+ attribute_value = "Value2"
+ identity_provider_id = "1234"
+ }
+ },
+ {
+ azure_ad = {
+ id = "group2"
+ identity_provider_id = "5678"
+ }
+ },
+ ]
}
diff --git a/internal/services/zero_trust_access_group/testdata/accessgroupconfigexclude.tf b/internal/services/zero_trust_access_group/testdata/accessgroupconfigexclude.tf
index 2cf5a5170d..80267cb0a6 100644
--- a/internal/services/zero_trust_access_group/testdata/accessgroupconfigexclude.tf
+++ b/internal/services/zero_trust_access_group/testdata/accessgroupconfigexclude.tf
@@ -1,13 +1,19 @@
resource "cloudflare_zero_trust_access_group" "%[1]s" {
account_id = "%[2]s"
- name = "%[1]s"
+ name = "%[1]s"
- include = [{
- email = { email = "%[3]s" }
- email_domain = { domain = "example.com" }
- }]
+ include = [
+ {
+ email = { email = "%[3]s" }
+ },
+ {
+ email_domain = { domain = "example.com" }
+ }
+ ]
- exclude = [{
- email = { email = "%[3]s" }
- }]
+ exclude = [
+ {
+ email = { email = "%[3]s" }
+ }
+ ]
}
diff --git a/internal/services/zero_trust_access_group/testdata/accessgroupconfigfullconfig.tf b/internal/services/zero_trust_access_group/testdata/accessgroupconfigfullconfig.tf
index 529c8e5025..77752a8946 100644
--- a/internal/services/zero_trust_access_group/testdata/accessgroupconfigfullconfig.tf
+++ b/internal/services/zero_trust_access_group/testdata/accessgroupconfigfullconfig.tf
@@ -5,7 +5,11 @@ resource "cloudflare_zero_trust_access_group" "%[1]s" {
include = [
{
email = { email = "%[3]s" }
+ },
+ {
email_domain = { domain = "example.com" }
+ },
+ {
common_name = { common_name = "common" }
},
{
@@ -13,11 +17,15 @@ resource "cloudflare_zero_trust_access_group" "%[1]s" {
}
]
- require = [{
- email = { email = "%[3]s" }
- }]
+ require = [
+ {
+ email = { email = "%[3]s" }
+ }
+ ]
- exclude = [{
- email = { email = "%[3]s" }
- }]
+ exclude = [
+ {
+ email = { email = "%[3]s" }
+ }
+ ]
}
diff --git a/internal/services/zero_trust_access_group/testdata/accessgroupconfigrequire.tf b/internal/services/zero_trust_access_group/testdata/accessgroupconfigrequire.tf
index e3f7920594..9fa6a78717 100644
--- a/internal/services/zero_trust_access_group/testdata/accessgroupconfigrequire.tf
+++ b/internal/services/zero_trust_access_group/testdata/accessgroupconfigrequire.tf
@@ -1,13 +1,19 @@
resource "cloudflare_zero_trust_access_group" "%[1]s" {
account_id = "%[2]s"
- name = "%[1]s"
+ name = "%[1]s"
- include = [{
- email = { email = "%[3]s" }
- email_domain = { domain = "example.com" }
- }]
+ include = [
+ {
+ email = { email = "%[3]s" }
+ },
+ {
+ email_domain = { domain = "example.com" }
+ }
+ ]
- require = [{
- email = { email = "%[3]s" }
- }]
+ require = [
+ {
+ email = { email = "%[3]s" }
+ }
+ ]
}
diff --git a/internal/services/zero_trust_access_identity_provider/model.go b/internal/services/zero_trust_access_identity_provider/model.go
index d6e3f85c5c..a327308c2b 100644
--- a/internal/services/zero_trust_access_identity_provider/model.go
+++ b/internal/services/zero_trust_access_identity_provider/model.go
@@ -56,7 +56,7 @@ type ZeroTrustAccessIdentityProviderConfigModel struct {
HeaderAttributes *[]*ZeroTrustAccessIdentityProviderConfigHeaderAttributesModel `tfsdk:"header_attributes" json:"header_attributes,optional"`
IdPPublicCERTs *[]types.String `tfsdk:"idp_public_certs" json:"idp_public_certs,optional"`
IssuerURL types.String `tfsdk:"issuer_url" json:"issuer_url,optional"`
- SignRequest types.Bool `tfsdk:"sign_request" json:"sign_request,computed_optional"`
+ SignRequest types.Bool `tfsdk:"sign_request" json:"sign_request,optional"`
SSOTargetURL types.String `tfsdk:"sso_target_url" json:"sso_target_url,optional"`
RedirectURL types.String `tfsdk:"redirect_url" json:"redirect_url,computed"`
}
diff --git a/internal/services/zero_trust_access_identity_provider/normalizations.go b/internal/services/zero_trust_access_identity_provider/normalizations.go
new file mode 100644
index 0000000000..fc45a6217c
--- /dev/null
+++ b/internal/services/zero_trust_access_identity_provider/normalizations.go
@@ -0,0 +1,91 @@
+package zero_trust_access_identity_provider
+
+import (
+ "context"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/customfield"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/tfsdk"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+)
+
+func normalizeFalseAndNullBool(data *basetypes.BoolValue, stateData basetypes.BoolValue) {
+ if data.ValueBool() || stateData.ValueBool() {
+ return
+ }
+ *data = stateData
+}
+
+func normalizeReadZeroTrustIDPScimConfigData(ctx context.Context, dataValue, stateValue *customfield.NestedObject[ZeroTrustAccessIdentityProviderSCIMConfigModel]) diag.Diagnostics {
+ var (
+ diags = make(diag.Diagnostics, 0)
+ dataScimConfig, stateScimConfig ZeroTrustAccessIdentityProviderSCIMConfigModel
+ )
+
+ diags.Append(dataValue.As(ctx, &dataScimConfig, basetypes.ObjectAsOptions{})...)
+ diags.Append(stateValue.As(ctx, &stateScimConfig, basetypes.ObjectAsOptions{})...)
+ if diags.HasError() {
+ return diags
+ }
+
+ if !stateScimConfig.Secret.IsUnknown() && !stateScimConfig.Secret.IsNull() {
+ // Scim secret is only generated and returned in the create request, and null on reads.
+ // so we need to load it from the state
+ dataScimConfig.Secret = stateScimConfig.Secret
+ }
+
+ *dataValue, diags = customfield.NewObject[ZeroTrustAccessIdentityProviderSCIMConfigModel](ctx, &dataScimConfig)
+ return diags
+}
+
+func normalizeReadZeroTrustIDPConfigData(ctx context.Context, dataValue *ZeroTrustAccessIdentityProviderModel, stateValue ZeroTrustAccessIdentityProviderModel) diag.Diagnostics {
+ diag := make(diag.Diagnostics, 0)
+ if dataValue.Config == nil || stateValue.Config == nil {
+ return diag
+ }
+
+ normalizeFalseAndNullBool(&dataValue.Config.SignRequest, stateValue.Config.SignRequest)
+ normalizeFalseAndNullBool(&dataValue.Config.ConditionalAccessEnabled, stateValue.Config.ConditionalAccessEnabled)
+ normalizeFalseAndNullBool(&dataValue.Config.SupportGroups, stateValue.Config.SupportGroups)
+
+ return diag
+}
+
+func normalizeReadZeroTrustIDPData(ctx context.Context, dataValue *ZeroTrustAccessIdentityProviderModel, state *tfsdk.State) diag.Diagnostics {
+ var (
+ diags = make(diag.Diagnostics, 0)
+ stateValue ZeroTrustAccessIdentityProviderModel
+ )
+ d := state.Get(ctx, &stateValue)
+
+ diags.Append(normalizeReadZeroTrustIDPConfigData(ctx, dataValue, stateValue)...)
+ if diags.HasError() {
+ return diags
+ }
+
+ // scim_config.secret is only returned when the app is first created, assigning here from the state
+ // to prevent a diff when the app is updated
+ if !dataValue.SCIMConfig.IsNull() && (!stateValue.SCIMConfig.IsNull() && !stateValue.SCIMConfig.IsUnknown()) {
+ diags.Append(normalizeReadZeroTrustIDPScimConfigData(ctx, &dataValue.SCIMConfig, &stateValue.SCIMConfig)...)
+ if diags.HasError() {
+ return diags
+ }
+ }
+
+ return d
+}
+
+// Some fields are write-only sensitive and should not be stored in the state.
+// Usually these secrets are injected in the config from a secret store.
+func loadConfigSensitiveValuesForWriting(ctx context.Context, data *ZeroTrustAccessIdentityProviderModel, cfg *tfsdk.Config) diag.Diagnostics {
+ var (
+ diags = make(diag.Diagnostics, 0)
+ cfgData *ZeroTrustAccessIdentityProviderModel
+ )
+ diags.Append(cfg.Get(ctx, &cfgData)...)
+
+ if data.Config != nil && cfgData.Config != nil {
+ data.Config.ClientSecret = cfgData.Config.ClientSecret
+ }
+
+ return diags
+}
diff --git a/internal/services/zero_trust_access_identity_provider/resource.go b/internal/services/zero_trust_access_identity_provider/resource.go
index 5c42bdb922..081b5c2cc4 100644
--- a/internal/services/zero_trust_access_identity_provider/resource.go
+++ b/internal/services/zero_trust_access_identity_provider/resource.go
@@ -59,16 +59,16 @@ func (r *ZeroTrustAccessIdentityProviderResource) Configure(ctx context.Context,
func (r *ZeroTrustAccessIdentityProviderResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data *ZeroTrustAccessIdentityProviderModel
- secret := types.StringNull()
- req.Config.GetAttribute(ctx, path.Root("config").AtName("client_secret"), &secret)
-
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
- data.Config.ClientSecret = secret
+ resp.Diagnostics.Append(loadConfigSensitiveValuesForWriting(ctx, data, &req.Config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
dataBytes, err := data.MarshalJSON()
if err != nil {
@@ -103,15 +103,12 @@ func (r *ZeroTrustAccessIdentityProviderResource) Create(ctx context.Context, re
return
}
data = &env.Result
- data.Config.ClientSecret = types.StringNull()
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *ZeroTrustAccessIdentityProviderResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data *ZeroTrustAccessIdentityProviderModel
- secret := types.StringNull()
- req.State.GetAttribute(ctx, path.Root("scim_config").AtName("secret"), &secret)
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
@@ -162,10 +159,9 @@ func (r *ZeroTrustAccessIdentityProviderResource) Update(ctx context.Context, re
}
data = &env.Result
+ normalizeReadZeroTrustIDPData(ctx, data, &req.State)
+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
- if !secret.IsNull() {
- resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("scim_config").AtName("secret"), secret)...)
- }
}
func (r *ZeroTrustAccessIdentityProviderResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
@@ -213,6 +209,8 @@ func (r *ZeroTrustAccessIdentityProviderResource) Read(ctx context.Context, req
}
data = &env.Result
+ normalizeReadZeroTrustIDPData(ctx, data, &req.State)
+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("scim_config").AtName("secret"), secret)...)
}
@@ -302,6 +300,5 @@ func (r *ZeroTrustAccessIdentityProviderResource) ImportState(ctx context.Contex
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
-func (r *ZeroTrustAccessIdentityProviderResource) ModifyPlan(_ context.Context, _ resource.ModifyPlanRequest, _ *resource.ModifyPlanResponse) {
-
+func (r *ZeroTrustAccessIdentityProviderResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, res *resource.ModifyPlanResponse) {
}
diff --git a/internal/services/zero_trust_access_identity_provider/resource_test.go b/internal/services/zero_trust_access_identity_provider/resource_test.go
index cbf4bf5e8f..57604b0718 100644
--- a/internal/services/zero_trust_access_identity_provider/resource_test.go
+++ b/internal/services/zero_trust_access_identity_provider/resource_test.go
@@ -87,6 +87,11 @@ func TestAccCloudflareAccessIdentityProvider_OneTimePin(t *testing.T) {
}),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCheckCloudflareAccessIdentityProviderOneTimePin(rnd, cloudflare.AccountIdentifier(accountID)),
+ PlanOnly: true,
+ },
},
})
@@ -110,6 +115,11 @@ func TestAccCloudflareAccessIdentityProvider_OneTimePin(t *testing.T) {
}),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCheckCloudflareAccessIdentityProviderOneTimePin(rnd, cloudflare.ZoneIdentifier(zoneID)),
+ PlanOnly: true,
+ },
},
})
}
@@ -133,9 +143,14 @@ func TestAccCloudflareAccessIdentityProvider_OAuth(t *testing.T) {
resource.TestCheckResourceAttr(resourceName, "name", rnd),
resource.TestCheckResourceAttr(resourceName, "type", "github"),
resource.TestCheckResourceAttr(resourceName, "config.client_id", "test"),
- resource.TestCheckResourceAttrSet(resourceName, "config.client_secret"),
+ resource.TestCheckNoResourceAttr(resourceName, "config.client_secret"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCheckCloudflareAccessIdentityProviderOAuth(accountID, rnd),
+ PlanOnly: true,
+ },
},
})
}
@@ -159,9 +174,14 @@ func TestAccCloudflareAccessIdentityProvider_OAuthWithUpdate(t *testing.T) {
resource.TestCheckResourceAttr(resourceName, "name", rnd),
resource.TestCheckResourceAttr(resourceName, "type", "github"),
resource.TestCheckResourceAttr(resourceName, "config.client_id", "test"),
- resource.TestCheckResourceAttrSet(resourceName, "config.client_secret"),
+ resource.TestCheckNoResourceAttr(resourceName, "config.client_secret"),
),
},
+ {
+ // Ensures no diff on second plan
+ Config: testAccCheckCloudflareAccessIdentityProviderOAuth(accountID, rnd),
+ PlanOnly: true,
+ },
{
Config: testAccCheckCloudflareAccessIdentityProviderOAuthUpdatedName(accountID, rnd),
Check: resource.ComposeTestCheckFunc(
@@ -169,9 +189,14 @@ func TestAccCloudflareAccessIdentityProvider_OAuthWithUpdate(t *testing.T) {
resource.TestCheckResourceAttr(resourceName, "name", rnd+"-updated"),
resource.TestCheckResourceAttr(resourceName, "type", "github"),
resource.TestCheckResourceAttr(resourceName, "config.client_id", "test"),
- resource.TestCheckResourceAttrSet(resourceName, "config.client_secret"),
+ resource.TestCheckNoResourceAttr(resourceName, "config.client_secret"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCheckCloudflareAccessIdentityProviderOAuthUpdatedName(accountID, rnd),
+ PlanOnly: true,
+ },
},
})
}
@@ -196,13 +221,17 @@ func TestAccCloudflareAccessIdentityProvider_SAML(t *testing.T) {
resource.TestCheckResourceAttr(resourceName, "type", "saml"),
resource.TestCheckResourceAttr(resourceName, "config.issuer_url", "jumpcloud"),
resource.TestCheckResourceAttr(resourceName, "config.sso_target_url", "https://sso.myexample.jumpcloud.com/saml2/cloudflareaccess"),
- resource.TestCheckResourceAttr(resourceName, "config.sign_request", "false"),
resource.TestCheckResourceAttr(resourceName, "config.attributes.#", "2"),
resource.TestCheckResourceAttr(resourceName, "config.attributes.0", "email"),
resource.TestCheckResourceAttr(resourceName, "config.attributes.1", "username"),
resource.TestCheckResourceAttr(resourceName, "config.idp_public_certs.#", "1"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCheckCloudflareAccessIdentityProviderSAML(accountID, rnd),
+ PlanOnly: true,
+ },
},
})
}
@@ -229,13 +258,16 @@ func TestAccCloudflareAccessIdentityProvider_AzureAD(t *testing.T) {
resource.TestCheckResourceAttr(resourceName, "type", "azureAD"),
resource.TestCheckResourceAttr(resourceName, "config.client_id", "test"),
resource.TestCheckResourceAttr(resourceName, "config.directory_id", "directory"),
- resource.TestCheckResourceAttr(resourceName, "config.condtional_access_enabled", "true"),
resource.TestCheckResourceAttr(resourceName, "scim_config.enabled", "true"),
resource.TestCheckResourceAttr(resourceName, "scim_config.user_deprovision", "true"),
resource.TestCheckResourceAttr(resourceName, "scim_config.seat_deprovision", "true"),
- resource.TestCheckResourceAttr(resourceName, "scim_config.group_member_deprovision", "true"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCheckCloudflareAccessIdentityProviderAzureAD(accountID, rnd),
+ PlanOnly: true,
+ },
},
})
}
@@ -251,7 +283,7 @@ func TestAccCloudflareAccessIdentityProvider_OAuth_Import(t *testing.T) {
resource.TestCheckResourceAttr(resourceName, "name", rnd),
resource.TestCheckResourceAttr(resourceName, "type", "github"),
resource.TestCheckResourceAttr(resourceName, "config.client_id", "test"),
- resource.TestCheckResourceAttrSet(resourceName, "config.client_secret"),
+ resource.TestCheckNoResourceAttr(resourceName, "config.client_secret"),
)
resource.Test(t, resource.TestCase{
@@ -265,13 +297,18 @@ func TestAccCloudflareAccessIdentityProvider_OAuth_Import(t *testing.T) {
Config: testAccCheckCloudflareAccessIdentityProviderOAuth(accountID, rnd),
Check: checkFn,
},
- // {
- // ImportState: true,
- // ImportStateVerify: true,
- // ResourceName: resourceName,
- // ImportStateIdPrefix: fmt.Sprintf("%s/", accountID),
- // Check: checkFn,
- // },
+ {
+ // Ensures no diff on second plan
+ Config: testAccCheckCloudflareAccessIdentityProviderOAuth(accountID, rnd),
+ PlanOnly: true,
+ },
+ {
+ ImportState: true,
+ ImportStateVerify: true,
+ ResourceName: resourceName,
+ ImportStateIdPrefix: fmt.Sprintf("accounts/%s/", accountID),
+ Check: checkFn,
+ },
},
})
}
@@ -303,10 +340,20 @@ func TestAccCloudflareAccessIdentityProvider_SCIM_Config_Secret(t *testing.T) {
Config: testAccCheckCloudflareAccessIdentityProviderAzureAD(accountID, rnd),
Check: checkFn,
},
+ {
+ // Ensures no diff on second plan
+ Config: testAccCheckCloudflareAccessIdentityProviderAzureAD(accountID, rnd),
+ PlanOnly: true,
+ },
{
Config: testAccCheckCloudflareAccessIdentityProviderAzureADUpdated(accountID, rnd),
Check: checkFn,
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCheckCloudflareAccessIdentityProviderAzureADUpdated(accountID, rnd),
+ PlanOnly: true,
+ },
},
})
}
@@ -322,11 +369,6 @@ func TestAccCloudflareAccessIdentityProvider_SCIM_Secret_Enabled_After_Resource_
if value == "" {
return errors.New("secret is empty")
}
-
- // if strings.Contains(value, "*") {
- // return errors.New("secret was redacted")
- // }
-
return nil
}),
)
@@ -344,14 +386,27 @@ func TestAccCloudflareAccessIdentityProvider_SCIM_Secret_Enabled_After_Resource_
resource.TestCheckNoResourceAttr(resourceName, "scim_config.secret"),
),
},
+ {
+ Config: testAccCheckCloudflareAccessIdentityProviderAzureADNoSCIM(accountID, rnd),
+ PlanOnly: true,
+ },
{
Config: testAccCheckCloudflareAccessIdentityProviderAzureAD(accountID, rnd),
Check: checkFn,
},
+ {
+ Config: testAccCheckCloudflareAccessIdentityProviderAzureAD(accountID, rnd),
+ PlanOnly: true,
+ },
{
Config: testAccCheckCloudflareAccessIdentityProviderAzureADUpdated(accountID, rnd),
Check: checkFn,
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccCheckCloudflareAccessIdentityProviderAzureADUpdated(accountID, rnd),
+ PlanOnly: true,
+ },
},
})
}
diff --git a/internal/services/zero_trust_access_identity_provider/schema.go b/internal/services/zero_trust_access_identity_provider/schema.go
index 1511b5e5a3..83e262c21b 100644
--- a/internal/services/zero_trust_access_identity_provider/schema.go
+++ b/internal/services/zero_trust_access_identity_provider/schema.go
@@ -4,6 +4,8 @@ package zero_trust_access_identity_provider
import (
"context"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/customvalidator"
+ "github.com/hashicorp/terraform-plugin-framework/path"
"github.com/cloudflare/terraform-provider-cloudflare/internal/customfield"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
@@ -44,8 +46,9 @@ func ResourceSchema(ctx context.Context) schema.Schema {
Required: true,
},
"type": schema.StringAttribute{
- Description: "The type of identity provider. To determine the value for a specific provider, refer to our [developer documentation](https://developers.cloudflare.com/cloudflare-one/identity/idp-integration/).\nAvailable values: \"onetimepin\", \"azureAD\", \"saml\", \"centrify\", \"facebook\", \"github\", \"google-apps\", \"google\", \"linkedin\", \"oidc\", \"okta\", \"onelogin\", \"pingone\", \"yandex\".",
- Required: true,
+ Description: "The type of identity provider. To determine the value for a specific provider, refer to our [developer documentation](https://developers.cloudflare.com/cloudflare-one/identity/idp-integration/).\nAvailable values: \"onetimepin\", \"azureAD\", \"saml\", \"centrify\", \"facebook\", \"github\", \"google-apps\", \"google\", \"linkedin\", \"oidc\", \"okta\", \"onelogin\", \"pingone\", \"yandex\".",
+ Required: true,
+ PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
Validators: []validator.String{
stringvalidator.OneOfCaseInsensitive(
"onetimepin",
@@ -87,10 +90,16 @@ func ResourceSchema(ctx context.Context) schema.Schema {
"conditional_access_enabled": schema.BoolAttribute{
Description: "Should Cloudflare try to load authentication contexts from your account",
Optional: true,
+ Validators: []validator.Bool{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), "azureAD"),
+ },
},
"directory_id": schema.StringAttribute{
Description: "Your Azure directory uuid",
Optional: true,
+ Validators: []validator.String{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), "azureAD"),
+ },
},
"email_claim_name": schema.StringAttribute{
Description: "The claim name for email in the id_token response.",
@@ -105,31 +114,50 @@ func ResourceSchema(ctx context.Context) schema.Schema {
"select_account",
"none",
),
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), "azureAD"),
},
},
"support_groups": schema.BoolAttribute{
Description: "Should Cloudflare try to load groups from your account",
Optional: true,
+ Validators: []validator.Bool{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), "azureAD"),
+ },
},
"centrify_account": schema.StringAttribute{
Description: "Your centrify account url",
Optional: true,
+ Validators: []validator.String{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), "centrify"),
+ },
},
"centrify_app_id": schema.StringAttribute{
Description: "Your centrify app id",
Optional: true,
+ Validators: []validator.String{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), "centrify"),
+ },
},
"apps_domain": schema.StringAttribute{
Description: "Your companies TLD",
Optional: true,
+ Validators: []validator.String{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), "google-apps"),
+ },
},
"auth_url": schema.StringAttribute{
Description: "The authorization_endpoint URL of your IdP",
Optional: true,
+ Validators: []validator.String{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), "oidc"),
+ },
},
"certs_url": schema.StringAttribute{
Description: "The jwks_uri endpoint of your IdP to allow the IdP keys to sign the tokens",
Optional: true,
+ Validators: []validator.String{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), "oidc"),
+ },
},
"pkce_enabled": schema.BoolAttribute{
Description: "Enable Proof Key for Code Exchange (PKCE)",
@@ -139,39 +167,66 @@ func ResourceSchema(ctx context.Context) schema.Schema {
Description: "OAuth scopes",
Optional: true,
ElementType: types.StringType,
+ Validators: []validator.List{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), "oidc"),
+ },
},
"token_url": schema.StringAttribute{
Description: "The token_endpoint URL of your IdP",
Optional: true,
+ Validators: []validator.String{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), "oidc"),
+ },
},
"authorization_server_id": schema.StringAttribute{
Description: "Your okta authorization server id",
Optional: true,
+ Validators: []validator.String{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), "okta"),
+ },
},
"okta_account": schema.StringAttribute{
Description: "Your okta account url",
Optional: true,
+ Validators: []validator.String{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), "okta"),
+ },
},
"onelogin_account": schema.StringAttribute{
Description: "Your OneLogin account url",
Optional: true,
+ Validators: []validator.String{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), "onelogin"),
+ },
},
"ping_env_id": schema.StringAttribute{
Description: "Your PingOne environment identifier",
Optional: true,
+ Validators: []validator.String{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), "pingone"),
+ },
},
"attributes": schema.ListAttribute{
Description: "A list of SAML attribute names that will be added to your signed JWT token and can be used in SAML policy rules.",
Optional: true,
ElementType: types.StringType,
+ Validators: []validator.List{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), "saml"),
+ },
},
"email_attribute_name": schema.StringAttribute{
Description: "The attribute name for email in the SAML response.",
Optional: true,
+ Validators: []validator.String{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), "saml"),
+ },
},
"header_attributes": schema.ListNestedAttribute{
Description: "Add a list of attribute names that will be returned in the response header from the Access callback.",
Optional: true,
+ Validators: []validator.List{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), "saml"),
+ },
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"attribute_name": schema.StringAttribute{
@@ -189,23 +244,38 @@ func ResourceSchema(ctx context.Context) schema.Schema {
Description: "X509 certificate to verify the signature in the SAML authentication response",
Optional: true,
ElementType: types.StringType,
+ Validators: []validator.List{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), "saml"),
+ },
},
"issuer_url": schema.StringAttribute{
Description: "IdP Entity ID or Issuer URL",
Optional: true,
+ Validators: []validator.String{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), "saml"),
+ },
},
"sign_request": schema.BoolAttribute{
Description: "Sign the SAML authentication request with Access credentials. To verify the signature, use the public key from the Access certs endpoints.",
Optional: true,
PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()},
+ Validators: []validator.Bool{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), "saml"),
+ },
},
"sso_target_url": schema.StringAttribute{
Description: "URL to send the SAML authentication requests to",
Optional: true,
+ Validators: []validator.String{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), "saml"),
+ },
},
"redirect_url": schema.StringAttribute{
Computed: true,
PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
+ Validators: []validator.String{
+ customvalidator.RequiresOtherStringAttributeToBeOneOf(path.MatchRoot("type"), "saml"),
+ },
},
},
},
@@ -251,6 +321,9 @@ func ResourceSchema(ctx context.Context) schema.Schema {
Description: "A read-only token generated when the SCIM integration is enabled for the first time. It is redacted on subsequent requests. If you lose this you will need to refresh it at /access/identity_providers/:idpID/refresh_scim_secret.",
Computed: true,
Sensitive: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
},
"user_deprovision": schema.BoolAttribute{
Description: "A flag to enable revoking a user's session in Access and Gateway when they have been deprovisioned in the Identity Provider.",
diff --git a/internal/services/zero_trust_access_identity_provider/testdata/accessidentityproviderazuread.tf b/internal/services/zero_trust_access_identity_provider/testdata/accessidentityproviderazuread.tf
index 14cc96add3..69214fafaf 100644
--- a/internal/services/zero_trust_access_identity_provider/testdata/accessidentityproviderazuread.tf
+++ b/internal/services/zero_trust_access_identity_provider/testdata/accessidentityproviderazuread.tf
@@ -9,9 +9,8 @@ resource "cloudflare_zero_trust_access_identity_provider" "%[2]s" {
support_groups = true
}
scim_config = {
- enabled = true
- group_member_deprovision = true
- seat_deprovision = true
- user_deprovision = true
+ enabled = true
+ seat_deprovision = true
+ user_deprovision = true
}
}
diff --git a/internal/services/zero_trust_access_identity_provider/testdata/accessidentityprovidersaml.tf b/internal/services/zero_trust_access_identity_provider/testdata/accessidentityprovidersaml.tf
index 5ff44525ac..f92dbbaeb3 100644
--- a/internal/services/zero_trust_access_identity_provider/testdata/accessidentityprovidersaml.tf
+++ b/internal/services/zero_trust_access_identity_provider/testdata/accessidentityprovidersaml.tf
@@ -4,10 +4,11 @@ resource "cloudflare_zero_trust_access_identity_provider" "%[2]s" {
name = "%[2]s"
type = "saml"
config = {
- issuer_url = "jumpcloud"
+ issuer_url = "jumpcloud"
sso_target_url = "https://sso.myexample.jumpcloud.com/saml2/cloudflareaccess"
- attributes = [ "email", "username" ]
- sign_request = false
- idp_public_certs = ["MIIDpDCCAoygAwIBAgIGAV2ka+55MA0GCSqGSIb3DQEBCwUAMIGSMQswCQYDVQQGEwJVUzETMBEG\nA1UEC…..GF/Q2/MHadws97cZg\nuTnQyuOqPuHbnN83d/2l1NSYKCbHt24o"]
-}
+ attributes = ["email", "username"]
+ idp_public_certs = [
+ "MIIDpDCCAoygAwIBAgIGAV2ka+55MA0GCSqGSIb3DQEBCwUAMIGSMQswCQYDVQQGEwJVUzETMBEG\nA1UEC…..GF/Q2/MHadws97cZg\nuTnQyuOqPuHbnN83d/2l1NSYKCbHt24o"
+ ]
+ }
}
diff --git a/internal/services/zero_trust_access_policy/model.go b/internal/services/zero_trust_access_policy/model.go
index 655357b089..532d034907 100644
--- a/internal/services/zero_trust_access_policy/model.go
+++ b/internal/services/zero_trust_access_policy/model.go
@@ -4,8 +4,6 @@ package zero_trust_access_policy
import (
"github.com/cloudflare/terraform-provider-cloudflare/internal/apijson"
- "github.com/cloudflare/terraform-provider-cloudflare/internal/customfield"
- "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes"
"github.com/hashicorp/terraform-plugin-framework/types"
)
@@ -14,23 +12,19 @@ type ZeroTrustAccessPolicyResultEnvelope struct {
}
type ZeroTrustAccessPolicyModel struct {
- ID types.String `tfsdk:"id" json:"id,computed"`
- AccountID types.String `tfsdk:"account_id" path:"account_id,required"`
- Decision types.String `tfsdk:"decision" json:"decision,required"`
- Name types.String `tfsdk:"name" json:"name,required"`
- ApprovalRequired types.Bool `tfsdk:"approval_required" json:"approval_required,optional"`
- IsolationRequired types.Bool `tfsdk:"isolation_required" json:"isolation_required,optional"`
- PurposeJustificationPrompt types.String `tfsdk:"purpose_justification_prompt" json:"purpose_justification_prompt,optional"`
- PurposeJustificationRequired types.Bool `tfsdk:"purpose_justification_required" json:"purpose_justification_required,optional"`
- ApprovalGroups *[]*ZeroTrustAccessPolicyApprovalGroupsModel `tfsdk:"approval_groups" json:"approval_groups,optional"`
- SessionDuration types.String `tfsdk:"session_duration" json:"session_duration,computed_optional"`
- Exclude customfield.NestedObjectList[ZeroTrustAccessPolicyExcludeModel] `tfsdk:"exclude" json:"exclude,computed_optional"`
- Include customfield.NestedObjectList[ZeroTrustAccessPolicyIncludeModel] `tfsdk:"include" json:"include,computed_optional"`
- Require customfield.NestedObjectList[ZeroTrustAccessPolicyRequireModel] `tfsdk:"require" json:"require,computed_optional"`
- AppCount types.Int64 `tfsdk:"app_count" json:"app_count,computed"`
- CreatedAt timetypes.RFC3339 `tfsdk:"created_at" json:"created_at,computed" format:"date-time"`
- Reusable types.Bool `tfsdk:"reusable" json:"reusable,computed"`
- UpdatedAt timetypes.RFC3339 `tfsdk:"updated_at" json:"updated_at,computed" format:"date-time"`
+ ID types.String `tfsdk:"id" json:"id,computed"`
+ AccountID types.String `tfsdk:"account_id" path:"account_id,required"`
+ Decision types.String `tfsdk:"decision" json:"decision,required"`
+ Name types.String `tfsdk:"name" json:"name,required"`
+ ApprovalRequired types.Bool `tfsdk:"approval_required" json:"approval_required,optional"`
+ IsolationRequired types.Bool `tfsdk:"isolation_required" json:"isolation_required,optional"`
+ PurposeJustificationPrompt types.String `tfsdk:"purpose_justification_prompt" json:"purpose_justification_prompt,optional"`
+ PurposeJustificationRequired types.Bool `tfsdk:"purpose_justification_required" json:"purpose_justification_required,optional"`
+ ApprovalGroups *[]*ZeroTrustAccessPolicyApprovalGroupsModel `tfsdk:"approval_groups" json:"approval_groups,optional"`
+ SessionDuration types.String `tfsdk:"session_duration" json:"session_duration,computed_optional"`
+ Exclude *[]*ZeroTrustAccessPolicyExcludeModel `tfsdk:"exclude" json:"exclude,optional"`
+ Include *[]*ZeroTrustAccessPolicyIncludeModel `tfsdk:"include" json:"include,optional"`
+ Require *[]*ZeroTrustAccessPolicyRequireModel `tfsdk:"require" json:"require,optional"`
}
func (m ZeroTrustAccessPolicyModel) MarshalJSON() (data []byte, err error) {
diff --git a/internal/services/zero_trust_access_policy/normalizations.go b/internal/services/zero_trust_access_policy/normalizations.go
new file mode 100644
index 0000000000..0566e55e2e
--- /dev/null
+++ b/internal/services/zero_trust_access_policy/normalizations.go
@@ -0,0 +1,30 @@
+package zero_trust_access_policy
+
+import (
+ "context"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+)
+
+type IsNull interface {
+ IsNull() bool
+}
+
+func normalizeEmptyAndNullSlice[T any](data **[]T, stateData *[]T) {
+ if (*data != nil && len(**data) != 0) || (stateData != nil && len(*stateData) != 0) {
+ return
+ }
+ *data = stateData
+}
+
+// Normalizing function to ensure consistency between the state/plan and the meaning of the API response.
+// Alters the API response before applying it to the state by laxing equalities between null & zero-value
+// for some attributes, and nullifies fields that terraform should not be saving in the state.
+func normalizeReadZeroTrustAccessPolicyAPIData(ctx context.Context, data, sourceData *ZeroTrustAccessPolicyModel) diag.Diagnostics {
+ diags := make(diag.Diagnostics, 0)
+
+ normalizeEmptyAndNullSlice(&data.Include, sourceData.Include)
+ normalizeEmptyAndNullSlice(&data.Require, sourceData.Require)
+ normalizeEmptyAndNullSlice(&data.Exclude, sourceData.Exclude)
+
+ return diags
+}
diff --git a/internal/services/zero_trust_access_policy/resource.go b/internal/services/zero_trust_access_policy/resource.go
index 9ee3638a23..5a0af5f22c 100644
--- a/internal/services/zero_trust_access_policy/resource.go
+++ b/internal/services/zero_trust_access_policy/resource.go
@@ -141,6 +141,13 @@ func (r *ZeroTrustAccessPolicyResource) Update(ctx context.Context, req resource
}
data = &env.Result
+ var planData *ZeroTrustAccessPolicyModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &planData)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(normalizeReadZeroTrustAccessPolicyAPIData(ctx, data, planData)...)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
@@ -181,6 +188,14 @@ func (r *ZeroTrustAccessPolicyResource) Read(ctx context.Context, req resource.R
}
data = &env.Result
+ var stateData *ZeroTrustAccessPolicyModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &stateData)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(normalizeReadZeroTrustAccessPolicyAPIData(ctx, data, stateData)...)
+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
diff --git a/internal/services/zero_trust_access_policy/resource_test.go b/internal/services/zero_trust_access_policy/resource_test.go
index a31b106ba0..20209c1f40 100644
--- a/internal/services/zero_trust_access_policy/resource_test.go
+++ b/internal/services/zero_trust_access_policy/resource_test.go
@@ -44,6 +44,11 @@ func TestAccCloudflareAccessPolicy_ServiceToken(t *testing.T) {
resource.TestCheckResourceAttr(name, "include.0.service_token.%", "1"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccessPolicyServiceTokenConfig(rnd, zone, accountID),
+ PlanOnly: true,
+ },
},
})
}
@@ -69,6 +74,11 @@ func TestAccCloudflareAccessPolicy_AnyServiceToken(t *testing.T) {
resource.TestCheckResourceAttrSet(name, "include.0.any_valid_service_token.%"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccessPolicyAnyServiceTokenConfig(rnd, zone, accountID),
+ PlanOnly: true,
+ },
},
})
}
@@ -110,6 +120,11 @@ func TestAccCloudflareAccessPolicy_Group(t *testing.T) {
resource.TestCheckResourceAttr(name, "include.0.group.%", "1"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccessPolicyGroupConfig(rnd, zone, accountID),
+ PlanOnly: true,
+ },
},
})
}
@@ -139,6 +154,11 @@ func TestAccCloudflareAccessPolicy_MTLS(t *testing.T) {
resource.TestCheckResourceAttrSet(name, "include.0.certificate.%"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccessPolicyMTLSConfig(rnd, zone, accountID),
+ PlanOnly: true,
+ },
},
})
}
@@ -173,6 +193,11 @@ func TestAccCloudflareAccessPolicy_EmailDomain(t *testing.T) {
resource.TestCheckResourceAttr(name, "session_duration", "12h"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccessPolicyEmailDomainConfig(rnd, zone, accountID),
+ PlanOnly: true,
+ },
},
})
}
@@ -202,6 +227,11 @@ func TestAccCloudflareAccessPolicy_Emails(t *testing.T) {
resource.TestCheckResourceAttr(name, "include.0.email.email", "a@example.com"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccessPolicyEmailsConfig(rnd, zone, accountID),
+ PlanOnly: true,
+ },
},
})
}
@@ -231,6 +261,11 @@ func TestAccCloudflareAccessPolicy_Everyone(t *testing.T) {
resource.TestCheckResourceAttrSet(name, "include.0.everyone.%"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccessPolicyEveryoneConfig(rnd, zone, accountID),
+ PlanOnly: true,
+ },
},
})
}
@@ -260,6 +295,11 @@ func TestAccCloudflareAccessPolicy_IPs(t *testing.T) {
resource.TestCheckResourceAttr(name, "include.0.ip.ip", "10.0.0.1/32"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccessPolicyIPsConfig(rnd, zone, accountID),
+ PlanOnly: true,
+ },
},
})
}
@@ -289,6 +329,11 @@ func TestAccCloudflareAccessPolicy_AuthMethod(t *testing.T) {
resource.TestCheckResourceAttr(name, "include.0.auth_method.auth_method", "hwk"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccessPolicyAuthMethodConfig(rnd, zone, accountID),
+ PlanOnly: true,
+ },
},
})
}
@@ -318,6 +363,11 @@ func TestAccCloudflareAccessPolicy_Geo(t *testing.T) {
resource.TestCheckResourceAttr(name, "include.0.geo.country_code", "US"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccessPolicyGeoConfig(rnd, zone, accountID),
+ PlanOnly: true,
+ },
},
})
}
@@ -348,6 +398,11 @@ func TestAccCloudflareAccessPolicy_Okta(t *testing.T) {
resource.TestCheckResourceAttr(name, "include.0.okta.identity_provider_id", "225934dc-14e4-4f55-87be-f5d798d23f91"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccessPolicyOktaConfig(rnd, zone, accountID),
+ PlanOnly: true,
+ },
},
})
}
@@ -378,6 +433,11 @@ func TestAccCloudflareAccessPolicy_PurposeJustification(t *testing.T) {
resource.TestCheckResourceAttr(name, "purpose_justification_prompt", "Why should we let you in?"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccessPolicyPurposeJustificationConfig(rnd, zone, accountID),
+ PlanOnly: true,
+ },
},
})
}
@@ -456,6 +516,11 @@ func TestAccCloudflareAccessPolicy_ApprovalGroup(t *testing.T) {
resource.TestCheckResourceAttr(name, "approval_groups.1.approvals_needed", "1"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccessPolicyApprovalGroupConfig(rnd, zone, accountID),
+ PlanOnly: true,
+ },
},
})
}
@@ -527,6 +592,11 @@ func TestAccCloudflareAccessPolicy_ExternalEvaluation(t *testing.T) {
resource.TestCheckResourceAttr(name, "include.0.external_evaluation.keys_url", "https://example.com/keys"),
),
},
+ {
+ // Ensures no diff on last plan
+ Config: testAccessPolicyExternalEvalautionConfig(rnd, zone, accountID),
+ PlanOnly: true,
+ },
},
})
}
@@ -535,6 +605,8 @@ func testAccessPolicyExternalEvalautionConfig(resourceID, zone, accountID string
return acctest.LoadTestCase("accesspolicyexternalevalautionconfig.tf", resourceID, zone, accountID)
}
+/*
+Commented out until cloudflare_zero_trust_gateway_settings gets fixed
func TestAccCloudflareAccessPolicy_IsolationRequired(t *testing.T) {
rnd := utils.GenerateRandomResourceName()
name := "cloudflare_zero_trust_access_policy." + rnd
@@ -559,6 +631,7 @@ func TestAccCloudflareAccessPolicy_IsolationRequired(t *testing.T) {
},
})
}
+*/
func testAccessPolicyIsolationRequiredConfig(resourceID, zone, accountID string) string {
return acctest.LoadTestCase("accesspolicyisolationrequiredconfig.tf", resourceID, zone, accountID)
diff --git a/internal/services/zero_trust_access_policy/schema.go b/internal/services/zero_trust_access_policy/schema.go
index 09b77673c0..726ad2b503 100644
--- a/internal/services/zero_trust_access_policy/schema.go
+++ b/internal/services/zero_trust_access_policy/schema.go
@@ -4,9 +4,8 @@ package zero_trust_access_policy
import (
"context"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/customvalidator"
- "github.com/cloudflare/terraform-provider-cloudflare/internal/customfield"
- "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes"
"github.com/hashicorp/terraform-plugin-framework-validators/float64validator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/resource"
@@ -97,10 +96,11 @@ func ResourceSchema(ctx context.Context) schema.Schema {
},
"exclude": schema.ListNestedAttribute{
Description: "Rules evaluated with a NOT logical operator. To match the policy, a user cannot meet any of the Exclude rules.",
- Computed: true,
Optional: true,
- CustomType: customfield.NewNestedObjectListType[ZeroTrustAccessPolicyExcludeModel](ctx),
NestedObject: schema.NestedAttributeObject{
+ Validators: []validator.Object{
+ customvalidator.ObjectSizeAtMost(1),
+ },
Attributes: map[string]schema.Attribute{
"group": schema.SingleNestedAttribute{
Optional: true,
@@ -332,10 +332,11 @@ func ResourceSchema(ctx context.Context) schema.Schema {
},
"include": schema.ListNestedAttribute{
Description: "Rules evaluated with an OR logical operator. A user needs to meet only one of the Include rules.",
- Computed: true,
- Optional: true,
- CustomType: customfield.NewNestedObjectListType[ZeroTrustAccessPolicyIncludeModel](ctx),
+ Required: true,
NestedObject: schema.NestedAttributeObject{
+ Validators: []validator.Object{
+ customvalidator.ObjectSizeAtMost(1),
+ },
Attributes: map[string]schema.Attribute{
"group": schema.SingleNestedAttribute{
Optional: true,
@@ -567,10 +568,11 @@ func ResourceSchema(ctx context.Context) schema.Schema {
},
"require": schema.ListNestedAttribute{
Description: "Rules evaluated with an AND logical operator. To match the policy, a user must meet all of the Require rules.",
- Computed: true,
Optional: true,
- CustomType: customfield.NewNestedObjectListType[ZeroTrustAccessPolicyRequireModel](ctx),
NestedObject: schema.NestedAttributeObject{
+ Validators: []validator.Object{
+ customvalidator.ObjectSizeAtMost(1),
+ },
Attributes: map[string]schema.Attribute{
"group": schema.SingleNestedAttribute{
Optional: true,
@@ -800,21 +802,6 @@ func ResourceSchema(ctx context.Context) schema.Schema {
},
},
},
- "app_count": schema.Int64Attribute{
- Description: "Number of access applications currently using this policy.",
- Computed: true,
- },
- "created_at": schema.StringAttribute{
- Computed: true,
- CustomType: timetypes.RFC3339Type{},
- },
- "reusable": schema.BoolAttribute{
- Computed: true,
- },
- "updated_at": schema.StringAttribute{
- Computed: true,
- CustomType: timetypes.RFC3339Type{},
- },
},
}
}
diff --git a/internal/services/zero_trust_dlp_custom_profile/data_source_schema.go b/internal/services/zero_trust_dlp_custom_profile/data_source_schema.go
index 340996ae77..99b31f4954 100644
--- a/internal/services/zero_trust_dlp_custom_profile/data_source_schema.go
+++ b/internal/services/zero_trust_dlp_custom_profile/data_source_schema.go
@@ -142,7 +142,7 @@ func DataSourceSchema(ctx context.Context) schema.Schema {
},
},
"type": schema.StringAttribute{
- Description: `Available values: "custom", "predefined", "integration", "exact_data", "document_template", "word_list".`,
+ Description: `Available values: "custom", "predefined", "integration", "exact_data", "document_fingerprint", "word_list".`,
Computed: true,
Validators: []validator.String{
stringvalidator.OneOfCaseInsensitive(
@@ -150,7 +150,7 @@ func DataSourceSchema(ctx context.Context) schema.Schema {
"predefined",
"integration",
"exact_data",
- "document_template",
+ "document_fingerprint",
"word_list",
),
},
diff --git a/internal/services/zero_trust_dlp_custom_profile/schema.go b/internal/services/zero_trust_dlp_custom_profile/schema.go
index 0b41400b3c..c30714ced6 100644
--- a/internal/services/zero_trust_dlp_custom_profile/schema.go
+++ b/internal/services/zero_trust_dlp_custom_profile/schema.go
@@ -112,7 +112,7 @@ func ResourceSchema(ctx context.Context) schema.Schema {
Required: true,
},
"entry_type": schema.StringAttribute{
- Description: `Available values: "custom", "predefined", "integration", "exact_data".`,
+ Description: `Available values: "custom", "predefined", "integration", "exact_data", "document_fingerprint".`,
Required: true,
Validators: []validator.String{
stringvalidator.OneOfCaseInsensitive(
@@ -120,6 +120,7 @@ func ResourceSchema(ctx context.Context) schema.Schema {
"predefined",
"integration",
"exact_data",
+ "document_fingerprint",
),
},
},
diff --git a/internal/services/zero_trust_dlp_entry/data_source_schema.go b/internal/services/zero_trust_dlp_entry/data_source_schema.go
index 8077e51688..8e80536dd7 100644
--- a/internal/services/zero_trust_dlp_entry/data_source_schema.go
+++ b/internal/services/zero_trust_dlp_entry/data_source_schema.go
@@ -49,7 +49,7 @@ func DataSourceSchema(ctx context.Context) schema.Schema {
Computed: true,
},
"type": schema.StringAttribute{
- Description: `Available values: "custom", "predefined", "integration", "exact_data", "document_template", "word_list".`,
+ Description: `Available values: "custom", "predefined", "integration", "exact_data", "document_fingerprint", "word_list".`,
Computed: true,
Validators: []validator.String{
stringvalidator.OneOfCaseInsensitive(
@@ -57,7 +57,7 @@ func DataSourceSchema(ctx context.Context) schema.Schema {
"predefined",
"integration",
"exact_data",
- "document_template",
+ "document_fingerprint",
"word_list",
),
},
diff --git a/internal/services/zero_trust_dlp_entry/list_data_source_schema.go b/internal/services/zero_trust_dlp_entry/list_data_source_schema.go
index 5736bbafa6..d412143c88 100644
--- a/internal/services/zero_trust_dlp_entry/list_data_source_schema.go
+++ b/internal/services/zero_trust_dlp_entry/list_data_source_schema.go
@@ -67,7 +67,7 @@ func ListDataSourceSchema(ctx context.Context) schema.Schema {
},
},
"type": schema.StringAttribute{
- Description: `Available values: "custom", "predefined", "integration", "exact_data", "document_template", "word_list".`,
+ Description: `Available values: "custom", "predefined", "integration", "exact_data", "document_fingerprint", "word_list".`,
Computed: true,
Validators: []validator.String{
stringvalidator.OneOfCaseInsensitive(
@@ -75,7 +75,7 @@ func ListDataSourceSchema(ctx context.Context) schema.Schema {
"predefined",
"integration",
"exact_data",
- "document_template",
+ "document_fingerprint",
"word_list",
),
},
diff --git a/internal/services/zero_trust_dlp_predefined_profile/data_source_schema.go b/internal/services/zero_trust_dlp_predefined_profile/data_source_schema.go
index 57e6cce3b6..064696c33d 100644
--- a/internal/services/zero_trust_dlp_predefined_profile/data_source_schema.go
+++ b/internal/services/zero_trust_dlp_predefined_profile/data_source_schema.go
@@ -142,7 +142,7 @@ func DataSourceSchema(ctx context.Context) schema.Schema {
},
},
"type": schema.StringAttribute{
- Description: `Available values: "custom", "predefined", "integration", "exact_data", "document_template", "word_list".`,
+ Description: `Available values: "custom", "predefined", "integration", "exact_data", "document_fingerprint", "word_list".`,
Computed: true,
Validators: []validator.String{
stringvalidator.OneOfCaseInsensitive(
@@ -150,7 +150,7 @@ func DataSourceSchema(ctx context.Context) schema.Schema {
"predefined",
"integration",
"exact_data",
- "document_template",
+ "document_fingerprint",
"word_list",
),
},
diff --git a/internal/services/zero_trust_gateway_settings/data_source_model.go b/internal/services/zero_trust_gateway_settings/data_source_model.go
index 032ba6de59..bd3d124384 100644
--- a/internal/services/zero_trust_gateway_settings/data_source_model.go
+++ b/internal/services/zero_trust_gateway_settings/data_source_model.go
@@ -35,7 +35,6 @@ func (m *ZeroTrustGatewaySettingsDataSourceModel) toReadParams(_ context.Context
type ZeroTrustGatewaySettingsSettingsDataSourceModel struct {
ActivityLog customfield.NestedObject[ZeroTrustGatewaySettingsSettingsActivityLogDataSourceModel] `tfsdk:"activity_log" json:"activity_log,computed"`
Antivirus customfield.NestedObject[ZeroTrustGatewaySettingsSettingsAntivirusDataSourceModel] `tfsdk:"antivirus" json:"antivirus,computed"`
- AppControlSettings customfield.NestedObject[ZeroTrustGatewaySettingsSettingsAppControlSettingsDataSourceModel] `tfsdk:"app_control_settings" json:"app-control-settings,computed"`
BlockPage customfield.NestedObject[ZeroTrustGatewaySettingsSettingsBlockPageDataSourceModel] `tfsdk:"block_page" json:"block_page,computed"`
BodyScanning customfield.NestedObject[ZeroTrustGatewaySettingsSettingsBodyScanningDataSourceModel] `tfsdk:"body_scanning" json:"body_scanning,computed"`
BrowserIsolation customfield.NestedObject[ZeroTrustGatewaySettingsSettingsBrowserIsolationDataSourceModel] `tfsdk:"browser_isolation" json:"browser_isolation,computed"`
@@ -67,10 +66,6 @@ type ZeroTrustGatewaySettingsSettingsAntivirusNotificationSettingsDataSourceMode
SupportURL types.String `tfsdk:"support_url" json:"support_url,computed"`
}
-type ZeroTrustGatewaySettingsSettingsAppControlSettingsDataSourceModel struct {
- Enabled types.Bool `tfsdk:"enabled" json:"enabled,computed"`
-}
-
type ZeroTrustGatewaySettingsSettingsBlockPageDataSourceModel struct {
BackgroundColor types.String `tfsdk:"background_color" json:"background_color,computed"`
Enabled types.Bool `tfsdk:"enabled" json:"enabled,computed"`
diff --git a/internal/services/zero_trust_gateway_settings/data_source_schema.go b/internal/services/zero_trust_gateway_settings/data_source_schema.go
index e710c5fb6d..fbf7fa27fd 100644
--- a/internal/services/zero_trust_gateway_settings/data_source_schema.go
+++ b/internal/services/zero_trust_gateway_settings/data_source_schema.go
@@ -87,17 +87,6 @@ func DataSourceSchema(ctx context.Context) schema.Schema {
},
},
},
- "app_control_settings": schema.SingleNestedAttribute{
- Description: "Setting to enable App Control",
- Computed: true,
- CustomType: customfield.NewNestedObjectType[ZeroTrustGatewaySettingsSettingsAppControlSettingsDataSourceModel](ctx),
- Attributes: map[string]schema.Attribute{
- "enabled": schema.BoolAttribute{
- Description: "Enable App Control",
- Computed: true,
- },
- },
- },
"block_page": schema.SingleNestedAttribute{
Description: "Block page layout settings.",
Computed: true,
diff --git a/internal/services/zero_trust_gateway_settings/model.go b/internal/services/zero_trust_gateway_settings/model.go
index d5989d54cf..dda7321cdf 100644
--- a/internal/services/zero_trust_gateway_settings/model.go
+++ b/internal/services/zero_trust_gateway_settings/model.go
@@ -32,7 +32,6 @@ func (m ZeroTrustGatewaySettingsModel) MarshalJSONForUpdate(state ZeroTrustGatew
type ZeroTrustGatewaySettingsSettingsModel struct {
ActivityLog *ZeroTrustGatewaySettingsSettingsActivityLogModel `tfsdk:"activity_log" json:"activity_log,optional"`
Antivirus *ZeroTrustGatewaySettingsSettingsAntivirusModel `tfsdk:"antivirus" json:"antivirus,optional"`
- AppControlSettings *ZeroTrustGatewaySettingsSettingsAppControlSettingsModel `tfsdk:"app_control_settings" json:"app-control-settings,optional"`
BlockPage customfield.NestedObject[ZeroTrustGatewaySettingsSettingsBlockPageModel] `tfsdk:"block_page" json:"block_page,computed_optional"`
BodyScanning *ZeroTrustGatewaySettingsSettingsBodyScanningModel `tfsdk:"body_scanning" json:"body_scanning,optional"`
BrowserIsolation *ZeroTrustGatewaySettingsSettingsBrowserIsolationModel `tfsdk:"browser_isolation" json:"browser_isolation,optional"`
@@ -64,10 +63,6 @@ type ZeroTrustGatewaySettingsSettingsAntivirusNotificationSettingsModel struct {
SupportURL types.String `tfsdk:"support_url" json:"support_url,optional"`
}
-type ZeroTrustGatewaySettingsSettingsAppControlSettingsModel struct {
- Enabled types.Bool `tfsdk:"enabled" json:"enabled,optional"`
-}
-
type ZeroTrustGatewaySettingsSettingsBlockPageModel struct {
BackgroundColor types.String `tfsdk:"background_color" json:"background_color,optional"`
Enabled types.Bool `tfsdk:"enabled" json:"enabled,optional"`
diff --git a/internal/services/zero_trust_gateway_settings/schema.go b/internal/services/zero_trust_gateway_settings/schema.go
index bbc0cca09b..218da3955d 100644
--- a/internal/services/zero_trust_gateway_settings/schema.go
+++ b/internal/services/zero_trust_gateway_settings/schema.go
@@ -85,16 +85,6 @@ func ResourceSchema(ctx context.Context) schema.Schema {
},
},
},
- "app_control_settings": schema.SingleNestedAttribute{
- Description: "Setting to enable App Control",
- Optional: true,
- Attributes: map[string]schema.Attribute{
- "enabled": schema.BoolAttribute{
- Description: "Enable App Control",
- Optional: true,
- },
- },
- },
"block_page": schema.SingleNestedAttribute{
Description: "Block page layout settings.",
Computed: true,
diff --git a/internal/services/zero_trust_list/model.go b/internal/services/zero_trust_list/model.go
index a7aab21d91..3a758518d8 100644
--- a/internal/services/zero_trust_list/model.go
+++ b/internal/services/zero_trust_list/model.go
@@ -4,7 +4,6 @@ package zero_trust_list
import (
"github.com/cloudflare/terraform-provider-cloudflare/internal/apijson"
- "github.com/cloudflare/terraform-provider-cloudflare/internal/customfield"
"github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes"
"github.com/hashicorp/terraform-plugin-framework/types"
)
@@ -14,15 +13,15 @@ type ZeroTrustListResultEnvelope struct {
}
type ZeroTrustListModel struct {
- ID types.String `tfsdk:"id" json:"id,computed"`
- AccountID types.String `tfsdk:"account_id" path:"account_id,required"`
- Type types.String `tfsdk:"type" json:"type,required"`
- Name types.String `tfsdk:"name" json:"name,required"`
- Description types.String `tfsdk:"description" json:"description,optional"`
- Items customfield.NestedObjectList[ZeroTrustListItemsModel] `tfsdk:"items" json:"items,computed_optional"`
- CreatedAt timetypes.RFC3339 `tfsdk:"created_at" json:"created_at,computed" format:"date-time"`
- ListCount types.Float64 `tfsdk:"list_count" json:"count,computed"`
- UpdatedAt timetypes.RFC3339 `tfsdk:"updated_at" json:"updated_at,computed" format:"date-time"`
+ ID types.String `tfsdk:"id" json:"id,computed"`
+ AccountID types.String `tfsdk:"account_id" path:"account_id,required"`
+ Type types.String `tfsdk:"type" json:"type,required"`
+ Name types.String `tfsdk:"name" json:"name,required"`
+ Description types.String `tfsdk:"description" json:"description,optional"`
+ Items *[]*ZeroTrustListItemsModel `tfsdk:"items" json:"items,optional"`
+ CreatedAt timetypes.RFC3339 `tfsdk:"created_at" json:"created_at,computed" format:"date-time"`
+ ListCount types.Float64 `tfsdk:"list_count" json:"count,computed"`
+ UpdatedAt timetypes.RFC3339 `tfsdk:"updated_at" json:"updated_at,computed" format:"date-time"`
}
func (m ZeroTrustListModel) MarshalJSON() (data []byte, err error) {
@@ -34,7 +33,6 @@ func (m ZeroTrustListModel) MarshalJSONForUpdate(state ZeroTrustListModel) (data
}
type ZeroTrustListItemsModel struct {
- CreatedAt timetypes.RFC3339 `tfsdk:"created_at" json:"created_at,computed" format:"date-time"`
- Description types.String `tfsdk:"description" json:"description,optional"`
- Value types.String `tfsdk:"value" json:"value,optional"`
+ Description types.String `tfsdk:"description" json:"description,optional"`
+ Value types.String `tfsdk:"value" json:"value,optional"`
}
diff --git a/internal/services/zero_trust_list/schema.go b/internal/services/zero_trust_list/schema.go
index 4f2fc9c5ee..c593962aaa 100644
--- a/internal/services/zero_trust_list/schema.go
+++ b/internal/services/zero_trust_list/schema.go
@@ -5,7 +5,6 @@ package zero_trust_list
import (
"context"
- "github.com/cloudflare/terraform-provider-cloudflare/internal/customfield"
"github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/resource"
@@ -52,16 +51,10 @@ func ResourceSchema(ctx context.Context) schema.Schema {
Optional: true,
},
"items": schema.ListNestedAttribute{
- Description: "The items in the list.",
- Computed: true,
+ Description: "items to add to the list.",
Optional: true,
- CustomType: customfield.NewNestedObjectListType[ZeroTrustListItemsModel](ctx),
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
- "created_at": schema.StringAttribute{
- Computed: true,
- CustomType: timetypes.RFC3339Type{},
- },
"description": schema.StringAttribute{
Description: "The description of the list item, if present",
Optional: true,
diff --git a/internal/services/zero_trust_tunnel_warp_connector/data_source.go b/internal/services/zero_trust_tunnel_warp_connector/data_source.go
new file mode 100644
index 0000000000..99bb2cc9a2
--- /dev/null
+++ b/internal/services/zero_trust_tunnel_warp_connector/data_source.go
@@ -0,0 +1,118 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package zero_trust_tunnel_warp_connector
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/cloudflare/cloudflare-go/v4"
+ "github.com/cloudflare/cloudflare-go/v4/option"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/apijson"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/logging"
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+)
+
+type ZeroTrustTunnelWARPConnectorDataSource struct {
+ client *cloudflare.Client
+}
+
+var _ datasource.DataSourceWithConfigure = (*ZeroTrustTunnelWARPConnectorDataSource)(nil)
+
+func NewZeroTrustTunnelWARPConnectorDataSource() datasource.DataSource {
+ return &ZeroTrustTunnelWARPConnectorDataSource{}
+}
+
+func (d *ZeroTrustTunnelWARPConnectorDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_zero_trust_tunnel_warp_connector"
+}
+
+func (d *ZeroTrustTunnelWARPConnectorDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ client, ok := req.ProviderData.(*cloudflare.Client)
+
+ if !ok {
+ resp.Diagnostics.AddError(
+ "unexpected resource configure type",
+ fmt.Sprintf("Expected *cloudflare.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+
+ return
+ }
+
+ d.client = client
+}
+
+func (d *ZeroTrustTunnelWARPConnectorDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data *ZeroTrustTunnelWARPConnectorDataSourceModel
+
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if data.Filter != nil {
+ params, diags := data.toListParams(ctx)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ env := ZeroTrustTunnelWARPConnectorsResultListDataSourceEnvelope{}
+ page, err := d.client.ZeroTrust.Tunnels.WARPConnector.List(ctx, params)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to make http request", err.Error())
+ return
+ }
+
+ bytes := []byte(page.JSON.RawJSON())
+ err = apijson.UnmarshalComputed(bytes, &env)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to unmarshal http request", err.Error())
+ return
+ }
+
+ if count := len(env.Result.Elements()); count != 1 {
+ resp.Diagnostics.AddError("failed to find exactly one result", fmt.Sprint(count)+" found")
+ return
+ }
+ ts, diags := env.Result.AsStructSliceT(ctx)
+ resp.Diagnostics.Append(diags...)
+ data.TunnelID = ts[0].ID
+ }
+
+ params, diags := data.toReadParams(ctx)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ res := new(http.Response)
+ env := ZeroTrustTunnelWARPConnectorResultDataSourceEnvelope{*data}
+ _, err := d.client.ZeroTrust.Tunnels.WARPConnector.Get(
+ ctx,
+ data.TunnelID.ValueString(),
+ params,
+ option.WithResponseBodyInto(&res),
+ option.WithMiddleware(logging.Middleware(ctx)),
+ )
+ if err != nil {
+ resp.Diagnostics.AddError("failed to make http request", err.Error())
+ return
+ }
+ bytes, _ := io.ReadAll(res.Body)
+ err = apijson.UnmarshalComputed(bytes, &env)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to deserialize http request", err.Error())
+ return
+ }
+ data = &env.Result
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/services/zero_trust_tunnel_warp_connector/data_source_model.go b/internal/services/zero_trust_tunnel_warp_connector/data_source_model.go
new file mode 100644
index 0000000000..e4e8aac6bb
--- /dev/null
+++ b/internal/services/zero_trust_tunnel_warp_connector/data_source_model.go
@@ -0,0 +1,109 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package zero_trust_tunnel_warp_connector
+
+import (
+ "context"
+
+ "github.com/cloudflare/cloudflare-go/v4"
+ "github.com/cloudflare/cloudflare-go/v4/zero_trust"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/customfield"
+ "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
+ "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+type ZeroTrustTunnelWARPConnectorResultDataSourceEnvelope struct {
+ Result ZeroTrustTunnelWARPConnectorDataSourceModel `json:"result,computed"`
+}
+
+type ZeroTrustTunnelWARPConnectorDataSourceModel struct {
+ ID types.String `tfsdk:"id" path:"tunnel_id,computed"`
+ TunnelID types.String `tfsdk:"tunnel_id" path:"tunnel_id,optional"`
+ AccountID types.String `tfsdk:"account_id" path:"account_id,required"`
+ AccountTag types.String `tfsdk:"account_tag" json:"account_tag,computed"`
+ ConnsActiveAt timetypes.RFC3339 `tfsdk:"conns_active_at" json:"conns_active_at,computed" format:"date-time"`
+ ConnsInactiveAt timetypes.RFC3339 `tfsdk:"conns_inactive_at" json:"conns_inactive_at,computed" format:"date-time"`
+ CreatedAt timetypes.RFC3339 `tfsdk:"created_at" json:"created_at,computed" format:"date-time"`
+ DeletedAt timetypes.RFC3339 `tfsdk:"deleted_at" json:"deleted_at,computed" format:"date-time"`
+ Name types.String `tfsdk:"name" json:"name,computed"`
+ RemoteConfig types.Bool `tfsdk:"remote_config" json:"remote_config,computed"`
+ Status types.String `tfsdk:"status" json:"status,computed"`
+ TunType types.String `tfsdk:"tun_type" json:"tun_type,computed"`
+ Connections customfield.NestedObjectList[ZeroTrustTunnelWARPConnectorConnectionsDataSourceModel] `tfsdk:"connections" json:"connections,computed"`
+ Metadata jsontypes.Normalized `tfsdk:"metadata" json:"metadata,computed"`
+ Filter *ZeroTrustTunnelWARPConnectorFindOneByDataSourceModel `tfsdk:"filter"`
+}
+
+func (m *ZeroTrustTunnelWARPConnectorDataSourceModel) toReadParams(_ context.Context) (params zero_trust.TunnelWARPConnectorGetParams, diags diag.Diagnostics) {
+ params = zero_trust.TunnelWARPConnectorGetParams{
+ AccountID: cloudflare.F(m.AccountID.ValueString()),
+ }
+
+ return
+}
+
+func (m *ZeroTrustTunnelWARPConnectorDataSourceModel) toListParams(_ context.Context) (params zero_trust.TunnelWARPConnectorListParams, diags diag.Diagnostics) {
+ mFilterWasActiveAt, errs := m.Filter.WasActiveAt.ValueRFC3339Time()
+ diags.Append(errs...)
+ mFilterWasInactiveAt, errs := m.Filter.WasInactiveAt.ValueRFC3339Time()
+ diags.Append(errs...)
+
+ params = zero_trust.TunnelWARPConnectorListParams{
+ AccountID: cloudflare.F(m.AccountID.ValueString()),
+ }
+
+ if !m.Filter.ExcludePrefix.IsNull() {
+ params.ExcludePrefix = cloudflare.F(m.Filter.ExcludePrefix.ValueString())
+ }
+ if !m.Filter.ExistedAt.IsNull() {
+ params.ExistedAt = cloudflare.F(m.Filter.ExistedAt.ValueString())
+ }
+ if !m.Filter.IncludePrefix.IsNull() {
+ params.IncludePrefix = cloudflare.F(m.Filter.IncludePrefix.ValueString())
+ }
+ if !m.Filter.IsDeleted.IsNull() {
+ params.IsDeleted = cloudflare.F(m.Filter.IsDeleted.ValueBool())
+ }
+ if !m.Filter.Name.IsNull() {
+ params.Name = cloudflare.F(m.Filter.Name.ValueString())
+ }
+ if !m.Filter.Status.IsNull() {
+ params.Status = cloudflare.F(zero_trust.TunnelWARPConnectorListParamsStatus(m.Filter.Status.ValueString()))
+ }
+ if !m.Filter.UUID.IsNull() {
+ params.UUID = cloudflare.F(m.Filter.UUID.ValueString())
+ }
+ if !m.Filter.WasActiveAt.IsNull() {
+ params.WasActiveAt = cloudflare.F(mFilterWasActiveAt)
+ }
+ if !m.Filter.WasInactiveAt.IsNull() {
+ params.WasInactiveAt = cloudflare.F(mFilterWasInactiveAt)
+ }
+
+ return
+}
+
+type ZeroTrustTunnelWARPConnectorConnectionsDataSourceModel struct {
+ ID types.String `tfsdk:"id" json:"id,computed"`
+ ClientID types.String `tfsdk:"client_id" json:"client_id,computed"`
+ ClientVersion types.String `tfsdk:"client_version" json:"client_version,computed"`
+ ColoName types.String `tfsdk:"colo_name" json:"colo_name,computed"`
+ IsPendingReconnect types.Bool `tfsdk:"is_pending_reconnect" json:"is_pending_reconnect,computed"`
+ OpenedAt timetypes.RFC3339 `tfsdk:"opened_at" json:"opened_at,computed" format:"date-time"`
+ OriginIP types.String `tfsdk:"origin_ip" json:"origin_ip,computed"`
+ UUID types.String `tfsdk:"uuid" json:"uuid,computed"`
+}
+
+type ZeroTrustTunnelWARPConnectorFindOneByDataSourceModel struct {
+ ExcludePrefix types.String `tfsdk:"exclude_prefix" query:"exclude_prefix,optional"`
+ ExistedAt types.String `tfsdk:"existed_at" query:"existed_at,optional"`
+ IncludePrefix types.String `tfsdk:"include_prefix" query:"include_prefix,optional"`
+ IsDeleted types.Bool `tfsdk:"is_deleted" query:"is_deleted,optional"`
+ Name types.String `tfsdk:"name" query:"name,optional"`
+ Status types.String `tfsdk:"status" query:"status,optional"`
+ UUID types.String `tfsdk:"uuid" query:"uuid,optional"`
+ WasActiveAt timetypes.RFC3339 `tfsdk:"was_active_at" query:"was_active_at,optional" format:"date-time"`
+ WasInactiveAt timetypes.RFC3339 `tfsdk:"was_inactive_at" query:"was_inactive_at,optional" format:"date-time"`
+}
diff --git a/internal/services/zero_trust_tunnel_warp_connector/data_source_schema.go b/internal/services/zero_trust_tunnel_warp_connector/data_source_schema.go
new file mode 100644
index 0000000000..8aac58ef37
--- /dev/null
+++ b/internal/services/zero_trust_tunnel_warp_connector/data_source_schema.go
@@ -0,0 +1,202 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package zero_trust_tunnel_warp_connector
+
+import (
+ "context"
+
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/customfield"
+ "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
+ "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes"
+ "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator"
+ "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+)
+
+var _ datasource.DataSourceWithConfigValidators = (*ZeroTrustTunnelWARPConnectorDataSource)(nil)
+
+func DataSourceSchema(ctx context.Context) schema.Schema {
+ return schema.Schema{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Description: "UUID of the tunnel.",
+ Computed: true,
+ },
+ "tunnel_id": schema.StringAttribute{
+ Description: "UUID of the tunnel.",
+ Optional: true,
+ },
+ "account_id": schema.StringAttribute{
+ Description: "Cloudflare account ID",
+ Required: true,
+ },
+ "account_tag": schema.StringAttribute{
+ Description: "Cloudflare account ID",
+ Computed: true,
+ },
+ "conns_active_at": schema.StringAttribute{
+ Description: "Timestamp of when the tunnel established at least one connection to Cloudflare's edge. If `null`, the tunnel is inactive.",
+ Computed: true,
+ CustomType: timetypes.RFC3339Type{},
+ },
+ "conns_inactive_at": schema.StringAttribute{
+ Description: "Timestamp of when the tunnel became inactive (no connections to Cloudflare's edge). If `null`, the tunnel is active.",
+ Computed: true,
+ CustomType: timetypes.RFC3339Type{},
+ },
+ "created_at": schema.StringAttribute{
+ Description: "Timestamp of when the resource was created.",
+ Computed: true,
+ CustomType: timetypes.RFC3339Type{},
+ },
+ "deleted_at": schema.StringAttribute{
+ Description: "Timestamp of when the resource was deleted. If `null`, the resource has not been deleted.",
+ Computed: true,
+ CustomType: timetypes.RFC3339Type{},
+ },
+ "name": schema.StringAttribute{
+ Description: "A user-friendly name for a tunnel.",
+ Computed: true,
+ },
+ "remote_config": schema.BoolAttribute{
+ Description: "If `true`, the tunnel can be configured remotely from the Zero Trust dashboard. If `false`, the tunnel must be configured locally on the origin machine.",
+ Computed: true,
+ },
+ "status": schema.StringAttribute{
+ Description: "The status of the tunnel. Valid values are `inactive` (tunnel has never been run), `degraded` (tunnel is active and able to serve traffic but in an unhealthy state), `healthy` (tunnel is active and able to serve traffic), or `down` (tunnel can not serve traffic as it has no connections to the Cloudflare Edge).\nAvailable values: \"inactive\", \"degraded\", \"healthy\", \"down\".",
+ Computed: true,
+ Validators: []validator.String{
+ stringvalidator.OneOfCaseInsensitive(
+ "inactive",
+ "degraded",
+ "healthy",
+ "down",
+ ),
+ },
+ },
+ "tun_type": schema.StringAttribute{
+ Description: "The type of tunnel.\nAvailable values: \"cfd_tunnel\", \"warp_connector\", \"warp\", \"magic\", \"ip_sec\", \"gre\", \"cni\".",
+ Computed: true,
+ Validators: []validator.String{
+ stringvalidator.OneOfCaseInsensitive(
+ "cfd_tunnel",
+ "warp_connector",
+ "warp",
+ "magic",
+ "ip_sec",
+ "gre",
+ "cni",
+ ),
+ },
+ },
+ "connections": schema.ListNestedAttribute{
+ Description: "The Cloudflare Tunnel connections between your origin and Cloudflare's edge.",
+ Computed: true,
+ DeprecationMessage: "This field will start returning an empty array. To fetch the connections of a given tunnel, please use the dedicated endpoint `/accounts/{account_id}/{tunnel_type}/{tunnel_id}/connections`",
+ CustomType: customfield.NewNestedObjectListType[ZeroTrustTunnelWARPConnectorConnectionsDataSourceModel](ctx),
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Description: "UUID of the Cloudflare Tunnel connection.",
+ Computed: true,
+ },
+ "client_id": schema.StringAttribute{
+ Description: "UUID of the Cloudflare Tunnel connector.",
+ Computed: true,
+ },
+ "client_version": schema.StringAttribute{
+ Description: "The cloudflared version used to establish this connection.",
+ Computed: true,
+ },
+ "colo_name": schema.StringAttribute{
+ Description: "The Cloudflare data center used for this connection.",
+ Computed: true,
+ },
+ "is_pending_reconnect": schema.BoolAttribute{
+ Description: "Cloudflare continues to track connections for several minutes after they disconnect. This is an optimization to improve latency and reliability of reconnecting. If `true`, the connection has disconnected but is still being tracked. If `false`, the connection is actively serving traffic.",
+ Computed: true,
+ },
+ "opened_at": schema.StringAttribute{
+ Description: "Timestamp of when the connection was established.",
+ Computed: true,
+ CustomType: timetypes.RFC3339Type{},
+ },
+ "origin_ip": schema.StringAttribute{
+ Description: "The public IP address of the host running cloudflared.",
+ Computed: true,
+ },
+ "uuid": schema.StringAttribute{
+ Description: "UUID of the Cloudflare Tunnel connection.",
+ Computed: true,
+ },
+ },
+ },
+ },
+ "metadata": schema.StringAttribute{
+ Description: "Metadata associated with the tunnel.",
+ Computed: true,
+ CustomType: jsontypes.NormalizedType{},
+ },
+ "filter": schema.SingleNestedAttribute{
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "exclude_prefix": schema.StringAttribute{
+ Optional: true,
+ },
+ "existed_at": schema.StringAttribute{
+ Description: "If provided, include only resources that were created (and not deleted) before this time. URL encoded.",
+ Optional: true,
+ },
+ "include_prefix": schema.StringAttribute{
+ Optional: true,
+ },
+ "is_deleted": schema.BoolAttribute{
+ Description: "If `true`, only include deleted tunnels. If `false`, exclude deleted tunnels. If empty, all tunnels will be included.",
+ Optional: true,
+ },
+ "name": schema.StringAttribute{
+ Description: "A user-friendly name for the tunnel.",
+ Optional: true,
+ },
+ "status": schema.StringAttribute{
+ Description: "The status of the tunnel. Valid values are `inactive` (tunnel has never been run), `degraded` (tunnel is active and able to serve traffic but in an unhealthy state), `healthy` (tunnel is active and able to serve traffic), or `down` (tunnel can not serve traffic as it has no connections to the Cloudflare Edge).\nAvailable values: \"inactive\", \"degraded\", \"healthy\", \"down\".",
+ Optional: true,
+ Validators: []validator.String{
+ stringvalidator.OneOfCaseInsensitive(
+ "inactive",
+ "degraded",
+ "healthy",
+ "down",
+ ),
+ },
+ },
+ "uuid": schema.StringAttribute{
+ Description: "UUID of the tunnel.",
+ Optional: true,
+ },
+ "was_active_at": schema.StringAttribute{
+ Optional: true,
+ CustomType: timetypes.RFC3339Type{},
+ },
+ "was_inactive_at": schema.StringAttribute{
+ Optional: true,
+ CustomType: timetypes.RFC3339Type{},
+ },
+ },
+ },
+ },
+ }
+}
+
+func (d *ZeroTrustTunnelWARPConnectorDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = DataSourceSchema(ctx)
+}
+
+func (d *ZeroTrustTunnelWARPConnectorDataSource) ConfigValidators(_ context.Context) []datasource.ConfigValidator {
+ return []datasource.ConfigValidator{
+ datasourcevalidator.ExactlyOneOf(path.MatchRoot("tunnel_id"), path.MatchRoot("filter")),
+ }
+}
diff --git a/internal/services/zero_trust_tunnel_warp_connector/data_source_schema_test.go b/internal/services/zero_trust_tunnel_warp_connector/data_source_schema_test.go
new file mode 100644
index 0000000000..58a796c99d
--- /dev/null
+++ b/internal/services/zero_trust_tunnel_warp_connector/data_source_schema_test.go
@@ -0,0 +1,19 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package zero_trust_tunnel_warp_connector_test
+
+import (
+ "context"
+ "testing"
+
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/services/zero_trust_tunnel_warp_connector"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/test_helpers"
+)
+
+func TestZeroTrustTunnelWARPConnectorDataSourceModelSchemaParity(t *testing.T) {
+ t.Parallel()
+ model := (*zero_trust_tunnel_warp_connector.ZeroTrustTunnelWARPConnectorDataSourceModel)(nil)
+ schema := zero_trust_tunnel_warp_connector.DataSourceSchema(context.TODO())
+ errs := test_helpers.ValidateDataSourceModelSchemaIntegrity(model, schema)
+ errs.Report(t)
+}
diff --git a/internal/services/zero_trust_tunnel_warp_connector/data_source_test.go b/internal/services/zero_trust_tunnel_warp_connector/data_source_test.go
new file mode 100644
index 0000000000..72e90ba113
--- /dev/null
+++ b/internal/services/zero_trust_tunnel_warp_connector/data_source_test.go
@@ -0,0 +1,36 @@
+package zero_trust_tunnel_warp_connector_test
+
+import (
+ "errors"
+ "testing"
+
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/acctest"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/utils"
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/hashicorp/terraform-plugin-testing/terraform"
+)
+
+func TestAccCloudflareZeroTrustTunnelWarpConnectorDataSource_Basic(t *testing.T) {
+ rnd := utils.GenerateRandomResourceName()
+ name := "cloudflare_zero_trust_tunnel_warp_connector." + rnd
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccZeroTrustTunnelWarpConnectorDataSourceConfig(rnd),
+ Check: resource.ComposeTestCheckFunc(
+ func(s *terraform.State) error {
+ return errors.New("test not implemented")
+ },
+ resource.TestCheckResourceAttr(name, "some_string_attribute", "string_value"),
+ ),
+ },
+ },
+ })
+}
+
+func testAccZeroTrustTunnelWarpConnectorDataSourceConfig(rnd string) string {
+ return acctest.LoadTestCase("datasource_basic.tf", rnd)
+}
diff --git a/internal/services/zero_trust_tunnel_warp_connector/list_data_source.go b/internal/services/zero_trust_tunnel_warp_connector/list_data_source.go
new file mode 100644
index 0000000000..86d1a342c5
--- /dev/null
+++ b/internal/services/zero_trust_tunnel_warp_connector/list_data_source.go
@@ -0,0 +1,100 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package zero_trust_tunnel_warp_connector
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/cloudflare/cloudflare-go/v4"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/apijson"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/customfield"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+)
+
+type ZeroTrustTunnelWARPConnectorsDataSource struct {
+ client *cloudflare.Client
+}
+
+var _ datasource.DataSourceWithConfigure = (*ZeroTrustTunnelWARPConnectorsDataSource)(nil)
+
+func NewZeroTrustTunnelWARPConnectorsDataSource() datasource.DataSource {
+ return &ZeroTrustTunnelWARPConnectorsDataSource{}
+}
+
+func (d *ZeroTrustTunnelWARPConnectorsDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_zero_trust_tunnel_warp_connectors"
+}
+
+func (d *ZeroTrustTunnelWARPConnectorsDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ client, ok := req.ProviderData.(*cloudflare.Client)
+
+ if !ok {
+ resp.Diagnostics.AddError(
+ "unexpected resource configure type",
+ fmt.Sprintf("Expected *cloudflare.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+
+ return
+ }
+
+ d.client = client
+}
+
+func (d *ZeroTrustTunnelWARPConnectorsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data *ZeroTrustTunnelWARPConnectorsDataSourceModel
+
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params, diags := data.toListParams(ctx)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ env := ZeroTrustTunnelWARPConnectorsResultListDataSourceEnvelope{}
+ maxItems := int(data.MaxItems.ValueInt64())
+ acc := []attr.Value{}
+ if maxItems <= 0 {
+ maxItems = 1000
+ }
+ page, err := d.client.ZeroTrust.Tunnels.WARPConnector.List(ctx, params)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to make http request", err.Error())
+ return
+ }
+
+ for page != nil && len(page.Result) > 0 {
+ bytes := []byte(page.JSON.RawJSON())
+ err = apijson.UnmarshalComputed(bytes, &env)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to unmarshal http request", err.Error())
+ return
+ }
+ acc = append(acc, env.Result.Elements()...)
+ if len(acc) >= maxItems {
+ break
+ }
+ page, err = page.GetNextPage()
+ if err != nil {
+ resp.Diagnostics.AddError("failed to fetch next page", err.Error())
+ return
+ }
+ }
+
+ acc = acc[:min(len(acc), maxItems)]
+ result, diags := customfield.NewObjectListFromAttributes[ZeroTrustTunnelWARPConnectorsResultDataSourceModel](ctx, acc)
+ resp.Diagnostics.Append(diags...)
+ data.Result = result
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/services/zero_trust_tunnel_warp_connector/list_data_source_model.go b/internal/services/zero_trust_tunnel_warp_connector/list_data_source_model.go
new file mode 100644
index 0000000000..2b2aa01102
--- /dev/null
+++ b/internal/services/zero_trust_tunnel_warp_connector/list_data_source_model.go
@@ -0,0 +1,101 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package zero_trust_tunnel_warp_connector
+
+import (
+ "context"
+
+ "github.com/cloudflare/cloudflare-go/v4"
+ "github.com/cloudflare/cloudflare-go/v4/zero_trust"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/customfield"
+ "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
+ "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+type ZeroTrustTunnelWARPConnectorsResultListDataSourceEnvelope struct {
+ Result customfield.NestedObjectList[ZeroTrustTunnelWARPConnectorsResultDataSourceModel] `json:"result,computed"`
+}
+
+type ZeroTrustTunnelWARPConnectorsDataSourceModel struct {
+ AccountID types.String `tfsdk:"account_id" path:"account_id,required"`
+ ExcludePrefix types.String `tfsdk:"exclude_prefix" query:"exclude_prefix,optional"`
+ ExistedAt types.String `tfsdk:"existed_at" query:"existed_at,optional"`
+ IncludePrefix types.String `tfsdk:"include_prefix" query:"include_prefix,optional"`
+ IsDeleted types.Bool `tfsdk:"is_deleted" query:"is_deleted,optional"`
+ Name types.String `tfsdk:"name" query:"name,optional"`
+ Status types.String `tfsdk:"status" query:"status,optional"`
+ UUID types.String `tfsdk:"uuid" query:"uuid,optional"`
+ WasActiveAt timetypes.RFC3339 `tfsdk:"was_active_at" query:"was_active_at,optional" format:"date-time"`
+ WasInactiveAt timetypes.RFC3339 `tfsdk:"was_inactive_at" query:"was_inactive_at,optional" format:"date-time"`
+ MaxItems types.Int64 `tfsdk:"max_items"`
+ Result customfield.NestedObjectList[ZeroTrustTunnelWARPConnectorsResultDataSourceModel] `tfsdk:"result"`
+}
+
+func (m *ZeroTrustTunnelWARPConnectorsDataSourceModel) toListParams(_ context.Context) (params zero_trust.TunnelWARPConnectorListParams, diags diag.Diagnostics) {
+ mWasActiveAt, errs := m.WasActiveAt.ValueRFC3339Time()
+ diags.Append(errs...)
+ mWasInactiveAt, errs := m.WasInactiveAt.ValueRFC3339Time()
+ diags.Append(errs...)
+
+ params = zero_trust.TunnelWARPConnectorListParams{
+ AccountID: cloudflare.F(m.AccountID.ValueString()),
+ }
+
+ if !m.ExcludePrefix.IsNull() {
+ params.ExcludePrefix = cloudflare.F(m.ExcludePrefix.ValueString())
+ }
+ if !m.ExistedAt.IsNull() {
+ params.ExistedAt = cloudflare.F(m.ExistedAt.ValueString())
+ }
+ if !m.IncludePrefix.IsNull() {
+ params.IncludePrefix = cloudflare.F(m.IncludePrefix.ValueString())
+ }
+ if !m.IsDeleted.IsNull() {
+ params.IsDeleted = cloudflare.F(m.IsDeleted.ValueBool())
+ }
+ if !m.Name.IsNull() {
+ params.Name = cloudflare.F(m.Name.ValueString())
+ }
+ if !m.Status.IsNull() {
+ params.Status = cloudflare.F(zero_trust.TunnelWARPConnectorListParamsStatus(m.Status.ValueString()))
+ }
+ if !m.UUID.IsNull() {
+ params.UUID = cloudflare.F(m.UUID.ValueString())
+ }
+ if !m.WasActiveAt.IsNull() {
+ params.WasActiveAt = cloudflare.F(mWasActiveAt)
+ }
+ if !m.WasInactiveAt.IsNull() {
+ params.WasInactiveAt = cloudflare.F(mWasInactiveAt)
+ }
+
+ return
+}
+
+type ZeroTrustTunnelWARPConnectorsResultDataSourceModel struct {
+ ID types.String `tfsdk:"id" json:"id,computed"`
+ AccountTag types.String `tfsdk:"account_tag" json:"account_tag,computed"`
+ Connections customfield.NestedObjectList[ZeroTrustTunnelWARPConnectorsConnectionsDataSourceModel] `tfsdk:"connections" json:"connections,computed"`
+ ConnsActiveAt timetypes.RFC3339 `tfsdk:"conns_active_at" json:"conns_active_at,computed" format:"date-time"`
+ ConnsInactiveAt timetypes.RFC3339 `tfsdk:"conns_inactive_at" json:"conns_inactive_at,computed" format:"date-time"`
+ CreatedAt timetypes.RFC3339 `tfsdk:"created_at" json:"created_at,computed" format:"date-time"`
+ DeletedAt timetypes.RFC3339 `tfsdk:"deleted_at" json:"deleted_at,computed" format:"date-time"`
+ Metadata jsontypes.Normalized `tfsdk:"metadata" json:"metadata,computed"`
+ Name types.String `tfsdk:"name" json:"name,computed"`
+ RemoteConfig types.Bool `tfsdk:"remote_config" json:"remote_config,computed"`
+ Status types.String `tfsdk:"status" json:"status,computed"`
+ TunType types.String `tfsdk:"tun_type" json:"tun_type,computed"`
+}
+
+type ZeroTrustTunnelWARPConnectorsConnectionsDataSourceModel struct {
+ ID types.String `tfsdk:"id" json:"id,computed"`
+ ClientID types.String `tfsdk:"client_id" json:"client_id,computed"`
+ ClientVersion types.String `tfsdk:"client_version" json:"client_version,computed"`
+ ColoName types.String `tfsdk:"colo_name" json:"colo_name,computed"`
+ IsPendingReconnect types.Bool `tfsdk:"is_pending_reconnect" json:"is_pending_reconnect,computed"`
+ OpenedAt timetypes.RFC3339 `tfsdk:"opened_at" json:"opened_at,computed" format:"date-time"`
+ OriginIP types.String `tfsdk:"origin_ip" json:"origin_ip,computed"`
+ UUID types.String `tfsdk:"uuid" json:"uuid,computed"`
+}
diff --git a/internal/services/zero_trust_tunnel_warp_connector/list_data_source_schema.go b/internal/services/zero_trust_tunnel_warp_connector/list_data_source_schema.go
new file mode 100644
index 0000000000..10a101bb0b
--- /dev/null
+++ b/internal/services/zero_trust_tunnel_warp_connector/list_data_source_schema.go
@@ -0,0 +1,206 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package zero_trust_tunnel_warp_connector
+
+import (
+ "context"
+
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/customfield"
+ "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
+ "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes"
+ "github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
+ "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+)
+
+var _ datasource.DataSourceWithConfigValidators = (*ZeroTrustTunnelWARPConnectorsDataSource)(nil)
+
+func ListDataSourceSchema(ctx context.Context) schema.Schema {
+ return schema.Schema{
+ Attributes: map[string]schema.Attribute{
+ "account_id": schema.StringAttribute{
+ Description: "Cloudflare account ID",
+ Required: true,
+ },
+ "exclude_prefix": schema.StringAttribute{
+ Optional: true,
+ },
+ "existed_at": schema.StringAttribute{
+ Description: "If provided, include only resources that were created (and not deleted) before this time. URL encoded.",
+ Optional: true,
+ },
+ "include_prefix": schema.StringAttribute{
+ Optional: true,
+ },
+ "is_deleted": schema.BoolAttribute{
+ Description: "If `true`, only include deleted tunnels. If `false`, exclude deleted tunnels. If empty, all tunnels will be included.",
+ Optional: true,
+ },
+ "name": schema.StringAttribute{
+ Description: "A user-friendly name for the tunnel.",
+ Optional: true,
+ },
+ "status": schema.StringAttribute{
+ Description: "The status of the tunnel. Valid values are `inactive` (tunnel has never been run), `degraded` (tunnel is active and able to serve traffic but in an unhealthy state), `healthy` (tunnel is active and able to serve traffic), or `down` (tunnel can not serve traffic as it has no connections to the Cloudflare Edge).\nAvailable values: \"inactive\", \"degraded\", \"healthy\", \"down\".",
+ Optional: true,
+ Validators: []validator.String{
+ stringvalidator.OneOfCaseInsensitive(
+ "inactive",
+ "degraded",
+ "healthy",
+ "down",
+ ),
+ },
+ },
+ "uuid": schema.StringAttribute{
+ Description: "UUID of the tunnel.",
+ Optional: true,
+ },
+ "was_active_at": schema.StringAttribute{
+ Optional: true,
+ CustomType: timetypes.RFC3339Type{},
+ },
+ "was_inactive_at": schema.StringAttribute{
+ Optional: true,
+ CustomType: timetypes.RFC3339Type{},
+ },
+ "max_items": schema.Int64Attribute{
+ Description: "Max items to fetch, default: 1000",
+ Optional: true,
+ Validators: []validator.Int64{
+ int64validator.AtLeast(0),
+ },
+ },
+ "result": schema.ListNestedAttribute{
+ Description: "The items returned by the data source",
+ Computed: true,
+ CustomType: customfield.NewNestedObjectListType[ZeroTrustTunnelWARPConnectorsResultDataSourceModel](ctx),
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Description: "UUID of the tunnel.",
+ Computed: true,
+ },
+ "account_tag": schema.StringAttribute{
+ Description: "Cloudflare account ID",
+ Computed: true,
+ },
+ "connections": schema.ListNestedAttribute{
+ Description: "The Cloudflare Tunnel connections between your origin and Cloudflare's edge.",
+ Computed: true,
+ DeprecationMessage: "This field will start returning an empty array. To fetch the connections of a given tunnel, please use the dedicated endpoint `/accounts/{account_id}/{tunnel_type}/{tunnel_id}/connections`",
+ CustomType: customfield.NewNestedObjectListType[ZeroTrustTunnelWARPConnectorsConnectionsDataSourceModel](ctx),
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Description: "UUID of the Cloudflare Tunnel connection.",
+ Computed: true,
+ },
+ "client_id": schema.StringAttribute{
+ Description: "UUID of the Cloudflare Tunnel connector.",
+ Computed: true,
+ },
+ "client_version": schema.StringAttribute{
+ Description: "The cloudflared version used to establish this connection.",
+ Computed: true,
+ },
+ "colo_name": schema.StringAttribute{
+ Description: "The Cloudflare data center used for this connection.",
+ Computed: true,
+ },
+ "is_pending_reconnect": schema.BoolAttribute{
+ Description: "Cloudflare continues to track connections for several minutes after they disconnect. This is an optimization to improve latency and reliability of reconnecting. If `true`, the connection has disconnected but is still being tracked. If `false`, the connection is actively serving traffic.",
+ Computed: true,
+ },
+ "opened_at": schema.StringAttribute{
+ Description: "Timestamp of when the connection was established.",
+ Computed: true,
+ CustomType: timetypes.RFC3339Type{},
+ },
+ "origin_ip": schema.StringAttribute{
+ Description: "The public IP address of the host running cloudflared.",
+ Computed: true,
+ },
+ "uuid": schema.StringAttribute{
+ Description: "UUID of the Cloudflare Tunnel connection.",
+ Computed: true,
+ },
+ },
+ },
+ },
+ "conns_active_at": schema.StringAttribute{
+ Description: "Timestamp of when the tunnel established at least one connection to Cloudflare's edge. If `null`, the tunnel is inactive.",
+ Computed: true,
+ CustomType: timetypes.RFC3339Type{},
+ },
+ "conns_inactive_at": schema.StringAttribute{
+ Description: "Timestamp of when the tunnel became inactive (no connections to Cloudflare's edge). If `null`, the tunnel is active.",
+ Computed: true,
+ CustomType: timetypes.RFC3339Type{},
+ },
+ "created_at": schema.StringAttribute{
+ Description: "Timestamp of when the resource was created.",
+ Computed: true,
+ CustomType: timetypes.RFC3339Type{},
+ },
+ "deleted_at": schema.StringAttribute{
+ Description: "Timestamp of when the resource was deleted. If `null`, the resource has not been deleted.",
+ Computed: true,
+ CustomType: timetypes.RFC3339Type{},
+ },
+ "metadata": schema.StringAttribute{
+ Description: "Metadata associated with the tunnel.",
+ Computed: true,
+ CustomType: jsontypes.NormalizedType{},
+ },
+ "name": schema.StringAttribute{
+ Description: "A user-friendly name for a tunnel.",
+ Computed: true,
+ },
+ "remote_config": schema.BoolAttribute{
+ Description: "If `true`, the tunnel can be configured remotely from the Zero Trust dashboard. If `false`, the tunnel must be configured locally on the origin machine.",
+ Computed: true,
+ },
+ "status": schema.StringAttribute{
+ Description: "The status of the tunnel. Valid values are `inactive` (tunnel has never been run), `degraded` (tunnel is active and able to serve traffic but in an unhealthy state), `healthy` (tunnel is active and able to serve traffic), or `down` (tunnel can not serve traffic as it has no connections to the Cloudflare Edge).\nAvailable values: \"inactive\", \"degraded\", \"healthy\", \"down\".",
+ Computed: true,
+ Validators: []validator.String{
+ stringvalidator.OneOfCaseInsensitive(
+ "inactive",
+ "degraded",
+ "healthy",
+ "down",
+ ),
+ },
+ },
+ "tun_type": schema.StringAttribute{
+ Description: "The type of tunnel.\nAvailable values: \"cfd_tunnel\", \"warp_connector\", \"warp\", \"magic\", \"ip_sec\", \"gre\", \"cni\".",
+ Computed: true,
+ Validators: []validator.String{
+ stringvalidator.OneOfCaseInsensitive(
+ "cfd_tunnel",
+ "warp_connector",
+ "warp",
+ "magic",
+ "ip_sec",
+ "gre",
+ "cni",
+ ),
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func (d *ZeroTrustTunnelWARPConnectorsDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = ListDataSourceSchema(ctx)
+}
+
+func (d *ZeroTrustTunnelWARPConnectorsDataSource) ConfigValidators(_ context.Context) []datasource.ConfigValidator {
+ return []datasource.ConfigValidator{}
+}
diff --git a/internal/services/zero_trust_tunnel_warp_connector/list_data_source_schema_test.go b/internal/services/zero_trust_tunnel_warp_connector/list_data_source_schema_test.go
new file mode 100644
index 0000000000..bdc0521f38
--- /dev/null
+++ b/internal/services/zero_trust_tunnel_warp_connector/list_data_source_schema_test.go
@@ -0,0 +1,19 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package zero_trust_tunnel_warp_connector_test
+
+import (
+ "context"
+ "testing"
+
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/services/zero_trust_tunnel_warp_connector"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/test_helpers"
+)
+
+func TestZeroTrustTunnelWARPConnectorsDataSourceModelSchemaParity(t *testing.T) {
+ t.Parallel()
+ model := (*zero_trust_tunnel_warp_connector.ZeroTrustTunnelWARPConnectorsDataSourceModel)(nil)
+ schema := zero_trust_tunnel_warp_connector.ListDataSourceSchema(context.TODO())
+ errs := test_helpers.ValidateDataSourceModelSchemaIntegrity(model, schema)
+ errs.Report(t)
+}
diff --git a/internal/services/zero_trust_tunnel_warp_connector/migrations.go b/internal/services/zero_trust_tunnel_warp_connector/migrations.go
new file mode 100644
index 0000000000..6d18b3fa46
--- /dev/null
+++ b/internal/services/zero_trust_tunnel_warp_connector/migrations.go
@@ -0,0 +1,15 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package zero_trust_tunnel_warp_connector
+
+import (
+ "context"
+
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+)
+
+var _ resource.ResourceWithUpgradeState = (*ZeroTrustTunnelWARPConnectorResource)(nil)
+
+func (r *ZeroTrustTunnelWARPConnectorResource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader {
+ return map[int64]resource.StateUpgrader{}
+}
diff --git a/internal/services/zero_trust_tunnel_warp_connector/model.go b/internal/services/zero_trust_tunnel_warp_connector/model.go
new file mode 100644
index 0000000000..0931cec11e
--- /dev/null
+++ b/internal/services/zero_trust_tunnel_warp_connector/model.go
@@ -0,0 +1,51 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package zero_trust_tunnel_warp_connector
+
+import (
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/apijson"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/customfield"
+ "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
+ "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+type ZeroTrustTunnelWARPConnectorResultEnvelope struct {
+ Result ZeroTrustTunnelWARPConnectorModel `json:"result"`
+}
+
+type ZeroTrustTunnelWARPConnectorModel struct {
+ ID types.String `tfsdk:"id" json:"id,computed"`
+ AccountID types.String `tfsdk:"account_id" path:"account_id,required"`
+ Name types.String `tfsdk:"name" json:"name,required"`
+ TunnelSecret types.String `tfsdk:"tunnel_secret" json:"tunnel_secret,optional,no_refresh"`
+ AccountTag types.String `tfsdk:"account_tag" json:"account_tag,computed"`
+ ConnsActiveAt timetypes.RFC3339 `tfsdk:"conns_active_at" json:"conns_active_at,computed" format:"date-time"`
+ ConnsInactiveAt timetypes.RFC3339 `tfsdk:"conns_inactive_at" json:"conns_inactive_at,computed" format:"date-time"`
+ CreatedAt timetypes.RFC3339 `tfsdk:"created_at" json:"created_at,computed" format:"date-time"`
+ DeletedAt timetypes.RFC3339 `tfsdk:"deleted_at" json:"deleted_at,computed" format:"date-time"`
+ RemoteConfig types.Bool `tfsdk:"remote_config" json:"remote_config,computed"`
+ Status types.String `tfsdk:"status" json:"status,computed"`
+ TunType types.String `tfsdk:"tun_type" json:"tun_type,computed"`
+ Connections customfield.NestedObjectList[ZeroTrustTunnelWARPConnectorConnectionsModel] `tfsdk:"connections" json:"connections,computed"`
+ Metadata jsontypes.Normalized `tfsdk:"metadata" json:"metadata,computed"`
+}
+
+func (m ZeroTrustTunnelWARPConnectorModel) MarshalJSON() (data []byte, err error) {
+ return apijson.MarshalRoot(m)
+}
+
+func (m ZeroTrustTunnelWARPConnectorModel) MarshalJSONForUpdate(state ZeroTrustTunnelWARPConnectorModel) (data []byte, err error) {
+ return apijson.MarshalForPatch(m, state)
+}
+
+type ZeroTrustTunnelWARPConnectorConnectionsModel struct {
+ ID types.String `tfsdk:"id" json:"id,computed"`
+ ClientID types.String `tfsdk:"client_id" json:"client_id,computed"`
+ ClientVersion types.String `tfsdk:"client_version" json:"client_version,computed"`
+ ColoName types.String `tfsdk:"colo_name" json:"colo_name,computed"`
+ IsPendingReconnect types.Bool `tfsdk:"is_pending_reconnect" json:"is_pending_reconnect,computed"`
+ OpenedAt timetypes.RFC3339 `tfsdk:"opened_at" json:"opened_at,computed" format:"date-time"`
+ OriginIP types.String `tfsdk:"origin_ip" json:"origin_ip,computed"`
+ UUID types.String `tfsdk:"uuid" json:"uuid,computed"`
+}
diff --git a/internal/services/zero_trust_tunnel_warp_connector/resource.go b/internal/services/zero_trust_tunnel_warp_connector/resource.go
new file mode 100644
index 0000000000..a692f77c9e
--- /dev/null
+++ b/internal/services/zero_trust_tunnel_warp_connector/resource.go
@@ -0,0 +1,259 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package zero_trust_tunnel_warp_connector
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/cloudflare/cloudflare-go/v4"
+ "github.com/cloudflare/cloudflare-go/v4/option"
+ "github.com/cloudflare/cloudflare-go/v4/zero_trust"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/apijson"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/importpath"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/logging"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Ensure provider defined types fully satisfy framework interfaces.
+var _ resource.ResourceWithConfigure = (*ZeroTrustTunnelWARPConnectorResource)(nil)
+var _ resource.ResourceWithModifyPlan = (*ZeroTrustTunnelWARPConnectorResource)(nil)
+var _ resource.ResourceWithImportState = (*ZeroTrustTunnelWARPConnectorResource)(nil)
+
+func NewResource() resource.Resource {
+ return &ZeroTrustTunnelWARPConnectorResource{}
+}
+
+// ZeroTrustTunnelWARPConnectorResource defines the resource implementation.
+type ZeroTrustTunnelWARPConnectorResource struct {
+ client *cloudflare.Client
+}
+
+func (r *ZeroTrustTunnelWARPConnectorResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_zero_trust_tunnel_warp_connector"
+}
+
+func (r *ZeroTrustTunnelWARPConnectorResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ client, ok := req.ProviderData.(*cloudflare.Client)
+
+ if !ok {
+ resp.Diagnostics.AddError(
+ "unexpected resource configure type",
+ fmt.Sprintf("Expected *cloudflare.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+
+ return
+ }
+
+ r.client = client
+}
+
+func (r *ZeroTrustTunnelWARPConnectorResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data *ZeroTrustTunnelWARPConnectorModel
+
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ dataBytes, err := data.MarshalJSON()
+ if err != nil {
+ resp.Diagnostics.AddError("failed to serialize http request", err.Error())
+ return
+ }
+ res := new(http.Response)
+ env := ZeroTrustTunnelWARPConnectorResultEnvelope{*data}
+ _, err = r.client.ZeroTrust.Tunnels.WARPConnector.New(
+ ctx,
+ zero_trust.TunnelWARPConnectorNewParams{
+ AccountID: cloudflare.F(data.AccountID.ValueString()),
+ },
+ option.WithRequestBody("application/json", dataBytes),
+ option.WithResponseBodyInto(&res),
+ option.WithMiddleware(logging.Middleware(ctx)),
+ )
+ if err != nil {
+ resp.Diagnostics.AddError("failed to make http request", err.Error())
+ return
+ }
+ bytes, _ := io.ReadAll(res.Body)
+ err = apijson.UnmarshalComputed(bytes, &env)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to deserialize http request", err.Error())
+ return
+ }
+ data = &env.Result
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ZeroTrustTunnelWARPConnectorResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var data *ZeroTrustTunnelWARPConnectorModel
+
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ var state *ZeroTrustTunnelWARPConnectorModel
+
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ dataBytes, err := data.MarshalJSONForUpdate(*state)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to serialize http request", err.Error())
+ return
+ }
+ res := new(http.Response)
+ env := ZeroTrustTunnelWARPConnectorResultEnvelope{*data}
+ _, err = r.client.ZeroTrust.Tunnels.WARPConnector.Edit(
+ ctx,
+ data.ID.ValueString(),
+ zero_trust.TunnelWARPConnectorEditParams{
+ AccountID: cloudflare.F(data.AccountID.ValueString()),
+ },
+ option.WithRequestBody("application/json", dataBytes),
+ option.WithResponseBodyInto(&res),
+ option.WithMiddleware(logging.Middleware(ctx)),
+ )
+ if err != nil {
+ resp.Diagnostics.AddError("failed to make http request", err.Error())
+ return
+ }
+ bytes, _ := io.ReadAll(res.Body)
+ err = apijson.UnmarshalComputed(bytes, &env)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to deserialize http request", err.Error())
+ return
+ }
+ data = &env.Result
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ZeroTrustTunnelWARPConnectorResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data *ZeroTrustTunnelWARPConnectorModel
+
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ res := new(http.Response)
+ env := ZeroTrustTunnelWARPConnectorResultEnvelope{*data}
+ _, err := r.client.ZeroTrust.Tunnels.WARPConnector.Get(
+ ctx,
+ data.ID.ValueString(),
+ zero_trust.TunnelWARPConnectorGetParams{
+ AccountID: cloudflare.F(data.AccountID.ValueString()),
+ },
+ option.WithResponseBodyInto(&res),
+ option.WithMiddleware(logging.Middleware(ctx)),
+ )
+ if res != nil && res.StatusCode == 404 {
+ resp.Diagnostics.AddWarning("Resource not found", "The resource was not found on the server and will be removed from state.")
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ if err != nil {
+ resp.Diagnostics.AddError("failed to make http request", err.Error())
+ return
+ }
+ bytes, _ := io.ReadAll(res.Body)
+ err = apijson.Unmarshal(bytes, &env)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to deserialize http request", err.Error())
+ return
+ }
+ data = &env.Result
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ZeroTrustTunnelWARPConnectorResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var data *ZeroTrustTunnelWARPConnectorModel
+
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ _, err := r.client.ZeroTrust.Tunnels.WARPConnector.Delete(
+ ctx,
+ data.ID.ValueString(),
+ zero_trust.TunnelWARPConnectorDeleteParams{
+ AccountID: cloudflare.F(data.AccountID.ValueString()),
+ },
+ option.WithMiddleware(logging.Middleware(ctx)),
+ )
+ if err != nil {
+ resp.Diagnostics.AddError("failed to make http request", err.Error())
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ZeroTrustTunnelWARPConnectorResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ var data *ZeroTrustTunnelWARPConnectorModel = new(ZeroTrustTunnelWARPConnectorModel)
+
+ path_account_id := ""
+ path_tunnel_id := ""
+ diags := importpath.ParseImportID(
+ req.ID,
+ "/",
+ &path_account_id,
+ &path_tunnel_id,
+ )
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ data.AccountID = types.StringValue(path_account_id)
+ data.ID = types.StringValue(path_tunnel_id)
+
+ res := new(http.Response)
+ env := ZeroTrustTunnelWARPConnectorResultEnvelope{*data}
+ _, err := r.client.ZeroTrust.Tunnels.WARPConnector.Get(
+ ctx,
+ path_tunnel_id,
+ zero_trust.TunnelWARPConnectorGetParams{
+ AccountID: cloudflare.F(path_account_id),
+ },
+ option.WithResponseBodyInto(&res),
+ option.WithMiddleware(logging.Middleware(ctx)),
+ )
+ if err != nil {
+ resp.Diagnostics.AddError("failed to make http request", err.Error())
+ return
+ }
+ bytes, _ := io.ReadAll(res.Body)
+ err = apijson.Unmarshal(bytes, &env)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to deserialize http request", err.Error())
+ return
+ }
+ data = &env.Result
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ZeroTrustTunnelWARPConnectorResource) ModifyPlan(_ context.Context, _ resource.ModifyPlanRequest, _ *resource.ModifyPlanResponse) {
+
+}
diff --git a/internal/services/zero_trust_tunnel_warp_connector/resource_schema_test.go b/internal/services/zero_trust_tunnel_warp_connector/resource_schema_test.go
new file mode 100644
index 0000000000..789c0fe786
--- /dev/null
+++ b/internal/services/zero_trust_tunnel_warp_connector/resource_schema_test.go
@@ -0,0 +1,19 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package zero_trust_tunnel_warp_connector_test
+
+import (
+ "context"
+ "testing"
+
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/services/zero_trust_tunnel_warp_connector"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/test_helpers"
+)
+
+func TestZeroTrustTunnelWARPConnectorModelSchemaParity(t *testing.T) {
+ t.Parallel()
+ model := (*zero_trust_tunnel_warp_connector.ZeroTrustTunnelWARPConnectorModel)(nil)
+ schema := zero_trust_tunnel_warp_connector.ResourceSchema(context.TODO())
+ errs := test_helpers.ValidateResourceModelSchemaIntegrity(model, schema)
+ errs.Report(t)
+}
diff --git a/internal/services/zero_trust_tunnel_warp_connector/resource_test.go b/internal/services/zero_trust_tunnel_warp_connector/resource_test.go
new file mode 100644
index 0000000000..b6c8f9934c
--- /dev/null
+++ b/internal/services/zero_trust_tunnel_warp_connector/resource_test.go
@@ -0,0 +1,36 @@
+package zero_trust_tunnel_warp_connector_test
+
+import (
+ "errors"
+ "testing"
+
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/acctest"
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/utils"
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/hashicorp/terraform-plugin-testing/terraform"
+)
+
+func TestAccCloudflareZeroTrustTunnelWarpConnector_Basic(t *testing.T) {
+ rnd := utils.GenerateRandomResourceName()
+ name := "cloudflare_zero_trust_tunnel_warp_connector." + rnd
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccZeroTrustTunnelWarpConnectorConfig(rnd),
+ Check: resource.ComposeTestCheckFunc(
+ func(s *terraform.State) error {
+ return errors.New("test not implemented")
+ },
+ resource.TestCheckResourceAttr(name, "some_string_attribute", "string_value"),
+ ),
+ },
+ },
+ })
+}
+
+func testAccZeroTrustTunnelWarpConnectorConfig(rnd string) string {
+ return acctest.LoadTestCase("basic.tf", rnd)
+}
diff --git a/internal/services/zero_trust_tunnel_warp_connector/schema.go b/internal/services/zero_trust_tunnel_warp_connector/schema.go
new file mode 100644
index 0000000000..e95ed4d947
--- /dev/null
+++ b/internal/services/zero_trust_tunnel_warp_connector/schema.go
@@ -0,0 +1,156 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package zero_trust_tunnel_warp_connector
+
+import (
+ "context"
+
+ "github.com/cloudflare/terraform-provider-cloudflare/internal/customfield"
+ "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
+ "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes"
+ "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+)
+
+var _ resource.ResourceWithConfigValidators = (*ZeroTrustTunnelWARPConnectorResource)(nil)
+
+func ResourceSchema(ctx context.Context) schema.Schema {
+ return schema.Schema{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Description: "UUID of the tunnel.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
+ },
+ "account_id": schema.StringAttribute{
+ Description: "Cloudflare account ID",
+ Required: true,
+ PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
+ },
+ "name": schema.StringAttribute{
+ Description: "A user-friendly name for a tunnel.",
+ Required: true,
+ },
+ "tunnel_secret": schema.StringAttribute{
+ Description: "Sets the password required to run a locally-managed tunnel. Must be at least 32 bytes and encoded as a base64 string.",
+ Optional: true,
+ Sensitive: true,
+ },
+ "account_tag": schema.StringAttribute{
+ Description: "Cloudflare account ID",
+ Computed: true,
+ },
+ "conns_active_at": schema.StringAttribute{
+ Description: "Timestamp of when the tunnel established at least one connection to Cloudflare's edge. If `null`, the tunnel is inactive.",
+ Computed: true,
+ CustomType: timetypes.RFC3339Type{},
+ },
+ "conns_inactive_at": schema.StringAttribute{
+ Description: "Timestamp of when the tunnel became inactive (no connections to Cloudflare's edge). If `null`, the tunnel is active.",
+ Computed: true,
+ CustomType: timetypes.RFC3339Type{},
+ },
+ "created_at": schema.StringAttribute{
+ Description: "Timestamp of when the resource was created.",
+ Computed: true,
+ CustomType: timetypes.RFC3339Type{},
+ },
+ "deleted_at": schema.StringAttribute{
+ Description: "Timestamp of when the resource was deleted. If `null`, the resource has not been deleted.",
+ Computed: true,
+ CustomType: timetypes.RFC3339Type{},
+ },
+ "remote_config": schema.BoolAttribute{
+ Description: "If `true`, the tunnel can be configured remotely from the Zero Trust dashboard. If `false`, the tunnel must be configured locally on the origin machine.",
+ Computed: true,
+ },
+ "status": schema.StringAttribute{
+ Description: "The status of the tunnel. Valid values are `inactive` (tunnel has never been run), `degraded` (tunnel is active and able to serve traffic but in an unhealthy state), `healthy` (tunnel is active and able to serve traffic), or `down` (tunnel can not serve traffic as it has no connections to the Cloudflare Edge).\nAvailable values: \"inactive\", \"degraded\", \"healthy\", \"down\".",
+ Computed: true,
+ Validators: []validator.String{
+ stringvalidator.OneOfCaseInsensitive(
+ "inactive",
+ "degraded",
+ "healthy",
+ "down",
+ ),
+ },
+ },
+ "tun_type": schema.StringAttribute{
+ Description: "The type of tunnel.\nAvailable values: \"cfd_tunnel\", \"warp_connector\", \"warp\", \"magic\", \"ip_sec\", \"gre\", \"cni\".",
+ Computed: true,
+ Validators: []validator.String{
+ stringvalidator.OneOfCaseInsensitive(
+ "cfd_tunnel",
+ "warp_connector",
+ "warp",
+ "magic",
+ "ip_sec",
+ "gre",
+ "cni",
+ ),
+ },
+ },
+ "connections": schema.ListNestedAttribute{
+ Description: "The Cloudflare Tunnel connections between your origin and Cloudflare's edge.",
+ Computed: true,
+ DeprecationMessage: "This field will start returning an empty array. To fetch the connections of a given tunnel, please use the dedicated endpoint `/accounts/{account_id}/{tunnel_type}/{tunnel_id}/connections`",
+ CustomType: customfield.NewNestedObjectListType[ZeroTrustTunnelWARPConnectorConnectionsModel](ctx),
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Description: "UUID of the Cloudflare Tunnel connection.",
+ Computed: true,
+ },
+ "client_id": schema.StringAttribute{
+ Description: "UUID of the Cloudflare Tunnel connector.",
+ Computed: true,
+ },
+ "client_version": schema.StringAttribute{
+ Description: "The cloudflared version used to establish this connection.",
+ Computed: true,
+ },
+ "colo_name": schema.StringAttribute{
+ Description: "The Cloudflare data center used for this connection.",
+ Computed: true,
+ },
+ "is_pending_reconnect": schema.BoolAttribute{
+ Description: "Cloudflare continues to track connections for several minutes after they disconnect. This is an optimization to improve latency and reliability of reconnecting. If `true`, the connection has disconnected but is still being tracked. If `false`, the connection is actively serving traffic.",
+ Computed: true,
+ },
+ "opened_at": schema.StringAttribute{
+ Description: "Timestamp of when the connection was established.",
+ Computed: true,
+ CustomType: timetypes.RFC3339Type{},
+ },
+ "origin_ip": schema.StringAttribute{
+ Description: "The public IP address of the host running cloudflared.",
+ Computed: true,
+ },
+ "uuid": schema.StringAttribute{
+ Description: "UUID of the Cloudflare Tunnel connection.",
+ Computed: true,
+ },
+ },
+ },
+ },
+ "metadata": schema.StringAttribute{
+ Description: "Metadata associated with the tunnel.",
+ Computed: true,
+ CustomType: jsontypes.NormalizedType{},
+ },
+ },
+ }
+}
+
+func (r *ZeroTrustTunnelWARPConnectorResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = ResourceSchema(ctx)
+}
+
+func (r *ZeroTrustTunnelWARPConnectorResource) ConfigValidators(_ context.Context) []resource.ConfigValidator {
+ return []resource.ConfigValidator{}
+}
diff --git a/internal/services/zero_trust_tunnel_warp_connector/testdata/basic.tf b/internal/services/zero_trust_tunnel_warp_connector/testdata/basic.tf
new file mode 100644
index 0000000000..d3b606f4c0
--- /dev/null
+++ b/internal/services/zero_trust_tunnel_warp_connector/testdata/basic.tf
@@ -0,0 +1 @@
+resource "cloudflare_zero_trust_tunnel_warp_connector" "%[1]s" {}
\ No newline at end of file
diff --git a/internal/services/zero_trust_tunnel_warp_connector/testdata/datasource_basic.tf b/internal/services/zero_trust_tunnel_warp_connector/testdata/datasource_basic.tf
new file mode 100644
index 0000000000..8fbfb45370
--- /dev/null
+++ b/internal/services/zero_trust_tunnel_warp_connector/testdata/datasource_basic.tf
@@ -0,0 +1 @@
+data "cloudflare_zero_trust_tunnel_warp_connector" "%[1]s" {}
\ No newline at end of file
diff --git a/internal/services/zone_lockdown/schema.go b/internal/services/zone_lockdown/schema.go
index 92640e74e1..33b6d008b4 100644
--- a/internal/services/zone_lockdown/schema.go
+++ b/internal/services/zone_lockdown/schema.go
@@ -33,7 +33,7 @@ func ResourceSchema(ctx context.Context) schema.Schema {
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
},
"description": schema.StringAttribute{
- Description: "An informative summary of the rate limit. This value is sanitized and any tags will be removed.",
+ Description: "An informative summary of the rule. This value is sanitized and any tags will be removed.",
Optional: true,
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
},
diff --git a/scripts/run-ci-acceptance-tests b/scripts/run-ci-acceptance-tests
new file mode 100755
index 0000000000..7e20b2c614
--- /dev/null
+++ b/scripts/run-ci-acceptance-tests
@@ -0,0 +1,152 @@
+#!/usr/bin/env bash
+
+go test -run "^TestAcc" -count 1 \
+ ./internal/services/account \
+ ./internal/services/account_api_token_permission_groups \
+ ./internal/services/account_dns_settings \
+ ./internal/services/account_dns_settings_internal_view \
+ ./internal/services/account_permission_group \
+ ./internal/services/account_role \
+ ./internal/services/account_subscription \
+ ./internal/services/account_token \
+ ./internal/services/address_map \
+ ./internal/services/api_shield \
+ ./internal/services/api_shield_discovery_operation \
+ ./internal/services/api_shield_schema \
+ ./internal/services/api_token_permission_groups \
+ ./internal/services/argo_smart_routing \
+ ./internal/services/argo_tiered_caching \
+ ./internal/services/authenticated_origin_pulls \
+ ./internal/services/authenticated_origin_pulls_certificate \
+ ./internal/services/authenticated_origin_pulls_settings \
+ ./internal/services/botnet_feed_config_asn \
+ ./internal/services/byo_ip_prefix \
+ ./internal/services/calls_sfu_app \
+ ./internal/services/calls_turn_app \
+ ./internal/services/certificate_pack \
+ ./internal/services/cloud_connector_rules \
+ ./internal/services/cloudforce_one_request \
+ ./internal/services/cloudforce_one_request_asset \
+ ./internal/services/cloudforce_one_request_message \
+ ./internal/services/cloudforce_one_request_priority \
+ ./internal/services/content_scanning_expression \
+ ./internal/services/custom_pages \
+ ./internal/services/custom_ssl \
+ ./internal/services/dcv_delegation \
+ ./internal/services/dns_record \
+ ./internal/services/dns_settings_internal_view \
+ ./internal/services/dns_zone_transfers_acl \
+ ./internal/services/dns_zone_transfers_peer \
+ ./internal/services/dns_zone_transfers_tsig \
+ ./internal/services/email_routing_address \
+ ./internal/services/email_routing_catch_all \
+ ./internal/services/email_routing_dns \
+ ./internal/services/email_routing_rule \
+ ./internal/services/email_security_block_sender \
+ ./internal/services/email_security_impersonation_registry \
+ ./internal/services/email_security_trusted_domains \
+ ./internal/services/image \
+ ./internal/services/image_variant \
+ ./internal/services/ip_ranges \
+ ./internal/services/leaked_credential_check \
+ ./internal/services/leaked_credential_check_rule \
+ ./internal/services/list \
+ ./internal/services/list_item \
+ ./internal/services/logpull_retention \
+ ./internal/services/logpush_dataset_field \
+ ./internal/services/logpush_dataset_job \
+ ./internal/services/logpush_job \
+ ./internal/services/logpush_ownership_challenge \
+ ./internal/services/magic_network_monitoring_configuration \
+ ./internal/services/magic_network_monitoring_rule \
+ ./internal/services/magic_transit_connector \
+ ./internal/services/magic_transit_site \
+ ./internal/services/magic_transit_site_acl \
+ ./internal/services/magic_transit_site_lan \
+ ./internal/services/magic_transit_site_wan \
+ ./internal/services/magic_wan_gre_tunnel \
+ ./internal/services/magic_wan_ipsec_tunnel \
+ ./internal/services/magic_wan_static_route \
+ ./internal/services/observatory_scheduled_test \
+ ./internal/services/origin_ca_certificate \
+ ./internal/services/page_rule \
+ ./internal/services/page_shield_connections \
+ ./internal/services/page_shield_cookies \
+ ./internal/services/page_shield_policy \
+ ./internal/services/page_shield_scripts \
+ ./internal/services/pages_domain \
+ ./internal/services/pages_project \
+ ./internal/services/queue \
+ ./internal/services/queue_consumer \
+ ./internal/services/r2_bucket \
+ ./internal/services/r2_bucket_cors \
+ ./internal/services/r2_bucket_event_notification \ ./internal/services/r2_bucket_lifecycle \
+ ./internal/services/r2_bucket_lock \
+ ./internal/services/r2_bucket_sippy \
+ ./internal/services/r2_custom_domain \
+ ./internal/services/r2_managed_domain \
+ ./internal/services/regional_hostname \
+ ./internal/services/regional_tiered_cache \
+ ./internal/services/registrar_domain \
+ ./internal/services/resource_group \
+ ./internal/services/ruleset \
+ ./internal/services/snippet_rules \
+ ./internal/services/snippets \
+ ./internal/services/stream \
+ ./internal/services/stream_audio_track \
+ ./internal/services/stream_caption_language \
+ ./internal/services/stream_download \
+ ./internal/services/stream_key \
+ ./internal/services/stream_live_input \
+ ./internal/services/stream_watermark \
+ ./internal/services/stream_webhook \
+ ./internal/services/tiered_cache \
+ ./internal/services/turnstile_widget \
+ ./internal/services/url_normalization_settings \
+ ./internal/services/user \
+ ./internal/services/waiting_room_settings \
+ ./internal/services/workers_cron_trigger \
+ ./internal/services/workers_custom_domain \
+ ./internal/services/workers_deployment \
+ ./internal/services/workers_for_platforms_dispatch_namespace \
+ ./internal/services/workers_kv_namespace \
+ ./internal/services/workers_route \
+ ./internal/services/workers_script \
+ ./internal/services/workers_script_subdomain \
+ ./internal/services/zero_trust_access_application \
+ ./internal/services/zero_trust_access_group \
+ ./internal/services/zero_trust_access_identity_provider \
+ ./internal/services/zero_trust_access_infrastructure_target \
+ ./internal/services/zero_trust_access_key_configuration \
+ ./internal/services/zero_trust_access_mtls_hostname_settings \
+ ./internal/services/zero_trust_access_policy \
+ ./internal/services/zero_trust_access_service_token \
+ ./internal/services/zero_trust_access_tag \
+ ./internal/services/zero_trust_device_custom_profile \
+ ./internal/services/zero_trust_device_default_profile \
+ ./internal/services/zero_trust_device_default_profile_certificates \
+ ./internal/services/zero_trust_device_managed_networks \
+ ./internal/services/zero_trust_dlp_custom_profile \
+ ./internal/services/zero_trust_dlp_dataset \
+ ./internal/services/zero_trust_dlp_entry \
+ ./internal/services/zero_trust_dlp_predefined_profile \
+ ./internal/services/zero_trust_gateway_app_types \
+ ./internal/services/zero_trust_gateway_categories \
+ ./internal/services/zero_trust_gateway_certificate \
+ ./internal/services/zero_trust_gateway_logging \
+ ./internal/services/zero_trust_gateway_proxy_endpoint \
+ ./internal/services/zero_trust_list \
+ ./internal/services/zero_trust_risk_behavior \
+ ./internal/services/zero_trust_risk_scoring_integration \
+ ./internal/services/zero_trust_tunnel_cloudflared \
+ ./internal/services/zero_trust_tunnel_cloudflared_route \
+ ./internal/services/zero_trust_tunnel_cloudflared_token \
+ ./internal/services/zero_trust_tunnel_warp_connector_token \
+ ./internal/services/zone \
+ ./internal/services/zone_cache_reserve \
+ ./internal/services/zone_cache_variants \
+ ./internal/services/zone_dns_settings \
+ ./internal/services/zone_hold \
+ ./internal/services/zone_lockdown \
+ ./internal/services/zone_setting \
+ ./internal/services/zone_subscription \