diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aa223d0..905af49 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -73,5 +73,5 @@ jobs: - env: TF_ACC: "1" CDN77_TOKEN: ${{ secrets.CDN77_TOKEN }} - run: go test -v ./internal/provider/ + run: go test -p 1 -v --count 1 ./... timeout-minutes: 10 diff --git a/.golangci.yml b/.golangci.yml index 104ddf1..7c19acc 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -13,7 +13,7 @@ linters: # 2022-07-21 modified - durationcheck - errorlint - exhaustive - - exportloopref + - copyloopvar - gci - gochecknoglobals - gochecknoinits @@ -33,7 +33,6 @@ linters: # 2022-07-21 modified - nilnil - nlreturn - nolintlint - # - paralleltest # @todo check later, maybe enable - prealloc - predeclared - revive diff --git a/Makefile b/Makefile index 725dc80..a1bc389 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ GOLANGCI_LINT ?= go run github.com/golangci/golangci-lint/cmd/golangci-lint@late .PHONY: testacc testacc: clearacc - TF_ACC=1 go test ./internal/provider -v -cover $(TESTARGS) --timeout 10m --count 1 + TF_ACC=1 go test ./... -p 1 -v -cover $(TESTARGS) --timeout 10m --count 1 .PHONY: clearacc clearacc: diff --git a/docs/data-sources/cdn.md b/docs/data-sources/cdn.md index b57ec4d..657e1ab 100644 --- a/docs/data-sources/cdn.md +++ b/docs/data-sources/cdn.md @@ -14,7 +14,7 @@ CDN resource allows you to manage your CDNs ```terraform data "cdn77_cdn" "example" { - id = 7364999192 + id = 1837865409 } ``` @@ -40,7 +40,6 @@ data "cdn77_cdn" "example" { - `note` (String) Optional note - `origin_headers` (Map of String) Custom HTTP headers included in requests sent to the origin server - `origin_id` (String) ID (UUID) of attached Origin (content source for CDN) -- `origin_protection_enabled` (Boolean) Enabled origin protection can ease the load on your server or even hide it from direct incoming traffic with our proxy servers. - `query_string` (Attributes) Enabling this feature will ignore the query string, allowing URLs with query strings to cache properly. This is particularly useful if you tag your URLs with tracking/marketing parameters, for example. (see [below for nested schema](#nestedatt--query_string)) - `rate_limit_enabled` (Boolean) When enabled, this feature limits the data transfer rate by setting "limit_rate" based on the "rs" URL parameter and "limit_rate_after" by the value from the "ri" URL parameter. - `secure_token` (Attributes) This feature allows you to serve your content using signed URLs. You can enable your users to download secured content from the CDN with a valid hash. Note: When you check this option, make sure to generate secured links to access your content. (see [below for nested schema](#nestedatt--secure_token)) @@ -123,12 +122,12 @@ Read-Only: Read-Only: - `token` (String, Sensitive) Token length is between 8 and 64 characters. -- `type` (String)
    +- `type` (String)
+ @@ -137,7 +136,7 @@ Read-Only: Read-Only: - `ssl_id` (String) ID (UUID) of the SSL certificate -- `type` (String) Available values: instantSsl, none, SNI +- `type` (String) Possible values: instantSsl, none, SNI, SAN diff --git a/docs/data-sources/cdns.md b/docs/data-sources/cdns.md index c8d43c8..932ee04 100644 --- a/docs/data-sources/cdns.md +++ b/docs/data-sources/cdns.md @@ -29,11 +29,127 @@ data "cdn77_cdns" "all" { Read-Only: +- `cache` (Attributes) Your files will remain cached for the specified duration, after which your origin will be checked for an updated version of your files. Expiry/cache-control headers override this setting. (see [below for nested schema](#nestedatt--cdns--cache)) - `cnames` (Set of String) CNAME assigned to CDN. CNAME should be mapped via DNS to CDN URL. Otherwise it's not possible to generate an SSL certificate for any related CNAME. - `creation_time` (String) Timestamp when CDN was created +- `geo_protection` (Attributes) Geo protection enables you to control which countries can access your content directly (see [below for nested schema](#nestedatt--cdns--geo_protection)) +- `headers` (Attributes) (see [below for nested schema](#nestedatt--cdns--headers)) +- `hotlink_protection` (Attributes) Hotlink protection enables you to control which hostnames/domains can link to and access your content directly (see [below for nested schema](#nestedatt--cdns--hotlink_protection)) +- `https_redirect` (Attributes) If enabled, all requests via HTTP are redirected to HTTPS. Verify HTTPS availability of CNAMEs before activating, if applicable. (see [below for nested schema](#nestedatt--cdns--https_redirect)) - `id` (Number) ID of the CDN. This is also used as the CDN URL +- `ip_protection` (Attributes) IP protection enables you to control which networks can access your content directly (see [below for nested schema](#nestedatt--cdns--ip_protection)) - `label` (String) The label helps you to identify your CDN - `mp4_pseudo_streaming_enabled` (Boolean) Turn this option on if using a flash-based video player with MP4 files. Pseudo-streaming is used mainly in flash players. HTML5 players use range-requests. When enabled the "query_string" option must be set to ignore all parameters. - `note` (String) Optional note +- `origin_headers` (Map of String) Custom HTTP headers included in requests sent to the origin server - `origin_id` (String) ID (UUID) of attached Origin (content source for CDN) +- `query_string` (Attributes) Enabling this feature will ignore the query string, allowing URLs with query strings to cache properly. This is particularly useful if you tag your URLs with tracking/marketing parameters, for example. (see [below for nested schema](#nestedatt--cdns--query_string)) +- `rate_limit_enabled` (Boolean) When enabled, this feature limits the data transfer rate by setting "limit_rate" based on the "rs" URL parameter and "limit_rate_after" by the value from the "ri" URL parameter. +- `secure_token` (Attributes) This feature allows you to serve your content using signed URLs. You can enable your users to download secured content from the CDN with a valid hash. Note: When you check this option, make sure to generate secured links to access your content. (see [below for nested schema](#nestedatt--cdns--secure_token)) +- `ssl` (Attributes) (see [below for nested schema](#nestedatt--cdns--ssl)) +- `stream` (Attributes) Detail parameters of stream CDN (see [below for nested schema](#nestedatt--cdns--stream)) - `url` (String) URL of the CDN. Automatically generated when the CDN is created. The number is the same as the CDN ID. +- `waf_enabled` (Boolean) Protect your website against XSS, SQL injection and more with our SmartWAF. We're using OWASP Core Rule Set (CRS) to protect your data against the most exploited vulnerabilities. + + +### Nested Schema for `cdns.cache` + +Read-Only: + +- `max_age` (Number) In minutes +- `max_age_404` (Number) In seconds +- `requests_with_cookies_enabled` (Boolean) When disabled, requests with cookies will ignore changing cookie headers allowing all requests to hit the cache. When enabled, requests with cookies will be handled separately, so when the cookie changes it will not hit the previously cached request with different cookie options. + + + +### Nested Schema for `cdns.geo_protection` + +Read-Only: + +- `countries` (Set of String) We are using ISO 3166-1 alpha-2 code +- `type` (String) With "type": "blocklist" all countries set in the "countries" parameter are not allowed. With "type": "passlist" only countries set in the "countries" parameter are allowed. + + + +### Nested Schema for `cdns.headers` + +Read-Only: + +- `content_disposition_type` (String) When the "type" is set to "parameter" the Content-Disposition is defined by the "cd" parameter in the URL, often set to "attachment". The filename is specified using the "fn" parameter in the URL. +- `cors_enabled` (Boolean) The "Access-Control-Allow-Origin:" response header will always act in accordance with the "Origin:" request header sent by the client. For example, a request including the HTTP header "Origin: https://www.cdn77.com" will translate to the response header "Access-Control-Allow-Origin: https://www.cdn77.com". Files remain cached while the request/response header changes. +- `cors_timing_enabled` (Boolean) When enabled "Timing-Allow-Origin" CORS header is set +- `cors_wildcard_enabled` (Boolean) When enabled the wildcard value (*) is set in CORS headers +- `host_header_forwarding_enabled` (Boolean) When fetching the content from the origin server, our edge servers will pass the host header that was included in the request between the user and our edge server. + + + +### Nested Schema for `cdns.hotlink_protection` + +Read-Only: + +- `domains` (Set of String) +- `empty_referer_denied` (Boolean) Enabling this parameter prevents your content from being directly accessed by sources that send empty referrers. +- `type` (String) With "type": "blocklist" all domains set in the "domains" parameter are not allowed. With "type": "passlist" only domains in "domains" parameter are allowed. + + + +### Nested Schema for `cdns.https_redirect` + +Read-Only: + +- `code` (Number) 301 for permanent redirect and 302 for temporary redirect. If you are not sure, select the default 301 code. +- `enabled` (Boolean) + + + +### Nested Schema for `cdns.ip_protection` + +Read-Only: + +- `ips` (Set of String) +- `type` (String) With "type": "blocklist" all IP addresses set in the "ips" parameter are not allowed. With "type": "passlist" only IP addresses in "ips" parameter are allowed. + + + +### Nested Schema for `cdns.query_string` + +Read-Only: + +- `ignore_type` (String) +- `parameters` (Set of String) List of parameters used when "ignore_type" is set to "list" + + + +### Nested Schema for `cdns.secure_token` + +Read-Only: + +- `token` (String, Sensitive) Token length is between 8 and 64 characters. +- `type` (String) + + + +### Nested Schema for `cdns.ssl` + +Read-Only: + +- `ssl_id` (String) ID (UUID) of the SSL certificate +- `type` (String) Possible values: instantSsl, none, SNI, SAN + + + +### Nested Schema for `cdns.stream` + +Read-Only: + +- `origin_url` (String) +- `password` (String, Sensitive) +- `path` (String) +- `port` (Number) +- `protocol` (String) +- `query_key` (String) diff --git a/docs/data-sources/object_storages.md b/docs/data-sources/object_storages.md index 423239b..3a8bdbf 100644 --- a/docs/data-sources/object_storages.md +++ b/docs/data-sources/object_storages.md @@ -24,8 +24,17 @@ Object Storages data source allows you to read all available Object Storage clus Read-Only: -- `host` (String) - `id` (String) ID (UUID) of the Object Storage cluster -- `label` (String) -- `port` (Number) -- `scheme` (String) +- `label` (String) Label of the Object Storage cluster +- `url` (String) Absolute URL of this resource. Alternative to the attribute "url_parts". +- `url_parts` (Attributes) Set of attributes describing the resource URL. Alternative to the attribute "url". (see [below for nested schema](#nestedatt--clusters--url_parts)) + + +### Nested Schema for `clusters.url_parts` + +Read-Only: + +- `base_path` (String) Path to the directory where the content is stored +- `host` (String) Network host; can be a domain name or an IP address +- `port` (Number) Port number between 1 and 65535 (if not specified, default scheme port is used) +- `scheme` (String) URL scheme; can be either "http" or "https" diff --git a/docs/data-sources/origin.md b/docs/data-sources/origin.md deleted file mode 100644 index 99bfdcc..0000000 --- a/docs/data-sources/origin.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -# generated by https://github.com/hashicorp/terraform-plugin-docs -page_title: "cdn77_origin Data Source - terraform-provider-cdn77" -subcategory: "" -description: |- - Origin resource allows you to manage your Origins ---- - -# cdn77_origin (Data Source) - -Origin resource allows you to manage your Origins - -## Example Usage - -```terraform -data "cdn77_origin" "example" { - id = "2a81317a-53f4-4c6b-b3ca-9c1f9bc0ac41" - type = "aws" -} -``` - - -## Schema - -### Required - -- `id` (String) Origin ID (UUID) -- `type` (String) Type of the origin; one of [aws object-storage url] - -### Read-Only - -- `access_key_id` (String) Access key to your Object Storage bucket -- `access_key_secret` (String, Sensitive) Access secret to your Object Storage bucket -- `acl` (String) Object Storage access key ACL -- `aws_access_key_id` (String) AWS access key ID -- `aws_access_key_secret` (String, Sensitive) AWS access key secret -- `aws_region` (String) AWS region -- `base_dir` (String) Directory where the content is stored on the URL Origin -- `bucket_name` (String) Name of your Object Storage bucket -- `cluster_id` (String) ID of the Object Storage storage cluster -- `host` (String) Origin host without scheme and port. Can be domain name or IP address -- `label` (String) The label helps you to identify your Origin -- `note` (String) Optional note for the Origin -- `port` (Number) Origin port number. If not specified, default scheme port is used -- `scheme` (String) Scheme of the Origin diff --git a/docs/data-sources/origin_aws.md b/docs/data-sources/origin_aws.md new file mode 100644 index 0000000..da14ad8 --- /dev/null +++ b/docs/data-sources/origin_aws.md @@ -0,0 +1,46 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "cdn77_origin_aws Data Source - terraform-provider-cdn77" +subcategory: "" +description: |- + AWS Origin resource allows you to manage your AWS Origins +--- + +# cdn77_origin_aws (Data Source) + +AWS Origin resource allows you to manage your AWS Origins + +## Example Usage + +```terraform +data "cdn77_origin_aws" "example" { + id = "4cd2378b-dec8-49e2-aa17-bf7561452998" +} +``` + + +## Schema + +### Required + +- `id` (String) Origin ID (UUID) + +### Read-Only + +- `access_key_id` (String) AWS access key ID +- `access_key_secret` (String, Sensitive) AWS access key secret +- `label` (String) The label helps you to identify your Origin +- `note` (String) Optional note for the Origin +- `region` (String) AWS region +- `url` (String) Absolute URL of this resource. Alternative to the attribute "url_parts". +- `url_parts` (Attributes) Set of attributes describing the resource URL. Alternative to the attribute "url". (see [below for nested schema](#nestedatt--url_parts)) + + +### Nested Schema for `url_parts` + +Read-Only: + +- `base_path` (String) Path to the directory where the content is stored +- `host` (String) Network host; can be a domain name or an IP address +- `port` (Number) Port number between 1 and 65535 (if not specified, default scheme port is used) +- `scheme` (String) URL scheme; can be either "http" or "https" diff --git a/docs/data-sources/origin_object_storage.md b/docs/data-sources/origin_object_storage.md new file mode 100644 index 0000000..15a0ab7 --- /dev/null +++ b/docs/data-sources/origin_object_storage.md @@ -0,0 +1,58 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "cdn77_origin_object_storage Data Source - terraform-provider-cdn77" +subcategory: "" +description: |- + Object Storage Origin resource allows you to manage your Object Storage Origins +--- + +# cdn77_origin_object_storage (Data Source) + +Object Storage Origin resource allows you to manage your Object Storage Origins + +## Example Usage + +```terraform +data "cdn77_origin_object_storage" "example" { + id = "b2d6a7df-18df-4931-8c78-3842bc6e12f0" +} +``` + + +## Schema + +### Required + +- `id` (String) Origin ID (UUID) + +### Read-Only + +- `access_key_id` (String) Access key to your Object Storage bucket +- `access_key_secret` (String, Sensitive) Access secret to your Object Storage bucket +- `acl` (String) Object Storage access key ACL +- `bucket_name` (String) Name of your Object Storage bucket +- `cluster_id` (String) ID of the Object Storage storage cluster +- `label` (String) The label helps you to identify your Origin +- `note` (String) Optional note for the Origin +- `url` (String) Absolute URL of this resource. Alternative to the attribute "url_parts". +- `url_parts` (Attributes) Set of attributes describing the resource URL. Alternative to the attribute "url". (see [below for nested schema](#nestedatt--url_parts)) +- `usage` (Attributes) Usage statistics of the Object Storage bucket (see [below for nested schema](#nestedatt--usage)) + + +### Nested Schema for `url_parts` + +Read-Only: + +- `base_path` (String) Path to the directory where the content is stored +- `host` (String) Network host; can be a domain name or an IP address +- `port` (Number) Port number between 1 and 65535 (if not specified, default scheme port is used) +- `scheme` (String) URL scheme; can be either "http" or "https" + + + +### Nested Schema for `usage` + +Read-Only: + +- `files` (Number) Number of files stored on the Object Storage bucket +- `size_bytes` (Number) Total size of the Object Storage bucket in bytes diff --git a/docs/data-sources/origin_url.md b/docs/data-sources/origin_url.md new file mode 100644 index 0000000..652e172 --- /dev/null +++ b/docs/data-sources/origin_url.md @@ -0,0 +1,43 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "cdn77_origin_url Data Source - terraform-provider-cdn77" +subcategory: "" +description: |- + URL Origin resource allows you to manage your custom URL Origins +--- + +# cdn77_origin_url (Data Source) + +URL Origin resource allows you to manage your custom URL Origins + +## Example Usage + +```terraform +data "cdn77_origin_url" "example" { + id = "8f2718e2-cf17-4552-816a-cbf2308e792b" +} +``` + + +## Schema + +### Required + +- `id` (String) Origin ID (UUID) + +### Read-Only + +- `label` (String) The label helps you to identify your Origin +- `note` (String) Optional note for the Origin +- `url` (String) Absolute URL of this resource. Alternative to the attribute "url_parts". +- `url_parts` (Attributes) Set of attributes describing the resource URL. Alternative to the attribute "url". (see [below for nested schema](#nestedatt--url_parts)) + + +### Nested Schema for `url_parts` + +Read-Only: + +- `base_path` (String) Path to the directory where the content is stored +- `host` (String) Network host; can be a domain name or an IP address +- `port` (Number) Port number between 1 and 65535 (if not specified, default scheme port is used) +- `scheme` (String) URL scheme; can be either "http" or "https" diff --git a/docs/data-sources/origins.md b/docs/data-sources/origins.md index a63c381..54c8c1e 100644 --- a/docs/data-sources/origins.md +++ b/docs/data-sources/origins.md @@ -22,26 +22,86 @@ data "cdn77_origins" "all" { ### Read-Only -- `origins` (Attributes List) List of all Origins (see [below for nested schema](#nestedatt--origins)) +- `aws` (Attributes List) List of all AWS Origins (see [below for nested schema](#nestedatt--aws)) +- `object_storage` (Attributes List) List of all Object Storage Origins (see [below for nested schema](#nestedatt--object_storage)) +- `url` (Attributes List) List of all URL Origins (see [below for nested schema](#nestedatt--url)) - -### Nested Schema for `origins` + +### Nested Schema for `aws` + +Read-Only: + +- `access_key_id` (String) AWS access key ID +- `id` (String) Origin ID (UUID) +- `label` (String) The label helps you to identify your Origin +- `note` (String) Optional note for the Origin +- `region` (String) AWS region +- `url` (String) Absolute URL of this resource. Alternative to the attribute "url_parts". +- `url_parts` (Attributes) Set of attributes describing the resource URL. Alternative to the attribute "url". (see [below for nested schema](#nestedatt--aws--url_parts)) + + +### Nested Schema for `aws.url_parts` + +Read-Only: + +- `base_path` (String) Path to the directory where the content is stored +- `host` (String) Network host; can be a domain name or an IP address +- `port` (Number) Port number between 1 and 65535 (if not specified, default scheme port is used) +- `scheme` (String) URL scheme; can be either "http" or "https" + + + + +### Nested Schema for `object_storage` Read-Only: -- `access_key_id` (String) Access key to your Object Storage bucket -- `access_key_secret` (String, Sensitive) Access secret to your Object Storage bucket -- `acl` (String) Object Storage access key ACL -- `aws_access_key_id` (String) AWS access key ID -- `aws_access_key_secret` (String, Sensitive) AWS access key secret -- `aws_region` (String) AWS region -- `base_dir` (String) Directory where the content is stored on the URL Origin - `bucket_name` (String) Name of your Object Storage bucket -- `cluster_id` (String) ID of the Object Storage storage cluster -- `host` (String) Origin host without scheme and port. Can be domain name or IP address - `id` (String) Origin ID (UUID) - `label` (String) The label helps you to identify your Origin - `note` (String) Optional note for the Origin -- `port` (Number) Origin port number. If not specified, default scheme port is used -- `scheme` (String) Scheme of the Origin -- `type` (String) Type of the origin; one of [aws object-storage url] +- `url` (String) Absolute URL of this resource. Alternative to the attribute "url_parts". +- `url_parts` (Attributes) Set of attributes describing the resource URL. Alternative to the attribute "url". (see [below for nested schema](#nestedatt--object_storage--url_parts)) +- `usage` (Attributes) Usage statistics of the Object Storage bucket (see [below for nested schema](#nestedatt--object_storage--usage)) + + +### Nested Schema for `object_storage.url_parts` + +Read-Only: + +- `base_path` (String) Path to the directory where the content is stored +- `host` (String) Network host; can be a domain name or an IP address +- `port` (Number) Port number between 1 and 65535 (if not specified, default scheme port is used) +- `scheme` (String) URL scheme; can be either "http" or "https" + + + +### Nested Schema for `object_storage.usage` + +Read-Only: + +- `files` (Number) Number of files stored on the Object Storage bucket +- `size_bytes` (Number) Total size of the Object Storage bucket in bytes + + + + +### Nested Schema for `url` + +Read-Only: + +- `id` (String) Origin ID (UUID) +- `label` (String) The label helps you to identify your Origin +- `note` (String) Optional note for the Origin +- `url` (String) Absolute URL of this resource. Alternative to the attribute "url_parts". +- `url_parts` (Attributes) Set of attributes describing the resource URL. Alternative to the attribute "url". (see [below for nested schema](#nestedatt--url--url_parts)) + + +### Nested Schema for `url.url_parts` + +Read-Only: + +- `base_path` (String) Path to the directory where the content is stored +- `host` (String) Network host; can be a domain name or an IP address +- `port` (Number) Port number between 1 and 65535 (if not specified, default scheme port is used) +- `scheme` (String) URL scheme; can be either "http" or "https" diff --git a/docs/data-sources/ssl.md b/docs/data-sources/ssl.md index 7f8c8c5..ed0088f 100644 --- a/docs/data-sources/ssl.md +++ b/docs/data-sources/ssl.md @@ -14,7 +14,7 @@ SSL resource allows you to managed your SSL certificates and keys ```terraform data "cdn77_ssl" "example" { - id = "7e9db4a8-3d98-4da0-8ac8-2f9bfe35c752" + id = "9b39930c-6324-4e1d-91b9-4d056a638ea7" } ``` diff --git a/docs/data-sources/ssls.md b/docs/data-sources/ssls.md index 01d096a..f0f463e 100644 --- a/docs/data-sources/ssls.md +++ b/docs/data-sources/ssls.md @@ -32,5 +32,4 @@ Read-Only: - `certificate` (String) SNI certificate - `expires_at` (String) Date and time of the SNI certificate expiration - `id` (String) ID (UUID) of the SSL certificate -- `private_key` (String, Sensitive) Private key associated with the certificate - `subjects` (Set of String) Subjects (domain names) of the certificate diff --git a/docs/resources/cdn.md b/docs/resources/cdn.md index 90d7730..8940ae7 100644 --- a/docs/resources/cdn.md +++ b/docs/resources/cdn.md @@ -14,8 +14,8 @@ CDN resource allows you to manage your CDNs ```terraform resource "cdn77_cdn" "example" { - origin_id = cdn77_origin.example.id label = "Static content for example.com" + origin_id = cdn77_origin_url.example.id cnames = ["cdn.example.com"] } ``` @@ -51,7 +51,6 @@ resource "cdn77_cdn" "example" { - `creation_time` (String) Timestamp when CDN was created - `id` (Number) ID of the CDN. This is also used as the CDN URL -- `origin_protection_enabled` (Boolean) Enabled origin protection can ease the load on your server or even hide it from direct incoming traffic with our proxy servers. - `url` (String) URL of the CDN. Automatically generated when the CDN is created. The number is the same as the CDN ID. @@ -128,12 +127,12 @@ Optional: Optional: - `token` (String, Sensitive) Token length is between 8 and 64 characters. -- `type` (String)
    +- `type` (String)
+ @@ -142,7 +141,7 @@ Optional: Optional: - `ssl_id` (String) ID (UUID) of the SSL certificate -- `type` (String) Available values: instantSsl, none, SNI +- `type` (String) Possible values: instantSsl, none, SNI, SAN @@ -165,6 +164,7 @@ Import is supported using the following syntax: $ terraform import cdn77_cdn.example # must be the ID (unsigned integer) of the CDN + # Example: $ terraform import cdn77_cdn.example 1837865409 ``` diff --git a/docs/resources/origin.md b/docs/resources/origin.md deleted file mode 100644 index fc6cd08..0000000 --- a/docs/resources/origin.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -# generated by https://github.com/hashicorp/terraform-plugin-docs -page_title: "cdn77_origin Resource - terraform-provider-cdn77" -subcategory: "" -description: |- - Origin resource allows you to manage your Origins ---- - -# cdn77_origin (Resource) - -Origin resource allows you to manage your Origins - -## Example Usage - -```terraform -resource "cdn77_origin" "example" { - type = "url" - label = "Static content for example.com" - scheme = "http" - host = "static.example.com" -} -``` - - -## Schema - -### Required - -- `label` (String) The label helps you to identify your Origin -- `type` (String) Type of the origin; one of [aws object-storage url] - -### Optional - -- `acl` (String) Object Storage access key ACL -- `aws_access_key_id` (String) AWS access key ID -- `aws_access_key_secret` (String, Sensitive) AWS access key secret -- `aws_region` (String) AWS region -- `base_dir` (String) Directory where the content is stored on the URL Origin -- `bucket_name` (String) Name of your Object Storage bucket -- `cluster_id` (String) ID of the Object Storage storage cluster -- `host` (String) Origin host without scheme and port. Can be domain name or IP address -- `note` (String) Optional note for the Origin -- `port` (Number) Origin port number. If not specified, default scheme port is used -- `scheme` (String) Scheme of the Origin - -### Read-Only - -- `access_key_id` (String) Access key to your Object Storage bucket -- `access_key_secret` (String, Sensitive) Access secret to your Object Storage bucket -- `id` (String) Origin ID (UUID) - -## Import - -Import is supported using the following syntax: - -```shell -$ terraform import cdn77_origin.example ,[,type-specific parameters,...] - -# must be the ID (UUID) of the Origin -# must be a type of the Origin (one of: "aws", "object-storage", "url") -# Depending on the type of the origin there may be other required parameters: - -# URL type doesn't need other parameters -$ terraform import cdn77_origin.example ,url - -# AWS type requires access key secret -# must be the secret of the AWS access key that you provided when creating the Origin -$ terraform import cdn77_origin.example ,aws, - -# Object Storage type requires ACL, cluster ID, access key ID and access key secret -# must be ACL type (one of: "authenticated-read", "private", "public-read", "public-read-write") -# must be an ID (UUID) of the Object Storage cluster -# must be the ID of the access key that was returned when creating the Origin -# must be the secret of the access key that was returned when creating the Origin -$ terraform import cdn77_origin.example ,object-storage,,,, - -# Examples: -$ terraform import cdn77_origin.example_url 4cd2378b-dec8-49e2-aa17-bf7561452998,url -$ terraform import cdn77_origin.example_aws 4cd2378b-dec8-49e2-aa17-bf7561452998,aws,\ -VWK92izmd7zpY8Khs/Dllv4yLYc4sFWNyg2XtuNF -$ terraform import cdn77_origin.example_object_storage 4cd2378b-dec8-49e2-aa17-bf7561452998,object-storage,\ -private,842b5641-b641-4723-ac81-f8cc286e288f,I17DXFE00GNJZVQUTQPW,7UG7WbcIz4VhZnVxV4XQcDR2X0APApuvthyATf2v -``` diff --git a/docs/resources/origin_aws.md b/docs/resources/origin_aws.md new file mode 100644 index 0000000..3890c01 --- /dev/null +++ b/docs/resources/origin_aws.md @@ -0,0 +1,70 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "cdn77_origin_aws Resource - terraform-provider-cdn77" +subcategory: "" +description: |- + AWS Origin resource allows you to manage your AWS Origins +--- + +# cdn77_origin_aws (Resource) + +AWS Origin resource allows you to manage your AWS Origins + +## Example Usage + +```terraform +resource "cdn77_origin_aws" "example" { + label = "Assets AWS bucket for example.com" + url = "https://examplecom-static-assets.s3.eu-central-1.amazonaws.com" + region = "eu-central-1" + access_key_id = "23478207027842073230762374023" + access_key_secret = "VWK92izmd7zpY8Khs/Dllv4yLYc4sFWNyg2XtuNF" +} +``` + + +## Schema + +### Required + +- `label` (String) The label helps you to identify your Origin + +### Optional + +- `access_key_id` (String) AWS access key ID +- `access_key_secret` (String, Sensitive) AWS access key secret +- `note` (String) Optional note for the Origin +- `region` (String) AWS region +- `url` (String) Absolute URL of this resource. Alternative to the attribute "url_parts". +- `url_parts` (Attributes) Set of attributes describing the resource URL. Alternative to the attribute "url". (see [below for nested schema](#nestedatt--url_parts)) + +### Read-Only + +- `id` (String) Origin ID (UUID) + + +### Nested Schema for `url_parts` + +Required: + +- `host` (String) Network host; can be a domain name or an IP address +- `scheme` (String) URL scheme; can be either "http" or "https" + +Optional: + +- `base_path` (String) Path to the directory where the content is stored +- `port` (Number) Port number between 1 and 65535 (if not specified, default scheme port is used) + +## Import + +Import is supported using the following syntax: + +```shell +$ terraform import cdn77_origin_aws.example , + +# must be the ID (UUID) of the AWS Origin +# must be the secret of the AWS access key that you provided when creating the AWS Origin + +# Example: +$ terraform import cdn77_origin_aws.example 4cd2378b-dec8-49e2-aa17-bf7561452998,VWK92izmd7zpY8Khs/Dllv4yLYc4sFWNyg2XtuNF +``` diff --git a/docs/resources/origin_object_storage.md b/docs/resources/origin_object_storage.md new file mode 100644 index 0000000..e8fa3ec --- /dev/null +++ b/docs/resources/origin_object_storage.md @@ -0,0 +1,94 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "cdn77_origin_object_storage Resource - terraform-provider-cdn77" +subcategory: "" +description: |- + Object Storage Origin resource allows you to manage your Object Storage Origins +--- + +# cdn77_origin_object_storage (Resource) + +Object Storage Origin resource allows you to manage your Object Storage Origins + +## Example Usage + +```terraform +data "cdn77_object_storages" "all" { +} + +locals { + eu_cluster_id = one([for os in data.cdn77_object_storages.all.clusters : os.id if os.label == "EU"]) +} + +resource "cdn77_origin_object_storage" "example" { + label = "Assets bucket for example.com" + bucket_name = "examplecom-static-assets" + acl = "private" + cluster_id = local.eu_cluster_id + access_key_id = "I17DXFE00GNJZVQUTQPW" + access_key_secret = "7UG7WbcIz4VhZnVxV4XQcDR2X0APApuvthyATf2v" +} +``` + + +## Schema + +### Required + +- `acl` (String) Object Storage access key ACL +- `bucket_name` (String) Name of your Object Storage bucket +- `cluster_id` (String) ID of the Object Storage storage cluster +- `label` (String) The label helps you to identify your Origin + +### Optional + +- `note` (String) Optional note for the Origin + +### Read-Only + +- `access_key_id` (String) Access key to your Object Storage bucket +- `access_key_secret` (String, Sensitive) Access secret to your Object Storage bucket +- `id` (String) Origin ID (UUID) +- `url` (String) Absolute URL of this resource. Alternative to the attribute "url_parts". +- `url_parts` (Attributes) Set of attributes describing the resource URL. Alternative to the attribute "url". (see [below for nested schema](#nestedatt--url_parts)) +- `usage` (Attributes) Usage statistics of the Object Storage bucket (see [below for nested schema](#nestedatt--usage)) + + +### Nested Schema for `url_parts` + +Optional: + +- `base_path` (String) Path to the directory where the content is stored +- `port` (Number) Port number between 1 and 65535 (if not specified, default scheme port is used) + +Read-Only: + +- `host` (String) Network host; can be a domain name or an IP address +- `scheme` (String) URL scheme; can be either "http" or "https" + + + +### Nested Schema for `usage` + +Read-Only: + +- `files` (Number) Number of files stored on the Object Storage bucket +- `size_bytes` (Number) Total size of the Object Storage bucket in bytes + +## Import + +Import is supported using the following syntax: + +```shell +$ terraform import cdn77_origin_object_storage.example ,,,, + +# must be the ID (UUID) of the Object Storage Origin +# must be ACL type (one of: "authenticated-read", "private", "public-read", "public-read-write") +# must be an ID (UUID) of the Object Storage cluster +# must be the ID of the access key that was returned when creating the Object Storage Origin +# must be the secret of the access key that was returned when creating the Object Storage Origin + +# Example: +$ terraform import cdn77_origin_object_storage.example b2d6a7df-18df-4931-8c78-3842bc6e12f0,private,\ +842b5641-b641-4723-ac81-f8cc286e288f,I17DXFE00GNJZVQUTQPW,7UG7WbcIz4VhZnVxV4XQcDR2X0APApuvthyATf2v +``` diff --git a/docs/resources/origin_url.md b/docs/resources/origin_url.md new file mode 100644 index 0000000..472dae1 --- /dev/null +++ b/docs/resources/origin_url.md @@ -0,0 +1,63 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "cdn77_origin_url Resource - terraform-provider-cdn77" +subcategory: "" +description: |- + URL Origin resource allows you to manage your custom URL Origins +--- + +# cdn77_origin_url (Resource) + +URL Origin resource allows you to manage your custom URL Origins + +## Example Usage + +```terraform +resource "cdn77_origin_url" "example" { + label = "Static content for example.com" + url = "http://static.example.com" +} +``` + + +## Schema + +### Required + +- `label` (String) The label helps you to identify your Origin + +### Optional + +- `note` (String) Optional note for the Origin +- `url` (String) Absolute URL of this resource. Alternative to the attribute "url_parts". +- `url_parts` (Attributes) Set of attributes describing the resource URL. Alternative to the attribute "url". (see [below for nested schema](#nestedatt--url_parts)) + +### Read-Only + +- `id` (String) Origin ID (UUID) + + +### Nested Schema for `url_parts` + +Required: + +- `host` (String) Network host; can be a domain name or an IP address +- `scheme` (String) URL scheme; can be either "http" or "https" + +Optional: + +- `base_path` (String) Path to the directory where the content is stored +- `port` (Number) Port number between 1 and 65535 (if not specified, default scheme port is used) + +## Import + +Import is supported using the following syntax: + +```shell +$ terraform import cdn77_origin_url.example + +# must be the ID (UUID) of the URL Origin + +# Example: +$ terraform import cdn77_origin_url.example 8f2718e2-cf17-4552-816a-cbf2308e792b +``` diff --git a/docs/resources/ssl.md b/docs/resources/ssl.md index 55ea725..58f1b0a 100644 --- a/docs/resources/ssl.md +++ b/docs/resources/ssl.md @@ -42,8 +42,9 @@ $ terraform import cdn77_ssl.example , # must be the ID (UUID) of the SSL certificate # must be an entire private key (including PEM headers) encoded via base64. + # Example: -$ key=$(base64 --wrap=0 key.pem < # must be the ID (unsigned integer) of the CDN + # Example: $ terraform import cdn77_cdn.example 1837865409 diff --git a/examples/resources/cdn77_cdn/resource.tf b/examples/resources/cdn77_cdn/resource.tf index 1628f88..6dd11bc 100644 --- a/examples/resources/cdn77_cdn/resource.tf +++ b/examples/resources/cdn77_cdn/resource.tf @@ -1,5 +1,5 @@ resource "cdn77_cdn" "example" { - origin_id = cdn77_origin.example.id label = "Static content for example.com" + origin_id = cdn77_origin_url.example.id cnames = ["cdn.example.com"] } diff --git a/examples/resources/cdn77_origin/import.sh b/examples/resources/cdn77_origin/import.sh deleted file mode 100644 index 3e4a5f9..0000000 --- a/examples/resources/cdn77_origin/import.sh +++ /dev/null @@ -1,26 +0,0 @@ -$ terraform import cdn77_origin.example ,[,type-specific parameters,...] - -# must be the ID (UUID) of the Origin -# must be a type of the Origin (one of: "aws", "object-storage", "url") -# Depending on the type of the origin there may be other required parameters: - -# URL type doesn't need other parameters -$ terraform import cdn77_origin.example ,url - -# AWS type requires access key secret -# must be the secret of the AWS access key that you provided when creating the Origin -$ terraform import cdn77_origin.example ,aws, - -# Object Storage type requires ACL, cluster ID, access key ID and access key secret -# must be ACL type (one of: "authenticated-read", "private", "public-read", "public-read-write") -# must be an ID (UUID) of the Object Storage cluster -# must be the ID of the access key that was returned when creating the Origin -# must be the secret of the access key that was returned when creating the Origin -$ terraform import cdn77_origin.example ,object-storage,,,, - -# Examples: -$ terraform import cdn77_origin.example_url 4cd2378b-dec8-49e2-aa17-bf7561452998,url -$ terraform import cdn77_origin.example_aws 4cd2378b-dec8-49e2-aa17-bf7561452998,aws,\ -VWK92izmd7zpY8Khs/Dllv4yLYc4sFWNyg2XtuNF -$ terraform import cdn77_origin.example_object_storage 4cd2378b-dec8-49e2-aa17-bf7561452998,object-storage,\ -private,842b5641-b641-4723-ac81-f8cc286e288f,I17DXFE00GNJZVQUTQPW,7UG7WbcIz4VhZnVxV4XQcDR2X0APApuvthyATf2v diff --git a/examples/resources/cdn77_origin/resource.tf b/examples/resources/cdn77_origin/resource.tf deleted file mode 100644 index e061b9a..0000000 --- a/examples/resources/cdn77_origin/resource.tf +++ /dev/null @@ -1,6 +0,0 @@ -resource "cdn77_origin" "example" { - type = "url" - label = "Static content for example.com" - scheme = "http" - host = "static.example.com" -} diff --git a/examples/resources/cdn77_origin_aws/import.sh b/examples/resources/cdn77_origin_aws/import.sh new file mode 100644 index 0000000..0c1ad05 --- /dev/null +++ b/examples/resources/cdn77_origin_aws/import.sh @@ -0,0 +1,7 @@ +$ terraform import cdn77_origin_aws.example , + +# must be the ID (UUID) of the AWS Origin +# must be the secret of the AWS access key that you provided when creating the AWS Origin + +# Example: +$ terraform import cdn77_origin_aws.example 4cd2378b-dec8-49e2-aa17-bf7561452998,VWK92izmd7zpY8Khs/Dllv4yLYc4sFWNyg2XtuNF diff --git a/examples/resources/cdn77_origin_aws/resource.tf b/examples/resources/cdn77_origin_aws/resource.tf new file mode 100644 index 0000000..6d3c91e --- /dev/null +++ b/examples/resources/cdn77_origin_aws/resource.tf @@ -0,0 +1,7 @@ +resource "cdn77_origin_aws" "example" { + label = "Assets AWS bucket for example.com" + url = "https://examplecom-static-assets.s3.eu-central-1.amazonaws.com" + region = "eu-central-1" + access_key_id = "23478207027842073230762374023" + access_key_secret = "VWK92izmd7zpY8Khs/Dllv4yLYc4sFWNyg2XtuNF" +} diff --git a/examples/resources/cdn77_origin_object_storage/import.sh b/examples/resources/cdn77_origin_object_storage/import.sh new file mode 100644 index 0000000..6bdb51b --- /dev/null +++ b/examples/resources/cdn77_origin_object_storage/import.sh @@ -0,0 +1,11 @@ +$ terraform import cdn77_origin_object_storage.example ,,,, + +# must be the ID (UUID) of the Object Storage Origin +# must be ACL type (one of: "authenticated-read", "private", "public-read", "public-read-write") +# must be an ID (UUID) of the Object Storage cluster +# must be the ID of the access key that was returned when creating the Object Storage Origin +# must be the secret of the access key that was returned when creating the Object Storage Origin + +# Example: +$ terraform import cdn77_origin_object_storage.example b2d6a7df-18df-4931-8c78-3842bc6e12f0,private,\ +842b5641-b641-4723-ac81-f8cc286e288f,I17DXFE00GNJZVQUTQPW,7UG7WbcIz4VhZnVxV4XQcDR2X0APApuvthyATf2v diff --git a/examples/resources/cdn77_origin_object_storage/resource.tf b/examples/resources/cdn77_origin_object_storage/resource.tf new file mode 100644 index 0000000..6048533 --- /dev/null +++ b/examples/resources/cdn77_origin_object_storage/resource.tf @@ -0,0 +1,15 @@ +data "cdn77_object_storages" "all" { +} + +locals { + eu_cluster_id = one([for os in data.cdn77_object_storages.all.clusters : os.id if os.label == "EU"]) +} + +resource "cdn77_origin_object_storage" "example" { + label = "Assets bucket for example.com" + bucket_name = "examplecom-static-assets" + acl = "private" + cluster_id = local.eu_cluster_id + access_key_id = "I17DXFE00GNJZVQUTQPW" + access_key_secret = "7UG7WbcIz4VhZnVxV4XQcDR2X0APApuvthyATf2v" +} diff --git a/examples/resources/cdn77_origin_url/import.sh b/examples/resources/cdn77_origin_url/import.sh new file mode 100644 index 0000000..9285636 --- /dev/null +++ b/examples/resources/cdn77_origin_url/import.sh @@ -0,0 +1,6 @@ +$ terraform import cdn77_origin_url.example + +# must be the ID (UUID) of the URL Origin + +# Example: +$ terraform import cdn77_origin_url.example 8f2718e2-cf17-4552-816a-cbf2308e792b diff --git a/examples/resources/cdn77_origin_url/resource.tf b/examples/resources/cdn77_origin_url/resource.tf new file mode 100644 index 0000000..0650f23 --- /dev/null +++ b/examples/resources/cdn77_origin_url/resource.tf @@ -0,0 +1,4 @@ +resource "cdn77_origin_url" "example" { + label = "Static content for example.com" + url = "http://static.example.com" +} diff --git a/examples/resources/cdn77_ssl/import.sh b/examples/resources/cdn77_ssl/import.sh index e51fac4..87917eb 100644 --- a/examples/resources/cdn77_ssl/import.sh +++ b/examples/resources/cdn77_ssl/import.sh @@ -2,8 +2,9 @@ $ terraform import cdn77_ssl.example , # must be the ID (UUID) of the SSL certificate # must be an entire private key (including PEM headers) encoded via base64. + # Example: -$ key=$(base64 --wrap=0 key.pem <= 200 && statusCode <= 204 { return nil } - var body string - if vBody := vResponse.FieldByName("Body"); vBody.IsValid() { - body = string(vBody.Bytes()) - } - - return fmt.Errorf(message, fmt.Sprintf("unexpected HTTP status code: %d; response: %s", statusCode, body)) + return fmt.Errorf( + message, + fmt.Sprintf("unexpected HTTP status code: %d; response: %s", statusCode, response.Bytes()), + ) } func Config(config string, keyAndValues ...any) string { @@ -130,3 +114,39 @@ func Config(config string, keyAndValues ...any) string { return config } + +func ConfigPlanChecks(rsc string, action plancheck.ResourceActionType) resource.ConfigPlanChecks { + return resource.ConfigPlanChecks{PreApply: []plancheck.PlanCheck{plancheck.ExpectResourceAction(rsc, action)}} +} + +func Run(t *testing.T, checkDestroy resource.TestCheckFunc, steps ...resource.TestStep) { + t.Helper() + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: GetProviderFactories(), + CheckDestroy: checkDestroy, + Steps: steps, + }) +} + +func CheckAndAssignAttr(rsc string, attr string, target *string) resource.TestCheckFunc { + return resource.TestCheckResourceAttrWith(rsc, attr, func(value string) error { + *target = value + + return NotEqual(value, "") + }) +} + +func CheckAndReassignAttr(rsc string, attr string, target *string) resource.TestCheckFunc { + return resource.TestCheckResourceAttrWith(rsc, attr, func(value string) error { + err := errors.Join(NotEqual(value, *target), NotEqual(value, "")) + *target = value + + return err + }) +} + +func CheckAttr(rsc string, attr string, target *string) resource.TestCheckFunc { + return resource.TestCheckResourceAttrWith(rsc, attr, func(value string) error { + return EqualField(attr, value, *target) + }) +} diff --git a/internal/acctest/check_destroy.go b/internal/acctest/check_destroy.go new file mode 100644 index 0000000..bdd988a --- /dev/null +++ b/internal/acctest/check_destroy.go @@ -0,0 +1,74 @@ +package acctest + +import ( + "context" + "errors" + "fmt" + + "github.com/cdn77/cdn77-client-go/v2" + "github.com/cdn77/terraform-provider-cdn77/internal/provider/origin" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func CheckOriginDestroyed(client cdn77.ClientWithResponsesInterface, originType string) resource.TestCheckFunc { + return func(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + var resourceTypeName string + + switch originType { + case origin.TypeAws: + resourceTypeName = "cdn77_origin_aws" + case origin.TypeObjectStorage: + resourceTypeName = "cdn77_origin_object_storage" + case origin.TypeUrl: + resourceTypeName = "cdn77_origin_url" + default: + return fmt.Errorf("unknown Origin type: %s", originType) + } + + if rs.Type != resourceTypeName { + continue + } + + response404, err := getOriginDetail(client, originType, rs.Primary.Attributes["id"]) + if err != nil { + return err + } + + if response404 == nil { + return errors.New("expected origin to be deleted") + } + } + + return nil + } +} + +func getOriginDetail(client cdn77.ClientWithResponsesInterface, originType string, id string) (*cdn77.Errors, error) { + switch originType { + case origin.TypeAws: + response, err := client.OriginDetailAwsWithResponse(context.Background(), id) + if err != nil { + return nil, fmt.Errorf("failed to fetch AWS Origin: %w", err) + } + + return response.JSON404, nil + case origin.TypeObjectStorage: + response, err := client.OriginDetailObjectStorageWithResponse(context.Background(), id) + if err != nil { + return nil, fmt.Errorf("failed to fetch Object Storage Origin: %w", err) + } + + return response.JSON404, nil + case origin.TypeUrl: + response, err := client.OriginDetailUrlWithResponse(context.Background(), id) + if err != nil { + return nil, fmt.Errorf("failed to fetch URL Origin: %w", err) + } + + return response.JSON404, nil + default: + return nil, fmt.Errorf("unknown Origin type: %s", originType) + } +} diff --git a/internal/acctest/delete.go b/internal/acctest/delete.go index 00910ff..8e0e596 100644 --- a/internal/acctest/delete.go +++ b/internal/acctest/delete.go @@ -5,19 +5,20 @@ import ( "fmt" "testing" - "github.com/cdn77/cdn77-client-go" - "github.com/cdn77/terraform-provider-cdn77/internal/provider" + "github.com/cdn77/cdn77-client-go/v2" + "github.com/cdn77/terraform-provider-cdn77/internal/provider/origin" + "github.com/cdn77/terraform-provider-cdn77/internal/util" ) func DeleteOrigin(client cdn77.ClientWithResponsesInterface, originType string, id string) (err error) { - var response any + var response util.Response switch originType { - case provider.OriginTypeAws: + case origin.TypeAws: response, err = client.OriginDeleteAwsWithResponse(context.Background(), id) - case provider.OriginTypeObjectStorage: + case origin.TypeObjectStorage: response, err = client.OriginDeleteObjectStorageWithResponse(context.Background(), id) - case provider.OriginTypeUrl: + case origin.TypeUrl: response, err = client.OriginDeleteUrlWithResponse(context.Background(), id) default: panic(fmt.Sprintf("unknown Origin type: %s", originType)) diff --git a/internal/provider/testdata/cert1.pem b/internal/acctest/testdata/cert1.pem similarity index 100% rename from internal/provider/testdata/cert1.pem rename to internal/acctest/testdata/cert1.pem diff --git a/internal/provider/testdata/cert2.pem b/internal/acctest/testdata/cert2.pem similarity index 100% rename from internal/provider/testdata/cert2.pem rename to internal/acctest/testdata/cert2.pem diff --git a/internal/acctest/testdata/embed.go b/internal/acctest/testdata/embed.go new file mode 100644 index 0000000..9a37fbc --- /dev/null +++ b/internal/acctest/testdata/embed.go @@ -0,0 +1,21 @@ +package testdata + +import ( + _ "embed" + "strings" +) + +//go:embed key.pem +var SslKey string + +//go:embed cert1.pem +var SslCert1 string + +//go:embed cert2.pem +var SslCert2 string + +func init() { //nolint:gochecknoinits // here the init makes sense + SslKey = strings.TrimSpace(SslKey) + SslCert1 = strings.TrimSpace(SslCert1) + SslCert2 = strings.TrimSpace(SslCert2) +} diff --git a/internal/provider/testdata/key.pem b/internal/acctest/testdata/key.pem similarity index 100% rename from internal/provider/testdata/key.pem rename to internal/acctest/testdata/key.pem diff --git a/internal/mapping/mapping.go b/internal/mapping/mapping.go new file mode 100644 index 0000000..44e72dd --- /dev/null +++ b/internal/mapping/mapping.go @@ -0,0 +1,120 @@ +package mapping + +import ( + "fmt" + + "github.com/cdn77/terraform-provider-cdn77/internal/provider/cdn" + "github.com/cdn77/terraform-provider-cdn77/internal/provider/object_storages" + "github.com/cdn77/terraform-provider-cdn77/internal/provider/origin" + "github.com/cdn77/terraform-provider-cdn77/internal/provider/ssl" + "github.com/cdn77/terraform-provider-cdn77/internal/util" + "github.com/hashicorp/terraform-plugin-framework/datasource" + ds_schema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource" + rsc_schema "github.com/hashicorp/terraform-plugin-framework/resource/schema" +) + +type Resource string + +const ( + Cdn = Resource("cdn") + Cdns = Resource("cdns") + ObjectStorages = Resource("object_storages") + OriginAws = Resource("origin_aws") + OriginObjectStorage = Resource("origin_object_storage") + OriginUrl = Resource("origin_url") + Ssl = Resource("ssl") + Ssls = Resource("ssls") +) + +func ResourceFactory(rsc Resource) func() resource.Resource { + return func() resource.Resource { + schemaProvider, reader := resourceSchemaProviderAndReader(rsc) + baseResource := util.NewBaseResource(string(rsc), schemaProvider, reader) + + switch rsc { + case Cdn: + return &cdn.Resource{BaseResource: baseResource} + case OriginAws: + return &origin.AwsResource{BaseResource: baseResource} + case OriginObjectStorage: + return &origin.ObjectStorageResource{BaseResource: baseResource} + case OriginUrl: + return &origin.UrlResource{BaseResource: baseResource} + case Ssl: + return &ssl.Resource{BaseResource: baseResource} + default: + panic(fmt.Sprintf("unexpected resource type %q", rsc)) + } + } +} + +func DataSourceFactory(rsc Resource) func() datasource.DataSource { + return func() datasource.DataSource { + schemaProvider, reader := dataSourceSchemaProviderAndReader(rsc) + baseDataSource := util.NewBaseDataSource(string(rsc), schemaProvider, reader) + + switch rsc { + case Cdn: + return &cdn.DataSource{BaseDataSource: baseDataSource} + case Cdns: + return &cdn.AllDataSource{BaseDataSource: baseDataSource} + case ObjectStorages: + return &object_storages.DataSource{BaseDataSource: baseDataSource} + case OriginAws: + return &origin.AwsDataSource{BaseDataSource: baseDataSource} + case OriginObjectStorage: + return &origin.ObjectStorageDataSource{BaseDataSource: baseDataSource} + case OriginUrl: + return &origin.UrlDataSource{BaseDataSource: baseDataSource} + case Ssl: + return &ssl.DataSource{BaseDataSource: baseDataSource} + case Ssls: + return &ssl.AllDataSource{BaseDataSource: baseDataSource} + default: + panic(fmt.Sprintf("unexpected resource type %q", rsc)) + } + } +} + +func resourceSchemaProviderAndReader(rsc Resource) (func() rsc_schema.Schema, util.Reader) { + switch rsc { + case Cdn, Cdns: + return cdn.CreateResourceSchema, util.NewUniversalReader(&cdn.Reader{}) + case OriginAws: + return origin.CreateAwsResourceSchema, util.NewUniversalReader(&origin.AwsReader{}) + case OriginObjectStorage: + return origin.CreateObjectStorageResourceSchema, util.NewUniversalReader(&origin.ObjectStorageReader{}) + case OriginUrl: + return origin.CreateUrlResourceSchema, util.NewUniversalReader(&origin.UrlReader{}) + case Ssl: + return ssl.CreateResourceSchema, util.NewUniversalReader(&ssl.Reader{}) + case Ssls: + return ssl.CreateBaseResourceSchema, nil + default: + panic(fmt.Sprintf("unexpected resource type %q", rsc)) + } +} + +func dataSourceSchemaProviderAndReader(rsc Resource) (func() ds_schema.Schema, util.Reader) { + if rsc == ObjectStorages { + return object_storages.CreateSchema, nil + } + + schemaProvider, reader := resourceSchemaProviderAndReader(rsc) + + switch rsc { + case Cdn, OriginAws, OriginObjectStorage, OriginUrl, Ssl: + return toDataSourceSchema(schemaProvider, "id"), reader + case Cdns, Ssls: + return toDataSourceSchema(schemaProvider), reader + default: + panic(fmt.Sprintf("unexpected resource type %q", rsc)) + } +} + +func toDataSourceSchema(schemaProvider func() rsc_schema.Schema, requiredAttrs ...string) func() ds_schema.Schema { + return func() ds_schema.Schema { + return util.NewResourceDataSourceSchemaConverter(requiredAttrs...).Convert(schemaProvider()) + } +} diff --git a/internal/provider/cdn/all.go b/internal/provider/cdn/all.go new file mode 100644 index 0000000..61e927e --- /dev/null +++ b/internal/provider/cdn/all.go @@ -0,0 +1,86 @@ +package cdn + +import ( + "cmp" + "context" + "slices" + "sync" + + "github.com/cdn77/cdn77-client-go/v2" + "github.com/cdn77/terraform-provider-cdn77/internal/util" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type AllModel struct { + Cdns []Model `tfsdk:"cdns"` +} + +var _ datasource.DataSourceWithConfigure = &AllDataSource{} + +type AllDataSource struct { + *util.BaseDataSource +} + +func (d *AllDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + d.BaseDataSource.Schema(ctx, req, resp) + + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "cdns": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{Attributes: resp.Schema.Attributes}, + Computed: true, + Description: "List of all CDNs", + }, + }, + Description: "CDNs data source allows you to read all your CDNs", + } +} + +func (d *AllDataSource) Read(ctx context.Context, _ datasource.ReadRequest, resp *datasource.ReadResponse) { + const errMessage = "Failed to fetch list of all CDNs" + + diags := &resp.Diagnostics + + response, err := d.Client.CdnListWithResponse(ctx) + if err != nil { + diags.AddError(errMessage, err.Error()) + + return + } + + util.ProcessResponse(diags, response, errMessage, response.JSON200, func(summaries *[]cdn77.CdnSummary) { + wg := sync.WaitGroup{} + mu := sync.Mutex{} + cdns := make([]Model, 0, len(*summaries)) + + wg.Add(len(*summaries)) + + for _, summary := range *summaries { + go func() { + var data any = Model{Id: types.Int64Value(int64(summary.Id))} + + ds := d.BaseDataSource.Reader().Fill(ctx, d.Client, &data) + + mu.Lock() + + if ds == nil { + cdns = append(cdns, data.(Model)) + } else { + diags.Append(ds...) + } + + mu.Unlock() + wg.Done() + }() + } + + if wg.Wait(); !diags.HasError() { + slices.SortStableFunc(cdns, func(a, b Model) int { + return cmp.Compare(a.Id.ValueInt64(), b.Id.ValueInt64()) + }) + diags.Append(resp.State.Set(ctx, AllModel{Cdns: cdns})...) + } + }) +} diff --git a/internal/provider/cdn/all_test.go b/internal/provider/cdn/all_test.go new file mode 100644 index 0000000..715e8f6 --- /dev/null +++ b/internal/provider/cdn/all_test.go @@ -0,0 +1,190 @@ +package cdn_test + +import ( + "context" + "fmt" + "sort" + "testing" + "time" + + "github.com/cdn77/cdn77-client-go/v2" + "github.com/cdn77/terraform-provider-cdn77/internal/acctest" + "github.com/cdn77/terraform-provider-cdn77/internal/provider/origin" + "github.com/cdn77/terraform-provider-cdn77/internal/util" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/oapi-codegen/nullable" +) + +func TestAccCdnAllDataSource(t *testing.T) { + const rsc = "data.cdn77_cdns.all" + client := acctest.GetClient(t) + + originRequest := cdn77.OriginCreateUrlJSONRequestBody{ + Label: "random origin", + Scheme: "https", + Host: "my-totally-random-custom-host.com", + } + originResponse, err := client.OriginCreateUrlWithResponse(context.Background(), originRequest) + acctest.AssertResponseOk(t, "Failed to create Origin: %s", originResponse, err) + + originId := originResponse.JSON201.Id + + t.Cleanup(func() { + acctest.MustDeleteOrigin(t, client, origin.TypeUrl, originId) + }) + + const cdn1Label = "some cdn" + const cdn1Note = "some note" + + cdn1Request := cdn77.CdnAddJSONRequestBody{ + Label: cdn1Label, + OriginId: originId, + Cnames: util.Pointer([]string{"my.cdn.cz", "another.cdn.com"}), + Note: nullable.NewNullableWithValue(cdn1Note), + } + cdn1Response, err := client.CdnAddWithResponse(context.Background(), cdn1Request) + acctest.AssertResponseOk(t, "Failed to create CDN: %s", cdn1Response, err) + + cdn1Id := cdn1Response.JSON201.Id + cdn1CreationTime := cdn1Response.JSON201.CreationTime.Format(time.DateTime) + cdn1Url := cdn1Response.JSON201.Url + + t.Cleanup(func() { + acctest.MustDeleteCdn(t, client, cdn1Id) + }) + + const cdn2Label = "another cdn" + + cdn2Request := cdn77.CdnAddJSONRequestBody{Label: cdn2Label, OriginId: originId} + cdn2Response, err := client.CdnAddWithResponse(context.Background(), cdn2Request) + acctest.AssertResponseOk(t, "Failed to create CDN: %s", cdn2Response, err) + + cdn2Id := cdn2Response.JSON201.Id + cdn2CreationTime := cdn2Response.JSON201.CreationTime.Format(time.DateTime) + cdn2Url := cdn2Response.JSON201.Url + + t.Cleanup(func() { + acctest.MustDeleteCdn(t, client, cdn2Id) + }) + + key := func(i int, k string) string { + return fmt.Sprintf("cdns.%d.%s", i, k) + } + attrEq := func(i int, attr, value string) resource.TestCheckFunc { + return resource.TestCheckResourceAttr(rsc, key(i, attr), value) + } + cdnIdAndTestCheckFnFactory := []struct { + id int + factory func(i int) []resource.TestCheckFunc + }{ + {id: cdn1Id, factory: func(i int) []resource.TestCheckFunc { + return []resource.TestCheckFunc{ + attrEq(i, "id", fmt.Sprintf("%d", cdn1Id)), + attrEq(i, "label", "some cdn"), + attrEq(i, "origin_id", originId), + attrEq(i, "creation_time", cdn1CreationTime), + attrEq(i, "url", cdn1Url), + attrEq(i, "cache.max_age", fmt.Sprintf("%d", cdn77.MaxAgeN17280)), + attrEq(i, "cache.requests_with_cookies_enabled", "true"), + attrEq(i, "cnames.#", "2"), + resource.TestCheckTypeSetElemAttr(rsc, key(i, "cnames.*"), "my.cdn.cz"), + resource.TestCheckTypeSetElemAttr(rsc, key(i, "cnames.*"), "another.cdn.com"), + attrEq(i, "geo_protection.type", string(cdn77.Disabled)), + attrEq(i, "headers.content_disposition_type", string(cdn77.ContentDispositionTypeNone)), + attrEq(i, "headers.cors_enabled", "false"), + attrEq(i, "headers.cors_timing_enabled", "false"), + attrEq(i, "headers.cors_wildcard_enabled", "false"), + attrEq(i, "headers.host_header_forwarding_enabled", "false"), + attrEq(i, "hotlink_protection.empty_referer_denied", "false"), + attrEq(i, "hotlink_protection.type", string(cdn77.Disabled)), + attrEq(i, "https_redirect.enabled", "false"), + attrEq(i, "ip_protection.type", string(cdn77.Disabled)), + attrEq(i, "mp4_pseudo_streaming_enabled", "false"), + attrEq(i, "note", cdn1Note), + attrEq(i, "origin_headers.#", "0"), + attrEq(i, "query_string.ignore_type", string(cdn77.QueryStringIgnoreTypeNone)), + attrEq(i, "rate_limit_enabled", "false"), + attrEq(i, "secure_token.type", string(cdn77.SecureTokenTypeNone)), + attrEq(i, "ssl.type", string(cdn77.InstantSsl)), + attrEq(i, "waf_enabled", "false"), + + resource.TestCheckNoResourceAttr(rsc, key(i, "stream")), + resource.TestCheckNoResourceAttr(rsc, key(i, "cache.max_age_404")), + resource.TestCheckNoResourceAttr(rsc, key(i, "geo_protection.countries")), + resource.TestCheckNoResourceAttr(rsc, key(i, "hotlink_protection.domains")), + resource.TestCheckNoResourceAttr(rsc, key(i, "https_redirect.code")), + resource.TestCheckNoResourceAttr(rsc, key(i, "ip_protection.ips")), + resource.TestCheckNoResourceAttr(rsc, key(i, "query_string.parameters")), + resource.TestCheckNoResourceAttr(rsc, key(i, "secure_token.token")), + resource.TestCheckNoResourceAttr(rsc, key(i, "ssl.ssl_id")), + } + }}, + {id: cdn2Id, factory: func(i int) []resource.TestCheckFunc { + return []resource.TestCheckFunc{ + attrEq(i, "id", fmt.Sprintf("%d", cdn2Id)), + attrEq(i, "label", "another cdn"), + attrEq(i, "origin_id", originId), + attrEq(i, "creation_time", cdn2CreationTime), + attrEq(i, "url", cdn2Url), + attrEq(i, "cache.max_age", fmt.Sprintf("%d", cdn77.MaxAgeN17280)), + attrEq(i, "cache.requests_with_cookies_enabled", "true"), + attrEq(i, "cnames.#", "0"), + attrEq(i, "geo_protection.type", string(cdn77.Disabled)), + attrEq(i, "headers.content_disposition_type", string(cdn77.ContentDispositionTypeNone)), + attrEq(i, "headers.cors_enabled", "false"), + attrEq(i, "headers.cors_timing_enabled", "false"), + attrEq(i, "headers.cors_wildcard_enabled", "false"), + attrEq(i, "headers.host_header_forwarding_enabled", "false"), + attrEq(i, "hotlink_protection.empty_referer_denied", "false"), + attrEq(i, "hotlink_protection.type", string(cdn77.Disabled)), + attrEq(i, "https_redirect.enabled", "false"), + attrEq(i, "ip_protection.type", string(cdn77.Disabled)), + attrEq(i, "mp4_pseudo_streaming_enabled", "false"), + attrEq(i, "origin_headers.#", "0"), + attrEq(i, "query_string.ignore_type", string(cdn77.QueryStringIgnoreTypeNone)), + attrEq(i, "rate_limit_enabled", "false"), + attrEq(i, "secure_token.type", string(cdn77.SecureTokenTypeNone)), + attrEq(i, "ssl.type", string(cdn77.InstantSsl)), + attrEq(i, "waf_enabled", "false"), + + resource.TestCheckNoResourceAttr(rsc, key(i, "stream")), + resource.TestCheckNoResourceAttr(rsc, key(i, "cache.max_age_404")), + resource.TestCheckNoResourceAttr(rsc, key(i, "geo_protection.countries")), + resource.TestCheckNoResourceAttr(rsc, key(i, "hotlink_protection.domains")), + resource.TestCheckNoResourceAttr(rsc, key(i, "https_redirect.code")), + resource.TestCheckNoResourceAttr(rsc, key(i, "ip_protection.ips")), + resource.TestCheckNoResourceAttr(rsc, key(i, "note")), + resource.TestCheckNoResourceAttr(rsc, key(i, "query_string.parameters")), + resource.TestCheckNoResourceAttr(rsc, key(i, "secure_token.token")), + resource.TestCheckNoResourceAttr(rsc, key(i, "ssl.ssl_id")), + } + }}, + } + + sort.SliceStable(cdnIdAndTestCheckFnFactory, func(i, j int) bool { + return cdnIdAndTestCheckFnFactory[i].id < cdnIdAndTestCheckFnFactory[j].id + }) + + testCheckFns := []resource.TestCheckFunc{resource.TestCheckResourceAttr(rsc, "cdns.#", "2")} + + for i, x := range cdnIdAndTestCheckFnFactory { + testCheckFns = append(testCheckFns, x.factory(i)...) + } + + acctest.Run(t, nil, resource.TestStep{ + Config: cdnsDataSourceConfig, + Check: resource.ComposeAggregateTestCheckFunc(testCheckFns...), + }) +} + +func TestAccCdnAllDataSource_Empty(t *testing.T) { + acctest.Run(t, nil, resource.TestStep{ + Config: cdnsDataSourceConfig, + Check: resource.TestCheckResourceAttr("data.cdn77_cdns.all", "cdns.#", "0"), + }) +} + +const cdnsDataSourceConfig = ` +data "cdn77_cdns" "all" { +} +` diff --git a/internal/provider/cdn_resource.go b/internal/provider/cdn/cdn.go similarity index 61% rename from internal/provider/cdn_resource.go rename to internal/provider/cdn/cdn.go index 1ea03d8..29269a8 100644 --- a/internal/provider/cdn_resource.go +++ b/internal/provider/cdn/cdn.go @@ -1,4 +1,4 @@ -package provider +package cdn import ( "context" @@ -6,8 +6,9 @@ import ( "strconv" "time" - "github.com/cdn77/cdn77-client-go" + "github.com/cdn77/cdn77-client-go/v2" "github.com/cdn77/terraform-provider-cdn77/internal/util" + "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -16,45 +17,31 @@ import ( ) var ( - _ resource.ResourceWithConfigure = &CdnResource{} - _ resource.ResourceWithConfigValidators = &CdnResource{} - _ resource.ResourceWithImportState = &CdnResource{} + _ resource.ResourceWithConfigure = &Resource{} + _ resource.ResourceWithConfigValidators = &Resource{} + _ resource.ResourceWithImportState = &Resource{} ) -func NewCdnResource() resource.Resource { - return &CdnResource{} +type Resource struct { + *util.BaseResource } -type CdnResource struct { - client cdn77.ClientWithResponsesInterface +func (*Resource) ConfigValidators(context.Context) []resource.ConfigValidator { + return []resource.ConfigValidator{NewNullableListsConfigValidator()} } -func (*CdnResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_cdn" -} - -func (*CdnResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = CreateCdnResourceSchema() -} - -func (r *CdnResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - resp.Diagnostics.Append(util.MaybeSetClient(req.ProviderData, &r.client)) -} - -func (*CdnResource) ConfigValidators(context.Context) []resource.ConfigValidator { - return []resource.ConfigValidator{NewCdnNullableListsConfigValidator()} -} +func (r *Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + diags := &resp.Diagnostics + var data Model -func (r *CdnResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var data CdnModel - if resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...); resp.Diagnostics.HasError() { + if diags.Append(req.Plan.Get(ctx, &data)...); diags.HasError() { return } var cnamesPtr *[]string if !data.Cnames.IsNull() { - cnames, ok := util.StringSetToSlice(ctx, &resp.Diagnostics, path.Root("cnames"), data.Cnames) + cnames, ok := util.StringSetToSlice(ctx, diags, path.Root("cnames"), data.Cnames) if !ok { return } @@ -65,130 +52,106 @@ func (r *CdnResource) Create(ctx context.Context, req resource.CreateRequest, re const errMessage = "Failed to create CDN" request := cdn77.CdnAddJSONRequestBody{ - Cnames: cnamesPtr, + OriginId: data.OriginId.ValueString(), Label: data.Label.ValueString(), + Cnames: cnamesPtr, Note: util.StringValueToNullable(data.Note), - OriginId: data.OriginId.ValueString(), } - response, err := r.client.CdnAddWithResponse(ctx, request) + response, err := r.Client.CdnAddWithResponse(ctx, request) if err != nil { - resp.Diagnostics.AddError(errMessage, err.Error()) + diags.AddError(errMessage, err.Error()) return } - if !util.CheckResponse(&resp.Diagnostics, errMessage, response, response.JSON422, response.JSONDefault) { - return - } + var id int - data.Id = types.Int64Value(int64(response.JSON201.Id)) - data.CreationTime = types.StringValue(response.JSON201.CreationTime.Format(time.DateTime)) - data.OriginProtectionEnabled = types.BoolValue(false) - data.Url = types.StringValue(response.JSON201.Url) + util.ProcessResponse(diags, response, errMessage, response.JSON201, func(detail *cdn77.CdnSummary) { + id = detail.Id + data.Id = types.Int64Value(int64(id)) + data.CreationTime = types.StringValue(detail.CreationTime.Format(time.DateTime)) + data.Url = types.StringValue(detail.Url) + }) - editRequest, ok := r.createEditRequest(ctx, &resp.Diagnostics, data) - if !ok { + if diags.HasError() { return } - const editErrMessage = "Failed to edit CDN after creation; going to remove it" - - editResponse, err := r.client.CdnEditWithResponse(ctx, response.JSON201.Id, editRequest) - if err == nil { - if util.CheckResponse( - &resp.Diagnostics, - editErrMessage, - editResponse, - editResponse.JSON404, - editResponse.JSON422, - editResponse.JSONDefault, - ) { - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + editRequest, ok := r.createEditRequest(ctx, diags, data) + if !ok { + r.deleteAfterFailedEdit(ctx, diags, id) - return - } - } else { - resp.Diagnostics.AddError(editErrMessage, err.Error()) + return } - const deleteErrMessage = "Failed to remove CDN after failed edit" + const editErrMessage = "Failed to edit CDN after creation" - deleteResponse, err := r.client.CdnDeleteWithResponse(ctx, response.JSON201.Id) + editResponse, err := r.Client.CdnEditWithResponse(ctx, response.JSON201.Id, editRequest) if err != nil { - resp.Diagnostics.AddError(deleteErrMessage, err.Error()) + diags.AddError(editErrMessage, err.Error()) + r.deleteAfterFailedEdit(ctx, diags, id) return } - util.CheckResponse( - &resp.Diagnostics, - deleteErrMessage, - deleteResponse, - deleteResponse.JSON404, - deleteResponse.JSONDefault, - ) -} + util.ProcessEmptyResponse(diags, editResponse, errMessage, func() { + diags.Append(resp.State.Set(ctx, data)...) + }) -func (r *CdnResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - NewCdnResourceReader(ctx, r.client).Read(&req.State, &resp.Diagnostics, &resp.State) + if diags.HasError() { + r.deleteAfterFailedEdit(ctx, diags, id) + } } -func (r *CdnResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var data CdnModel - if resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...); resp.Diagnostics.HasError() { +func (r *Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + diags := &resp.Diagnostics + var data Model + + if diags.Append(req.Plan.Get(ctx, &data)...); diags.HasError() { return } - request, ok := r.createEditRequest(ctx, &resp.Diagnostics, data) + request, ok := r.createEditRequest(ctx, diags, data) if !ok { return } const errMessage = "Failed to update CDN" - response, err := r.client.CdnEditWithResponse(ctx, int(data.Id.ValueInt64()), request) + response, err := r.Client.CdnEditWithResponse(ctx, int(data.Id.ValueInt64()), request) if err != nil { - resp.Diagnostics.AddError(errMessage, err.Error()) + diags.AddError(errMessage, err.Error()) return } - if util.CheckResponse( - &resp.Diagnostics, - errMessage, - response, - response.JSON404, - response.JSON422, - response.JSONDefault, - ) { - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) - } + util.ProcessEmptyResponse(diags, response, errMessage, func() { + diags.Append(resp.State.Set(ctx, data)...) + }) } -func (r *CdnResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - var data CdnModel - if resp.Diagnostics.Append(req.State.Get(ctx, &data)...); resp.Diagnostics.HasError() { +func (r *Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + diags := &resp.Diagnostics + var data Model + + if diags.Append(req.State.Get(ctx, &data)...); diags.HasError() { return } const errMessage = "Failed to delete CDN" - response, err := r.client.CdnDeleteWithResponse(ctx, int(data.Id.ValueInt64())) + response, err := r.Client.CdnDeleteWithResponse(ctx, int(data.Id.ValueInt64())) if err != nil { - resp.Diagnostics.AddError(errMessage, err.Error()) - - return - } + diags.AddError(errMessage, err.Error()) - if maybeRemoveMissingResource(ctx, response.StatusCode(), data.Id.ValueInt64(), &resp.State) { return } - util.CheckResponse(&resp.Diagnostics, errMessage, response, response.JSON404, response.JSONDefault) + util.ValidateDeletionResponse(diags, response, errMessage) } -func (*CdnResource) ImportState( +func (*Resource) ImportState( ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse, @@ -206,37 +169,14 @@ func (*CdnResource) ImportState( resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...) } -func (*CdnResource) createDefaultEditRequest() cdn77.CdnEditJSONRequestBody { - return cdn77.CdnEditJSONRequestBody{ - Cache: &cdn77.Cache{ - MaxAge: util.Pointer(cdn77.MaxAgeN17280), - MaxAge404: nullable.NewNullNullable[cdn77.MaxAge404](), - RequestsWithCookiesEnabled: util.Pointer(true), - }, - GeoProtection: &cdn77.GeoProtection{Type: cdn77.Disabled}, - Headers: &cdn77.Headers{ - ContentDisposition: &cdn77.ContentDisposition{Type: util.Pointer(cdn77.ContentDispositionTypeNone)}, - }, - HotlinkProtection: &cdn77.HotlinkProtection{Type: cdn77.Disabled}, - HttpsRedirect: &cdn77.HttpsRedirect{}, - IpProtection: &cdn77.IpProtection{Type: cdn77.Disabled}, - Mp4PseudoStreaming: &cdn77.Mp4PseudoStreaming{Enabled: util.Pointer(false)}, - Note: nullable.NewNullNullable[string](), - OriginHeaders: &cdn77.OriginHeaders{Custom: nullable.NewNullNullable[map[string]string]()}, - QueryString: &cdn77.QueryString{IgnoreType: cdn77.QueryStringIgnoreTypeNone}, - RateLimit: &cdn77.RateLimit{}, - SecureToken: &cdn77.SecureToken{Type: cdn77.SecureTokenTypeNone}, - Ssl: &cdn77.CdnSsl{Type: cdn77.InstantSsl}, - Waf: &cdn77.Waf{}, - } -} - -func (r *CdnResource) createEditRequest( //nolint:cyclop +func (r *Resource) createEditRequest( //nolint:cyclop ctx context.Context, diags *diag.Diagnostics, - data CdnModel, + data Model, ) (cdn77.CdnEditJSONRequestBody, bool) { request := r.createDefaultEditRequest() + request.Label = data.Label.ValueStringPointer() + request.OriginId = data.OriginId.ValueStringPointer() request.Cache.MaxAge = util.Pointer(cdn77.MaxAge(data.Cache.MaxAge.ValueInt64())) request.Cache.MaxAge404 = util.Int64ValueToNullable[cdn77.MaxAge404](data.Cache.MaxAge404) @@ -302,8 +242,6 @@ func (r *CdnResource) createEditRequest( //nolint:cyclop } request.IpProtection.Type = cdn77.AccessProtectionType(data.IpProtection.Type.ValueString()) - - request.Label = data.Label.ValueStringPointer() request.Mp4PseudoStreaming.Enabled = data.Mp4PseudoStreamingEnabled.ValueBoolPointer() request.Note = util.StringValueToNullable(data.Note) @@ -316,8 +254,6 @@ func (r *CdnResource) createEditRequest( //nolint:cyclop request.OriginHeaders.Custom = nullable.NewNullableWithValue(headers) } - request.OriginId = data.OriginId.ValueStringPointer() - if !data.QueryString.Parameters.IsNull() { paramsPath := path.Root("query_string").AtName("parameters") params, ok := util.StringSetToSlice(ctx, diags, paramsPath, data.QueryString.Parameters) @@ -349,3 +285,47 @@ func (r *CdnResource) createEditRequest( //nolint:cyclop return request, true } + +func (*Resource) createDefaultEditRequest() cdn77.CdnEditJSONRequestBody { + return cdn77.CdnEditJSONRequestBody{ + Cache: &cdn77.Cache{ + MaxAge: util.Pointer(cdn77.MaxAgeN17280), + MaxAge404: nullable.NewNullNullable[cdn77.MaxAge404](), + RequestsWithCookiesEnabled: util.Pointer(true), + }, + GeoProtection: &cdn77.GeoProtection{Type: cdn77.Disabled}, + Headers: &cdn77.Headers{ + ContentDisposition: &cdn77.ContentDisposition{Type: util.Pointer(cdn77.ContentDispositionTypeNone)}, + }, + HotlinkProtection: &cdn77.HotlinkProtection{Type: cdn77.Disabled}, + HttpsRedirect: &cdn77.HttpsRedirect{}, + IpProtection: &cdn77.IpProtection{Type: cdn77.Disabled}, + Mp4PseudoStreaming: &cdn77.Mp4PseudoStreaming{Enabled: util.Pointer(false)}, + Note: nullable.NewNullNullable[string](), + OriginHeaders: &cdn77.OriginHeaders{Custom: nullable.NewNullNullable[map[string]string]()}, + QueryString: &cdn77.QueryString{IgnoreType: cdn77.QueryStringIgnoreTypeNone}, + RateLimit: &cdn77.RateLimit{}, + SecureToken: &cdn77.SecureToken{Type: cdn77.SecureTokenTypeNone}, + Ssl: &cdn77.CdnSsl{Type: cdn77.InstantSsl}, + Waf: &cdn77.Waf{}, + } +} + +func (r *Resource) deleteAfterFailedEdit(ctx context.Context, diags *diag.Diagnostics, id int) { + const errMessage = "Failed to remove CDN after failed edit" + + response, err := r.Client.CdnDeleteWithResponse(ctx, id) + if err != nil { + diags.AddError(errMessage, err.Error()) + + return + } + + util.ValidateDeletionResponse(diags, response, errMessage) +} + +var _ datasource.DataSourceWithConfigure = &DataSource{} + +type DataSource struct { + *util.BaseDataSource +} diff --git a/internal/provider/cdn/cdn_test.go b/internal/provider/cdn/cdn_test.go new file mode 100644 index 0000000..e187ea4 --- /dev/null +++ b/internal/provider/cdn/cdn_test.go @@ -0,0 +1,853 @@ +package cdn_test + +import ( + "cmp" + "context" + "errors" + "fmt" + "regexp" + "slices" + "sort" + "strconv" + "testing" + "time" + + "github.com/cdn77/cdn77-client-go/v2" + "github.com/cdn77/terraform-provider-cdn77/internal/acctest" + "github.com/cdn77/terraform-provider-cdn77/internal/acctest/testdata" + "github.com/cdn77/terraform-provider-cdn77/internal/provider/origin" + "github.com/cdn77/terraform-provider-cdn77/internal/util" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/oapi-codegen/nullable" +) + +func TestAccCdnResource(t *testing.T) { + const rsc = "cdn77_cdn.lorem" + const originRsc = "cdn77_origin_url.url" + + client := acctest.GetClient(t) + var cdnId string + var originId string + var cdnCreationTime string + var cdnUrl string + sslId := acctest.MustAddSslWithCleanup(t, client, testdata.SslCert1, testdata.SslKey) + + attrEq := func(key, value string) resource.TestCheckFunc { + return resource.TestCheckResourceAttr(rsc, key, value) + } + + acctest.Run(t, checkCdnsAndOriginDestroyed(client), + resource.TestStep{ + Config: OriginResourceConfig + `resource "cdn77_cdn" "lorem" { + label = "my cdn" + origin_id = cdn77_origin_url.url.id + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(originRsc, plancheck.ResourceActionCreate), + plancheck.ExpectResourceAction(rsc, plancheck.ResourceActionCreate), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + acctest.CheckAndAssignAttr(originRsc, "id", &originId), + acctest.CheckAndAssignAttr(rsc, "id", &cdnId), + attrEq("label", "my cdn"), + acctest.CheckAttr(rsc, "origin_id", &originId), + acctest.CheckAndAssignAttr(rsc, "creation_time", &cdnCreationTime), + acctest.CheckAndAssignAttr(rsc, "url", &cdnUrl), + attrEq("cache.max_age", fmt.Sprintf("%d", cdn77.MaxAgeN17280)), + attrEq("cache.requests_with_cookies_enabled", "true"), + attrEq("cnames.#", "0"), + attrEq("geo_protection.type", string(cdn77.Disabled)), + attrEq("headers.content_disposition_type", string(cdn77.ContentDispositionTypeNone)), + attrEq("headers.cors_enabled", "false"), + attrEq("headers.cors_timing_enabled", "false"), + attrEq("headers.cors_wildcard_enabled", "false"), + attrEq("headers.host_header_forwarding_enabled", "false"), + attrEq("hotlink_protection.empty_referer_denied", "false"), + attrEq("hotlink_protection.type", string(cdn77.Disabled)), + attrEq("https_redirect.enabled", "false"), + attrEq("ip_protection.type", string(cdn77.Disabled)), + attrEq("mp4_pseudo_streaming_enabled", "false"), + attrEq("origin_headers.#", "0"), + attrEq("query_string.ignore_type", string(cdn77.QueryStringIgnoreTypeNone)), + attrEq("rate_limit_enabled", "false"), + attrEq("secure_token.type", string(cdn77.SecureTokenTypeNone)), + attrEq("ssl.type", string(cdn77.InstantSsl)), + attrEq("waf_enabled", "false"), + + resource.TestCheckNoResourceAttr(rsc, "stream"), + resource.TestCheckNoResourceAttr(rsc, "cache.max_age_404"), + resource.TestCheckNoResourceAttr(rsc, "geo_protection.countries"), + resource.TestCheckNoResourceAttr(rsc, "hotlink_protection.domains"), + resource.TestCheckNoResourceAttr(rsc, "https_redirect.code"), + resource.TestCheckNoResourceAttr(rsc, "ip_protection.ips"), + resource.TestCheckNoResourceAttr(rsc, "note"), + resource.TestCheckNoResourceAttr(rsc, "query_string.parameters"), + resource.TestCheckNoResourceAttr(rsc, "secure_token.token"), + resource.TestCheckNoResourceAttr(rsc, "ssl.ssl_id"), + + checkCdnDefaults(client, &cdnId, &originId, "my cdn"), + ), + }, + resource.TestStep{ + Config: OriginResourceConfig + `resource "cdn77_cdn" "lorem" { + label = "changed the label" + origin_id = cdn77_origin_url.url.id + cache = { + max_age = 60 + max_age_404 = 5 + requests_with_cookies_enabled = false + } + cnames = ["my.cdn.cz", "other.cdn.com"] + headers = { + cors_enabled = true + cors_timing_enabled = true + cors_wildcard_enabled = true + host_header_forwarding_enabled = true + content_disposition_type = "parameter" + } + https_redirect = { + enabled = true + code = 301 + } + note = "custom note" + query_string = { + ignore_type = "list" + parameters = ["param"] + } + secure_token = { + type = "path" + token = "abcd1234" + } + }`, + ConfigPlanChecks: acctest.ConfigPlanChecks(rsc, plancheck.ResourceActionUpdate), + Check: resource.ComposeAggregateTestCheckFunc( + acctest.CheckAttr(rsc, "id", &cdnId), + attrEq("label", "changed the label"), + acctest.CheckAttr(rsc, "origin_id", &originId), + acctest.CheckAttr(rsc, "creation_time", &cdnCreationTime), + acctest.CheckAttr(rsc, "url", &cdnUrl), + attrEq("cache.max_age", fmt.Sprintf("%d", cdn77.MaxAgeN60)), + attrEq("cache.max_age_404", fmt.Sprintf("%d", cdn77.MaxAge404N5)), + attrEq("cache.requests_with_cookies_enabled", "false"), + attrEq("cnames.#", "2"), + resource.TestCheckTypeSetElemAttr(rsc, "cnames.*", "my.cdn.cz"), + resource.TestCheckTypeSetElemAttr(rsc, "cnames.*", "other.cdn.com"), + attrEq("geo_protection.type", string(cdn77.Disabled)), + attrEq("headers.content_disposition_type", string(cdn77.ContentDispositionTypeParameter)), + attrEq("headers.cors_enabled", "true"), + attrEq("headers.cors_timing_enabled", "true"), + attrEq("headers.cors_wildcard_enabled", "true"), + attrEq("headers.host_header_forwarding_enabled", "true"), + attrEq("hotlink_protection.empty_referer_denied", "false"), + attrEq("hotlink_protection.type", string(cdn77.Disabled)), + attrEq("https_redirect.code", fmt.Sprintf("%d", cdn77.N301)), + attrEq("https_redirect.enabled", "true"), + attrEq("ip_protection.type", string(cdn77.Disabled)), + attrEq("mp4_pseudo_streaming_enabled", "false"), + attrEq("note", "custom note"), + attrEq("origin_headers.#", "0"), + attrEq("query_string.parameters.#", "1"), + resource.TestCheckTypeSetElemAttr(rsc, "query_string.parameters.*", "param"), + attrEq("query_string.ignore_type", string(cdn77.QueryStringIgnoreTypeList)), + attrEq("rate_limit_enabled", "false"), + attrEq("secure_token.token", "abcd1234"), + attrEq("secure_token.type", string(cdn77.SecureTokenTypePath)), + attrEq("ssl.type", string(cdn77.InstantSsl)), + attrEq("waf_enabled", "false"), + + resource.TestCheckNoResourceAttr(rsc, "stream"), + resource.TestCheckNoResourceAttr(rsc, "geo_protection.countries"), + resource.TestCheckNoResourceAttr(rsc, "hotlink_protection.domains"), + resource.TestCheckNoResourceAttr(rsc, "ip_protection.ips"), + resource.TestCheckNoResourceAttr(rsc, "ssl.ssl_id"), + + checkCdn(client, &cdnId, func(c *cdn77.Cdn) error { + slices.SortStableFunc(c.Cnames, func(a, b cdn77.Cname) int { + return cmp.Compare(a.Cname, b.Cname) + }) + + return errors.Join( + acctest.EqualField("label", c.Label, "changed the label"), + acctest.NullFieldEqual("origin_id", c.OriginId, originId), + acctest.EqualField("cache.max_age", *c.Cache.MaxAge, cdn77.MaxAgeN60), + acctest.NullFieldEqual("cache.max_age_404", c.Cache.MaxAge404, cdn77.MaxAge404N5), + acctest.EqualField( + "cache.requests_with_cookies_enabled", + *c.Cache.RequestsWithCookiesEnabled, + false, + ), + acctest.EqualField("cnames.#", len(c.Cnames), 2), + acctest.EqualField("cnames.0", c.Cnames[0].Cname, "my.cdn.cz"), + acctest.EqualField("cnames.1", c.Cnames[1].Cname, "other.cdn.com"), + acctest.EqualField("headers.cors_enabled", *c.Headers.CorsEnabled, true), + acctest.EqualField("headers.cors_timing_enabled", *c.Headers.CorsTimingEnabled, true), + acctest.EqualField("headers.cors_wildcard_enabled", *c.Headers.CorsWildcardEnabled, true), + acctest.EqualField( + "headers.host_header_forwarding_enabled", + *c.Headers.HostHeaderForwardingEnabled, + true, + ), + acctest.EqualField( + "headers.content_disposition_type", + *c.Headers.ContentDisposition.Type, + cdn77.ContentDispositionTypeParameter, + ), + acctest.EqualField("https_redirect.enabled", c.HttpsRedirect.Enabled, true), + acctest.EqualField("https_redirect.code", *c.HttpsRedirect.Code, cdn77.N301), + acctest.NullFieldEqual("note", c.Note, "custom note"), + acctest.EqualField( + "query_string.ignore_type", + c.QueryString.IgnoreType, + cdn77.QueryStringIgnoreTypeList, + ), + acctest.EqualField("query_string.parameters.#", len(*c.QueryString.Parameters), 1), + acctest.EqualField("query_string.parameters.0", (*c.QueryString.Parameters)[0], "param"), + acctest.EqualField("secure_token.type", c.SecureToken.Type, cdn77.SecureTokenTypePath), + acctest.EqualField("secure_token.token", *c.SecureToken.Token, "abcd1234"), + ) + }), + ), + }, + resource.TestStep{ + Config: OriginResourceConfig + acctest.Config( + `resource "cdn77_cdn" "lorem" { + label = "changed the label" + origin_id = cdn77_origin_url.url.id + cache = { + max_age = 60 + max_age_404 = 5 + requests_with_cookies_enabled = false + } + cnames = ["my.cdn.cz", "other.cdn.com"] + geo_protection = { + type = "blocklist" + countries = ["SK"] + } + headers = { + cors_enabled = true + cors_timing_enabled = true + cors_wildcard_enabled = true + host_header_forwarding_enabled = true + content_disposition_type = "parameter" + } + hotlink_protection = { + type = "blocklist" + domains = ["example.com"] + empty_referer_denied = true + } + https_redirect = { + enabled = true + code = 301 + } + ip_protection = { + type = "passlist" + ips = ["1.1.1.1/32", "8.8.8.8/32"] + } + mp4_pseudo_streaming_enabled = true + note = "custom note" + origin_headers = { + abc = "v1" + def = "v2" + } + query_string = { + ignore_type = "all" + } + rate_limit_enabled = true + secure_token = { + type = "path" + token = "abcd1234" + } + ssl = { + type = "SNI" + ssl_id = "{sslId}" + } + waf_enabled = true + }`, + "sslId", sslId, + ), + ConfigPlanChecks: acctest.ConfigPlanChecks(rsc, plancheck.ResourceActionUpdate), + Check: resource.ComposeAggregateTestCheckFunc( + acctest.CheckAttr(rsc, "id", &cdnId), + attrEq("label", "changed the label"), + acctest.CheckAttr(rsc, "origin_id", &originId), + acctest.CheckAttr(rsc, "creation_time", &cdnCreationTime), + acctest.CheckAttr(rsc, "url", &cdnUrl), + resource.TestCheckResourceAttrWith(rsc, "creation_time", func(value string) (err error) { + return acctest.Equal(value, cdnCreationTime) + }), + attrEq("cache.max_age", fmt.Sprintf("%d", cdn77.MaxAgeN60)), + attrEq("cache.max_age_404", fmt.Sprintf("%d", cdn77.MaxAge404N5)), + attrEq("cache.requests_with_cookies_enabled", "false"), + attrEq("cnames.#", "2"), + resource.TestCheckTypeSetElemAttr(rsc, "cnames.*", "my.cdn.cz"), + resource.TestCheckTypeSetElemAttr(rsc, "cnames.*", "other.cdn.com"), + attrEq("geo_protection.countries.#", "1"), + resource.TestCheckTypeSetElemAttr(rsc, "geo_protection.countries.*", "SK"), + attrEq("geo_protection.type", string(cdn77.Blocklist)), + attrEq("headers.content_disposition_type", string(cdn77.ContentDispositionTypeParameter)), + attrEq("headers.cors_enabled", "true"), + attrEq("headers.cors_timing_enabled", "true"), + attrEq("headers.cors_wildcard_enabled", "true"), + attrEq("headers.host_header_forwarding_enabled", "true"), + attrEq("hotlink_protection.domains.#", "1"), + resource.TestCheckTypeSetElemAttr(rsc, "hotlink_protection.domains.*", "example.com"), + attrEq("hotlink_protection.empty_referer_denied", "true"), + attrEq("hotlink_protection.type", string(cdn77.Blocklist)), + attrEq("https_redirect.code", fmt.Sprintf("%d", cdn77.N301)), + attrEq("https_redirect.enabled", "true"), + attrEq("ip_protection.ips.#", "2"), + resource.TestCheckTypeSetElemAttr(rsc, "ip_protection.ips.*", "1.1.1.1/32"), + resource.TestCheckTypeSetElemAttr(rsc, "ip_protection.ips.*", "8.8.8.8/32"), + attrEq("ip_protection.type", string(cdn77.Passlist)), + attrEq("mp4_pseudo_streaming_enabled", "true"), + attrEq("note", "custom note"), + attrEq("origin_headers.%", "2"), + attrEq("origin_headers.abc", "v1"), + attrEq("origin_headers.def", "v2"), + attrEq("query_string.ignore_type", string(cdn77.QueryStringIgnoreTypeAll)), + attrEq("query_string.parameters.#", "0"), + attrEq("rate_limit_enabled", "true"), + attrEq("secure_token.token", "abcd1234"), + attrEq("secure_token.type", string(cdn77.SecureTokenTypePath)), + attrEq("ssl.ssl_id", sslId), + attrEq("ssl.type", string(cdn77.SNI)), + attrEq("waf_enabled", "true"), + + resource.TestCheckNoResourceAttr(rsc, "stream"), + + checkCdn(client, &cdnId, func(c *cdn77.Cdn) error { + slices.SortStableFunc(c.Cnames, func(a, b cdn77.Cname) int { + return cmp.Compare(a.Cname, b.Cname) + }) + slices.SortStableFunc(*c.IpProtection.Ips, cmp.Compare) + + return errors.Join( + acctest.EqualField("label", c.Label, "changed the label"), + acctest.NullFieldEqual("origin_id", c.OriginId, originId), + acctest.EqualField("cache.max_age", *c.Cache.MaxAge, cdn77.MaxAgeN60), + acctest.NullFieldEqual("cache.max_age_404", c.Cache.MaxAge404, cdn77.MaxAge404N5), + acctest.EqualField( + "cache.requests_with_cookies_enabled", + *c.Cache.RequestsWithCookiesEnabled, + false, + ), + acctest.EqualField("cnames.#", len(c.Cnames), 2), + acctest.EqualField("cnames.0", c.Cnames[0].Cname, "my.cdn.cz"), + acctest.EqualField("cnames.1", c.Cnames[1].Cname, "other.cdn.com"), + acctest.EqualField("geo_protection.type", c.GeoProtection.Type, cdn77.Blocklist), + acctest.EqualField("geo_protection.countries.0", (*c.GeoProtection.Countries)[0], "SK"), + acctest.EqualField("headers.cors_enabled", *c.Headers.CorsEnabled, true), + acctest.EqualField("headers.cors_timing_enabled", *c.Headers.CorsTimingEnabled, true), + acctest.EqualField("headers.cors_wildcard_enabled", *c.Headers.CorsWildcardEnabled, true), + acctest.EqualField( + "headers.host_header_forwarding_enabled", + *c.Headers.HostHeaderForwardingEnabled, + true, + ), + acctest.EqualField( + "headers.content_disposition_type", + *c.Headers.ContentDisposition.Type, + cdn77.ContentDispositionTypeParameter, + ), + acctest.EqualField("hotlink_protection.type", c.HotlinkProtection.Type, cdn77.Blocklist), + acctest.EqualField("hotlink_protection.domains.#", len(*c.HotlinkProtection.Domains), 1), + acctest.EqualField( + "hotlink_protection.domains.0", + (*c.HotlinkProtection.Domains)[0], + "example.com", + ), + acctest.EqualField( + "hotlink_protection.empty_referer_denied", + c.HotlinkProtection.EmptyRefererDenied, + true, + ), + acctest.EqualField("https_redirect.enabled", c.HttpsRedirect.Enabled, true), + acctest.EqualField("https_redirect.code", *c.HttpsRedirect.Code, cdn77.N301), + acctest.EqualField("ip_protection.type", c.IpProtection.Type, cdn77.Passlist), + acctest.EqualField("ip_protection.ips.#", len(*c.IpProtection.Ips), 2), + acctest.EqualField("ip_protection.ips.0", (*c.IpProtection.Ips)[0], "1.1.1.1/32"), + acctest.EqualField("ip_protection.ips.1", (*c.IpProtection.Ips)[1], "8.8.8.8/32"), + acctest.EqualField("mp4_pseudo_streaming_enabled", *c.Mp4PseudoStreaming.Enabled, true), + acctest.NullFieldEqual("note", c.Note, "custom note"), + func() error { + expected := map[string]string{"abc": "v1", "def": "v2"} + + if value, err := c.OriginHeaders.Custom.Get(); err == nil { + if len(value) == len(expected) { + valid := true + + for k, v := range expected { + if value[k] != v { + valid = false + } + } + + if valid { + return nil + } + } + } + + return fmt.Errorf( + "field origin_headers: expected %+v, got: %+v", + nullable.NewNullableWithValue(expected), + c.OriginHeaders.Custom, + ) + }(), + acctest.EqualField( + "query_string.ignore_type", + c.QueryString.IgnoreType, + cdn77.QueryStringIgnoreTypeAll, + ), + acctest.EqualField("query_string.parameters", c.QueryString.Parameters, nil), + acctest.EqualField("rate_limit_enabled", c.RateLimit.Enabled, true), + acctest.EqualField("secure_token.type", c.SecureToken.Type, cdn77.SecureTokenTypePath), + acctest.EqualField("secure_token.token", *c.SecureToken.Token, "abcd1234"), + acctest.EqualField("ssl.type", c.Ssl.Type, cdn77.SNI), + acctest.EqualField("ssl.ssl_id", *c.Ssl.SslId, sslId), + acctest.EqualField("waf_enabled", c.Waf.Enabled, true), + ) + }), + ), + }, + resource.TestStep{ + Config: OriginResourceConfig + `resource "cdn77_cdn" "lorem" { + label = "my cdn" + origin_id = cdn77_origin_url.url.id + }`, + ConfigPlanChecks: acctest.ConfigPlanChecks(rsc, plancheck.ResourceActionUpdate), + Check: resource.ComposeAggregateTestCheckFunc( + acctest.CheckAttr(originRsc, "id", &originId), + acctest.CheckAttr(rsc, "id", &cdnId), + attrEq("label", "my cdn"), + acctest.CheckAttr(rsc, "origin_id", &originId), + acctest.CheckAttr(rsc, "creation_time", &cdnCreationTime), + acctest.CheckAttr(rsc, "url", &cdnUrl), + attrEq("cache.max_age", fmt.Sprintf("%d", cdn77.MaxAgeN17280)), + attrEq("cache.requests_with_cookies_enabled", "true"), + attrEq("cnames.#", "0"), + attrEq("geo_protection.type", string(cdn77.Disabled)), + attrEq("headers.content_disposition_type", string(cdn77.ContentDispositionTypeNone)), + attrEq("headers.cors_enabled", "false"), + attrEq("headers.cors_timing_enabled", "false"), + attrEq("headers.cors_wildcard_enabled", "false"), + attrEq("headers.host_header_forwarding_enabled", "false"), + attrEq("hotlink_protection.empty_referer_denied", "false"), + attrEq("hotlink_protection.type", string(cdn77.Disabled)), + attrEq("https_redirect.enabled", "false"), + attrEq("ip_protection.type", string(cdn77.Disabled)), + attrEq("mp4_pseudo_streaming_enabled", "false"), + attrEq("origin_headers.#", "0"), + attrEq("query_string.ignore_type", string(cdn77.QueryStringIgnoreTypeNone)), + attrEq("rate_limit_enabled", "false"), + attrEq("secure_token.type", string(cdn77.SecureTokenTypeNone)), + attrEq("ssl.type", string(cdn77.InstantSsl)), + attrEq("waf_enabled", "false"), + + resource.TestCheckNoResourceAttr(rsc, "stream"), + resource.TestCheckNoResourceAttr(rsc, "cache.max_age_404"), + resource.TestCheckNoResourceAttr(rsc, "geo_protection.countries"), + resource.TestCheckNoResourceAttr(rsc, "hotlink_protection.domains"), + resource.TestCheckNoResourceAttr(rsc, "https_redirect.code"), + resource.TestCheckNoResourceAttr(rsc, "ip_protection.ips"), + resource.TestCheckNoResourceAttr(rsc, "note"), + resource.TestCheckNoResourceAttr(rsc, "query_string.parameters"), + resource.TestCheckNoResourceAttr(rsc, "secure_token.token"), + resource.TestCheckNoResourceAttr(rsc, "ssl.ssl_id"), + + checkCdnDefaults(client, &cdnId, &originId, "my cdn"), + ), + }, + ) +} + +func TestAccCdnResource_Import(t *testing.T) { + client := acctest.GetClient(t) + rsc := "cdn77_cdn.lorem" + var cdnId string + + acctest.Run(t, checkCdnsAndOriginDestroyed(client), + resource.TestStep{ + Config: OriginResourceConfig + `resource "cdn77_cdn" "lorem" { + origin_id = cdn77_origin_url.url.id + label = "my cdn" + note = "custom note" + }`, + Check: acctest.CheckAndAssignAttr(rsc, "id", &cdnId), + }, + resource.TestStep{ + ResourceName: rsc, + ImportState: true, + ImportStateIdFunc: func(*terraform.State) (string, error) { + return cdnId, nil + }, + ImportStateVerify: true, + }, + ) +} + +func TestAccCdnDataSource_OnlyRequiredFields(t *testing.T) { + const rsc = "data.cdn77_cdn.lorem" + const nonExistingCdnId = 7495732 + client := acctest.GetClient(t) + + originRequest := cdn77.OriginCreateUrlJSONRequestBody{ + Label: "random origin", + Scheme: "https", + Host: "my-totally-random-custom-host.com", + } + originResponse, err := client.OriginCreateUrlWithResponse(context.Background(), originRequest) + acctest.AssertResponseOk(t, "Failed to create Origin: %s", originResponse, err) + + originId := originResponse.JSON201.Id + + t.Cleanup(func() { + acctest.MustDeleteOrigin(t, client, origin.TypeUrl, originId) + }) + + const cdnLabel = "some cdn" + + cdnRequest := cdn77.CdnAddJSONRequestBody{Label: cdnLabel, OriginId: originId} + cdnResponse, err := client.CdnAddWithResponse(context.Background(), cdnRequest) + acctest.AssertResponseOk(t, "Failed to create CDN: %s", cdnResponse, err) + + cdnId := cdnResponse.JSON201.Id + cdnCreationTime := cdnResponse.JSON201.CreationTime.Format(time.DateTime) + cdnUrl := cdnResponse.JSON201.Url + + t.Cleanup(func() { + acctest.MustDeleteCdn(t, client, cdnId) + }) + + attrEq := func(key, value string) resource.TestCheckFunc { + return resource.TestCheckResourceAttr(rsc, key, value) + } + + acctest.Run(t, nil, + resource.TestStep{ + Config: acctest.Config(cdnDataSourceConfig, "id", nonExistingCdnId), + ExpectError: regexp.MustCompile( + fmt.Sprintf(`CDN Resource with id "%d" could not be found`, nonExistingCdnId), + ), + }, + resource.TestStep{ + Config: acctest.Config(cdnDataSourceConfig, "id", cdnId), + Check: resource.ComposeAggregateTestCheckFunc( + attrEq("id", fmt.Sprintf("%d", cdnId)), + attrEq("label", cdnLabel), + attrEq("origin_id", originId), + attrEq("creation_time", cdnCreationTime), + attrEq("url", cdnUrl), + attrEq("cache.max_age", fmt.Sprintf("%d", cdn77.MaxAgeN17280)), + attrEq("cache.requests_with_cookies_enabled", "true"), + attrEq("cnames.#", "0"), + attrEq("geo_protection.type", string(cdn77.Disabled)), + attrEq("headers.cors_enabled", "false"), + attrEq("headers.cors_timing_enabled", "false"), + attrEq("headers.cors_wildcard_enabled", "false"), + attrEq("headers.host_header_forwarding_enabled", "false"), + attrEq("headers.content_disposition_type", string(cdn77.ContentDispositionTypeNone)), + attrEq("hotlink_protection.type", string(cdn77.Disabled)), + attrEq("hotlink_protection.empty_referer_denied", "false"), + attrEq("https_redirect.enabled", "false"), + attrEq("ip_protection.type", string(cdn77.Disabled)), + attrEq("mp4_pseudo_streaming_enabled", "false"), + attrEq("origin_headers.#", "0"), + attrEq("query_string.ignore_type", string(cdn77.QueryStringIgnoreTypeNone)), + attrEq("rate_limit_enabled", "false"), + attrEq("secure_token.type", string(cdn77.SecureTokenTypeNone)), + attrEq("ssl.type", string(cdn77.InstantSsl)), + attrEq("waf_enabled", "false"), + + resource.TestCheckNoResourceAttr(rsc, "stream"), + resource.TestCheckNoResourceAttr(rsc, "cache.max_age_404"), + resource.TestCheckNoResourceAttr(rsc, "geo_protection.countries"), + resource.TestCheckNoResourceAttr(rsc, "hotlink_protection.domains"), + resource.TestCheckNoResourceAttr(rsc, "https_redirect.code"), + resource.TestCheckNoResourceAttr(rsc, "ip_protection.ips"), + resource.TestCheckNoResourceAttr(rsc, "note"), + resource.TestCheckNoResourceAttr(rsc, "query_string.parameters"), + resource.TestCheckNoResourceAttr(rsc, "secure_token.token"), + resource.TestCheckNoResourceAttr(rsc, "ssl.ssl_id"), + ), + }, + ) +} + +func TestAccCdnDataSource_AllFields(t *testing.T) { + const rsc = "data.cdn77_cdn.lorem" + client := acctest.GetClient(t) + + originRequest := cdn77.OriginCreateUrlJSONRequestBody{ + Label: "random origin", + Scheme: "https", + Host: "my-totally-random-custom-host.com", + } + originResponse, err := client.OriginCreateUrlWithResponse(context.Background(), originRequest) + acctest.AssertResponseOk(t, "Failed to create Origin: %s", originResponse, err) + + originId := originResponse.JSON201.Id + + t.Cleanup(func() { + acctest.MustDeleteOrigin(t, client, origin.TypeUrl, originId) + }) + + cdnCnames := []string{"my.cdn.com", "another-cname.example.com"} + const cdnLabel = "some cdn" + const cdnNote = "some note" + + sslId := acctest.MustAddSslWithCleanup(t, client, testdata.SslCert1, testdata.SslKey) + + cdnAddRequest := cdn77.CdnAddJSONRequestBody{ + Label: cdnLabel, + OriginId: originId, + Cnames: util.Pointer(cdnCnames), + Note: nullable.NewNullableWithValue(cdnNote), + } + cdnAddResponse, err := client.CdnAddWithResponse(context.Background(), cdnAddRequest) + acctest.AssertResponseOk(t, "Failed to create CDN: %s", cdnAddResponse, err) + + cdnId := cdnAddResponse.JSON201.Id + cdnCreationTime := cdnAddResponse.JSON201.CreationTime.Format(time.DateTime) + cdnUrl := cdnAddResponse.JSON201.Url + + t.Cleanup(func() { + acctest.MustDeleteCdn(t, client, cdnId) + }) + + cdnEditRequest := cdn77.CdnEditJSONRequestBody{ + Cache: &cdn77.Cache{ + MaxAge: util.Pointer(cdn77.MaxAgeN60), + MaxAge404: nullable.NewNullableWithValue(cdn77.MaxAge404N5), + RequestsWithCookiesEnabled: util.Pointer(false), + }, + GeoProtection: &cdn77.GeoProtection{Countries: util.Pointer([]string{"CZ"}), Type: cdn77.Passlist}, + Headers: &cdn77.Headers{ + ContentDisposition: &cdn77.ContentDisposition{ + Type: util.Pointer(cdn77.ContentDispositionTypeParameter), + }, + CorsEnabled: util.Pointer(true), + CorsTimingEnabled: util.Pointer(true), + CorsWildcardEnabled: util.Pointer(true), + HostHeaderForwardingEnabled: util.Pointer(true), + }, + HotlinkProtection: &cdn77.HotlinkProtection{ + Domains: util.Pointer([]string{"xxx.cz"}), + EmptyRefererDenied: true, + Type: cdn77.Passlist, + }, + HttpsRedirect: &cdn77.HttpsRedirect{Code: util.Pointer(cdn77.N301), Enabled: true}, + IpProtection: &cdn77.IpProtection{ + Ips: util.Pointer([]string{"1.1.1.1/32", "8.8.8.8/32"}), + Type: cdn77.Blocklist, + }, + OriginHeaders: &cdn77.OriginHeaders{ + Custom: nullable.NewNullableWithValue(map[string]string{"abc": "v1", "def": "v2"}), + }, + QueryString: &cdn77.QueryString{ + IgnoreType: cdn77.QueryStringIgnoreTypeList, + Parameters: util.Pointer([]string{"param"}), + }, + RateLimit: &cdn77.RateLimit{Enabled: true}, + SecureToken: &cdn77.SecureToken{Token: util.Pointer("abcd1234"), Type: cdn77.SecureTokenTypePath}, + Ssl: &cdn77.CdnSsl{SslId: util.Pointer(sslId), Type: cdn77.SNI}, + Waf: &cdn77.Waf{Enabled: true}, + } + cdnEditResponse, err := client.CdnEditWithResponse(context.Background(), cdnId, cdnEditRequest) + acctest.AssertResponseOk(t, "Failed to edit CDN: %s", cdnEditResponse, err) + + attrEq := func(key, value string) resource.TestCheckFunc { + return resource.TestCheckResourceAttr(rsc, key, value) + } + + acctest.Run(t, nil, resource.TestStep{ + Config: acctest.Config(cdnDataSourceConfig, "id", cdnId), + Check: resource.ComposeAggregateTestCheckFunc( + attrEq("id", fmt.Sprintf("%d", cdnId)), + attrEq("label", cdnLabel), + attrEq("origin_id", originId), + attrEq("creation_time", cdnCreationTime), + attrEq("url", cdnUrl), + attrEq("cache.max_age", fmt.Sprintf("%d", cdn77.MaxAgeN60)), + attrEq("cache.max_age_404", fmt.Sprintf("%d", cdn77.MaxAge404N5)), + attrEq("cache.requests_with_cookies_enabled", "false"), + attrEq("cnames.#", "2"), + resource.TestCheckTypeSetElemAttr(rsc, "cnames.*", cdnCnames[0]), + resource.TestCheckTypeSetElemAttr(rsc, "cnames.*", cdnCnames[1]), + attrEq("geo_protection.countries.#", "1"), + resource.TestCheckTypeSetElemAttr(rsc, "geo_protection.countries.*", "CZ"), + attrEq("geo_protection.type", string(cdn77.Passlist)), + attrEq("headers.content_disposition_type", string(cdn77.ContentDispositionTypeParameter)), + attrEq("headers.cors_enabled", "true"), + attrEq("headers.cors_timing_enabled", "true"), + attrEq("headers.cors_wildcard_enabled", "true"), + attrEq("headers.host_header_forwarding_enabled", "true"), + attrEq("hotlink_protection.domains.#", "1"), + resource.TestCheckTypeSetElemAttr(rsc, "hotlink_protection.domains.*", "xxx.cz"), + attrEq("hotlink_protection.empty_referer_denied", "true"), + attrEq("hotlink_protection.type", string(cdn77.Passlist)), + attrEq("https_redirect.code", fmt.Sprintf("%d", cdn77.N301)), + attrEq("https_redirect.enabled", "true"), + attrEq("ip_protection.ips.#", "2"), + resource.TestCheckTypeSetElemAttr(rsc, "ip_protection.ips.*", "1.1.1.1/32"), + resource.TestCheckTypeSetElemAttr(rsc, "ip_protection.ips.*", "8.8.8.8/32"), + attrEq("ip_protection.type", string(cdn77.Blocklist)), + attrEq("mp4_pseudo_streaming_enabled", "false"), + attrEq("note", cdnNote), + attrEq("origin_headers.%", "2"), + attrEq("origin_headers.abc", "v1"), + attrEq("origin_headers.def", "v2"), + attrEq("query_string.ignore_type", string(cdn77.QueryStringIgnoreTypeList)), + attrEq("query_string.parameters.#", "1"), + resource.TestCheckTypeSetElemAttr(rsc, "query_string.parameters.*", "param"), + attrEq("rate_limit_enabled", "true"), + attrEq("secure_token.token", "abcd1234"), + attrEq("secure_token.type", string(cdn77.SecureTokenTypePath)), + attrEq("ssl.ssl_id", sslId), + attrEq("ssl.type", string(cdn77.SNI)), + attrEq("waf_enabled", "true"), + + resource.TestCheckNoResourceAttr(rsc, "stream"), + ), + }) +} + +func checkCdn( + client cdn77.ClientWithResponsesInterface, + cdnId *string, + fn func(o *cdn77.Cdn) error, +) resource.TestCheckFunc { + return func(*terraform.State) error { + cdnIdInt, err := strconv.Atoi(*cdnId) + if err != nil { + return fmt.Errorf("failed to convert CDN ID to int: %w", err) + } + + response, err := client.CdnDetailWithResponse(context.Background(), cdnIdInt) + message := fmt.Sprintf("failed to get CDN[id=%d]: %%s", cdnIdInt) + + if err = acctest.CheckResponse(message, response, err); err != nil { + return err + } + + return fn(response.JSON200) + } +} + +func checkCdnDefaults( + client cdn77.ClientWithResponsesInterface, + cdnId *string, + originId *string, + cdnLabel string, +) resource.TestCheckFunc { + return checkCdn(client, cdnId, func(c *cdn77.Cdn) error { + sort.SliceStable(c.Cnames, func(i, j int) bool { + return c.Cnames[i].Cname < c.Cnames[j].Cname + }) + + return errors.Join( + acctest.NullFieldEqual("origin_id", c.OriginId, *originId), + acctest.EqualField("label", c.Label, cdnLabel), + acctest.NullField("note", c.Note), + acctest.EqualField("len(cnames)", len(c.Cnames), 0), + acctest.EqualField("cache.max_age", *c.Cache.MaxAge, cdn77.MaxAgeN17280), + acctest.NullField("cache.max_age_404", c.Cache.MaxAge404), + acctest.EqualField( + "cache.requests_with_cookies_enabled", + *c.Cache.RequestsWithCookiesEnabled, + true, + ), + acctest.EqualField("secure_token.type", c.SecureToken.Type, cdn77.SecureTokenTypeNone), + acctest.EqualField("secure_token.token", c.SecureToken.Token, nil), + acctest.EqualField( + "query_string.ignore_type", + c.QueryString.IgnoreType, + cdn77.QueryStringIgnoreTypeNone, + ), + acctest.EqualField("query_string.parameters", c.QueryString.Parameters, nil), + acctest.EqualField("headers.cors_enabled", *c.Headers.CorsEnabled, false), + acctest.EqualField("headers.cors_timing_enabled", *c.Headers.CorsTimingEnabled, false), + acctest.EqualField("headers.cors_wildcard_enabled", *c.Headers.CorsWildcardEnabled, false), + acctest.EqualField( + "headers.host_header_forwarding_enabled", + *c.Headers.HostHeaderForwardingEnabled, + false, + ), + acctest.EqualField( + "headers.content_disposition_type", + *c.Headers.ContentDisposition.Type, + cdn77.ContentDispositionTypeNone, + ), + acctest.EqualField("https_redirect.enabled", c.HttpsRedirect.Enabled, false), + acctest.EqualField("https_redirect.code", c.HttpsRedirect.Code, nil), + + acctest.EqualField("mp4_pseudo_streaming_enabled", *c.Mp4PseudoStreaming.Enabled, false), + acctest.EqualField("waf_enabled", c.Waf.Enabled, false), + acctest.EqualField("ssl.type", c.Ssl.Type, cdn77.InstantSsl), + acctest.EqualField("ssl.ssl_id", c.Ssl.SslId, nil), + acctest.EqualField("hotlink_protection.code", c.HotlinkProtection.Type, cdn77.Disabled), + acctest.EqualField( + "hotlink_protection.empty_referer_denied", + c.HotlinkProtection.EmptyRefererDenied, + false, + ), + acctest.EqualField("hotlink_protection.domains", c.HotlinkProtection.Domains, nil), + acctest.EqualField("ip_protection.type", c.IpProtection.Type, cdn77.Disabled), + acctest.EqualField("ip_protection.ips", c.IpProtection.Ips, nil), + acctest.EqualField("geo_protection.type", c.GeoProtection.Type, cdn77.Disabled), + acctest.EqualField("geo_protection.countries", c.GeoProtection.Countries, nil), + acctest.EqualField("rate_limit_enabled", c.RateLimit.Enabled, false), + acctest.EqualField("origin_headers", c.OriginHeaders, nil), + ) + }) +} + +func checkCdnsAndOriginDestroyed(client cdn77.ClientWithResponsesInterface) resource.TestCheckFunc { + return resource.ComposeAggregateTestCheckFunc( + checkCdnsDestroyed(client), + acctest.CheckOriginDestroyed(client, origin.TypeUrl), + ) +} + +func checkCdnsDestroyed(client cdn77.ClientWithResponsesInterface) resource.TestCheckFunc { + return func(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "cdn77_cdn" { + continue + } + + cdnId, err := strconv.Atoi(rs.Primary.Attributes["id"]) + if err != nil { + return fmt.Errorf("unexpected CDN id: %s", rs.Primary.Attributes["id"]) + } + + response, err := client.CdnDetailWithResponse(context.Background(), cdnId) + if err != nil { + return fmt.Errorf("failed to fetch CDN: %w", err) + } + + if response.JSON404 == nil { + return errors.New("expected CDN to be deleted") + } + } + + return nil + } +} + +const OriginResourceConfig = ` +resource "cdn77_origin_url" "url" { + label = "origin label" + url = "http://my-totally-random-custom-host.com" +} +` + +const cdnDataSourceConfig = ` +data "cdn77_cdn" "lorem" { + id = "{id}" +} +` diff --git a/internal/provider/cdn/reader.go b/internal/provider/cdn/reader.go new file mode 100644 index 0000000..11aec21 --- /dev/null +++ b/internal/provider/cdn/reader.go @@ -0,0 +1,141 @@ +package cdn + +import ( + "context" + "time" + + "github.com/cdn77/cdn77-client-go/v2" + "github.com/cdn77/terraform-provider-cdn77/internal/util" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type Reader struct{} + +func (*Reader) ErrMessage() string { + return "Failed to fetch CDN" +} + +func (*Reader) Fetch( + ctx context.Context, + client cdn77.ClientWithResponsesInterface, + model Model, +) (*cdn77.CdnDetailResponse, *cdn77.Cdn, error) { + response, err := client.CdnDetailWithResponse(ctx, int(model.Id.ValueInt64())) + if err != nil { + return nil, nil, err + } + + return response, response.JSON200, nil +} + +func (r *Reader) Process(ctx context.Context, model Model, cdn *cdn77.Cdn, diags *diag.Diagnostics) Model { + var stream *ModelStream + if cdn.Stream != nil { + stream = &ModelStream{ + OriginUrl: types.StringValue(cdn.Stream.OriginUrl), + Password: types.StringPointerValue(cdn.Stream.Password), + QueryKey: types.StringValue(cdn.Stream.QueryKey), + Protocol: types.StringValue(cdn.Stream.Protocol), + Port: types.Int64Value(int64(cdn.Stream.Port)), + Path: types.StringPointerValue(cdn.Stream.Path), + } + } + + cnames := r.getCnamesSet(ctx, cdn, diags) + + geoProtectionCountries := types.SetNull(types.StringType) + if cdn.GeoProtection.Countries != nil { + geoProtectionCountries = util.SetValueFrom(ctx, diags, types.StringType, *cdn.GeoProtection.Countries) + } + + hotlinkProtectionDomains := types.SetNull(types.StringType) + if cdn.HotlinkProtection.Domains != nil { + hotlinkProtectionDomains = util.SetValueFrom(ctx, diags, types.StringType, *cdn.HotlinkProtection.Domains) + } + + ipProtectionIps := types.SetNull(types.StringType) + if cdn.IpProtection.Ips != nil { + ipProtectionIps = util.SetValueFrom(ctx, diags, types.StringType, *cdn.IpProtection.Ips) + } + + originHeaders := types.MapNull(types.StringType) + if cdn.OriginHeaders != nil && !cdn.OriginHeaders.Custom.IsNull() && cdn.OriginHeaders.Custom.IsSpecified() { + originHeaders = util.MapValueFrom(ctx, diags, types.StringType, cdn.OriginHeaders.Custom.MustGet()) + } + + queryStringParameters := types.SetNull(types.StringType) + if cdn.QueryString.Parameters != nil { + queryStringParameters = util.SetValueFrom(ctx, diags, types.StringType, *cdn.QueryString.Parameters) + } + + if diags.HasError() { + return model + } + + return Model{ + Id: model.Id, + Label: types.StringValue(cdn.Label), + OriginId: util.NullableToStringValue(cdn.OriginId), + CreationTime: types.StringValue(cdn.CreationTime.Format(time.DateTime)), + Url: types.StringValue(cdn.Url), + Stream: stream, + Cache: &ModelCache{ + MaxAge: util.IntPointerToInt64Value(cdn.Cache.MaxAge), + MaxAge404: util.NullableIntToInt64Value(cdn.Cache.MaxAge404), + RequestsWithCookiesEnabled: types.BoolPointerValue(cdn.Cache.RequestsWithCookiesEnabled), + }, + Cnames: cnames, + GeoProtection: &ModelGeoProtection{ + Countries: geoProtectionCountries, + Type: types.StringValue(string(cdn.GeoProtection.Type)), + }, + Headers: &ModelHeaders{ + CorsEnabled: types.BoolPointerValue(cdn.Headers.CorsEnabled), + CorsTimingEnabled: types.BoolPointerValue(cdn.Headers.CorsTimingEnabled), + CorsWildcardEnabled: types.BoolPointerValue(cdn.Headers.CorsWildcardEnabled), + HostHeaderForwardingEnabled: types.BoolPointerValue(cdn.Headers.HostHeaderForwardingEnabled), + ContentDispositionType: types.StringValue(string(*cdn.Headers.ContentDisposition.Type)), + }, + HotlinkProtection: &ModelHotlinkProtection{ + Domains: hotlinkProtectionDomains, + Type: types.StringValue(string(cdn.HotlinkProtection.Type)), + EmptyRefererDenied: types.BoolValue(cdn.HotlinkProtection.EmptyRefererDenied), + }, + HttpsRedirect: &ModelHttpsRedirect{ + Code: util.IntPointerToInt64Value(cdn.HttpsRedirect.Code), + Enabled: types.BoolValue(cdn.HttpsRedirect.Enabled), + }, + IpProtection: &ModelIpProtection{ + Ips: ipProtectionIps, + Type: types.StringValue(string(cdn.IpProtection.Type)), + }, + Mp4PseudoStreamingEnabled: types.BoolPointerValue(cdn.Mp4PseudoStreaming.Enabled), + Note: util.NullableToStringValue(cdn.Note), + OriginHeaders: originHeaders, + QueryString: &ModelQueryString{ + Parameters: queryStringParameters, + IgnoreType: types.StringValue(string(cdn.QueryString.IgnoreType)), + }, + RateLimitEnabled: types.BoolValue(cdn.RateLimit.Enabled), + SecureToken: &ModelSecureToken{ + Token: types.StringPointerValue(cdn.SecureToken.Token), + Type: types.StringValue(string(cdn.SecureToken.Type)), + }, + Ssl: &ModelSsl{ + Type: types.StringValue(string(cdn.Ssl.Type)), + SslId: types.StringPointerValue(cdn.Ssl.SslId), + }, + WafEnabled: types.BoolValue(cdn.Waf.Enabled), + } +} + +func (*Reader) getCnamesSet(ctx context.Context, cdn *cdn77.Cdn, diags *diag.Diagnostics) types.Set { + cnames := make([]string, len(cdn.Cnames)) + + for i, c := range cdn.Cnames { + cnames[i] = c.Cname + } + + return util.SetValueFrom(ctx, diags, types.StringType, cnames) +} diff --git a/internal/provider/cdn_resource_schema.go b/internal/provider/cdn/schema.go similarity index 84% rename from internal/provider/cdn_resource_schema.go rename to internal/provider/cdn/schema.go index 8b25b88..df77c29 100644 --- a/internal/provider/cdn_resource_schema.go +++ b/internal/provider/cdn/schema.go @@ -1,14 +1,13 @@ -package provider +package cdn import ( - "github.com/cdn77/cdn77-client-go" + "github.com/cdn77/cdn77-client-go/v2" "github.com/cdn77/terraform-provider-cdn77/internal/util" "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "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/boolplanmodifier" "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/objectdefault" @@ -21,7 +20,90 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) -func CreateCdnResourceSchema() schema.Schema { +type Model struct { + Id types.Int64 `tfsdk:"id"` + Label types.String `tfsdk:"label"` + OriginId types.String `tfsdk:"origin_id"` + CreationTime types.String `tfsdk:"creation_time"` + Url types.String `tfsdk:"url"` + Stream *ModelStream `tfsdk:"stream"` + Cache *ModelCache `tfsdk:"cache"` + Cnames types.Set `tfsdk:"cnames"` + GeoProtection *ModelGeoProtection `tfsdk:"geo_protection"` + Headers *ModelHeaders `tfsdk:"headers"` + HotlinkProtection *ModelHotlinkProtection `tfsdk:"hotlink_protection"` + HttpsRedirect *ModelHttpsRedirect `tfsdk:"https_redirect"` + IpProtection *ModelIpProtection `tfsdk:"ip_protection"` + Mp4PseudoStreamingEnabled types.Bool `tfsdk:"mp4_pseudo_streaming_enabled"` + Note types.String `tfsdk:"note"` + OriginHeaders types.Map `tfsdk:"origin_headers"` + QueryString *ModelQueryString `tfsdk:"query_string"` + RateLimitEnabled types.Bool `tfsdk:"rate_limit_enabled"` + SecureToken *ModelSecureToken `tfsdk:"secure_token"` + Ssl *ModelSsl `tfsdk:"ssl"` + WafEnabled types.Bool `tfsdk:"waf_enabled"` +} + +type ModelStream struct { + OriginUrl types.String `tfsdk:"origin_url"` + Password types.String `tfsdk:"password"` + QueryKey types.String `tfsdk:"query_key"` + Protocol types.String `tfsdk:"protocol"` + Port types.Int64 `tfsdk:"port"` + Path types.String `tfsdk:"path"` +} + +type ModelCache struct { + MaxAge types.Int64 `tfsdk:"max_age"` + MaxAge404 types.Int64 `tfsdk:"max_age_404"` + RequestsWithCookiesEnabled types.Bool `tfsdk:"requests_with_cookies_enabled"` +} + +type ModelGeoProtection struct { + Countries types.Set `tfsdk:"countries"` + Type types.String `tfsdk:"type"` +} + +type ModelHeaders struct { + CorsEnabled types.Bool `tfsdk:"cors_enabled"` + CorsTimingEnabled types.Bool `tfsdk:"cors_timing_enabled"` + CorsWildcardEnabled types.Bool `tfsdk:"cors_wildcard_enabled"` + HostHeaderForwardingEnabled types.Bool `tfsdk:"host_header_forwarding_enabled"` + ContentDispositionType types.String `tfsdk:"content_disposition_type"` +} + +type ModelHotlinkProtection struct { + Domains types.Set `tfsdk:"domains"` + Type types.String `tfsdk:"type"` + EmptyRefererDenied types.Bool `tfsdk:"empty_referer_denied"` +} + +type ModelHttpsRedirect struct { + Code types.Int64 `tfsdk:"code"` + Enabled types.Bool `tfsdk:"enabled"` +} + +type ModelIpProtection struct { + Ips types.Set `tfsdk:"ips"` + Type types.String `tfsdk:"type"` +} + +type ModelQueryString struct { + Parameters types.Set `tfsdk:"parameters"` + IgnoreType types.String `tfsdk:"ignore_type"` +} + +type ModelSecureToken struct { + Token types.String `tfsdk:"token"` + Type types.String `tfsdk:"type"` +} + +type ModelSsl struct { + Type types.String `tfsdk:"type"` + SslId types.String `tfsdk:"ssl_id"` +} + +func CreateResourceSchema() schema.Schema { return schema.Schema{ Description: "CDN resource allows you to manage your CDNs", Attributes: map[string]schema.Attribute{ @@ -30,38 +112,18 @@ func CreateCdnResourceSchema() schema.Schema { Description: "ID of the CDN. This is also used as the CDN URL", PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()}, }, - "cnames": schema.SetAttribute{ - ElementType: types.StringType, - Optional: true, - Computed: true, - Description: "CNAME assigned to CDN. " + - "CNAME should be mapped via DNS to CDN URL. " + - "Otherwise it's not possible to generate an SSL certificate for any related CNAME.", - Default: setdefault.StaticValue(types.SetValueMust(types.StringType, nil)), - }, - "creation_time": schema.StringAttribute{ - Computed: true, - Description: "Timestamp when CDN was created", - PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, - }, "label": schema.StringAttribute{ Description: "The label helps you to identify your CDN", Required: true, }, - "note": schema.StringAttribute{ - Description: "Optional note", - Optional: true, - }, "origin_id": schema.StringAttribute{ Description: "ID (UUID) of attached Origin (content source for CDN)", Required: true, }, - "origin_protection_enabled": schema.BoolAttribute{ - Computed: true, - Description: "Enabled origin protection can ease the load on your server or even hide it " + - "from direct incoming traffic with our proxy servers.", - PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, - Default: booldefault.StaticBool(false), + "creation_time": schema.StringAttribute{ + Computed: true, + Description: "Timestamp when CDN was created", + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, }, "url": schema.StringAttribute{ Computed: true, @@ -69,6 +131,31 @@ func CreateCdnResourceSchema() schema.Schema { "The number is the same as the CDN ID.", PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, }, + "stream": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "origin_url": schema.StringAttribute{ + Optional: true, + }, + "password": schema.StringAttribute{ + Optional: true, + Sensitive: true, + }, + "query_key": schema.StringAttribute{ + Optional: true, + }, + "protocol": schema.StringAttribute{ + Optional: true, + }, + "port": schema.Int64Attribute{ + Optional: true, + }, + "path": schema.StringAttribute{ + Optional: true, + }, + }, + Optional: true, + Description: "Detail parameters of stream CDN", + }, "cache": schema.SingleNestedAttribute{ Attributes: map[string]schema.Attribute{ "max_age": schema.Int64Attribute{ @@ -124,76 +211,48 @@ func CreateCdnResourceSchema() schema.Schema { }, )), }, - "secure_token": schema.SingleNestedAttribute{ - Attributes: map[string]schema.Attribute{ - "token": schema.StringAttribute{ - Optional: true, - Sensitive: true, - Description: "Token length is between 8 and 64 characters.", - Validators: []validator.String{stringvalidator.LengthBetween(8, 64)}, - }, - "type": schema.StringAttribute{ - Optional: true, - Computed: true, - MarkdownDescription: `
    -
  1. parameter - Token will be in the query string - e.g.: /video.mp4?secure=MY_SECURE_TOKEN.
  2. -
  3. path - Token will be in the path - e.g.: /MY_SECURE_TOKEN/video.mp4.
  4. -
  5. none - Use to disable secure token.
  6. -
  7. highwinds
  8. -
`, - Validators: []validator.String{stringvalidator.OneOf( - string(cdn77.SecureTokenTypeHighwinds), - string(cdn77.SecureTokenTypeNone), - string(cdn77.SecureTokenTypeParameter), - string(cdn77.SecureTokenTypePath), - )}, - Default: stringdefault.StaticString(string(cdn77.SecureTokenTypeNone)), - }, - }, - Optional: true, - Computed: true, - Description: "This feature allows you to serve your content using signed URLs. " + - "You can enable your users to download secured content from the CDN with a valid hash. " + - "Note: When you check this option, make sure to generate secured links to access your content.", - Default: objectdefault.StaticValue(types.ObjectValueMust( - map[string]attr.Type{"token": basetypes.StringType{}, "type": basetypes.StringType{}}, - map[string]attr.Value{ - "token": types.StringNull(), - "type": types.StringValue(string(cdn77.SecureTokenTypeNone)), - }, - )), + "cnames": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + Computed: true, + Description: "CNAME assigned to CDN. " + + "CNAME should be mapped via DNS to CDN URL. " + + "Otherwise it's not possible to generate an SSL certificate for any related CNAME.", + Default: setdefault.StaticValue(types.SetValueMust(types.StringType, nil)), }, - "query_string": schema.SingleNestedAttribute{ + "geo_protection": schema.SingleNestedAttribute{ Attributes: map[string]schema.Attribute{ - "parameters": schema.SetAttribute{ + "countries": schema.SetAttribute{ ElementType: types.StringType, Optional: true, - Description: `List of parameters used when "ignore_type" is set to "list"`, + Description: "We are using ISO 3166-1 alpha-2 code", }, - "ignore_type": schema.StringAttribute{ + "type": schema.StringAttribute{ Optional: true, Computed: true, + Description: `With "type": "blocklist" all countries set in the "countries" ` + + `parameter are not allowed. With "type": "passlist" only countries set in the ` + + `"countries" parameter are allowed.`, Validators: []validator.String{stringvalidator.OneOf( - string(cdn77.QueryStringIgnoreTypeAll), - string(cdn77.QueryStringIgnoreTypeList), - string(cdn77.QueryStringIgnoreTypeNone), + string(cdn77.Blocklist), + string(cdn77.Disabled), + string(cdn77.Passlist), )}, - Default: stringdefault.StaticString(string(cdn77.QueryStringIgnoreTypeNone)), + Default: stringdefault.StaticString(string(cdn77.Disabled)), }, }, Optional: true, Computed: true, - Description: "Enabling this feature will ignore the query string, allowing URLs with " + - "query strings to cache properly. This is particularly useful if you tag your URLs with " + - "tracking/marketing parameters, for example.", + Description: "Geo protection enables you to control which countries can access your " + + "content directly", Default: objectdefault.StaticValue(types.ObjectValueMust( map[string]attr.Type{ - "parameters": basetypes.SetType{ElemType: basetypes.StringType{}}, - "ignore_type": basetypes.StringType{}, + "countries": basetypes.SetType{ElemType: basetypes.StringType{}}, + "type": basetypes.StringType{}, }, map[string]attr.Value{ - "parameters": types.SetNull(basetypes.StringType{}), - "ignore_type": types.StringValue(string(cdn77.QueryStringIgnoreTypeNone)), + "countries": types.SetNull(basetypes.StringType{}), + "type": types.StringValue(string(cdn77.Disabled)), }, )), }, @@ -263,108 +322,6 @@ func CreateCdnResourceSchema() schema.Schema { }, )), }, - "https_redirect": schema.SingleNestedAttribute{ - Attributes: map[string]schema.Attribute{ - "code": schema.Int64Attribute{ - Optional: true, - Description: "301 for permanent redirect and 302 for temporary redirect. " + - "If you are not sure, select the default 301 code.", - Validators: []validator.Int64{int64validator.OneOf(301, 302)}, - }, - "enabled": schema.BoolAttribute{ - Optional: true, - Computed: true, - Default: booldefault.StaticBool(false), - }, - }, - Optional: true, - Computed: true, - Description: "If enabled, all requests via HTTP are redirected to HTTPS. " + - "Verify HTTPS availability of CNAMEs before activating, if applicable.", - Default: objectdefault.StaticValue(types.ObjectValueMust( - map[string]attr.Type{ - "code": basetypes.Int64Type{}, - "enabled": basetypes.BoolType{}, - }, - map[string]attr.Value{ - "code": types.Int64Null(), - "enabled": types.BoolValue(false), - }, - )), - }, - "mp4_pseudo_streaming_enabled": schema.BoolAttribute{ - Optional: true, - Computed: true, - Description: "Turn this option on if using a flash-based video player with MP4 files. " + - "Pseudo-streaming is used mainly in flash players. HTML5 players use range-requests. " + - `When enabled the "query_string" option must be set to ignore all parameters.`, - Default: booldefault.StaticBool(false), - }, - "waf_enabled": schema.BoolAttribute{ - Optional: true, - Computed: true, - Description: `Protect your website against XSS, SQL injection and more with our SmartWAF. ` + - `We're using OWASP Core Rule Set (CRS) to protect your data against the most exploited ` + - `vulnerabilities.`, - Default: booldefault.StaticBool(false), - }, - "ssl": schema.SingleNestedAttribute{ - Attributes: map[string]schema.Attribute{ - "type": schema.StringAttribute{ - Optional: true, - Computed: true, - Description: "Available values: instantSsl, none, SNI", - Validators: []validator.String{stringvalidator.OneOf( - string(cdn77.InstantSsl), - string(cdn77.None), - string(cdn77.SNI), - string(cdn77.SAN), - )}, - Default: stringdefault.StaticString(string(cdn77.InstantSsl)), - }, - "ssl_id": schema.StringAttribute{ - Optional: true, - Description: "ID (UUID) of the SSL certificate", - }, - }, - Optional: true, - Computed: true, - Default: objectdefault.StaticValue(types.ObjectValueMust( - map[string]attr.Type{ - "type": basetypes.StringType{}, - "ssl_id": basetypes.StringType{}, - }, - map[string]attr.Value{ - "type": types.StringValue(string(cdn77.InstantSsl)), - "ssl_id": types.StringNull(), - }, - )), - }, - "stream": schema.SingleNestedAttribute{ - Attributes: map[string]schema.Attribute{ - "origin_url": schema.StringAttribute{ - Optional: true, - }, - "password": schema.StringAttribute{ - Optional: true, - Sensitive: true, - }, - "query_key": schema.StringAttribute{ - Optional: true, - }, - "protocol": schema.StringAttribute{ - Optional: true, - }, - "port": schema.Int64Attribute{ - Optional: true, - }, - "path": schema.StringAttribute{ - Optional: true, - }, - }, - Optional: true, - Description: "Detail parameters of stream CDN", - }, "hotlink_protection": schema.SingleNestedAttribute{ Attributes: map[string]schema.Attribute{ "domains": schema.SetAttribute{ @@ -408,6 +365,35 @@ func CreateCdnResourceSchema() schema.Schema { }, )), }, + "https_redirect": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "code": schema.Int64Attribute{ + Optional: true, + Description: "301 for permanent redirect and 302 for temporary redirect. " + + "If you are not sure, select the default 301 code.", + Validators: []validator.Int64{int64validator.OneOf(301, 302)}, + }, + "enabled": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + }, + Optional: true, + Computed: true, + Description: "If enabled, all requests via HTTP are redirected to HTTPS. " + + "Verify HTTPS availability of CNAMEs before activating, if applicable.", + Default: objectdefault.StaticValue(types.ObjectValueMust( + map[string]attr.Type{ + "code": basetypes.Int64Type{}, + "enabled": basetypes.BoolType{}, + }, + map[string]attr.Value{ + "code": types.Int64Null(), + "enabled": types.BoolValue(false), + }, + )), + }, "ip_protection": schema.SingleNestedAttribute{ Attributes: map[string]schema.Attribute{ "ips": schema.SetAttribute{ @@ -442,39 +428,54 @@ func CreateCdnResourceSchema() schema.Schema { }, )), }, - "geo_protection": schema.SingleNestedAttribute{ + "mp4_pseudo_streaming_enabled": schema.BoolAttribute{ + Optional: true, + Computed: true, + Description: "Turn this option on if using a flash-based video player with MP4 files. " + + "Pseudo-streaming is used mainly in flash players. HTML5 players use range-requests. " + + `When enabled the "query_string" option must be set to ignore all parameters.`, + Default: booldefault.StaticBool(false), + }, + "note": schema.StringAttribute{ + Description: "Optional note", + Optional: true, + }, + "origin_headers": schema.MapAttribute{ + ElementType: types.StringType, + Optional: true, + Description: "Custom HTTP headers included in requests sent to the origin server", + }, + "query_string": schema.SingleNestedAttribute{ Attributes: map[string]schema.Attribute{ - "countries": schema.SetAttribute{ + "parameters": schema.SetAttribute{ ElementType: types.StringType, Optional: true, - Description: "We are using ISO 3166-1 alpha-2 code", + Description: `List of parameters used when "ignore_type" is set to "list"`, }, - "type": schema.StringAttribute{ + "ignore_type": schema.StringAttribute{ Optional: true, Computed: true, - Description: `With "type": "blocklist" all countries set in the "countries" ` + - `parameter are not allowed. With "type": "passlist" only countries set in the ` + - `"countries" parameter are allowed.`, Validators: []validator.String{stringvalidator.OneOf( - string(cdn77.Blocklist), - string(cdn77.Disabled), - string(cdn77.Passlist), + string(cdn77.QueryStringIgnoreTypeAll), + string(cdn77.QueryStringIgnoreTypeList), + string(cdn77.QueryStringIgnoreTypeNone), )}, - Default: stringdefault.StaticString(string(cdn77.Disabled)), + Default: stringdefault.StaticString(string(cdn77.QueryStringIgnoreTypeNone)), }, }, Optional: true, Computed: true, - Description: "Geo protection enables you to control which countries can access your " + - "content directly", + Description: "Enabling this feature will ignore the query string, allowing URLs with " + + "query strings to cache properly. This is particularly useful if you tag your URLs with " + + "tracking/marketing parameters, for example.", Default: objectdefault.StaticValue(types.ObjectValueMust( map[string]attr.Type{ - "countries": basetypes.SetType{ElemType: basetypes.StringType{}}, - "type": basetypes.StringType{}, + "parameters": basetypes.SetType{ElemType: basetypes.StringType{}}, + "ignore_type": basetypes.StringType{}, }, map[string]attr.Value{ - "countries": types.SetNull(basetypes.StringType{}), - "type": types.StringValue(string(cdn77.Disabled)), + "parameters": types.SetNull(basetypes.StringType{}), + "ignore_type": types.StringValue(string(cdn77.QueryStringIgnoreTypeNone)), }, )), }, @@ -486,10 +487,84 @@ func CreateCdnResourceSchema() schema.Schema { `URL parameter.`, Default: booldefault.StaticBool(false), }, - "origin_headers": schema.MapAttribute{ - ElementType: types.StringType, - Optional: true, - Description: "Custom HTTP headers included in requests sent to the origin server", + "secure_token": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "token": schema.StringAttribute{ + Optional: true, + Sensitive: true, + Description: "Token length is between 8 and 64 characters.", + Validators: []validator.String{stringvalidator.LengthBetween(8, 64)}, + }, + "type": schema.StringAttribute{ + Optional: true, + Computed: true, + MarkdownDescription: `
    +
  • parameter - Token will be in the query string - e.g.: /video.mp4?secure=MY_SECURE_TOKEN.
  • +
  • path - Token will be in the path - e.g.: /MY_SECURE_TOKEN/video.mp4.
  • +
  • none - Use to disable secure token.
  • +
  • highwinds
  • +
`, + Validators: []validator.String{stringvalidator.OneOf( + string(cdn77.SecureTokenTypeHighwinds), + string(cdn77.SecureTokenTypeNone), + string(cdn77.SecureTokenTypeParameter), + string(cdn77.SecureTokenTypePath), + )}, + Default: stringdefault.StaticString(string(cdn77.SecureTokenTypeNone)), + }, + }, + Optional: true, + Computed: true, + Description: "This feature allows you to serve your content using signed URLs. " + + "You can enable your users to download secured content from the CDN with a valid hash. " + + "Note: When you check this option, make sure to generate secured links to access your content.", + Default: objectdefault.StaticValue(types.ObjectValueMust( + map[string]attr.Type{"token": basetypes.StringType{}, "type": basetypes.StringType{}}, + map[string]attr.Value{ + "token": types.StringNull(), + "type": types.StringValue(string(cdn77.SecureTokenTypeNone)), + }, + )), + }, + "ssl": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "Possible values: instantSsl, none, SNI, SAN", + Validators: []validator.String{stringvalidator.OneOf( + string(cdn77.InstantSsl), + string(cdn77.None), + string(cdn77.SNI), + string(cdn77.SAN), + )}, + Default: stringdefault.StaticString(string(cdn77.InstantSsl)), + }, + "ssl_id": schema.StringAttribute{ + Optional: true, + Description: "ID (UUID) of the SSL certificate", + }, + }, + Optional: true, + Computed: true, + Default: objectdefault.StaticValue(types.ObjectValueMust( + map[string]attr.Type{ + "type": basetypes.StringType{}, + "ssl_id": basetypes.StringType{}, + }, + map[string]attr.Value{ + "type": types.StringValue(string(cdn77.InstantSsl)), + "ssl_id": types.StringNull(), + }, + )), + }, + "waf_enabled": schema.BoolAttribute{ + Optional: true, + Computed: true, + Description: `Protect your website against XSS, SQL injection and more with our SmartWAF. ` + + `We're using OWASP Core Rule Set (CRS) to protect your data against the most exploited ` + + `vulnerabilities.`, + Default: booldefault.StaticBool(false), }, }, } diff --git a/internal/provider/cdn_switchable_attrs_config_validator.go b/internal/provider/cdn/switchable_attrs_config_validator.go similarity index 84% rename from internal/provider/cdn_switchable_attrs_config_validator.go rename to internal/provider/cdn/switchable_attrs_config_validator.go index 7474358..787f9ed 100644 --- a/internal/provider/cdn_switchable_attrs_config_validator.go +++ b/internal/provider/cdn/switchable_attrs_config_validator.go @@ -1,11 +1,11 @@ -package provider +package cdn import ( "context" "fmt" "slices" - "github.com/cdn77/cdn77-client-go" + "github.com/cdn77/cdn77-client-go/v2" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -15,8 +15,8 @@ import ( ) var ( - _ datasource.ConfigValidator = &CdnSwitchableAttrsConfigValidator{} - _ resource.ConfigValidator = &CdnSwitchableAttrsConfigValidator{} + _ datasource.ConfigValidator = &SwitchableAttrsConfigValidator{} + _ resource.ConfigValidator = &SwitchableAttrsConfigValidator{} ) type cdnSwitchableAttribute struct { @@ -28,21 +28,21 @@ type cdnSwitchableAttribute struct { controlledValueIsNull bool } -type CdnSwitchableAttrsConfigValidator struct{} +type SwitchableAttrsConfigValidator struct{} -func NewCdnNullableListsConfigValidator() *CdnSwitchableAttrsConfigValidator { - return &CdnSwitchableAttrsConfigValidator{} +func NewNullableListsConfigValidator() *SwitchableAttrsConfigValidator { + return &SwitchableAttrsConfigValidator{} } -func (v CdnSwitchableAttrsConfigValidator) Description(ctx context.Context) string { +func (v SwitchableAttrsConfigValidator) Description(ctx context.Context) string { return v.MarkdownDescription(ctx) } -func (CdnSwitchableAttrsConfigValidator) MarkdownDescription(_ context.Context) string { +func (SwitchableAttrsConfigValidator) MarkdownDescription(_ context.Context) string { return "Checks that nested attributes with switchable attribute have the controlled attributes set to null" } -func (v CdnSwitchableAttrsConfigValidator) ValidateDataSource( +func (v SwitchableAttrsConfigValidator) ValidateDataSource( ctx context.Context, req datasource.ValidateConfigRequest, resp *datasource.ValidateConfigResponse, @@ -50,7 +50,7 @@ func (v CdnSwitchableAttrsConfigValidator) ValidateDataSource( resp.Diagnostics = v.Validate(ctx, req.Config) } -func (v CdnSwitchableAttrsConfigValidator) ValidateResource( +func (v SwitchableAttrsConfigValidator) ValidateResource( ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse, @@ -58,9 +58,12 @@ func (v CdnSwitchableAttrsConfigValidator) ValidateResource( resp.Diagnostics = v.Validate(ctx, req.Config) } -func (v CdnSwitchableAttrsConfigValidator) Validate(ctx context.Context, config tfsdk.Config) diag.Diagnostics { - var data CdnModel - diags := config.Get(ctx, &data) +func (v SwitchableAttrsConfigValidator) Validate(ctx context.Context, config tfsdk.Config) (diags diag.Diagnostics) { + var data Model + + if diags.Append(config.Get(ctx, &data)...); diags.HasError() { + return diags + } for _, switchableAttribute := range v.getSwitchableAttributes(data) { if !slices.Contains(switchableAttribute.switchDisabledValues, switchableAttribute.switchValue) { @@ -100,31 +103,28 @@ func (v CdnSwitchableAttrsConfigValidator) Validate(ctx context.Context, config return diags } -func (CdnSwitchableAttrsConfigValidator) getSwitchableAttributes(data CdnModel) []cdnSwitchableAttribute { +func (SwitchableAttrsConfigValidator) getSwitchableAttributes(data Model) []cdnSwitchableAttribute { var switchableAttributes []cdnSwitchableAttribute - if data.SecureToken != nil { + if data.GeoProtection != nil { switchableAttributes = append(switchableAttributes, cdnSwitchableAttribute{ - attr: "secure_token", + attr: "geo_protection", switchAttr: "type", - switchValue: data.SecureToken.Type.ValueString(), - switchDisabledValues: []any{string(cdn77.SecureTokenTypeNone)}, - controlledAttr: "token", - controlledValueIsNull: data.SecureToken.Token.IsNull(), + switchValue: data.GeoProtection.Type.ValueString(), + switchDisabledValues: []any{string(cdn77.Disabled)}, + controlledAttr: "countries", + controlledValueIsNull: data.GeoProtection.Countries.IsNull(), }) } - if data.QueryString != nil { + if data.HotlinkProtection != nil { switchableAttributes = append(switchableAttributes, cdnSwitchableAttribute{ - attr: "query_string", - switchAttr: "ignore_type", - switchValue: data.QueryString.IgnoreType.ValueString(), - switchDisabledValues: []any{ - string(cdn77.QueryStringIgnoreTypeNone), - string(cdn77.QueryStringIgnoreTypeAll), - }, - controlledAttr: "parameters", - controlledValueIsNull: data.QueryString.Parameters.IsNull(), + attr: "hotlink_protection", + switchAttr: "type", + switchValue: data.HotlinkProtection.Type.ValueString(), + switchDisabledValues: []any{string(cdn77.Disabled)}, + controlledAttr: "domains", + controlledValueIsNull: data.HotlinkProtection.Domains.IsNull(), }) } @@ -139,47 +139,50 @@ func (CdnSwitchableAttrsConfigValidator) getSwitchableAttributes(data CdnModel) }) } - if data.Ssl != nil { + if data.IpProtection != nil { switchableAttributes = append(switchableAttributes, cdnSwitchableAttribute{ - attr: "ssl", + attr: "ip_protection", switchAttr: "type", - switchValue: data.Ssl.Type.ValueString(), - switchDisabledValues: []any{string(cdn77.InstantSsl), string(cdn77.None)}, - controlledAttr: "ssl_id", - controlledValueIsNull: data.Ssl.SslId.IsNull(), + switchValue: data.IpProtection.Type.ValueString(), + switchDisabledValues: []any{string(cdn77.Disabled)}, + controlledAttr: "ips", + controlledValueIsNull: data.IpProtection.Ips.IsNull(), }) } - if data.HotlinkProtection != nil { + if data.QueryString != nil { switchableAttributes = append(switchableAttributes, cdnSwitchableAttribute{ - attr: "hotlink_protection", - switchAttr: "type", - switchValue: data.HotlinkProtection.Type.ValueString(), - switchDisabledValues: []any{string(cdn77.Disabled)}, - controlledAttr: "domains", - controlledValueIsNull: data.HotlinkProtection.Domains.IsNull(), + attr: "query_string", + switchAttr: "ignore_type", + switchValue: data.QueryString.IgnoreType.ValueString(), + switchDisabledValues: []any{ + string(cdn77.QueryStringIgnoreTypeNone), + string(cdn77.QueryStringIgnoreTypeAll), + }, + controlledAttr: "parameters", + controlledValueIsNull: data.QueryString.Parameters.IsNull(), }) } - if data.IpProtection != nil { + if data.SecureToken != nil { switchableAttributes = append(switchableAttributes, cdnSwitchableAttribute{ - attr: "ip_protection", + attr: "secure_token", switchAttr: "type", - switchValue: data.IpProtection.Type.ValueString(), - switchDisabledValues: []any{string(cdn77.Disabled)}, - controlledAttr: "ips", - controlledValueIsNull: data.IpProtection.Ips.IsNull(), + switchValue: data.SecureToken.Type.ValueString(), + switchDisabledValues: []any{string(cdn77.SecureTokenTypeNone)}, + controlledAttr: "token", + controlledValueIsNull: data.SecureToken.Token.IsNull(), }) } - if data.GeoProtection != nil { + if data.Ssl != nil { switchableAttributes = append(switchableAttributes, cdnSwitchableAttribute{ - attr: "geo_protection", + attr: "ssl", switchAttr: "type", - switchValue: data.GeoProtection.Type.ValueString(), - switchDisabledValues: []any{string(cdn77.Disabled)}, - controlledAttr: "countries", - controlledValueIsNull: data.GeoProtection.Countries.IsNull(), + switchValue: data.Ssl.Type.ValueString(), + switchDisabledValues: []any{string(cdn77.InstantSsl), string(cdn77.None)}, + controlledAttr: "ssl_id", + controlledValueIsNull: data.Ssl.SslId.IsNull(), }) } diff --git a/internal/provider/cdn_data_reader.go b/internal/provider/cdn_data_reader.go deleted file mode 100644 index c6ea04e..0000000 --- a/internal/provider/cdn_data_reader.go +++ /dev/null @@ -1,271 +0,0 @@ -package provider - -import ( - "context" - "time" - - "github.com/cdn77/cdn77-client-go" - "github.com/cdn77/terraform-provider-cdn77/internal/util" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-framework/types" -) - -type CdnModel struct { - Id types.Int64 `tfsdk:"id"` - Cnames types.Set `tfsdk:"cnames"` - CreationTime types.String `tfsdk:"creation_time"` - Label types.String `tfsdk:"label"` - Note types.String `tfsdk:"note"` - OriginId types.String `tfsdk:"origin_id"` - OriginProtectionEnabled types.Bool `tfsdk:"origin_protection_enabled"` - Url types.String `tfsdk:"url"` - Cache *CdnModelCache `tfsdk:"cache"` - SecureToken *CdnModelSecureToken `tfsdk:"secure_token"` - QueryString *CdnModelQueryString `tfsdk:"query_string"` - Headers *CdnModelHeaders `tfsdk:"headers"` - HttpsRedirect *CdnModelHttpsRedirect `tfsdk:"https_redirect"` - Mp4PseudoStreamingEnabled types.Bool `tfsdk:"mp4_pseudo_streaming_enabled"` - WafEnabled types.Bool `tfsdk:"waf_enabled"` - Ssl *CdnModelSsl `tfsdk:"ssl"` - Stream *CdnModelStream `tfsdk:"stream"` - HotlinkProtection *CdnModelHotlinkProtection `tfsdk:"hotlink_protection"` - IpProtection *CdnModelIpProtection `tfsdk:"ip_protection"` - GeoProtection *CdnModelGeoProtection `tfsdk:"geo_protection"` - RateLimitEnabled types.Bool `tfsdk:"rate_limit_enabled"` - OriginHeaders types.Map `tfsdk:"origin_headers"` -} - -type CdnModelCache struct { - MaxAge types.Int64 `tfsdk:"max_age"` - MaxAge404 types.Int64 `tfsdk:"max_age_404"` - RequestsWithCookiesEnabled types.Bool `tfsdk:"requests_with_cookies_enabled"` -} - -type CdnModelSecureToken struct { - Token types.String `tfsdk:"token"` - Type types.String `tfsdk:"type"` -} - -type CdnModelQueryString struct { - Parameters types.Set `tfsdk:"parameters"` - IgnoreType types.String `tfsdk:"ignore_type"` -} - -type CdnModelHeaders struct { - CorsEnabled types.Bool `tfsdk:"cors_enabled"` - CorsTimingEnabled types.Bool `tfsdk:"cors_timing_enabled"` - CorsWildcardEnabled types.Bool `tfsdk:"cors_wildcard_enabled"` - HostHeaderForwardingEnabled types.Bool `tfsdk:"host_header_forwarding_enabled"` - ContentDispositionType types.String `tfsdk:"content_disposition_type"` -} - -type CdnModelHttpsRedirect struct { - Code types.Int64 `tfsdk:"code"` - Enabled types.Bool `tfsdk:"enabled"` -} - -type CdnModelSsl struct { - Type types.String `tfsdk:"type"` - SslId types.String `tfsdk:"ssl_id"` -} - -type CdnModelStream struct { - OriginUrl types.String `tfsdk:"origin_url"` - Password types.String `tfsdk:"password"` - QueryKey types.String `tfsdk:"query_key"` - Protocol types.String `tfsdk:"protocol"` - Port types.Int64 `tfsdk:"port"` - Path types.String `tfsdk:"path"` -} - -type CdnModelHotlinkProtection struct { - Domains types.Set `tfsdk:"domains"` - Type types.String `tfsdk:"type"` - EmptyRefererDenied types.Bool `tfsdk:"empty_referer_denied"` -} - -type CdnModelIpProtection struct { - Ips types.Set `tfsdk:"ips"` - Type types.String `tfsdk:"type"` -} - -type CdnModelGeoProtection struct { - Countries types.Set `tfsdk:"countries"` - Type types.String `tfsdk:"type"` -} - -type CdnDataReader struct { - ctx context.Context - client cdn77.ClientWithResponsesInterface - removeMissingResource bool -} - -func NewCdnDataSourceReader(ctx context.Context, client cdn77.ClientWithResponsesInterface) *CdnDataReader { - return &CdnDataReader{ctx: ctx, client: client, removeMissingResource: false} -} - -func NewCdnResourceReader(ctx context.Context, client cdn77.ClientWithResponsesInterface) *CdnDataReader { - return &CdnDataReader{ctx: ctx, client: client, removeMissingResource: true} -} - -func (d *CdnDataReader) Read(provider StateProvider, diags *diag.Diagnostics, state *tfsdk.State) { //nolint:cyclop - var data CdnModel - if diags.Append(provider.Get(d.ctx, &data)...); diags.HasError() { - return - } - - const errMessage = "Failed to fetch CDN" - - response, err := d.client.CdnDetailWithResponse(d.ctx, int(data.Id.ValueInt64())) - if err != nil { - diags.AddError(errMessage, err.Error()) - - return - } - - if d.removeMissingResource && - maybeRemoveMissingResource(d.ctx, response.StatusCode(), data.Id.ValueInt64(), state) { - return - } - - if !util.CheckResponse(diags, errMessage, response, response.JSON404, response.JSONDefault) { - return - } - - cdn := *response.JSON200 - - cnamesRaw := make([]string, len(cdn.Cnames)) - - for i, c := range cdn.Cnames { - cnamesRaw[i] = c.Cname - } - - cnames, ds := types.SetValueFrom(d.ctx, types.StringType, cnamesRaw) - if ds != nil { - diags.Append(ds...) - - return - } - - queryStringParameters := types.SetNull(types.StringType) - if cdn.QueryString.Parameters != nil { - queryStringParameters, ds = types.SetValueFrom(d.ctx, types.StringType, *cdn.QueryString.Parameters) - if ds != nil { - diags.Append(ds...) - - return - } - } - - var stream *CdnModelStream - if cdn.Stream != nil { - stream = &CdnModelStream{ - OriginUrl: types.StringValue(cdn.Stream.OriginUrl), - Password: types.StringPointerValue(cdn.Stream.Password), - QueryKey: types.StringValue(cdn.Stream.QueryKey), - Protocol: types.StringValue(cdn.Stream.Protocol), - Port: types.Int64Value(int64(cdn.Stream.Port)), - Path: types.StringPointerValue(cdn.Stream.Path), - } - } - - hotlinkProtectionDomains := types.SetNull(types.StringType) - if cdn.HotlinkProtection.Domains != nil { - hotlinkProtectionDomains, ds = types.SetValueFrom(d.ctx, types.StringType, *cdn.HotlinkProtection.Domains) - if ds != nil { - diags.Append(ds...) - - return - } - } - - ipProtectionIps := types.SetNull(types.StringType) - if cdn.IpProtection.Ips != nil { - ipProtectionIps, ds = types.SetValueFrom(d.ctx, types.StringType, *cdn.IpProtection.Ips) - if ds != nil { - diags.Append(ds...) - - return - } - } - - geoProtectionCountries := types.SetNull(types.StringType) - if cdn.GeoProtection.Countries != nil { - geoProtectionCountries, ds = types.SetValueFrom(d.ctx, types.StringType, *cdn.GeoProtection.Countries) - if ds != nil { - diags.Append(ds...) - - return - } - } - - originHeaders := types.MapNull(types.StringType) - if cdn.OriginHeaders != nil && !cdn.OriginHeaders.Custom.IsNull() && cdn.OriginHeaders.Custom.IsSpecified() { - originHeaders, ds = types.MapValueFrom(d.ctx, types.StringType, cdn.OriginHeaders.Custom.MustGet()) - if ds != nil { - diags.Append(ds...) - - return - } - } - - data = CdnModel{ - Id: data.Id, - Cnames: cnames, - CreationTime: types.StringValue(cdn.CreationTime.Format(time.DateTime)), - Label: types.StringValue(cdn.Label), - Note: util.NullableToStringValue(cdn.Note), - OriginId: util.NullableToStringValue(cdn.OriginId), - OriginProtectionEnabled: types.BoolValue(cdn.OriginProtection.Enabled), - Url: types.StringValue(cdn.Url), - Cache: &CdnModelCache{ - MaxAge: util.IntPointerToInt64Value(cdn.Cache.MaxAge), - MaxAge404: util.NullableIntToInt64Value(cdn.Cache.MaxAge404), - RequestsWithCookiesEnabled: types.BoolPointerValue(cdn.Cache.RequestsWithCookiesEnabled), - }, - SecureToken: &CdnModelSecureToken{ - Token: types.StringPointerValue(cdn.SecureToken.Token), - Type: types.StringValue(string(cdn.SecureToken.Type)), - }, - QueryString: &CdnModelQueryString{ - Parameters: queryStringParameters, - IgnoreType: types.StringValue(string(cdn.QueryString.IgnoreType)), - }, - Headers: &CdnModelHeaders{ - CorsEnabled: types.BoolPointerValue(cdn.Headers.CorsEnabled), - CorsTimingEnabled: types.BoolPointerValue(cdn.Headers.CorsTimingEnabled), - CorsWildcardEnabled: types.BoolPointerValue(cdn.Headers.CorsWildcardEnabled), - HostHeaderForwardingEnabled: types.BoolPointerValue(cdn.Headers.HostHeaderForwardingEnabled), - ContentDispositionType: types.StringValue(string(*cdn.Headers.ContentDisposition.Type)), - }, - HttpsRedirect: &CdnModelHttpsRedirect{ - Code: util.IntPointerToInt64Value(cdn.HttpsRedirect.Code), - Enabled: types.BoolValue(cdn.HttpsRedirect.Enabled), - }, - Mp4PseudoStreamingEnabled: types.BoolPointerValue(cdn.Mp4PseudoStreaming.Enabled), - WafEnabled: types.BoolValue(cdn.Waf.Enabled), - Ssl: &CdnModelSsl{ - Type: types.StringValue(string(cdn.Ssl.Type)), - SslId: types.StringPointerValue(cdn.Ssl.SslId), - }, - Stream: stream, - HotlinkProtection: &CdnModelHotlinkProtection{ - Domains: hotlinkProtectionDomains, - Type: types.StringValue(string(cdn.HotlinkProtection.Type)), - EmptyRefererDenied: types.BoolValue(cdn.HotlinkProtection.EmptyRefererDenied), - }, - IpProtection: &CdnModelIpProtection{ - Ips: ipProtectionIps, - Type: types.StringValue(string(cdn.IpProtection.Type)), - }, - GeoProtection: &CdnModelGeoProtection{ - Countries: geoProtectionCountries, - Type: types.StringValue(string(cdn.GeoProtection.Type)), - }, - RateLimitEnabled: types.BoolValue(cdn.RateLimit.Enabled), - OriginHeaders: originHeaders, - } - - diags.Append(state.Set(d.ctx, &data)...) -} diff --git a/internal/provider/cdn_data_source.go b/internal/provider/cdn_data_source.go deleted file mode 100644 index 348f8c9..0000000 --- a/internal/provider/cdn_data_source.go +++ /dev/null @@ -1,39 +0,0 @@ -package provider //nolint:dupl // false-positive - -import ( - "context" - - "github.com/cdn77/cdn77-client-go" - "github.com/cdn77/terraform-provider-cdn77/internal/util" - "github.com/hashicorp/terraform-plugin-framework/datasource" -) - -var _ datasource.DataSourceWithConfigure = &CdnDataSource{} - -func NewCdnDataSource() datasource.DataSource { - return &CdnDataSource{} -} - -type CdnDataSource struct { - client cdn77.ClientWithResponsesInterface -} - -func (*CdnDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_cdn" -} - -func (*CdnDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = util.NewResourceDataSourceSchemaConverter("id").Convert(CreateCdnResourceSchema()) -} - -func (d *CdnDataSource) Configure( - _ context.Context, - req datasource.ConfigureRequest, - resp *datasource.ConfigureResponse, -) { - resp.Diagnostics.Append(util.MaybeSetClient(req.ProviderData, &d.client)) -} - -func (d *CdnDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - NewCdnDataSourceReader(ctx, d.client).Read(&req.Config, &resp.Diagnostics, &resp.State) -} diff --git a/internal/provider/cdn_data_source_test.go b/internal/provider/cdn_data_source_test.go deleted file mode 100644 index 556346d..0000000 --- a/internal/provider/cdn_data_source_test.go +++ /dev/null @@ -1,279 +0,0 @@ -package provider_test - -import ( - "context" - "fmt" - "regexp" - "testing" - "time" - - "github.com/cdn77/cdn77-client-go" - "github.com/cdn77/terraform-provider-cdn77/internal/acctest" - "github.com/cdn77/terraform-provider-cdn77/internal/provider" - "github.com/cdn77/terraform-provider-cdn77/internal/util" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/oapi-codegen/nullable" -) - -func TestAccCdnDataSource_NonExistingCdn(t *testing.T) { - const cdnId = 7495732 - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctest.GetProviderFactories(), - Steps: []resource.TestStep{ - { - Config: acctest.Config(cdnDataSourceConfig, "id", cdnId), - ExpectError: regexp.MustCompile(fmt.Sprintf(`CDN Resource with id "%d" could not be found`, cdnId)), - }, - }, - }) -} - -func TestAccCdnDataSource_OnlyRequiredFields(t *testing.T) { - client := acctest.GetClient(t) - - originRequest := cdn77.OriginAddUrlJSONRequestBody{ - Host: "my-totally-random-custom-host.com", - Label: "random origin", - Scheme: "https", - } - originResponse, err := client.OriginAddUrlWithResponse(context.Background(), originRequest) - acctest.AssertResponseOk(t, "Failed to create Origin: %s", originResponse, err) - - originId := originResponse.JSON201.Id - - t.Cleanup(func() { - acctest.MustDeleteOrigin(t, client, provider.OriginTypeUrl, originId) - }) - - const cdnLabel = "some cdn" - - cdnRequest := cdn77.CdnAddJSONRequestBody{Label: cdnLabel, OriginId: originId} - cdnResponse, err := client.CdnAddWithResponse(context.Background(), cdnRequest) - acctest.AssertResponseOk(t, "Failed to create CDN: %s", cdnResponse, err) - - cdnId := cdnResponse.JSON201.Id - cdnCreationTime := cdnResponse.JSON201.CreationTime.Format(time.DateTime) - cdnUrl := cdnResponse.JSON201.Url - - t.Cleanup(func() { - acctest.MustDeleteCdn(t, client, cdnId) - }) - - attrEq := func(key, value string) resource.TestCheckFunc { - return resource.TestCheckResourceAttr("data.cdn77_cdn.lorem", key, value) - } - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctest.GetProviderFactories(), - Steps: []resource.TestStep{ - { - Config: acctest.Config(cdnDataSourceConfig, "id", cdnId), - Check: resource.ComposeAggregateTestCheckFunc( - attrEq("id", fmt.Sprintf("%d", cdnId)), - attrEq("cnames.#", "0"), - attrEq("creation_time", cdnCreationTime), - attrEq("label", cdnLabel), - attrEq("origin_id", originId), - attrEq("origin_protection_enabled", "false"), - attrEq("url", cdnUrl), - attrEq("cache.max_age", fmt.Sprintf("%d", cdn77.MaxAgeN17280)), - attrEq("cache.requests_with_cookies_enabled", "true"), - attrEq("secure_token.type", string(cdn77.SecureTokenTypeNone)), - attrEq("query_string.ignore_type", string(cdn77.QueryStringIgnoreTypeNone)), - attrEq("headers.cors_enabled", "false"), - attrEq("headers.cors_timing_enabled", "false"), - attrEq("headers.cors_wildcard_enabled", "false"), - attrEq("headers.host_header_forwarding_enabled", "false"), - attrEq("headers.content_disposition_type", string(cdn77.ContentDispositionTypeNone)), - attrEq("https_redirect.enabled", "false"), - attrEq("mp4_pseudo_streaming_enabled", "false"), - attrEq("waf_enabled", "false"), - attrEq("ssl.type", string(cdn77.InstantSsl)), - attrEq("hotlink_protection.type", string(cdn77.Disabled)), - attrEq("hotlink_protection.empty_referer_denied", "false"), - attrEq("ip_protection.type", string(cdn77.Disabled)), - attrEq("geo_protection.type", string(cdn77.Disabled)), - attrEq("rate_limit_enabled", "false"), - attrEq("origin_headers.#", "0"), - - resource.TestCheckNoResourceAttr("data.cdn77_cdn.lorem", "note"), - resource.TestCheckNoResourceAttr("data.cdn77_cdn.lorem", "cache.max_age_404"), - resource.TestCheckNoResourceAttr("data.cdn77_cdn.lorem", "secure_token.token"), - resource.TestCheckNoResourceAttr("data.cdn77_cdn.lorem", "query_string.parameters"), - resource.TestCheckNoResourceAttr("data.cdn77_cdn.lorem", "https_redirect.code"), - resource.TestCheckNoResourceAttr("data.cdn77_cdn.lorem", "ssl.ssl_id"), - resource.TestCheckNoResourceAttr("data.cdn77_cdn.lorem", "stream"), - resource.TestCheckNoResourceAttr("data.cdn77_cdn.lorem", "hotlink_protection.domains"), - resource.TestCheckNoResourceAttr("data.cdn77_cdn.lorem", "ip_protection.ips"), - resource.TestCheckNoResourceAttr("data.cdn77_cdn.lorem", "geo_protection.countries"), - ), - }, - }, - }) -} - -func TestAccCdnDataSource_AllFields(t *testing.T) { - client := acctest.GetClient(t) - - originRequest := cdn77.OriginAddUrlJSONRequestBody{ - Host: "my-totally-random-custom-host.com", - Label: "random origin", - Scheme: "https", - } - originResponse, err := client.OriginAddUrlWithResponse(context.Background(), originRequest) - acctest.AssertResponseOk(t, "Failed to create Origin: %s", originResponse, err) - - originId := originResponse.JSON201.Id - - t.Cleanup(func() { - acctest.MustDeleteOrigin(t, client, provider.OriginTypeUrl, originId) - }) - - cdnCnames := []string{"my.cdn.com", "another-cname.example.com"} - const cdnLabel = "some cdn" - const cdnNote = "some note" - - sslId := acctest.MustAddSslWithCleanup(t, client, sslTestCert1, sslTestKey) - - cdnAddRequest := cdn77.CdnAddJSONRequestBody{ - Cnames: util.Pointer(cdnCnames), - Label: cdnLabel, - Note: nullable.NewNullableWithValue(cdnNote), - OriginId: originId, - } - cdnAddResponse, err := client.CdnAddWithResponse(context.Background(), cdnAddRequest) - acctest.AssertResponseOk(t, "Failed to create CDN: %s", cdnAddResponse, err) - - cdnId := cdnAddResponse.JSON201.Id - cdnCreationTime := cdnAddResponse.JSON201.CreationTime.Format(time.DateTime) - cdnUrl := cdnAddResponse.JSON201.Url - - t.Cleanup(func() { - acctest.MustDeleteCdn(t, client, cdnId) - }) - - cdnEditRequest := cdn77.CdnEditJSONRequestBody{ - Cache: &cdn77.Cache{ - MaxAge: util.Pointer(cdn77.MaxAgeN60), - MaxAge404: nullable.NewNullableWithValue(cdn77.MaxAge404N5), - RequestsWithCookiesEnabled: util.Pointer(false), - }, - GeoProtection: &cdn77.GeoProtection{Countries: util.Pointer([]string{"CZ"}), Type: cdn77.Passlist}, - Headers: &cdn77.Headers{ - ContentDisposition: &cdn77.ContentDisposition{ - Type: util.Pointer(cdn77.ContentDispositionTypeParameter), - }, - CorsEnabled: util.Pointer(true), - CorsTimingEnabled: util.Pointer(true), - CorsWildcardEnabled: util.Pointer(true), - HostHeaderForwardingEnabled: util.Pointer(true), - }, - HotlinkProtection: &cdn77.HotlinkProtection{ - Domains: util.Pointer([]string{"xxx.cz"}), - EmptyRefererDenied: true, - Type: cdn77.Passlist, - }, - HttpsRedirect: &cdn77.HttpsRedirect{Code: util.Pointer(cdn77.N301), Enabled: true}, - IpProtection: &cdn77.IpProtection{ - Ips: util.Pointer([]string{"1.1.1.1/32", "8.8.8.8/32"}), - Type: cdn77.Blocklist, - }, - OriginHeaders: &cdn77.OriginHeaders{ - Custom: nullable.NewNullableWithValue(map[string]string{"abc": "v1", "def": "v2"}), - }, - QueryString: &cdn77.QueryString{ - IgnoreType: cdn77.QueryStringIgnoreTypeList, - Parameters: util.Pointer([]string{"param"}), - }, - RateLimit: &cdn77.RateLimit{Enabled: true}, - SecureToken: &cdn77.SecureToken{Token: util.Pointer("abcd1234"), Type: cdn77.SecureTokenTypePath}, - Ssl: &cdn77.CdnSsl{SslId: util.Pointer(sslId), Type: cdn77.SNI}, - Waf: &cdn77.Waf{Enabled: true}, - } - cdnEditResponse, err := client.CdnEditWithResponse(context.Background(), cdnId, cdnEditRequest) - acctest.AssertResponseOk(t, "Failed to edit CDN: %s", cdnEditResponse, err) - - attrEq := func(key, value string) resource.TestCheckFunc { - return resource.TestCheckResourceAttr("data.cdn77_cdn.lorem", key, value) - } - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctest.GetProviderFactories(), - Steps: []resource.TestStep{ - { - Config: acctest.Config(cdnDataSourceConfig, "id", cdnId), - Check: resource.ComposeAggregateTestCheckFunc( - attrEq("id", fmt.Sprintf("%d", cdnId)), - attrEq("cnames.#", "2"), - resource.TestCheckTypeSetElemAttr("data.cdn77_cdn.lorem", "cnames.*", cdnCnames[0]), - resource.TestCheckTypeSetElemAttr("data.cdn77_cdn.lorem", "cnames.*", cdnCnames[1]), - attrEq("creation_time", cdnCreationTime), - attrEq("label", cdnLabel), - attrEq("note", cdnNote), - attrEq("origin_id", originId), - attrEq("origin_protection_enabled", "false"), - attrEq("url", cdnUrl), - attrEq("cache.max_age", fmt.Sprintf("%d", cdn77.MaxAgeN60)), - attrEq("cache.max_age_404", fmt.Sprintf("%d", cdn77.MaxAge404N5)), - attrEq("cache.requests_with_cookies_enabled", "false"), - attrEq("secure_token.type", string(cdn77.SecureTokenTypePath)), - attrEq("secure_token.token", "abcd1234"), - attrEq("query_string.ignore_type", string(cdn77.QueryStringIgnoreTypeList)), - attrEq("query_string.parameters.#", "1"), - resource.TestCheckTypeSetElemAttr("data.cdn77_cdn.lorem", "query_string.parameters.*", "param"), - attrEq("headers.cors_enabled", "true"), - attrEq("headers.cors_timing_enabled", "true"), - attrEq("headers.cors_wildcard_enabled", "true"), - attrEq("headers.host_header_forwarding_enabled", "true"), - attrEq("headers.content_disposition_type", string(cdn77.ContentDispositionTypeParameter)), - attrEq("https_redirect.enabled", "true"), - attrEq("https_redirect.code", fmt.Sprintf("%d", cdn77.N301)), - attrEq("mp4_pseudo_streaming_enabled", "false"), - attrEq("waf_enabled", "true"), - attrEq("ssl.type", string(cdn77.SNI)), - attrEq("ssl.ssl_id", sslId), - attrEq("hotlink_protection.type", string(cdn77.Passlist)), - attrEq("hotlink_protection.empty_referer_denied", "true"), - attrEq("hotlink_protection.domains.#", "1"), - resource.TestCheckTypeSetElemAttr("data.cdn77_cdn.lorem", "hotlink_protection.domains.*", "xxx.cz"), - attrEq("ip_protection.type", string(cdn77.Blocklist)), - attrEq("ip_protection.ips.#", "2"), - resource.TestCheckTypeSetElemAttr("data.cdn77_cdn.lorem", "ip_protection.ips.*", "1.1.1.1/32"), - resource.TestCheckTypeSetElemAttr("data.cdn77_cdn.lorem", "ip_protection.ips.*", "8.8.8.8/32"), - attrEq("geo_protection.countries.#", "1"), - resource.TestCheckTypeSetElemAttr("data.cdn77_cdn.lorem", "geo_protection.countries.*", "CZ"), - attrEq("geo_protection.type", string(cdn77.Passlist)), - attrEq("rate_limit_enabled", "true"), - attrEq("origin_headers.%", "2"), - attrEq("origin_headers.abc", "v1"), - attrEq("origin_headers.def", "v2"), - - resource.TestCheckNoResourceAttr("data.cdn77_cdn.lorem", "stream"), - ), - }, - { - PreConfig: func() { - cdnEditRequest.Mp4PseudoStreaming = &cdn77.Mp4PseudoStreaming{Enabled: util.Pointer(true)} - cdnEditRequest.QueryString.IgnoreType = cdn77.QueryStringIgnoreTypeAll - cdnEditRequest.QueryString.Parameters = nil - - cdnEditResponse, err = client.CdnEditWithResponse(context.Background(), cdnId, cdnEditRequest) - acctest.AssertResponseOk(t, "Failed to edit CDN: %s", cdnEditResponse, err) - }, - Config: acctest.Config(cdnDataSourceConfig, "id", cdnId), - Check: resource.ComposeAggregateTestCheckFunc( - attrEq("query_string.ignore_type", string(cdn77.QueryStringIgnoreTypeAll)), - attrEq("query_string.#", "0"), - attrEq("mp4_pseudo_streaming_enabled", "true"), - ), - }, - }, - }) -} - -const cdnDataSourceConfig = ` -data "cdn77_cdn" "lorem" { - id = "{id}" -} -` diff --git a/internal/provider/cdn_resource_test.go b/internal/provider/cdn_resource_test.go deleted file mode 100644 index af3b863..0000000 --- a/internal/provider/cdn_resource_test.go +++ /dev/null @@ -1,670 +0,0 @@ -package provider_test - -import ( - "context" - "errors" - "fmt" - "sort" - "strconv" - "testing" - - "github.com/cdn77/cdn77-client-go" - "github.com/cdn77/terraform-provider-cdn77/internal/acctest" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/plancheck" - "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/oapi-codegen/nullable" -) - -func TestAccCdnResource(t *testing.T) { - client := acctest.GetClient(t) - var originId string - var cdnId int - var cdnCreationTime string - var cdnUrl string - sslId := acctest.MustAddSslWithCleanup(t, client, sslTestCert1, sslTestKey) - - rsc := "cdn77_cdn.lorem" - attrEq := func(key, value string) resource.TestCheckFunc { - return resource.TestCheckResourceAttr(rsc, key, value) - } - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctest.GetProviderFactories(), - CheckDestroy: func(state *terraform.State) error { - return errors.Join(checkCdnsDestroyed(client)(state), checkOriginsDestroyed(client)(state)) - }, - Steps: []resource.TestStep{ - { - Config: OriginResourceConfig + `resource "cdn77_cdn" "lorem" { - origin_id = cdn77_origin.url.id - label = "my cdn" - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("cdn77_origin.url", plancheck.ResourceActionCreate), - plancheck.ExpectResourceAction(rsc, plancheck.ResourceActionCreate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrWith("cdn77_origin.url", "id", func(value string) error { - originId = value - - return acctest.NotEqual(value, "") - }), - resource.TestCheckResourceAttrWith(rsc, "id", func(value string) (err error) { - cdnId, err = strconv.Atoi(value) - - return err - }), - attrEq("cnames.#", "0"), - resource.TestCheckResourceAttrWith(rsc, "creation_time", func(value string) (err error) { - cdnCreationTime = value - - return acctest.NotEqual(value, "") - }), - attrEq("label", "my cdn"), - resource.TestCheckResourceAttrWith(rsc, "origin_id", func(value string) error { - return acctest.Equal(value, originId) - }), - attrEq("origin_protection_enabled", "false"), - resource.TestCheckResourceAttrWith(rsc, "url", func(value string) (err error) { - cdnUrl = value - - return acctest.NotEqual(value, "") - }), - attrEq("cache.max_age", fmt.Sprintf("%d", cdn77.MaxAgeN17280)), - attrEq("cache.requests_with_cookies_enabled", "true"), - attrEq("secure_token.type", string(cdn77.SecureTokenTypeNone)), - attrEq("query_string.ignore_type", string(cdn77.QueryStringIgnoreTypeNone)), - attrEq("headers.cors_enabled", "false"), - attrEq("headers.cors_timing_enabled", "false"), - attrEq("headers.cors_wildcard_enabled", "false"), - attrEq("headers.host_header_forwarding_enabled", "false"), - attrEq("headers.content_disposition_type", string(cdn77.ContentDispositionTypeNone)), - attrEq("https_redirect.enabled", "false"), - attrEq("mp4_pseudo_streaming_enabled", "false"), - attrEq("waf_enabled", "false"), - attrEq("ssl.type", string(cdn77.InstantSsl)), - attrEq("hotlink_protection.type", string(cdn77.Disabled)), - attrEq("hotlink_protection.empty_referer_denied", "false"), - attrEq("ip_protection.type", string(cdn77.Disabled)), - attrEq("geo_protection.type", string(cdn77.Disabled)), - attrEq("rate_limit_enabled", "false"), - attrEq("origin_headers.#", "0"), - - resource.TestCheckNoResourceAttr(rsc, "note"), - resource.TestCheckNoResourceAttr(rsc, "cache.max_age_404"), - resource.TestCheckNoResourceAttr(rsc, "secure_token.token"), - resource.TestCheckNoResourceAttr(rsc, "query_string.parameters"), - resource.TestCheckNoResourceAttr(rsc, "https_redirect.code"), - resource.TestCheckNoResourceAttr(rsc, "ssl.ssl_id"), - resource.TestCheckNoResourceAttr(rsc, "stream"), - resource.TestCheckNoResourceAttr(rsc, "hotlink_protection.domains"), - resource.TestCheckNoResourceAttr(rsc, "ip_protection.ips"), - resource.TestCheckNoResourceAttr(rsc, "geo_protection.countries"), - - checkCdnDefaults(client, &cdnId, &originId, "my cdn"), - ), - }, - { - Config: OriginResourceConfig + `resource "cdn77_cdn" "lorem" { - origin_id = cdn77_origin.url.id - label = "changed the label" - note = "custom note" - cnames = ["my.cdn.cz", "other.cdn.com"] - cache = { - max_age = 60 - max_age_404 = 5 - requests_with_cookies_enabled = false - } - secure_token = { - type = "path" - token = "abcd1234" - } - query_string = { - ignore_type = "list" - parameters = ["param"] - } - headers = { - cors_enabled = true - cors_timing_enabled = true - cors_wildcard_enabled = true - host_header_forwarding_enabled = true - content_disposition_type = "parameter" - } - https_redirect = { - enabled = true - code = 301 - } - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction(rsc, plancheck.ResourceActionUpdate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrWith(rsc, "id", func(value string) (err error) { - return acctest.Equal(value, strconv.Itoa(cdnId)) - }), - attrEq("cnames.#", "2"), - resource.TestCheckTypeSetElemAttr(rsc, "cnames.*", "my.cdn.cz"), - resource.TestCheckTypeSetElemAttr(rsc, "cnames.*", "other.cdn.com"), - resource.TestCheckResourceAttrWith(rsc, "creation_time", func(value string) (err error) { - return acctest.Equal(value, cdnCreationTime) - }), - attrEq("label", "changed the label"), - attrEq("note", "custom note"), - resource.TestCheckResourceAttrWith(rsc, "origin_id", func(value string) error { - return acctest.Equal(value, originId) - }), - attrEq("origin_protection_enabled", "false"), - resource.TestCheckResourceAttrWith(rsc, "url", func(value string) error { - return acctest.Equal(value, cdnUrl) - }), - attrEq("cache.max_age", fmt.Sprintf("%d", cdn77.MaxAgeN60)), - attrEq("cache.max_age_404", fmt.Sprintf("%d", cdn77.MaxAge404N5)), - attrEq("cache.requests_with_cookies_enabled", "false"), - attrEq("secure_token.type", string(cdn77.SecureTokenTypePath)), - attrEq("secure_token.token", "abcd1234"), - attrEq("query_string.ignore_type", string(cdn77.QueryStringIgnoreTypeList)), - attrEq("query_string.parameters.#", "1"), - resource.TestCheckTypeSetElemAttr(rsc, "query_string.parameters.*", "param"), - attrEq("headers.cors_enabled", "true"), - attrEq("headers.cors_timing_enabled", "true"), - attrEq("headers.cors_wildcard_enabled", "true"), - attrEq("headers.host_header_forwarding_enabled", "true"), - attrEq("headers.content_disposition_type", string(cdn77.ContentDispositionTypeParameter)), - attrEq("https_redirect.enabled", "true"), - attrEq("https_redirect.code", fmt.Sprintf("%d", cdn77.N301)), - attrEq("mp4_pseudo_streaming_enabled", "false"), - attrEq("waf_enabled", "false"), - attrEq("ssl.type", string(cdn77.InstantSsl)), - attrEq("hotlink_protection.type", string(cdn77.Disabled)), - attrEq("hotlink_protection.empty_referer_denied", "false"), - attrEq("ip_protection.type", string(cdn77.Disabled)), - attrEq("geo_protection.type", string(cdn77.Disabled)), - attrEq("rate_limit_enabled", "false"), - attrEq("origin_headers.#", "0"), - - resource.TestCheckNoResourceAttr(rsc, "ssl.ssl_id"), - resource.TestCheckNoResourceAttr(rsc, "stream"), - resource.TestCheckNoResourceAttr(rsc, "hotlink_protection.domains"), - resource.TestCheckNoResourceAttr(rsc, "ip_protection.ips"), - resource.TestCheckNoResourceAttr(rsc, "geo_protection.countries"), - - checkCdn(client, &cdnId, func(c *cdn77.Cdn) error { - sort.SliceStable(c.Cnames, func(i, j int) bool { - return c.Cnames[i].Cname < c.Cnames[j].Cname - }) - - return errors.Join( - acctest.NullFieldEqual("origin_id", c.OriginId, originId), - acctest.EqualField("label", c.Label, "changed the label"), - acctest.NullFieldEqual("note", c.Note, "custom note"), - acctest.EqualField("cnames.0", c.Cnames[0].Cname, "my.cdn.cz"), - acctest.EqualField("cnames.1", c.Cnames[1].Cname, "other.cdn.com"), - acctest.EqualField("cache.max_age", *c.Cache.MaxAge, cdn77.MaxAgeN60), - acctest.NullFieldEqual("cache.max_age_404", c.Cache.MaxAge404, cdn77.MaxAge404N5), - acctest.EqualField( - "cache.requests_with_cookies_enabled", - *c.Cache.RequestsWithCookiesEnabled, - false, - ), - acctest.EqualField("secure_token.type", c.SecureToken.Type, cdn77.SecureTokenTypePath), - acctest.EqualField("secure_token.token", *c.SecureToken.Token, "abcd1234"), - acctest.EqualField( - "query_string.ignore_type", - c.QueryString.IgnoreType, - cdn77.QueryStringIgnoreTypeList, - ), - acctest.EqualField("query_string.parameters.0", (*c.QueryString.Parameters)[0], "param"), - acctest.EqualField("headers.cors_enabled", *c.Headers.CorsEnabled, true), - acctest.EqualField("headers.cors_timing_enabled", *c.Headers.CorsTimingEnabled, true), - acctest.EqualField("headers.cors_wildcard_enabled", *c.Headers.CorsWildcardEnabled, true), - acctest.EqualField( - "headers.host_header_forwarding_enabled", - *c.Headers.HostHeaderForwardingEnabled, - true, - ), - acctest.EqualField( - "headers.content_disposition_type", - *c.Headers.ContentDisposition.Type, - cdn77.ContentDispositionTypeParameter, - ), - acctest.EqualField("https_redirect.enabled", c.HttpsRedirect.Enabled, true), - acctest.EqualField("https_redirect.code", *c.HttpsRedirect.Code, cdn77.N301), - ) - }), - ), - }, - { - Config: OriginResourceConfig + acctest.Config( - `resource "cdn77_cdn" "lorem" { - origin_id = cdn77_origin.url.id - label = "changed the label" - note = "custom note" - cnames = ["my.cdn.cz", "other.cdn.com"] - cache = { - max_age = 60 - max_age_404 = 5 - requests_with_cookies_enabled = false - } - secure_token = { - type = "path" - token = "abcd1234" - } - query_string = { - ignore_type = "all" - } - headers = { - cors_enabled = true - cors_timing_enabled = true - cors_wildcard_enabled = true - host_header_forwarding_enabled = true - content_disposition_type = "parameter" - } - https_redirect = { - enabled = true - code = 301 - } - mp4_pseudo_streaming_enabled = true - waf_enabled = true - ssl = { - type = "SNI" - ssl_id = "{sslId}" - } - hotlink_protection = { - type = "blocklist" - domains = ["example.com"] - empty_referer_denied = true - } - ip_protection = { - type = "passlist" - ips = ["1.1.1.1/32", "8.8.8.8/32"] - } - geo_protection = { - type = "blocklist" - countries = ["SK"] - } - rate_limit_enabled = true - origin_headers = { - abc = "v1" - def = "v2" - } - }`, - "sslId", sslId, - ), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction(rsc, plancheck.ResourceActionUpdate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrWith(rsc, "id", func(value string) (err error) { - return acctest.Equal(value, strconv.Itoa(cdnId)) - }), - attrEq("cnames.#", "2"), - resource.TestCheckTypeSetElemAttr(rsc, "cnames.*", "my.cdn.cz"), - resource.TestCheckTypeSetElemAttr(rsc, "cnames.*", "other.cdn.com"), - resource.TestCheckResourceAttrWith(rsc, "creation_time", func(value string) (err error) { - return acctest.Equal(value, cdnCreationTime) - }), - attrEq("label", "changed the label"), - attrEq("note", "custom note"), - resource.TestCheckResourceAttrWith(rsc, "origin_id", func(value string) error { - return acctest.Equal(value, originId) - }), - attrEq("origin_protection_enabled", "false"), - resource.TestCheckResourceAttrWith(rsc, "url", func(value string) error { - return acctest.Equal(value, cdnUrl) - }), - attrEq("cache.max_age", fmt.Sprintf("%d", cdn77.MaxAgeN60)), - attrEq("cache.max_age_404", fmt.Sprintf("%d", cdn77.MaxAge404N5)), - attrEq("cache.requests_with_cookies_enabled", "false"), - attrEq("secure_token.type", string(cdn77.SecureTokenTypePath)), - attrEq("secure_token.token", "abcd1234"), - attrEq("query_string.ignore_type", string(cdn77.QueryStringIgnoreTypeAll)), - attrEq("query_string.parameters.#", "0"), - attrEq("headers.cors_enabled", "true"), - attrEq("headers.cors_timing_enabled", "true"), - attrEq("headers.cors_wildcard_enabled", "true"), - attrEq("headers.host_header_forwarding_enabled", "true"), - attrEq("headers.content_disposition_type", string(cdn77.ContentDispositionTypeParameter)), - attrEq("https_redirect.enabled", "true"), - attrEq("https_redirect.code", fmt.Sprintf("%d", cdn77.N301)), - attrEq("mp4_pseudo_streaming_enabled", "true"), - attrEq("waf_enabled", "true"), - attrEq("ssl.type", string(cdn77.SNI)), - attrEq("ssl.ssl_id", sslId), - attrEq("hotlink_protection.type", string(cdn77.Blocklist)), - attrEq("hotlink_protection.empty_referer_denied", "true"), - attrEq("hotlink_protection.domains.#", "1"), - resource.TestCheckTypeSetElemAttr(rsc, "hotlink_protection.domains.*", "example.com"), - attrEq("ip_protection.type", string(cdn77.Passlist)), - attrEq("ip_protection.ips.#", "2"), - resource.TestCheckTypeSetElemAttr(rsc, "ip_protection.ips.*", "1.1.1.1/32"), - resource.TestCheckTypeSetElemAttr(rsc, "ip_protection.ips.*", "8.8.8.8/32"), - attrEq("geo_protection.countries.#", "1"), - resource.TestCheckTypeSetElemAttr(rsc, "geo_protection.countries.*", "SK"), - attrEq("geo_protection.type", string(cdn77.Blocklist)), - attrEq("rate_limit_enabled", "true"), - attrEq("origin_headers.%", "2"), - attrEq("origin_headers.abc", "v1"), - attrEq("origin_headers.def", "v2"), - - resource.TestCheckNoResourceAttr(rsc, "stream"), - - checkCdn(client, &cdnId, func(c *cdn77.Cdn) error { - sort.SliceStable(c.Cnames, func(i, j int) bool { - return c.Cnames[i].Cname < c.Cnames[j].Cname - }) - - sort.SliceStable(*c.IpProtection.Ips, func(i, j int) bool { - return (*c.IpProtection.Ips)[i] < (*c.IpProtection.Ips)[j] - }) - - return errors.Join( - acctest.NullFieldEqual("origin_id", c.OriginId, originId), - acctest.EqualField("label", c.Label, "changed the label"), - acctest.NullFieldEqual("note", c.Note, "custom note"), - acctest.EqualField("cnames.0", c.Cnames[0].Cname, "my.cdn.cz"), - acctest.EqualField("cnames.1", c.Cnames[1].Cname, "other.cdn.com"), - acctest.EqualField("cache.max_age", *c.Cache.MaxAge, cdn77.MaxAgeN60), - acctest.NullFieldEqual("cache.max_age_404", c.Cache.MaxAge404, cdn77.MaxAge404N5), - acctest.EqualField( - "cache.requests_with_cookies_enabled", - *c.Cache.RequestsWithCookiesEnabled, - false, - ), - acctest.EqualField("secure_token.type", c.SecureToken.Type, cdn77.SecureTokenTypePath), - acctest.EqualField("secure_token.token", *c.SecureToken.Token, "abcd1234"), - acctest.EqualField( - "query_string.ignore_type", - c.QueryString.IgnoreType, - cdn77.QueryStringIgnoreTypeAll, - ), - acctest.EqualField("query_string.parameters", c.QueryString.Parameters, nil), - acctest.EqualField("headers.cors_enabled", *c.Headers.CorsEnabled, true), - acctest.EqualField("headers.cors_timing_enabled", *c.Headers.CorsTimingEnabled, true), - acctest.EqualField("headers.cors_wildcard_enabled", *c.Headers.CorsWildcardEnabled, true), - acctest.EqualField( - "headers.host_header_forwarding_enabled", - *c.Headers.HostHeaderForwardingEnabled, - true, - ), - acctest.EqualField( - "headers.content_disposition_type", - *c.Headers.ContentDisposition.Type, - cdn77.ContentDispositionTypeParameter, - ), - acctest.EqualField("https_redirect.enabled", c.HttpsRedirect.Enabled, true), - acctest.EqualField("https_redirect.code", *c.HttpsRedirect.Code, cdn77.N301), - acctest.EqualField("mp4_pseudo_streaming_enabled", *c.Mp4PseudoStreaming.Enabled, true), - acctest.EqualField("waf_enabled", c.Waf.Enabled, true), - acctest.EqualField("ssl.type", c.Ssl.Type, cdn77.SNI), - acctest.EqualField("ssl.ssl_id", *c.Ssl.SslId, sslId), - acctest.EqualField("hotlink_protection.type", c.HotlinkProtection.Type, cdn77.Blocklist), - acctest.EqualField( - "hotlink_protection.domains.0", - (*c.HotlinkProtection.Domains)[0], - "example.com", - ), - acctest.EqualField( - "hotlink_protection.empty_referer_denied", - c.HotlinkProtection.EmptyRefererDenied, - true, - ), - acctest.EqualField("ip_protection.type", c.IpProtection.Type, cdn77.Passlist), - acctest.EqualField("ip_protection.ips.0", (*c.IpProtection.Ips)[0], "1.1.1.1/32"), - acctest.EqualField("ip_protection.ips.1", (*c.IpProtection.Ips)[1], "8.8.8.8/32"), - acctest.EqualField("geo_protection.type", c.GeoProtection.Type, cdn77.Blocklist), - acctest.EqualField("geo_protection.countries.0", (*c.GeoProtection.Countries)[0], "SK"), - acctest.EqualField("rate_limit_enabled", c.RateLimit.Enabled, true), - func() error { - expected := map[string]string{"abc": "v1", "def": "v2"} - - if value, err := c.OriginHeaders.Custom.Get(); err == nil { - if len(value) == len(expected) { - valid := true - - for k, v := range expected { - if value[k] != v { - valid = false - } - } - - if valid { - return nil - } - } - } - - return fmt.Errorf( - "field origin_headers: expected %+v, got: %+v", - nullable.NewNullableWithValue(expected), - c.OriginHeaders.Custom, - ) - }(), - ) - }), - ), - }, - { - Config: OriginResourceConfig + `resource "cdn77_cdn" "lorem" { - origin_id = cdn77_origin.url.id - label = "my cdn" - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction(rsc, plancheck.ResourceActionUpdate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrWith(rsc, "id", func(value string) (err error) { - return acctest.Equal(value, strconv.Itoa(cdnId)) - }), - attrEq("cnames.#", "0"), - resource.TestCheckResourceAttrWith(rsc, "creation_time", func(value string) (err error) { - return acctest.Equal(value, cdnCreationTime) - }), - attrEq("label", "my cdn"), - resource.TestCheckResourceAttrWith(rsc, "origin_id", func(value string) error { - return acctest.Equal(value, originId) - }), - attrEq("origin_protection_enabled", "false"), - resource.TestCheckResourceAttrWith(rsc, "url", func(value string) error { - return acctest.Equal(value, cdnUrl) - }), - attrEq("cache.max_age", fmt.Sprintf("%d", cdn77.MaxAgeN17280)), - attrEq("cache.requests_with_cookies_enabled", "true"), - attrEq("secure_token.type", string(cdn77.SecureTokenTypeNone)), - attrEq("query_string.ignore_type", string(cdn77.QueryStringIgnoreTypeNone)), - attrEq("headers.cors_enabled", "false"), - attrEq("headers.cors_timing_enabled", "false"), - attrEq("headers.cors_wildcard_enabled", "false"), - attrEq("headers.host_header_forwarding_enabled", "false"), - attrEq("headers.content_disposition_type", string(cdn77.ContentDispositionTypeNone)), - attrEq("https_redirect.enabled", "false"), - attrEq("mp4_pseudo_streaming_enabled", "false"), - attrEq("waf_enabled", "false"), - attrEq("ssl.type", string(cdn77.InstantSsl)), - attrEq("hotlink_protection.type", string(cdn77.Disabled)), - attrEq("hotlink_protection.empty_referer_denied", "false"), - attrEq("ip_protection.type", string(cdn77.Disabled)), - attrEq("geo_protection.type", string(cdn77.Disabled)), - attrEq("rate_limit_enabled", "false"), - attrEq("origin_headers.#", "0"), - - resource.TestCheckNoResourceAttr(rsc, "note"), - resource.TestCheckNoResourceAttr(rsc, "cache.max_age_404"), - resource.TestCheckNoResourceAttr(rsc, "secure_token.token"), - resource.TestCheckNoResourceAttr(rsc, "query_string.parameters"), - resource.TestCheckNoResourceAttr(rsc, "https_redirect.code"), - resource.TestCheckNoResourceAttr(rsc, "ssl.ssl_id"), - resource.TestCheckNoResourceAttr(rsc, "stream"), - resource.TestCheckNoResourceAttr(rsc, "hotlink_protection.domains"), - resource.TestCheckNoResourceAttr(rsc, "ip_protection.ips"), - resource.TestCheckNoResourceAttr(rsc, "geo_protection.countries"), - - checkCdnDefaults(client, &cdnId, &originId, "my cdn"), - ), - }, - }, - }) -} - -func TestAccCdnResourceImport(t *testing.T) { - client := acctest.GetClient(t) - rsc := "cdn77_cdn.lorem" - var cdnId string - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctest.GetProviderFactories(), - CheckDestroy: func(state *terraform.State) error { - return errors.Join(checkCdnsDestroyed(client)(state), checkOriginsDestroyed(client)(state)) - }, - Steps: []resource.TestStep{ - { - Config: OriginResourceConfig + `resource "cdn77_cdn" "lorem" { - origin_id = cdn77_origin.url.id - label = "my cdn" - note = "custom note" - }`, - Check: resource.TestCheckResourceAttrWith(rsc, "id", func(value string) (err error) { - cdnId = value - - return nil - }), - }, - { - ResourceName: rsc, - ImportState: true, - ImportStateIdFunc: func(*terraform.State) (string, error) { - return cdnId, nil - }, - ImportStateVerify: true, - }, - }, - }) -} - -func checkCdn( - client cdn77.ClientWithResponsesInterface, - cdnId *int, - fn func(o *cdn77.Cdn) error, -) func(*terraform.State) error { - return func(*terraform.State) error { - response, err := client.CdnDetailWithResponse(context.Background(), *cdnId) - message := fmt.Sprintf("failed to get CDN[id=%d]: %%s", *cdnId) - - if err = acctest.CheckResponse(message, response, err); err != nil { - return err - } - - return fn(response.JSON200) - } -} - -func checkCdnDefaults( - client cdn77.ClientWithResponsesInterface, - cdnId *int, - originId *string, - cdnLabel string, -) func(*terraform.State) error { - return checkCdn(client, cdnId, func(c *cdn77.Cdn) error { - sort.SliceStable(c.Cnames, func(i, j int) bool { - return c.Cnames[i].Cname < c.Cnames[j].Cname - }) - - return errors.Join( - acctest.NullFieldEqual("origin_id", c.OriginId, *originId), - acctest.EqualField("label", c.Label, cdnLabel), - acctest.NullField("note", c.Note), - acctest.EqualField("len(cnames)", len(c.Cnames), 0), - acctest.EqualField("cache.max_age", *c.Cache.MaxAge, cdn77.MaxAgeN17280), - acctest.NullField("cache.max_age_404", c.Cache.MaxAge404), - acctest.EqualField( - "cache.requests_with_cookies_enabled", - *c.Cache.RequestsWithCookiesEnabled, - true, - ), - acctest.EqualField("secure_token.type", c.SecureToken.Type, cdn77.SecureTokenTypeNone), - acctest.EqualField("secure_token.token", c.SecureToken.Token, nil), - acctest.EqualField( - "query_string.ignore_type", - c.QueryString.IgnoreType, - cdn77.QueryStringIgnoreTypeNone, - ), - acctest.EqualField("query_string.parameters", c.QueryString.Parameters, nil), - acctest.EqualField("headers.cors_enabled", *c.Headers.CorsEnabled, false), - acctest.EqualField("headers.cors_timing_enabled", *c.Headers.CorsTimingEnabled, false), - acctest.EqualField("headers.cors_wildcard_enabled", *c.Headers.CorsWildcardEnabled, false), - acctest.EqualField( - "headers.host_header_forwarding_enabled", - *c.Headers.HostHeaderForwardingEnabled, - false, - ), - acctest.EqualField( - "headers.content_disposition_type", - *c.Headers.ContentDisposition.Type, - cdn77.ContentDispositionTypeNone, - ), - acctest.EqualField("https_redirect.enabled", c.HttpsRedirect.Enabled, false), - acctest.EqualField("https_redirect.code", c.HttpsRedirect.Code, nil), - - acctest.EqualField("mp4_pseudo_streaming_enabled", *c.Mp4PseudoStreaming.Enabled, false), - acctest.EqualField("waf_enabled", c.Waf.Enabled, false), - acctest.EqualField("ssl.type", c.Ssl.Type, cdn77.InstantSsl), - acctest.EqualField("ssl.ssl_id", c.Ssl.SslId, nil), - acctest.EqualField("hotlink_protection.code", c.HotlinkProtection.Type, cdn77.Disabled), - acctest.EqualField( - "hotlink_protection.empty_referer_denied", - c.HotlinkProtection.EmptyRefererDenied, - false, - ), - acctest.EqualField("hotlink_protection.domains", c.HotlinkProtection.Domains, nil), - acctest.EqualField("ip_protection.type", c.IpProtection.Type, cdn77.Disabled), - acctest.EqualField("ip_protection.ips", c.IpProtection.Ips, nil), - acctest.EqualField("geo_protection.type", c.GeoProtection.Type, cdn77.Disabled), - acctest.EqualField("geo_protection.countries", c.GeoProtection.Countries, nil), - acctest.EqualField("rate_limit_enabled", c.RateLimit.Enabled, false), - acctest.EqualField("origin_headers", c.OriginHeaders, nil), - ) - }) -} - -func checkCdnsDestroyed(client cdn77.ClientWithResponsesInterface) func(*terraform.State) error { - return func(s *terraform.State) error { - for _, rs := range s.RootModule().Resources { - if rs.Type != "cdn77_cdn" { - continue - } - - cdnId, err := strconv.Atoi(rs.Primary.Attributes["id"]) - if err != nil { - return fmt.Errorf("unexpected CDN id: %s", rs.Primary.Attributes["id"]) - } - - response, err := client.CdnDetailWithResponse(context.Background(), cdnId) - if err != nil { - return fmt.Errorf("failed to fetch CDN: %w", err) - } - - if response.JSON404 == nil { - return errors.New("expected CDN to be deleted") - } - } - - return nil - } -} - -const OriginResourceConfig = ` -resource "cdn77_origin" "url" { - type = "url" - label = "origin label" - scheme = "http" - host = "my-totally-random-custom-host.com" -} -` diff --git a/internal/provider/cdns_data_source.go b/internal/provider/cdns_data_source.go deleted file mode 100644 index 1a8a1fb..0000000 --- a/internal/provider/cdns_data_source.go +++ /dev/null @@ -1,129 +0,0 @@ -package provider - -import ( - "context" - "sort" - "time" - - "github.com/cdn77/cdn77-client-go" - "github.com/cdn77/terraform-provider-cdn77/internal/util" - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - "golang.org/x/exp/maps" -) - -type CdnsModel struct { - Cdns []CdnSummaryModel `tfsdk:"cdns"` -} - -type CdnSummaryModel struct { - Id types.Int64 `tfsdk:"id"` - Cnames types.Set `tfsdk:"cnames"` - CreationTime types.String `tfsdk:"creation_time"` - Label types.String `tfsdk:"label"` - Note types.String `tfsdk:"note"` - OriginId types.String `tfsdk:"origin_id"` - Url types.String `tfsdk:"url"` - Mp4PseudoStreamingEnabled types.Bool `tfsdk:"mp4_pseudo_streaming_enabled"` -} - -var _ datasource.DataSourceWithConfigure = &CdnsDataSource{} - -func NewCdnsDataSource() datasource.DataSource { - return &CdnsDataSource{} -} - -type CdnsDataSource struct { - client cdn77.ClientWithResponsesInterface -} - -func (*CdnsDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_cdns" -} - -func (*CdnsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - cdnsSchema := util.NewResourceDataSourceSchemaConverter().Convert(CreateCdnResourceSchema()) - - maps.DeleteFunc(cdnsSchema.Attributes, func(name string, _ schema.Attribute) bool { - switch name { - case "id", "cnames", "creation_time", "label", "mp4_pseudo_streaming_enabled", "note", "origin_id", "url": - return false - default: - return true - } - }) - - resp.Schema = schema.Schema{ - Attributes: map[string]schema.Attribute{ - "cdns": schema.ListNestedAttribute{ - NestedObject: schema.NestedAttributeObject{Attributes: cdnsSchema.Attributes}, - Computed: true, - Description: "List of all CDNs", - }, - }, - Description: "CDNs data source allows you to read all your CDNs", - } -} - -func (d *CdnsDataSource) Configure( - _ context.Context, - req datasource.ConfigureRequest, - resp *datasource.ConfigureResponse, -) { - resp.Diagnostics.Append(util.MaybeSetClient(req.ProviderData, &d.client)) -} - -func (d *CdnsDataSource) Read(ctx context.Context, _ datasource.ReadRequest, resp *datasource.ReadResponse) { - const errMessage = "Failed to fetch list of all CDNs" - - response, err := d.client.CdnListWithResponse(ctx) - if err != nil { - resp.Diagnostics.AddError(errMessage, err.Error()) - - return - } - - if !util.CheckResponse(&resp.Diagnostics, errMessage, response, response.JSONDefault) { - return - } - - cdns := make([]CdnSummaryModel, 0, len(*response.JSON200)) - - for _, cdn := range *response.JSON200 { - cnamesRaw := make([]string, len(cdn.Cnames)) - - for i, c := range cdn.Cnames { - cnamesRaw[i] = c.Cname - } - - cnames, ds := types.SetValueFrom(ctx, types.StringType, cnamesRaw) - if ds != nil { - resp.Diagnostics.Append(ds...) - - return - } - - mp4PseudoStreamingEnabled := false - if cdn.Mp4PseudoStreaming != nil && cdn.Mp4PseudoStreaming.Enabled != nil { - mp4PseudoStreamingEnabled = *cdn.Mp4PseudoStreaming.Enabled - } - - cdns = append(cdns, CdnSummaryModel{ - Id: types.Int64Value(int64(cdn.Id)), - Cnames: cnames, - CreationTime: types.StringValue(cdn.CreationTime.Format(time.DateTime)), - Label: types.StringValue(cdn.Label), - Note: util.NullableToStringValue(cdn.Note), - OriginId: util.NullableToStringValue(cdn.OriginId), - Url: types.StringValue(cdn.Url), - Mp4PseudoStreamingEnabled: types.BoolValue(mp4PseudoStreamingEnabled), - }) - } - - sort.SliceStable(cdns, func(i, j int) bool { - return cdns[i].Id.ValueInt64() < cdns[j].Id.ValueInt64() - }) - - resp.Diagnostics.Append(resp.State.Set(ctx, &CdnsModel{Cdns: cdns})...) -} diff --git a/internal/provider/cdns_data_source_test.go b/internal/provider/cdns_data_source_test.go deleted file mode 100644 index b1206de..0000000 --- a/internal/provider/cdns_data_source_test.go +++ /dev/null @@ -1,142 +0,0 @@ -package provider_test - -import ( - "context" - "fmt" - "sort" - "testing" - "time" - - "github.com/cdn77/cdn77-client-go" - "github.com/cdn77/terraform-provider-cdn77/internal/acctest" - "github.com/cdn77/terraform-provider-cdn77/internal/provider" - "github.com/cdn77/terraform-provider-cdn77/internal/util" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/oapi-codegen/nullable" -) - -func TestAccCdnsDataSource_All(t *testing.T) { - client := acctest.GetClient(t) - - originRequest := cdn77.OriginAddUrlJSONRequestBody{ - Host: "my-totally-random-custom-host.com", - Label: "random origin", - Scheme: "https", - } - originResponse, err := client.OriginAddUrlWithResponse(context.Background(), originRequest) - acctest.AssertResponseOk(t, "Failed to create Origin: %s", originResponse, err) - - originId := originResponse.JSON201.Id - - t.Cleanup(func() { - acctest.MustDeleteOrigin(t, client, provider.OriginTypeUrl, originId) - }) - - const cdn1Label = "some cdn" - const cdn1Note = "some note" - - cdn1Request := cdn77.CdnAddJSONRequestBody{ - Cnames: util.Pointer([]string{"my.cdn.cz", "another.cdn.com"}), - Label: cdn1Label, - Note: nullable.NewNullableWithValue(cdn1Note), - OriginId: originId, - } - cdn1Response, err := client.CdnAddWithResponse(context.Background(), cdn1Request) - acctest.AssertResponseOk(t, "Failed to create CDN: %s", cdn1Response, err) - - cdn1Id := cdn1Response.JSON201.Id - cdn1CreationTime := cdn1Response.JSON201.CreationTime.Format(time.DateTime) - cdn1Url := cdn1Response.JSON201.Url - - t.Cleanup(func() { - acctest.MustDeleteCdn(t, client, cdn1Id) - }) - - const cdn2Label = "another cdn" - - cdn2Request := cdn77.CdnAddJSONRequestBody{Label: cdn2Label, OriginId: originId} - cdn2Response, err := client.CdnAddWithResponse(context.Background(), cdn2Request) - acctest.AssertResponseOk(t, "Failed to create CDN: %s", cdn2Response, err) - - cdn2Id := cdn2Response.JSON201.Id - cdn2CreationTime := cdn2Response.JSON201.CreationTime.Format(time.DateTime) - cdn2Url := cdn2Response.JSON201.Url - - t.Cleanup(func() { - acctest.MustDeleteCdn(t, client, cdn2Id) - }) - - rsc := "data.cdn77_cdns.all" - key := func(i int, k string) string { - return fmt.Sprintf("cdns.%d.%s", i, k) - } - cdnIdAndTestCheckFnFactory := []struct { - id int - factory func(i int) []resource.TestCheckFunc - }{ - {id: cdn1Id, factory: func(i int) []resource.TestCheckFunc { - return []resource.TestCheckFunc{ - resource.TestCheckResourceAttr(rsc, key(i, "id"), fmt.Sprintf("%d", cdn1Id)), - resource.TestCheckResourceAttr(rsc, key(i, "cnames.#"), "2"), - resource.TestCheckTypeSetElemAttr(rsc, key(i, "cnames.*"), "my.cdn.cz"), - resource.TestCheckTypeSetElemAttr(rsc, key(i, "cnames.*"), "another.cdn.com"), - resource.TestCheckResourceAttr(rsc, key(i, "creation_time"), cdn1CreationTime), - resource.TestCheckResourceAttr(rsc, key(i, "label"), cdn1Label), - resource.TestCheckResourceAttr(rsc, key(i, "note"), cdn1Note), - resource.TestCheckResourceAttr(rsc, key(i, "origin_id"), originId), - resource.TestCheckResourceAttr(rsc, key(i, "url"), cdn1Url), - resource.TestCheckResourceAttr(rsc, key(i, "mp4_pseudo_streaming_enabled"), "false"), - } - }}, - {id: cdn2Id, factory: func(i int) []resource.TestCheckFunc { - return []resource.TestCheckFunc{ - resource.TestCheckResourceAttr(rsc, key(i, "id"), fmt.Sprintf("%d", cdn2Id)), - resource.TestCheckResourceAttr(rsc, key(i, "cnames.#"), "0"), - resource.TestCheckResourceAttr(rsc, key(i, "creation_time"), cdn2CreationTime), - resource.TestCheckResourceAttr(rsc, key(i, "label"), cdn2Label), - resource.TestCheckResourceAttr(rsc, key(i, "origin_id"), originId), - resource.TestCheckResourceAttr(rsc, key(i, "url"), cdn2Url), - resource.TestCheckResourceAttr(rsc, key(i, "mp4_pseudo_streaming_enabled"), "false"), - - resource.TestCheckNoResourceAttr(rsc, key(i, "note")), - } - }}, - } - - sort.SliceStable(cdnIdAndTestCheckFnFactory, func(i, j int) bool { - return cdnIdAndTestCheckFnFactory[i].id < cdnIdAndTestCheckFnFactory[j].id - }) - - testCheckFns := []resource.TestCheckFunc{resource.TestCheckResourceAttr(rsc, "cdns.#", "2")} - - for i, x := range cdnIdAndTestCheckFnFactory { - testCheckFns = append(testCheckFns, x.factory(i)...) - } - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctest.GetProviderFactories(), - Steps: []resource.TestStep{ - { - Config: cdnsDataSourceConfig, - Check: resource.ComposeAggregateTestCheckFunc(testCheckFns...), - }, - }, - }) -} - -func TestAccCdnsDataSource_Empty(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctest.GetProviderFactories(), - Steps: []resource.TestStep{ - { - Config: cdnsDataSourceConfig, - Check: resource.TestCheckResourceAttr("data.cdn77_cdns.all", "cdns.#", "0"), - }, - }, - }) -} - -const cdnsDataSourceConfig = ` -data "cdn77_cdns" "all" { -} -` diff --git a/internal/provider/object_storages/all.go b/internal/provider/object_storages/all.go new file mode 100644 index 0000000..df667f5 --- /dev/null +++ b/internal/provider/object_storages/all.go @@ -0,0 +1,55 @@ +package object_storages + +import ( + "context" + "sort" + + "github.com/cdn77/cdn77-client-go/v2" + "github.com/cdn77/terraform-provider-cdn77/internal/provider/shared" + "github.com/cdn77/terraform-provider-cdn77/internal/util" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/oapi-codegen/nullable" +) + +var _ datasource.DataSourceWithConfigure = &DataSource{} + +type DataSource struct { + *util.BaseDataSource +} + +func (d *DataSource) Read(ctx context.Context, _ datasource.ReadRequest, resp *datasource.ReadResponse) { + const errMessage = "Failed to fetch list of all Object Storages" + + diags := &resp.Diagnostics + + response, err := d.Client.ObjectStorageClusterListWithResponse(ctx) + if err != nil { + resp.Diagnostics.AddError(errMessage, err.Error()) + + return + } + + util.ProcessResponse(diags, response, errMessage, response.JSON200, func(list *cdn77.ObjectStorageClusters) { + objectStorages := make([]Model, 0, len(*list)) + + for _, os := range *list { + port := nullable.NewNullNullable[int]() + if os.Port != nil { + port.Set(*os.Port) + } + + objectStorages = append(objectStorages, Model{ + UrlModel: shared.NewUrlModel(ctx, os.Scheme, os.Host, port, nullable.NewNullNullable[string]()), + Id: types.StringValue(os.Id), + Label: types.StringValue(os.Label), + }) + } + + sort.SliceStable(objectStorages, func(i, j int) bool { + return objectStorages[i].Id.ValueString() < objectStorages[j].Id.ValueString() + }) + + resp.Diagnostics.Append(resp.State.Set(ctx, AllModel{Clusters: objectStorages})...) + }) +} diff --git a/internal/provider/object_storages/all_test.go b/internal/provider/object_storages/all_test.go new file mode 100644 index 0000000..a6f04bd --- /dev/null +++ b/internal/provider/object_storages/all_test.go @@ -0,0 +1,52 @@ +package object_storages_test + +import ( + "regexp" + "testing" + + "github.com/cdn77/terraform-provider-cdn77/internal/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestAccObjectStoragesDataSource(t *testing.T) { + acctest.Run(t, nil, resource.TestStep{ + Config: objectStoragesDataSourceConfig, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("data.cdn77_object_storages.all", + tfjsonpath.New("clusters"), + knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "id": knownvalue.StringRegexp(regexp.MustCompile( + `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`, + )), + "label": knownvalue.StringExact("EU"), + "url": knownvalue.StringRegexp(regexp.MustCompile( + `^https://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}(:\d+)?$`, + )), + "url_parts": knownvalue.ObjectExact(map[string]knownvalue.Check{ + "scheme": knownvalue.StringExact("https"), + "host": knownvalue.StringRegexp(regexp.MustCompile( + `^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$`, + )), + "port": knownvalue.Int64Exact(443), + "base_path": knownvalue.Null(), + }), + }), + }), + ), + }, + }) +} + +const objectStoragesDataSourceConfig = ` +data "cdn77_object_storages" "all" { +} + +locals { + eu_cluster_id = one([for os in data.cdn77_object_storages.all.clusters : os.id if os.label == "EU"]) + us_cluster_id = one([for os in data.cdn77_object_storages.all.clusters : os.id if os.label == "US"]) +} +` diff --git a/internal/provider/object_storages/schema.go b/internal/provider/object_storages/schema.go new file mode 100644 index 0000000..843e152 --- /dev/null +++ b/internal/provider/object_storages/schema.go @@ -0,0 +1,46 @@ +package object_storages + +import ( + "github.com/cdn77/terraform-provider-cdn77/internal/provider/shared" + "github.com/cdn77/terraform-provider-cdn77/internal/util" + ds_schema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + rsc_schema "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type AllModel struct { + Clusters []Model `tfsdk:"clusters"` +} + +type Model struct { + shared.UrlModel + + Id types.String `tfsdk:"id"` + Label types.String `tfsdk:"label"` +} + +func CreateSchema() ds_schema.Schema { + s := util.NewResourceDataSourceSchemaConverter().Convert(shared.WithComputedUrlSchemaAttrs(rsc_schema.Schema{ + Attributes: map[string]rsc_schema.Attribute{ + "id": rsc_schema.StringAttribute{ + Description: "ID (UUID) of the Object Storage cluster", + Computed: true, + }, + "label": rsc_schema.StringAttribute{ + Computed: true, + Description: "Label of the Object Storage cluster", + }, + }, + })) + + return ds_schema.Schema{ + Attributes: map[string]ds_schema.Attribute{ + "clusters": ds_schema.ListNestedAttribute{ + NestedObject: ds_schema.NestedAttributeObject{Attributes: s.Attributes}, + Computed: true, + Description: "List of all Object Storage clusters", + }, + }, + Description: "Object Storages data source allows you to read all available Object Storage clusters", + } +} diff --git a/internal/provider/object_storages_data_source.go b/internal/provider/object_storages_data_source.go deleted file mode 100644 index 91acf16..0000000 --- a/internal/provider/object_storages_data_source.go +++ /dev/null @@ -1,119 +0,0 @@ -package provider - -import ( - "context" - "sort" - - "github.com/cdn77/cdn77-client-go" - "github.com/cdn77/terraform-provider-cdn77/internal/util" - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" -) - -type ObjectStoragesModel struct { - Clusters []ObjectStorageModel `tfsdk:"clusters"` -} - -type ObjectStorageModel struct { - Id types.String `tfsdk:"id"` - Host types.String `tfsdk:"host"` - Label types.String `tfsdk:"label"` - Port types.Int64 `tfsdk:"port"` - Scheme types.String `tfsdk:"scheme"` -} - -var _ datasource.DataSourceWithConfigure = &ObjectStoragesDataSource{} - -func NewObjectStoragesDataSource() datasource.DataSource { - return &ObjectStoragesDataSource{} -} - -type ObjectStoragesDataSource struct { - client cdn77.ClientWithResponsesInterface -} - -func (*ObjectStoragesDataSource) Metadata( - _ context.Context, - req datasource.MetadataRequest, - resp *datasource.MetadataResponse, -) { - resp.TypeName = req.ProviderTypeName + "_object_storages" -} - -func (*ObjectStoragesDataSource) Schema( - _ context.Context, - _ datasource.SchemaRequest, - resp *datasource.SchemaResponse, -) { - resp.Schema = schema.Schema{ - Attributes: map[string]schema.Attribute{ - "clusters": schema.ListNestedAttribute{ - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "ID (UUID) of the Object Storage cluster", - Computed: true, - }, - "host": schema.StringAttribute{ - Computed: true, - }, - "label": schema.StringAttribute{ - Computed: true, - }, - "port": schema.Int64Attribute{ - Computed: true, - }, - "scheme": schema.StringAttribute{ - Computed: true, - }, - }, - }, - Computed: true, - Description: "List of all Object Storage clusters", - }, - }, - Description: "Object Storages data source allows you to read all available Object Storage clusters", - } -} - -func (d *ObjectStoragesDataSource) Configure( - _ context.Context, - req datasource.ConfigureRequest, - resp *datasource.ConfigureResponse, -) { - resp.Diagnostics.Append(util.MaybeSetClient(req.ProviderData, &d.client)) -} - -func (d *ObjectStoragesDataSource) Read(ctx context.Context, _ datasource.ReadRequest, resp *datasource.ReadResponse) { - const errMessage = "Failed to fetch list of all Object Storages" - - response, err := d.client.ObjectStorageClusterListWithResponse(ctx) - if err != nil { - resp.Diagnostics.AddError(errMessage, err.Error()) - - return - } - - if !util.CheckResponse(&resp.Diagnostics, errMessage, response, response.JSONDefault) { - return - } - - objectStorages := make([]ObjectStorageModel, 0, len(*response.JSON200)) - - for _, objectStorage := range *response.JSON200 { - objectStorages = append(objectStorages, ObjectStorageModel{ - Id: types.StringValue(objectStorage.Id), - Host: types.StringValue(objectStorage.Host), - Label: types.StringValue(objectStorage.Label), - Port: util.IntPointerToInt64Value(objectStorage.Port), - Scheme: types.StringValue(objectStorage.Scheme), - }) - } - - sort.SliceStable(objectStorages, func(i, j int) bool { - return objectStorages[i].Id.ValueString() < objectStorages[j].Id.ValueString() - }) - - resp.Diagnostics.Append(resp.State.Set(ctx, &ObjectStoragesModel{Clusters: objectStorages})...) -} diff --git a/internal/provider/object_storages_data_source_test.go b/internal/provider/object_storages_data_source_test.go deleted file mode 100644 index e06dd38..0000000 --- a/internal/provider/object_storages_data_source_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package provider_test - -import ( - "regexp" - "testing" - - "github.com/cdn77/terraform-provider-cdn77/internal/acctest" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/knownvalue" - "github.com/hashicorp/terraform-plugin-testing/statecheck" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" -) - -func TestAccObjectStoragesDataSource_All(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctest.GetProviderFactories(), - Steps: []resource.TestStep{ - { - Config: objectStoragesDataSourceConfig, - ConfigStateChecks: []statecheck.StateCheck{ - statecheck.ExpectKnownValue("data.cdn77_object_storages.all", - tfjsonpath.New("clusters"), - knownvalue.SetPartial([]knownvalue.Check{ - knownvalue.ObjectExact(map[string]knownvalue.Check{ - "id": knownvalue.StringRegexp(regexp.MustCompile( - `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`, - )), - "host": knownvalue.StringRegexp(regexp.MustCompile( - `^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$`, - )), - "label": knownvalue.StringExact("EU"), - "port": knownvalue.Int64Exact(443), - "scheme": knownvalue.StringExact("https"), - }), - }), - ), - }, - }, - }, - }) -} - -const objectStoragesDataSourceConfig = ` -data "cdn77_object_storages" "all" { -} - -locals { - eu_cluster_id = one([for os in data.cdn77_object_storages.all.clusters : os.id if os.label == "EU"]) - us_cluster_id = one([for os in data.cdn77_object_storages.all.clusters : os.id if os.label == "US"]) -} -` diff --git a/internal/provider/origin/all.go b/internal/provider/origin/all.go new file mode 100644 index 0000000..30110ab --- /dev/null +++ b/internal/provider/origin/all.go @@ -0,0 +1,140 @@ +package origin + +import ( + "cmp" + "context" + "fmt" + "slices" + "strings" + + "github.com/cdn77/cdn77-client-go/v2" + "github.com/cdn77/terraform-provider-cdn77/internal/provider/shared" + "github.com/cdn77/terraform-provider-cdn77/internal/util" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/oapi-codegen/nullable" +) + +type AllModel struct { + Aws []AwsBaseModel `tfsdk:"aws"` + ObjectStorage []ObjectStorageBaseModel `tfsdk:"object_storage"` + Url []UrlModel `tfsdk:"url"` +} + +var _ datasource.DataSourceWithConfigure = &AllDataSource{} + +type AllDataSource struct { + client cdn77.ClientWithResponsesInterface +} + +func NewAllDataSource() datasource.DataSource { + return &AllDataSource{} +} + +func (*AllDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = strings.Join([]string{req.ProviderTypeName, "origins"}, "_") +} + +func (*AllDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + converter := util.NewResourceDataSourceSchemaConverter() + awsAttrs := converter.Convert(CreateAwsBaseResourceSchema()).Attributes + objectStorageAttrs := converter.Convert(CreateObjectStorageBaseResourceSchema()).Attributes + urlAttrs := converter.Convert(CreateUrlResourceSchema()).Attributes + + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "aws": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{Attributes: awsAttrs}, + Computed: true, + Description: "List of all AWS Origins", + }, + "object_storage": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{Attributes: objectStorageAttrs}, + Computed: true, + Description: "List of all Object Storage Origins", + }, + "url": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{Attributes: urlAttrs}, + Computed: true, + Description: "List of all URL Origins", + }, + }, + Description: "Origins data source allows you to read all your Origins", + } +} + +func (d *AllDataSource) Configure( + _ context.Context, + req datasource.ConfigureRequest, + resp *datasource.ConfigureResponse, +) { + resp.Diagnostics.Append(util.MaybeSetClient(req.ProviderData, &d.client)) +} + +func (d *AllDataSource) Read(ctx context.Context, _ datasource.ReadRequest, resp *datasource.ReadResponse) { + const errMessage = "Failed to fetch list of all Origins" + + diags := &resp.Diagnostics + + response, err := d.client.OriginListWithResponse(ctx) + if err != nil { + diags.AddError(errMessage, err.Error()) + + return + } + + util.ProcessResponse(diags, response, errMessage, response.JSON200, func(list *cdn77.OriginList) { + data := AllModel{Aws: []AwsBaseModel{}, ObjectStorage: []ObjectStorageBaseModel{}, Url: []UrlModel{}} + + for _, item := range *list { + origin, err := item.ValueByDiscriminator() + + switch o := origin.(type) { + case cdn77.S3OriginDetail: + data.Aws = append(data.Aws, AwsBaseModel{ + SharedModel: NewSharedModel(types.StringValue(o.Id), o.Label, o.Note), + UrlModel: shared.NewUrlModel(ctx, string(o.Scheme), o.Host, o.Port, o.BaseDir), + AccessKeyId: util.NullableToStringValue(o.AwsAccessKeyId), + Region: util.NullableToStringValue(o.AwsRegion), + }) + case cdn77.ObjectStorageOriginDetail: + data.ObjectStorage = append(data.ObjectStorage, ObjectStorageBaseModel{ + SharedModel: NewSharedModel(types.StringValue(o.Id), o.Label, o.Note), + UrlModel: shared.NewUrlModel( + ctx, + string(o.Scheme), + o.Host, + o.Port, + nullable.NewNullNullable[string](), + ), + BucketName: types.StringValue(o.BucketName), + Usage: &ObjectStorageUsageModel{ + Files: util.IntPointerToInt64Value(o.Usage.FileCount), + SizeBytes: util.IntPointerToInt64Value(o.Usage.SizeBytes), + }, + }) + case cdn77.UrlOriginDetail: + data.Url = append(data.Url, UrlModel{ + SharedModel: NewSharedModel(types.StringValue(o.Id), o.Label, o.Note), + UrlModel: shared.NewUrlModel(ctx, string(o.Scheme), o.Host, o.Port, o.BaseDir), + }) + default: + diags.AddError(errMessage, fmt.Sprintf("Failed to convert Origin to a specific type: %s", err)) + + return + } + } + + slices.SortStableFunc(data.Aws, func(a, b AwsBaseModel) int { + return cmp.Compare(a.Id.ValueString(), b.Id.ValueString()) + }) + slices.SortStableFunc(data.ObjectStorage, func(a, b ObjectStorageBaseModel) int { + return cmp.Compare(a.Id.ValueString(), b.Id.ValueString()) + }) + slices.SortStableFunc(data.Url, func(a, b UrlModel) int { + return cmp.Compare(a.Id.ValueString(), b.Id.ValueString()) + }) + diags.Append(resp.State.Set(ctx, data)...) + }) +} diff --git a/internal/provider/origin/all_test.go b/internal/provider/origin/all_test.go new file mode 100644 index 0000000..1ecd10a --- /dev/null +++ b/internal/provider/origin/all_test.go @@ -0,0 +1,164 @@ +package origin_test + +import ( + "context" + "strconv" + "testing" + + "github.com/cdn77/cdn77-client-go/v2" + "github.com/cdn77/terraform-provider-cdn77/internal/acctest" + "github.com/cdn77/terraform-provider-cdn77/internal/provider/origin" + "github.com/cdn77/terraform-provider-cdn77/internal/provider/shared" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/oapi-codegen/nullable" +) + +func TestAccOrigin_AllDataSource(t *testing.T) { + const rsc = "data.cdn77_origins.all" + client := acctest.GetClient(t) + + const urlLabel = "random origin" + const urlNote = "some note" + const urlScheme = "https" + const urlHost = "my-totally-random-custom-host.com" + const urlUrl = "https://my-totally-random-custom-host.com" + urlRequest := cdn77.OriginCreateUrlJSONRequestBody{ + Label: urlLabel, + Note: nullable.NewNullableWithValue(urlNote), + Scheme: urlScheme, + Host: urlHost, + } + + urlResponse, err := client.OriginCreateUrlWithResponse(context.Background(), urlRequest) + acctest.AssertResponseOk(t, "Failed to create Origin: %s", urlResponse, err) + + urlId := urlResponse.JSON201.Id + + t.Cleanup(func() { + acctest.MustDeleteOrigin(t, client, origin.TypeUrl, urlId) + }) + + const awsLabel = "another origin" + const awsScheme = "http" + const awsHost = "some-other-totally-random-custom-host.com" + const awsBasePath = "some-dir" + const awsUrl = "http://some-other-totally-random-custom-host.com/some-dir" + const awsAccessKeyId = "someKeyId" + const awsAccessKeySecret = "someKeySecret" + const awsRegion = "eu" + awsRequest := cdn77.OriginCreateAwsJSONRequestBody{ + AwsAccessKeyId: nullable.NewNullableWithValue(awsAccessKeyId), + AwsAccessKeySecret: nullable.NewNullableWithValue(awsAccessKeySecret), + AwsRegion: nullable.NewNullableWithValue(awsRegion), + BaseDir: nullable.NewNullableWithValue(awsBasePath), + Host: awsHost, + Label: awsLabel, + Scheme: awsScheme, + } + + awsResponse, err := client.OriginCreateAwsWithResponse(context.Background(), awsRequest) + acctest.AssertResponseOk(t, "Failed to create Origin: %s", awsResponse, err) + + awsId := awsResponse.JSON201.Id + + t.Cleanup(func() { + acctest.MustDeleteOrigin(t, client, origin.TypeAws, awsId) + }) + + const osLabel = "yet another origin" + const osNote = "just a note" + osBucketName := "my-bucket-" + uuid.New().String() + osRequest := cdn77.OriginCreateObjectStorageJSONRequestBody{ + Acl: cdn77.AuthenticatedRead, + BucketName: osBucketName, + ClusterId: "842b5641-b641-4723-ac81-f8cc286e288f", + Label: osLabel, + Note: nullable.NewNullableWithValue(osNote), + } + + osResponse, err := client.OriginCreateObjectStorageWithResponse(context.Background(), osRequest) + acctest.AssertResponseOk(t, "Failed to create Origin: %s", osResponse, err) + + osId := osResponse.JSON201.Id + osScheme := string(osResponse.JSON201.Scheme) + osHost := osResponse.JSON201.Host + osPort := osResponse.JSON201.Port + osUrlModel := shared.NewUrlModel(context.Background(), osScheme, osHost, osPort, nullable.NewNullNullable[string]()) + osUrl := osUrlModel.Url.ValueString() + + t.Cleanup(func() { + acctest.MustDeleteOrigin(t, client, origin.TypeObjectStorage, osId) + }) + + acctest.Run(t, nil, resource.TestStep{ + Config: allDataSourceConfig, + Check: resource.ComposeAggregateTestCheckFunc( + // URL origin + resource.TestCheckResourceAttr(rsc, "url.#", "1"), + resource.TestCheckResourceAttr(rsc, "url.0.id", urlId), + resource.TestCheckResourceAttr(rsc, "url.0.label", urlLabel), + resource.TestCheckResourceAttr(rsc, "url.0.note", urlNote), + resource.TestCheckResourceAttr(rsc, "url.0.url", urlUrl), + resource.TestCheckResourceAttr(rsc, "url.0.url_parts.scheme", urlScheme), + resource.TestCheckResourceAttr(rsc, "url.0.url_parts.host", urlHost), + resource.TestCheckNoResourceAttr(rsc, "url.0.url_parts.port"), + resource.TestCheckNoResourceAttr(rsc, "url.0.url_parts.base_path"), + + // AWS origin + resource.TestCheckResourceAttr(rsc, "aws.#", "1"), + resource.TestCheckResourceAttr(rsc, "aws.0.id", awsId), + resource.TestCheckResourceAttr(rsc, "aws.0.label", awsLabel), + resource.TestCheckResourceAttr(rsc, "aws.0.url", awsUrl), + resource.TestCheckResourceAttr(rsc, "aws.0.url_parts.scheme", awsScheme), + resource.TestCheckResourceAttr(rsc, "aws.0.url_parts.host", awsHost), + resource.TestCheckResourceAttr(rsc, "aws.0.url_parts.base_path", awsBasePath), + resource.TestCheckResourceAttr(rsc, "aws.0.access_key_id", awsAccessKeyId), + resource.TestCheckResourceAttr(rsc, "aws.0.region", awsRegion), + resource.TestCheckNoResourceAttr(rsc, "aws.0.note"), + resource.TestCheckNoResourceAttr(rsc, "aws.0.url_parts.port"), + resource.TestCheckNoResourceAttr(rsc, "aws.0.access_key_secret"), + + // Object Storage Origin + resource.TestCheckResourceAttr(rsc, "object_storage.#", "1"), + resource.TestCheckResourceAttr(rsc, "object_storage.0.id", osId), + resource.TestCheckResourceAttr(rsc, "object_storage.0.label", osLabel), + resource.TestCheckResourceAttr(rsc, "object_storage.0.note", osNote), + resource.TestCheckResourceAttr(rsc, "object_storage.0.url", osUrl), + resource.TestCheckResourceAttr(rsc, "object_storage.0.url_parts.scheme", osScheme), + resource.TestCheckResourceAttr(rsc, "object_storage.0.url_parts.host", osHost), + func(state *terraform.State) error { + if port, err := osPort.Get(); err == nil { + return resource.TestCheckResourceAttr( + rsc, "object_storage.0.url_parts.port", strconv.Itoa(port), + )(state) + } + + return resource.TestCheckNoResourceAttr(rsc, "object_storage.0.url_parts.port")(state) + }, + resource.TestCheckResourceAttr(rsc, "object_storage.0.bucket_name", osBucketName), + resource.TestCheckNoResourceAttr(rsc, "object_storage.0.url_parts.base_path"), + resource.TestCheckNoResourceAttr(rsc, "object_storage.0.acl"), + resource.TestCheckNoResourceAttr(rsc, "object_storage.0.cluster_id"), + resource.TestCheckNoResourceAttr(rsc, "object_storage.0.access_key_id"), + resource.TestCheckNoResourceAttr(rsc, "object_storage.0.access_key_secret"), + ), + }) +} + +func TestAccOrigin_AllDataSource_Empty(t *testing.T) { + acctest.Run(t, nil, resource.TestStep{ + Config: allDataSourceConfig, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.cdn77_origins.all", "aws.#", "0"), + resource.TestCheckResourceAttr("data.cdn77_origins.all", "object_storage.#", "0"), + resource.TestCheckResourceAttr("data.cdn77_origins.all", "url.#", "0"), + ), + }) +} + +const allDataSourceConfig = ` +data "cdn77_origins" "all" { +} +` diff --git a/internal/provider/origin/aws.go b/internal/provider/origin/aws.go new file mode 100644 index 0000000..928ac15 --- /dev/null +++ b/internal/provider/origin/aws.go @@ -0,0 +1,208 @@ +package origin + +import ( + "context" + "fmt" + "strings" + + "github.com/cdn77/cdn77-client-go/v2" + "github.com/cdn77/terraform-provider-cdn77/internal/provider/shared" + "github.com/cdn77/terraform-provider-cdn77/internal/util" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.ResourceWithConfigure = &AwsResource{} + _ resource.ResourceWithImportState = &AwsResource{} + _ resource.ResourceWithMoveState = &AwsResource{} +) + +type AwsResource struct { + *util.BaseResource +} + +func (r *AwsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + diags := &resp.Diagnostics + var data AwsModel + + if diags.Append(req.Plan.Get(ctx, &data)...); diags.HasError() { + return + } + + const errMessage = "Failed to create AWS Origin" + + scheme, host, port, basePath := data.UrlModel.Parts(ctx) + request := cdn77.OriginCreateAwsJSONRequestBody{ + Label: data.Label.ValueString(), + Note: util.StringValueToNullable(data.Note), + Scheme: cdn77.OriginScheme(scheme), + Host: host, + Port: port, + BaseDir: basePath, + AwsAccessKeyId: util.StringValueToNullable(data.AccessKeyId), + AwsAccessKeySecret: util.StringValueToNullable(data.AccessKeySecret), + AwsRegion: util.StringValueToNullable(data.Region), + } + + response, err := r.Client.OriginCreateAwsWithResponse(ctx, request) + if err != nil { + diags.AddError(errMessage, err.Error()) + + return + } + + util.ProcessResponse(diags, response, errMessage, response.JSON201, func(detail *cdn77.S3OriginDetail) { + data.Id = types.StringValue(detail.Id) + + diags.Append(resp.State.Set(ctx, data)...) + }) +} + +func (r *AwsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + diags := &resp.Diagnostics + var data AwsModel + + if diags.Append(req.Plan.Get(ctx, &data)...); diags.HasError() { + return + } + + const errMessage = "Failed to update AWS Origin" + + scheme, host, port, basePath := data.UrlModel.Parts(ctx) + request := cdn77.OriginEditAwsJSONRequestBody{ + Label: data.Label.ValueStringPointer(), + Note: util.StringValueToNullable(data.Note), + Scheme: util.Pointer(cdn77.OriginScheme(scheme)), + Host: &host, + Port: port, + BaseDir: basePath, + AwsAccessKeyId: util.StringValueToNullable(data.AccessKeyId), + AwsAccessKeySecret: util.StringValueToNullable(data.AccessKeySecret), + AwsRegion: util.StringValueToNullable(data.Region), + } + + response, err := r.Client.OriginEditAwsWithResponse(ctx, data.Id.ValueString(), request) + if err != nil { + diags.AddError(errMessage, err.Error()) + + return + } + + util.ProcessEmptyResponse(diags, response, errMessage, func() { + diags.Append(resp.State.Set(ctx, data)...) + }) +} + +func (r *AwsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + diags := &resp.Diagnostics + var data AwsModel + + if diags.Append(req.State.Get(ctx, &data)...); diags.HasError() { + return + } + + const errMessage = "Failed to delete AWS Origin" + + response, err := r.Client.OriginDeleteAwsWithResponse(ctx, data.Id.ValueString()) + if err != nil { + diags.AddError(errMessage, err.Error()) + + return + } + + util.ValidateDeletionResponse(diags, response, errMessage) +} + +func (*AwsResource) ImportState( + ctx context.Context, + req resource.ImportStateRequest, + resp *resource.ImportStateResponse, +) { + idParts := strings.Split(req.ID, ",") + if len(idParts) < 2 { + resp.Diagnostics.AddError( + "Invalid Import Identifier", + fmt.Sprintf("Expected: ,\nGot: %q", req.ID), + ) + + return + } + + id, accessKeySecret := idParts[0], idParts[1] + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...) + + if accessKeySecret != "" { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("access_key_secret"), accessKeySecret)...) + } +} + +func (r *AwsResource) MoveState(context.Context) []resource.StateMover { + deprecatedOriginSchema := CreateDeprecatedOriginResourceSchema() + + return []resource.StateMover{ + { + SourceSchema: &deprecatedOriginSchema, + StateMover: func(ctx context.Context, req resource.MoveStateRequest, resp *resource.MoveStateResponse) { + if req.SourceTypeName != DeprecatedOriginResourceType || + !strings.HasSuffix(req.SourceProviderAddress, "cdn77/cdn77") { + return + } + + diags := &resp.Diagnostics + var oldModel DeprecatedOriginModel + + if diags.Append(req.SourceState.Get(ctx, &oldModel)...); diags.HasError() { + return + } + + if oldModel.Type.ValueString() != TypeAws { + diags.AddError( + "Unable to Move Resource State", + fmt.Sprintf( + "Only %q resources with type=%q can be moved to %q.\nLabel: %s\nType: %s\n", + DeprecatedOriginResourceType, + TypeAws, + r.FullName(), + oldModel.Label.ValueString(), + oldModel.Type.ValueString(), + ), + ) + + return + } + + model := AwsModel{ + AwsBaseModel: AwsBaseModel{ + SharedModel: NewSharedModel( + oldModel.Id, + oldModel.Label.ValueString(), + util.StringValueToNullable(oldModel.Note), + ), + UrlModel: shared.NewUrlModel( + ctx, + oldModel.Scheme.ValueString(), + oldModel.Host.ValueString(), + util.Int64ValueToNullable[int](oldModel.Port), + util.StringValueToNullable(oldModel.BaseDir), + ), + AccessKeyId: oldModel.AwsAccessKeyId, + Region: oldModel.AwsRegion, + }, + AccessKeySecret: oldModel.AwsAccessKeySecret, + } + + diags.Append(resp.TargetState.Set(ctx, model)...) + }, + }, + } +} + +var _ datasource.DataSourceWithConfigure = &AwsDataSource{} + +type AwsDataSource struct { + *util.BaseDataSource +} diff --git a/internal/provider/origin/aws_reader.go b/internal/provider/origin/aws_reader.go new file mode 100644 index 0000000..9add59b --- /dev/null +++ b/internal/provider/origin/aws_reader.go @@ -0,0 +1,53 @@ +package origin + +import ( + "context" + "fmt" + + "github.com/cdn77/cdn77-client-go/v2" + "github.com/cdn77/terraform-provider-cdn77/internal/provider/shared" + "github.com/cdn77/terraform-provider-cdn77/internal/util" + "github.com/hashicorp/terraform-plugin-framework/diag" +) + +type AwsReader struct{} + +func (*AwsReader) ErrMessage() string { + return "Failed to fetch AWS Origin" +} + +func (*AwsReader) Fetch( + ctx context.Context, + client cdn77.ClientWithResponsesInterface, + model AwsModel, +) (*cdn77.OriginDetailAwsResponse, *cdn77.S3OriginDetail, error) { + response, err := client.OriginDetailAwsWithResponse(ctx, model.Id.ValueString()) + if err != nil { + return nil, nil, err + } + + return response, response.JSON200, nil +} + +func (r *AwsReader) Process( + ctx context.Context, + model AwsModel, + detail *cdn77.S3OriginDetail, + diags *diag.Diagnostics, +) AwsModel { + if detail.Type != TypeAws { + diags.AddError(r.ErrMessage(), fmt.Sprintf("Origin with id=\"%s\" is not an AWS Origin", detail.Id)) + + return model + } + + return AwsModel{ + AwsBaseModel: AwsBaseModel{ + SharedModel: NewSharedModel(model.Id, detail.Label, detail.Note), + UrlModel: shared.NewUrlModel(ctx, string(detail.Scheme), detail.Host, detail.Port, detail.BaseDir), + AccessKeyId: util.NullableToStringValue(detail.AwsAccessKeyId), + Region: util.NullableToStringValue(detail.AwsRegion), + }, + AccessKeySecret: model.AccessKeySecret, + } +} diff --git a/internal/provider/origin/aws_schema.go b/internal/provider/origin/aws_schema.go new file mode 100644 index 0000000..fbdae92 --- /dev/null +++ b/internal/provider/origin/aws_schema.go @@ -0,0 +1,60 @@ +package origin + +import ( + "github.com/cdn77/terraform-provider-cdn77/internal/provider/shared" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type AwsModel struct { + AwsBaseModel + + AccessKeySecret types.String `tfsdk:"access_key_secret"` +} + +type AwsBaseModel struct { + SharedModel + shared.UrlModel + + AccessKeyId types.String `tfsdk:"access_key_id"` + Region types.String `tfsdk:"region"` +} + +func CreateAwsResourceSchema() schema.Schema { + s := CreateAwsBaseResourceSchema() + s.Attributes["access_key_secret"] = schema.StringAttribute{ + Description: "AWS access key secret", + Optional: true, + Sensitive: true, + Validators: []validator.String{ + stringvalidator.AlsoRequires(path.MatchRoot("access_key_id"), path.MatchRoot("region")), + }, + } + + return s +} + +func CreateAwsBaseResourceSchema() schema.Schema { + return WithSharedSchemaAttrs(shared.WithUrlSchemaAttrs(schema.Schema{ + MarkdownDescription: "AWS Origin resource allows you to manage your AWS Origins", + Attributes: map[string]schema.Attribute{ + "access_key_id": schema.StringAttribute{ + Description: "AWS access key ID", + Optional: true, + Validators: []validator.String{ + stringvalidator.AlsoRequires(path.MatchRoot("access_key_secret"), path.MatchRoot("region")), + }, + }, + "region": schema.StringAttribute{ + Description: "AWS region", + Optional: true, + Validators: []validator.String{ + stringvalidator.AlsoRequires(path.MatchRoot("access_key_secret"), path.MatchRoot("access_key_id")), + }, + }, + }, + })) +} diff --git a/internal/provider/origin/aws_test.go b/internal/provider/origin/aws_test.go new file mode 100644 index 0000000..9aac50d --- /dev/null +++ b/internal/provider/origin/aws_test.go @@ -0,0 +1,378 @@ +package origin_test + +import ( + "context" + "errors" + "fmt" + "regexp" + "strconv" + "testing" + + "github.com/cdn77/cdn77-client-go/v2" + "github.com/cdn77/terraform-provider-cdn77/internal/acctest" + "github.com/cdn77/terraform-provider-cdn77/internal/provider/origin" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/oapi-codegen/nullable" +) + +func TestAccOrigin_AwsResource(t *testing.T) { + const rsc = "cdn77_origin_aws.aws" + client := acctest.GetClient(t) + var originId string + + acctest.Run(t, acctest.CheckOriginDestroyed(client, origin.TypeAws), + resource.TestStep{ + Config: `resource "cdn77_origin_aws" "aws" { + label = "some label" + url = "http://my-totally-random-custom-host.com" + url_parts = { + scheme = "http" + host = "my-totally-random-custom-host.com" + } + }`, + ExpectError: regexp.MustCompile(`(?s)Invalid Attribute Combination.*` + + `attributes specified when one \(and only one\) of \[url,url_parts\] is required`), + PlanOnly: true, + }, + resource.TestStep{ + Config: `resource "cdn77_origin_aws" "aws" { + label = "some label" + url = "http://my-totally-random-custom-host.com" + }`, + ConfigPlanChecks: acctest.ConfigPlanChecks(rsc, plancheck.ResourceActionCreate), + Check: resource.ComposeAggregateTestCheckFunc( + acctest.CheckAndAssignAttr(rsc, "id", &originId), + resource.TestCheckResourceAttr(rsc, "label", "some label"), + resource.TestCheckResourceAttr(rsc, "url", "http://my-totally-random-custom-host.com"), + resource.TestCheckResourceAttr(rsc, "url_parts.scheme", "http"), + resource.TestCheckResourceAttr(rsc, "url_parts.host", "my-totally-random-custom-host.com"), + resource.TestCheckNoResourceAttr(rsc, "note"), + resource.TestCheckNoResourceAttr(rsc, "url_parts.port"), + resource.TestCheckNoResourceAttr(rsc, "url_parts.base_path"), + resource.TestCheckNoResourceAttr(rsc, "access_key_id"), + resource.TestCheckNoResourceAttr(rsc, "access_key_secret"), + resource.TestCheckNoResourceAttr(rsc, "region"), + checkAws(client, &originId, func(o *cdn77.S3OriginDetail) error { + return errors.Join( + acctest.EqualField("type", o.Type, origin.TypeAws), + acctest.EqualField("label", o.Label, "some label"), + acctest.NullField("note", o.Note), + acctest.EqualField("scheme", o.Scheme, "http"), + acctest.EqualField("host", o.Host, "my-totally-random-custom-host.com"), + acctest.NullField("port", o.Port), + acctest.NullField("base_dir", o.BaseDir), + acctest.NullField("access_key_id", o.AwsAccessKeyId), + acctest.NullField("region", o.AwsRegion), + ) + }), + ), + }, + resource.TestStep{ + Config: `resource "cdn77_origin_aws" "aws" { + label = "some label" + url_parts = { + scheme = "http" + host = "my-totally-random-custom-host.com" + } + }`, + ConfigPlanChecks: acctest.ConfigPlanChecks(rsc, plancheck.ResourceActionNoop), + }, + resource.TestStep{ + Config: `resource "cdn77_origin_aws" "aws" { + label = "another label" + note = "some note" + url = "http://my-totally-random-custom-host.com:12345" + }`, + ConfigPlanChecks: acctest.ConfigPlanChecks(rsc, plancheck.ResourceActionUpdate), + Check: resource.ComposeAggregateTestCheckFunc( + acctest.CheckAttr(rsc, "id", &originId), + resource.TestCheckResourceAttr(rsc, "label", "another label"), + resource.TestCheckResourceAttr(rsc, "note", "some note"), + resource.TestCheckResourceAttr(rsc, "url", "http://my-totally-random-custom-host.com:12345"), + resource.TestCheckResourceAttr(rsc, "url_parts.scheme", "http"), + resource.TestCheckResourceAttr(rsc, "url_parts.host", "my-totally-random-custom-host.com"), + resource.TestCheckResourceAttr(rsc, "url_parts.port", "12345"), + resource.TestCheckNoResourceAttr(rsc, "url_parts.base_path"), + resource.TestCheckNoResourceAttr(rsc, "access_key_id"), + resource.TestCheckNoResourceAttr(rsc, "access_key_secret"), + resource.TestCheckNoResourceAttr(rsc, "region"), + checkAws(client, &originId, func(o *cdn77.S3OriginDetail) error { + return errors.Join( + acctest.EqualField("type", o.Type, origin.TypeAws), + acctest.EqualField("label", o.Label, "another label"), + acctest.NullFieldEqual("note", o.Note, "some note"), + acctest.EqualField("scheme", o.Scheme, "http"), + acctest.EqualField("host", o.Host, "my-totally-random-custom-host.com"), + acctest.NullFieldEqual("port", o.Port, 12345), + acctest.NullField("base_dir", o.BaseDir), + acctest.NullField("access_key_id", o.AwsAccessKeyId), + acctest.NullField("region", o.AwsRegion), + ) + }), + ), + }, + resource.TestStep{ + Config: `resource "cdn77_origin_aws" "aws" { + label = "another label" + note = "some note" + url = "http://my-totally-random-custom-host.com:12345/some-dir" + access_key_id = "keyid" + access_key_secret = "keysecret" + region = "eu" + }`, + ConfigPlanChecks: acctest.ConfigPlanChecks(rsc, plancheck.ResourceActionUpdate), + Check: resource.ComposeAggregateTestCheckFunc( + acctest.CheckAttr(rsc, "id", &originId), + resource.TestCheckResourceAttr(rsc, "label", "another label"), + resource.TestCheckResourceAttr(rsc, "note", "some note"), + resource.TestCheckResourceAttr(rsc, "access_key_id", "keyid"), + resource.TestCheckResourceAttr(rsc, "access_key_secret", "keysecret"), + resource.TestCheckResourceAttr(rsc, "region", "eu"), + resource.TestCheckResourceAttr(rsc, "url", "http://my-totally-random-custom-host.com:12345/some-dir"), + resource.TestCheckResourceAttr(rsc, "url_parts.scheme", "http"), + resource.TestCheckResourceAttr(rsc, "url_parts.host", "my-totally-random-custom-host.com"), + resource.TestCheckResourceAttr(rsc, "url_parts.port", "12345"), + resource.TestCheckResourceAttr(rsc, "url_parts.base_path", "some-dir"), + checkAws(client, &originId, func(o *cdn77.S3OriginDetail) error { + return errors.Join( + acctest.EqualField("type", o.Type, origin.TypeAws), + acctest.EqualField("label", o.Label, "another label"), + acctest.NullFieldEqual("note", o.Note, "some note"), + acctest.EqualField("scheme", o.Scheme, "http"), + acctest.EqualField("host", o.Host, "my-totally-random-custom-host.com"), + acctest.NullFieldEqual("port", o.Port, 12345), + acctest.NullFieldEqual("base_dir", o.BaseDir, "some-dir"), + acctest.NullFieldEqual("access_key_id", o.AwsAccessKeyId, "keyid"), + acctest.NullFieldEqual("region", o.AwsRegion, "eu"), + ) + }), + ), + }, + resource.TestStep{ + Config: `resource "cdn77_origin_aws" "aws" { + label = "another label" + note = "some note" + url_parts = { + scheme = "http" + host = "my-totally-random-custom-host.com" + port = 12345 + base_path = "some-dir" + } + access_key_id = "keyid" + access_key_secret = "keysecret" + region = "eu" + }`, + ConfigPlanChecks: acctest.ConfigPlanChecks(rsc, plancheck.ResourceActionNoop), + }, + resource.TestStep{ + Config: `resource "cdn77_origin_aws" "aws" { + label = "another label" + url = "http://my-totally-random-custom-host.com" + }`, + ConfigPlanChecks: acctest.ConfigPlanChecks(rsc, plancheck.ResourceActionUpdate), + Check: resource.ComposeAggregateTestCheckFunc( + acctest.CheckAttr(rsc, "id", &originId), + resource.TestCheckResourceAttr(rsc, "label", "another label"), + resource.TestCheckResourceAttr(rsc, "url", "http://my-totally-random-custom-host.com"), + resource.TestCheckResourceAttr(rsc, "url_parts.scheme", "http"), + resource.TestCheckResourceAttr(rsc, "url_parts.host", "my-totally-random-custom-host.com"), + resource.TestCheckNoResourceAttr(rsc, "note"), + resource.TestCheckNoResourceAttr(rsc, "url_parts.port"), + resource.TestCheckNoResourceAttr(rsc, "url_parts.base_path"), + resource.TestCheckNoResourceAttr(rsc, "access_key_id"), + resource.TestCheckNoResourceAttr(rsc, "access_key_secret"), + resource.TestCheckNoResourceAttr(rsc, "region"), + checkAws(client, &originId, func(o *cdn77.S3OriginDetail) error { + return errors.Join( + acctest.EqualField("type", o.Type, origin.TypeAws), + acctest.EqualField("label", o.Label, "another label"), + acctest.NullField("note", o.Note), + acctest.EqualField("scheme", o.Scheme, "http"), + acctest.EqualField("host", o.Host, "my-totally-random-custom-host.com"), + acctest.NullField("port", o.Port), + acctest.NullField("base_dir", o.BaseDir), + acctest.NullField("access_key_id", o.AwsAccessKeyId), + acctest.NullField("region", o.AwsRegion), + ) + }), + ), + }, + ) +} + +func TestAccOrigin_AwsResource_Import(t *testing.T) { + const rsc = "cdn77_origin_aws.aws" + client := acctest.GetClient(t) + var originId string + + acctest.Run(t, acctest.CheckOriginDestroyed(client, origin.TypeAws), + resource.TestStep{ + Config: `resource "cdn77_origin_aws" "aws" { + label = "some label" + note = "some note" + url = "http://my-totally-random-custom-host.com" + access_key_id = "keyid" + access_key_secret = "keysecret" + region = "eu" + }`, + Check: acctest.CheckAndAssignAttr(rsc, "id", &originId), + }, + resource.TestStep{ + ResourceName: rsc, + ImportState: true, + ImportStateIdFunc: func(*terraform.State) (string, error) { + return fmt.Sprintf("%s,keysecret", originId), nil + }, + ImportStateVerify: true, + }, + ) +} + +func TestAccOrigin_AwsResource_Import_NoKey(t *testing.T) { + const rsc = "cdn77_origin_aws.aws" + client := acctest.GetClient(t) + var originId string + + acctest.Run(t, acctest.CheckOriginDestroyed(client, origin.TypeAws), + resource.TestStep{ + Config: `resource "cdn77_origin_aws" "aws" { + label = "some label" + note = "some note" + url = "http://my-totally-random-custom-host.com" + }`, + Check: acctest.CheckAndAssignAttr(rsc, "id", &originId), + }, + resource.TestStep{ + ResourceName: rsc, + ImportState: true, + ImportStateIdFunc: func(*terraform.State) (string, error) { + return fmt.Sprintf("%s,", originId), nil + }, + ImportStateVerify: true, + }, + ) +} + +func TestAccOrigin_AwsDataSource_OnlyRequiredFields(t *testing.T) { + const nonExistingOriginId = "bcd7b5bb-a044-4611-82e4-3f3b2a3cda13" + const rsc = "data.cdn77_origin_aws.aws" + const label = "random origin" + const originUrl = "http://my-totally-random-custom-host.com" + const scheme = "http" + const host = "my-totally-random-custom-host.com" + client := acctest.GetClient(t) + request := cdn77.OriginCreateAwsJSONRequestBody{ + Label: label, + Scheme: scheme, + Host: host, + } + + response, err := client.OriginCreateAwsWithResponse(context.Background(), request) + acctest.AssertResponseOk(t, "Failed to create Origin: %s", response, err) + + originId := response.JSON201.Id + + t.Cleanup(func() { + acctest.MustDeleteOrigin(t, client, origin.TypeAws, originId) + }) + + acctest.Run(t, nil, + resource.TestStep{ + Config: acctest.Config(awsDataSourceConfig, "id", nonExistingOriginId), + ExpectError: regexp.MustCompile(fmt.Sprintf(`.*?"%s".*?not found.*?`, nonExistingOriginId)), + }, + resource.TestStep{ + Config: acctest.Config(awsDataSourceConfig, "id", originId), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(rsc, "id", originId), + resource.TestCheckResourceAttr(rsc, "label", label), + resource.TestCheckResourceAttr(rsc, "url", originUrl), + resource.TestCheckResourceAttr(rsc, "url_parts.scheme", scheme), + resource.TestCheckResourceAttr(rsc, "url_parts.host", host), + resource.TestCheckNoResourceAttr(rsc, "note"), + resource.TestCheckNoResourceAttr(rsc, "url_parts.port"), + resource.TestCheckNoResourceAttr(rsc, "url_parts.base_path"), + resource.TestCheckNoResourceAttr(rsc, "access_key_id"), + resource.TestCheckNoResourceAttr(rsc, "access_key_secret"), + resource.TestCheckNoResourceAttr(rsc, "region"), + ), + }, + ) +} + +func TestAccOrigin_AwsDataSource_AllFields(t *testing.T) { + const rsc = "data.cdn77_origin_aws.aws" + const label = "random origin" + const note = "some note" + const originUrl = "https://my-totally-random-custom-host.com:12345/some-dir" + const scheme = "https" + const host = "my-totally-random-custom-host.com" + const port = 12345 + const basePath = "some-dir" + const accessKeyId = "someKeyId" + const awsKeySecret = "someKeySecret" + const region = "eu" + client := acctest.GetClient(t) + request := cdn77.OriginCreateAwsJSONRequestBody{ + Label: label, + Note: nullable.NewNullableWithValue(note), + Scheme: scheme, + Host: host, + Port: nullable.NewNullableWithValue(port), + BaseDir: nullable.NewNullableWithValue(basePath), + AwsAccessKeyId: nullable.NewNullableWithValue(accessKeyId), + AwsAccessKeySecret: nullable.NewNullableWithValue(awsKeySecret), + AwsRegion: nullable.NewNullableWithValue(region), + } + + response, err := client.OriginCreateAwsWithResponse(context.Background(), request) + acctest.AssertResponseOk(t, "Failed to create Origin: %s", response, err) + + originId := response.JSON201.Id + + t.Cleanup(func() { + acctest.MustDeleteOrigin(t, client, origin.TypeAws, originId) + }) + + acctest.Run(t, nil, resource.TestStep{ + Config: acctest.Config(awsDataSourceConfig, "id", originId), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(rsc, "id", originId), + resource.TestCheckResourceAttr(rsc, "label", label), + resource.TestCheckResourceAttr(rsc, "note", note), + resource.TestCheckResourceAttr(rsc, "url", originUrl), + resource.TestCheckResourceAttr(rsc, "url_parts.scheme", scheme), + resource.TestCheckResourceAttr(rsc, "url_parts.host", host), + resource.TestCheckResourceAttr(rsc, "url_parts.port", strconv.Itoa(port)), + resource.TestCheckResourceAttr(rsc, "url_parts.base_path", basePath), + resource.TestCheckResourceAttr(rsc, "access_key_id", accessKeyId), + resource.TestCheckResourceAttr(rsc, "region", region), + resource.TestCheckNoResourceAttr(rsc, "access_key_secret"), + ), + }) +} + +func checkAws( + client cdn77.ClientWithResponsesInterface, + originId *string, + fn func(o *cdn77.S3OriginDetail) error, +) func(*terraform.State) error { + return func(*terraform.State) error { + response, err := client.OriginDetailAwsWithResponse(context.Background(), *originId) + message := fmt.Sprintf("failed to get Origin[id=%s]: %%s", *originId) + + if err = acctest.CheckResponse(message, response, err); err != nil { + return err + } + + return fn(response.JSON200) + } +} + +const awsDataSourceConfig = ` +data "cdn77_origin_aws" "aws" { + id = "{id}" +} +` diff --git a/internal/provider/origin/object_storage.go b/internal/provider/origin/object_storage.go new file mode 100644 index 0000000..8ff7ce0 --- /dev/null +++ b/internal/provider/origin/object_storage.go @@ -0,0 +1,204 @@ +package origin + +import ( + "context" + "fmt" + "strings" + + "github.com/cdn77/cdn77-client-go/v2" + "github.com/cdn77/terraform-provider-cdn77/internal/provider/shared" + "github.com/cdn77/terraform-provider-cdn77/internal/util" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/oapi-codegen/nullable" +) + +var ( + _ resource.ResourceWithConfigure = &ObjectStorageResource{} + _ resource.ResourceWithImportState = &ObjectStorageResource{} + _ resource.ResourceWithMoveState = &ObjectStorageResource{} +) + +type ObjectStorageResource struct { + *util.BaseResource +} + +func (r *ObjectStorageResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + diags := &resp.Diagnostics + var data ObjectStorageModel + + if diags.Append(req.Plan.Get(ctx, &data)...); diags.HasError() { + return + } + + const errMessage = "Failed to create Object Storage Origin" + + request := cdn77.OriginCreateObjectStorageJSONRequestBody{ + Label: data.Label.ValueString(), + Note: util.StringValueToNullable(data.Note), + BucketName: data.BucketName.ValueString(), + Acl: cdn77.AclType(data.Acl.ValueString()), + ClusterId: data.ClusterId.ValueString(), + } + + response, err := r.Client.OriginCreateObjectStorageWithResponse(ctx, request) + if err != nil { + diags.AddError(errMessage, err.Error()) + + return + } + + util.ProcessResponse(diags, response, errMessage, response.JSON201, func(detail *cdn77.ObjectStorageOriginDetail) { + data.Id = types.StringValue(detail.Id) + data.UrlModel = shared.NewUrlModel( + ctx, + string(detail.Scheme), + detail.Host, + detail.Port, + nullable.NewNullNullable[string](), + ) + data.AccessKeyId = types.StringPointerValue(detail.AccessKeyId) + data.AccessKeySecret = types.StringPointerValue(detail.AccessSecret) + data.Usage = &ObjectStorageUsageModel{ + Files: util.IntPointerToInt64Value(detail.Usage.FileCount), + SizeBytes: util.IntPointerToInt64Value(detail.Usage.SizeBytes), + } + + diags.Append(resp.State.Set(ctx, data)...) + }) +} + +func (r *ObjectStorageResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + diags := &resp.Diagnostics + var data ObjectStorageModel + + if diags.Append(req.Plan.Get(ctx, &data)...); diags.HasError() { + return + } + + const errMessage = "Failed to update Object Storage Origin" + + request := cdn77.OriginEditObjectStorageJSONRequestBody{ + Label: data.Label.ValueStringPointer(), + Note: util.StringValueToNullable(data.Note), + } + + response, err := r.Client.OriginEditObjectStorageWithResponse(ctx, data.Id.ValueString(), request) + if err != nil { + diags.AddError(errMessage, err.Error()) + + return + } + + util.ProcessEmptyResponse(diags, response, errMessage, func() { + diags.Append(resp.State.Set(ctx, data)...) + }) +} + +func (r *ObjectStorageResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + diags := &resp.Diagnostics + var data ObjectStorageModel + + if diags.Append(req.State.Get(ctx, &data)...); diags.HasError() { + return + } + + const errMessage = "Failed to delete Object Storage Origin" + + response, err := r.Client.OriginDeleteObjectStorageWithResponse(ctx, data.Id.ValueString()) + if err != nil { + diags.AddError(errMessage, err.Error()) + + return + } + + util.ValidateDeletionResponse(diags, response, errMessage) +} + +func (*ObjectStorageResource) ImportState( + ctx context.Context, + req resource.ImportStateRequest, + resp *resource.ImportStateResponse, +) { + idParts := strings.Split(req.ID, ",") + if len(idParts) != 5 { + resp.Diagnostics.AddError( + "Invalid Import Identifier", + fmt.Sprintf("Expected: ,,,,\nGot: %q", req.ID), + ) + + return + } + + id, acl, clusterId, accessKeyId, accessKeySecret := idParts[0], idParts[1], idParts[2], idParts[3], idParts[4] + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("acl"), acl)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("cluster_id"), clusterId)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("access_key_id"), accessKeyId)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("access_key_secret"), accessKeySecret)...) +} + +func (r *ObjectStorageResource) MoveState(context.Context) []resource.StateMover { + deprecatedOriginSchema := CreateDeprecatedOriginResourceSchema() + + return []resource.StateMover{ + { + SourceSchema: &deprecatedOriginSchema, + StateMover: func(ctx context.Context, req resource.MoveStateRequest, resp *resource.MoveStateResponse) { + if req.SourceTypeName != DeprecatedOriginResourceType || + !strings.HasSuffix(req.SourceProviderAddress, "cdn77/cdn77") { + return + } + + diags := &resp.Diagnostics + var oldModel DeprecatedOriginModel + + if diags.Append(req.SourceState.Get(ctx, &oldModel)...); diags.HasError() { + return + } + + if oldModel.Type.ValueString() != TypeObjectStorage { + diags.AddError( + "Unable to Move Resource State", + fmt.Sprintf( + "Only %q resources with type=%q can be moved to %q.\nLabel: %s\nType: %s\n", + DeprecatedOriginResourceType, + TypeObjectStorage, + r.FullName(), + oldModel.Label.ValueString(), + oldModel.Type.ValueString(), + ), + ) + + return + } + + model := ObjectStorageModel{ + ObjectStorageBaseModel: ObjectStorageBaseModel{ + SharedModel: NewSharedModel( + oldModel.Id, + oldModel.Label.ValueString(), + util.StringValueToNullable(oldModel.Note), + ), + BucketName: oldModel.BucketName, + }, + Acl: oldModel.Acl, + ClusterId: oldModel.ClusterId, + AccessKeyId: oldModel.AccessKeyId, + AccessKeySecret: oldModel.AccessKeySecret, + } + + diags.Append(resp.TargetState.Set(ctx, model)...) + }, + }, + } +} + +var _ datasource.DataSourceWithConfigure = &ObjectStorageDataSource{} + +type ObjectStorageDataSource struct { + *util.BaseDataSource +} diff --git a/internal/provider/origin/object_storage_reader.go b/internal/provider/origin/object_storage_reader.go new file mode 100644 index 0000000..da30e50 --- /dev/null +++ b/internal/provider/origin/object_storage_reader.go @@ -0,0 +1,66 @@ +package origin + +import ( + "context" + "fmt" + + "github.com/cdn77/cdn77-client-go/v2" + "github.com/cdn77/terraform-provider-cdn77/internal/provider/shared" + "github.com/cdn77/terraform-provider-cdn77/internal/util" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/oapi-codegen/nullable" +) + +type ObjectStorageReader struct{} + +func (*ObjectStorageReader) ErrMessage() string { + return "Failed to fetch Object Storage Origin" +} + +func (*ObjectStorageReader) Fetch( + ctx context.Context, + client cdn77.ClientWithResponsesInterface, + model ObjectStorageModel, +) (*cdn77.OriginDetailObjectStorageResponse, *cdn77.ObjectStorageOriginDetail, error) { + response, err := client.OriginDetailObjectStorageWithResponse(ctx, model.Id.ValueString()) + if err != nil { + return response, nil, err + } + + return response, response.JSON200, nil +} + +func (r *ObjectStorageReader) Process( + ctx context.Context, + model ObjectStorageModel, + detail *cdn77.ObjectStorageOriginDetail, + diags *diag.Diagnostics, +) ObjectStorageModel { + if detail.Type != TypeObjectStorage { + diags.AddError(r.ErrMessage(), fmt.Sprintf("Origin with id=\"%s\" is not an Object Storage Origin", detail.Id)) + + return model + } + + return ObjectStorageModel{ + ObjectStorageBaseModel: ObjectStorageBaseModel{ + SharedModel: NewSharedModel(model.Id, detail.Label, detail.Note), + UrlModel: shared.NewUrlModel( + ctx, + string(detail.Scheme), + detail.Host, + detail.Port, + nullable.NewNullNullable[string]()), + BucketName: types.StringValue(detail.BucketName), + Usage: &ObjectStorageUsageModel{ + Files: util.IntPointerToInt64Value(detail.Usage.FileCount), + SizeBytes: util.IntPointerToInt64Value(detail.Usage.SizeBytes), + }, + }, + Acl: model.Acl, + ClusterId: model.ClusterId, + AccessKeyId: model.AccessKeyId, + AccessKeySecret: model.AccessKeySecret, + } +} diff --git a/internal/provider/origin/object_storage_schema.go b/internal/provider/origin/object_storage_schema.go new file mode 100644 index 0000000..67e754e --- /dev/null +++ b/internal/provider/origin/object_storage_schema.go @@ -0,0 +1,114 @@ +package origin + +import ( + "regexp" + + "github.com/cdn77/terraform-provider-cdn77/internal/provider/shared" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" + "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" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type ObjectStorageModel struct { + ObjectStorageBaseModel + + Acl types.String `tfsdk:"acl"` + ClusterId types.String `tfsdk:"cluster_id"` + AccessKeyId types.String `tfsdk:"access_key_id"` + AccessKeySecret types.String `tfsdk:"access_key_secret"` +} + +type ObjectStorageBaseModel struct { + SharedModel + shared.UrlModel + + BucketName types.String `tfsdk:"bucket_name"` + Usage *ObjectStorageUsageModel `tfsdk:"usage"` +} + +type ObjectStorageUsageModel struct { + Files types.Int64 `tfsdk:"files"` + SizeBytes types.Int64 `tfsdk:"size_bytes"` +} + +func CreateObjectStorageResourceSchema() schema.Schema { + s := CreateObjectStorageBaseResourceSchema() + s.Attributes["acl"] = schema.StringAttribute{ + Description: "Object Storage access key ACL", + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf("authenticated-read", "private", "public-read", "public-read-write"), + }, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + } + s.Attributes["cluster_id"] = schema.StringAttribute{ + Description: "ID of the Object Storage storage cluster", + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + } + s.Attributes["access_key_id"] = schema.StringAttribute{ + Description: "Access key to your Object Storage bucket", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + } + s.Attributes["access_key_secret"] = schema.StringAttribute{ + Description: "Access secret to your Object Storage bucket", + Computed: true, + Sensitive: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + } + + return s +} + +func CreateObjectStorageBaseResourceSchema() schema.Schema { + return WithSharedSchemaAttrs(shared.WithComputedUrlSchemaAttrs(schema.Schema{ + MarkdownDescription: "Object Storage Origin resource allows you to manage your Object Storage Origins", + Attributes: map[string]schema.Attribute{ + "bucket_name": schema.StringAttribute{ + Description: "Name of your Object Storage bucket", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(3, 63), + stringvalidator.RegexMatches( + regexp.MustCompile(`^([a-z0-9][a-z0-9-]{1,61}[a-z0-9])?$`), + "Allowed characters are lowercase letters, digits and a dash. "+ + "Dash isn't allowed at the start and end of the bucket name.", + ), + }, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "usage": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "files": schema.Int64Attribute{ + Computed: true, + Description: "Number of files stored on the Object Storage bucket", + PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()}, + }, + "size_bytes": schema.Int64Attribute{ + Computed: true, + Description: "Total size of the Object Storage bucket in bytes", + PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()}, + }, + }, + Computed: true, + Description: "Usage statistics of the Object Storage bucket", + Default: objectdefault.StaticValue(types.ObjectValueMust( + map[string]attr.Type{"files": types.Int64Type, "size_bytes": types.Int64Type}, + map[string]attr.Value{ + "files": types.Int64Value(0), + "size_bytes": types.Int64Value(0), + }, + )), + PlanModifiers: []planmodifier.Object{objectplanmodifier.UseStateForUnknown()}, + }, + }, + })) +} diff --git a/internal/provider/origin/object_storage_test.go b/internal/provider/origin/object_storage_test.go new file mode 100644 index 0000000..8e7a6b4 --- /dev/null +++ b/internal/provider/origin/object_storage_test.go @@ -0,0 +1,401 @@ +package origin_test + +import ( + "context" + "errors" + "fmt" + "regexp" + "strconv" + "testing" + + "github.com/cdn77/cdn77-client-go/v2" + "github.com/cdn77/terraform-provider-cdn77/internal/acctest" + "github.com/cdn77/terraform-provider-cdn77/internal/provider/origin" + "github.com/cdn77/terraform-provider-cdn77/internal/provider/shared" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/oapi-codegen/nullable" +) + +func TestAccOrigin_ObjectStorageResource(t *testing.T) { + const rsc = "cdn77_origin_object_storage.os" + client := acctest.GetClient(t) + bucketName := "my-bucket-" + uuid.New().String() + anotherBucketName := "my-bucket-" + uuid.New().String() + var originId string + var clusterId string + + acctest.Run(t, acctest.CheckOriginDestroyed(client, origin.TypeObjectStorage), + resource.TestStep{ + Config: acctest.Config(objectStoragesDataSourceConfig+`resource "cdn77_origin_object_storage" "os" { + label = "some label" + acl = "private" + cluster_id = local.eu_cluster_id + bucket_name = "{bucketName}" + }`, "bucketName", bucketName), + ConfigPlanChecks: acctest.ConfigPlanChecks(rsc, plancheck.ResourceActionCreate), + Check: resource.ComposeAggregateTestCheckFunc( + acctest.CheckAndAssignAttr(rsc, "id", &originId), + resource.TestCheckResourceAttr(rsc, "label", "some label"), + resource.TestCheckResourceAttr(rsc, "url", "https://eu-1.cdn77-storage.com:443"), + resource.TestCheckResourceAttr(rsc, "url_parts.scheme", "https"), + resource.TestCheckResourceAttr(rsc, "url_parts.host", "eu-1.cdn77-storage.com"), + resource.TestCheckResourceAttr(rsc, "url_parts.port", "443"), + resource.TestCheckResourceAttr(rsc, "bucket_name", bucketName), + resource.TestCheckResourceAttr(rsc, "acl", "private"), + acctest.CheckAndAssignAttr(rsc, "cluster_id", &clusterId), + resource.TestCheckResourceAttrSet(rsc, "access_key_id"), + resource.TestCheckResourceAttrSet(rsc, "access_key_secret"), + resource.TestCheckResourceAttr(rsc, "usage.files", "0"), + resource.TestCheckResourceAttr(rsc, "usage.size_bytes", "0"), + resource.TestCheckNoResourceAttr(rsc, "note"), + resource.TestCheckNoResourceAttr(rsc, "url_parts.base_path"), + checkObjectStorage(client, &originId, func(o *cdn77.ObjectStorageOriginDetail) error { + return errors.Join( + acctest.EqualField("type", o.Type, origin.TypeObjectStorage), + acctest.EqualField("label", o.Label, "some label"), + acctest.NullField("note", o.Note), + acctest.EqualField("scheme", o.Scheme, "https"), + acctest.EqualField("host", o.Host, "eu-1.cdn77-storage.com"), + acctest.NullFieldEqual("port", o.Port, 443), + acctest.EqualField("bucket_name", o.BucketName, bucketName), + ) + }), + ), + }, + resource.TestStep{ + Config: acctest.Config(objectStoragesDataSourceConfig+`resource "cdn77_origin_object_storage" "os" { + label = "another label" + note = "some note" + acl = "private" + cluster_id = local.eu_cluster_id + bucket_name = "{bucketName}" + }`, "bucketName", bucketName), + ConfigPlanChecks: acctest.ConfigPlanChecks(rsc, plancheck.ResourceActionUpdate), + Check: resource.ComposeAggregateTestCheckFunc( + acctest.CheckAttr(rsc, "id", &originId), + resource.TestCheckResourceAttr(rsc, "label", "another label"), + resource.TestCheckResourceAttr(rsc, "note", "some note"), + resource.TestCheckResourceAttr(rsc, "url", "https://eu-1.cdn77-storage.com:443"), + resource.TestCheckResourceAttr(rsc, "url_parts.scheme", "https"), + resource.TestCheckResourceAttr(rsc, "url_parts.host", "eu-1.cdn77-storage.com"), + resource.TestCheckResourceAttr(rsc, "url_parts.port", "443"), + resource.TestCheckResourceAttr(rsc, "bucket_name", bucketName), + resource.TestCheckResourceAttr(rsc, "acl", "private"), + acctest.CheckAttr(rsc, "cluster_id", &clusterId), + resource.TestCheckResourceAttrSet(rsc, "access_key_id"), + resource.TestCheckResourceAttrSet(rsc, "access_key_secret"), + resource.TestCheckResourceAttr(rsc, "usage.files", "0"), + resource.TestCheckResourceAttr(rsc, "usage.size_bytes", "0"), + resource.TestCheckNoResourceAttr(rsc, "url_parts.base_path"), + checkObjectStorage(client, &originId, func(o *cdn77.ObjectStorageOriginDetail) error { + return errors.Join( + acctest.EqualField("type", o.Type, origin.TypeObjectStorage), + acctest.EqualField("label", o.Label, "another label"), + acctest.NullFieldEqual("note", o.Note, "some note"), + acctest.EqualField("scheme", o.Scheme, "https"), + acctest.EqualField("host", o.Host, "eu-1.cdn77-storage.com"), + acctest.NullFieldEqual("port", o.Port, 443), + acctest.EqualField("bucket_name", o.BucketName, bucketName), + ) + }), + ), + }, + resource.TestStep{ + Config: acctest.Config(objectStoragesDataSourceConfig+`resource "cdn77_origin_object_storage" "os" { + label = "another label" + note = "some note" + acl = "authenticated-read" + cluster_id = local.eu_cluster_id + bucket_name = "{bucketName}" + }`, "bucketName", bucketName), + ConfigPlanChecks: acctest.ConfigPlanChecks(rsc, plancheck.ResourceActionDestroyBeforeCreate), + Check: resource.ComposeAggregateTestCheckFunc( + acctest.CheckAndReassignAttr(rsc, "id", &originId), + resource.TestCheckResourceAttr(rsc, "label", "another label"), + resource.TestCheckResourceAttr(rsc, "note", "some note"), + resource.TestCheckResourceAttr(rsc, "url", "https://eu-1.cdn77-storage.com:443"), + resource.TestCheckResourceAttr(rsc, "url_parts.scheme", "https"), + resource.TestCheckResourceAttr(rsc, "url_parts.host", "eu-1.cdn77-storage.com"), + resource.TestCheckResourceAttr(rsc, "url_parts.port", "443"), + resource.TestCheckResourceAttr(rsc, "bucket_name", bucketName), + resource.TestCheckResourceAttr(rsc, "acl", "authenticated-read"), + acctest.CheckAndAssignAttr(rsc, "cluster_id", &clusterId), + resource.TestCheckResourceAttrSet(rsc, "access_key_id"), + resource.TestCheckResourceAttrSet(rsc, "access_key_secret"), + resource.TestCheckResourceAttr(rsc, "usage.files", "0"), + resource.TestCheckResourceAttr(rsc, "usage.size_bytes", "0"), + resource.TestCheckNoResourceAttr(rsc, "url_parts.base_path"), + checkObjectStorage(client, &originId, func(o *cdn77.ObjectStorageOriginDetail) error { + return errors.Join( + acctest.EqualField("type", o.Type, origin.TypeObjectStorage), + acctest.EqualField("label", o.Label, "another label"), + acctest.NullFieldEqual("note", o.Note, "some note"), + acctest.EqualField("scheme", o.Scheme, "https"), + acctest.EqualField("host", o.Host, "eu-1.cdn77-storage.com"), + acctest.NullFieldEqual("port", o.Port, 443), + acctest.EqualField("bucket_name", o.BucketName, bucketName), + ) + }), + ), + }, + resource.TestStep{ + Config: acctest.Config(objectStoragesDataSourceConfig+`resource "cdn77_origin_object_storage" "os" { + label = "another label" + note = "some note" + acl = "authenticated-read" + cluster_id = local.us_cluster_id + bucket_name = "{bucketName}" + }`, "bucketName", bucketName), + ConfigPlanChecks: acctest.ConfigPlanChecks(rsc, plancheck.ResourceActionDestroyBeforeCreate), + Check: resource.ComposeAggregateTestCheckFunc( + acctest.CheckAndReassignAttr(rsc, "id", &originId), + resource.TestCheckResourceAttr(rsc, "label", "another label"), + resource.TestCheckResourceAttr(rsc, "note", "some note"), + resource.TestCheckResourceAttr(rsc, "url", "https://us-1.cdn77-storage.com:443"), + resource.TestCheckResourceAttr(rsc, "url_parts.scheme", "https"), + resource.TestCheckResourceAttr(rsc, "url_parts.host", "us-1.cdn77-storage.com"), + resource.TestCheckResourceAttr(rsc, "url_parts.port", "443"), + resource.TestCheckResourceAttr(rsc, "bucket_name", bucketName), + resource.TestCheckResourceAttr(rsc, "acl", "authenticated-read"), + acctest.CheckAndReassignAttr(rsc, "cluster_id", &clusterId), + resource.TestCheckResourceAttrSet(rsc, "access_key_id"), + resource.TestCheckResourceAttrSet(rsc, "access_key_secret"), + resource.TestCheckResourceAttr(rsc, "usage.files", "0"), + resource.TestCheckResourceAttr(rsc, "usage.size_bytes", "0"), + resource.TestCheckNoResourceAttr(rsc, "url_parts.base_path"), + checkObjectStorage(client, &originId, func(o *cdn77.ObjectStorageOriginDetail) error { + return errors.Join( + acctest.EqualField("type", o.Type, origin.TypeObjectStorage), + acctest.EqualField("label", o.Label, "another label"), + acctest.NullFieldEqual("note", o.Note, "some note"), + acctest.EqualField("scheme", o.Scheme, "https"), + acctest.EqualField("host", o.Host, "us-1.cdn77-storage.com"), + acctest.NullFieldEqual("port", o.Port, 443), + acctest.EqualField("bucket_name", o.BucketName, bucketName), + ) + }), + ), + }, + resource.TestStep{ + Config: acctest.Config(objectStoragesDataSourceConfig+`resource "cdn77_origin_object_storage" "os" { + label = "another label" + note = "some note" + acl = "authenticated-read" + cluster_id = local.us_cluster_id + bucket_name = "{bucketName}" + }`, "bucketName", anotherBucketName), + ConfigPlanChecks: acctest.ConfigPlanChecks(rsc, plancheck.ResourceActionDestroyBeforeCreate), + Check: resource.ComposeAggregateTestCheckFunc( + acctest.CheckAndReassignAttr(rsc, "id", &originId), + resource.TestCheckResourceAttr(rsc, "label", "another label"), + resource.TestCheckResourceAttr(rsc, "note", "some note"), + resource.TestCheckResourceAttr(rsc, "url", "https://us-1.cdn77-storage.com:443"), + resource.TestCheckResourceAttr(rsc, "url_parts.scheme", "https"), + resource.TestCheckResourceAttr(rsc, "url_parts.host", "us-1.cdn77-storage.com"), + resource.TestCheckResourceAttr(rsc, "url_parts.port", "443"), + resource.TestCheckResourceAttr(rsc, "bucket_name", anotherBucketName), + resource.TestCheckResourceAttr(rsc, "acl", "authenticated-read"), + acctest.CheckAttr(rsc, "cluster_id", &clusterId), + resource.TestCheckResourceAttrSet(rsc, "access_key_id"), + resource.TestCheckResourceAttrSet(rsc, "access_key_secret"), + resource.TestCheckResourceAttr(rsc, "usage.files", "0"), + resource.TestCheckResourceAttr(rsc, "usage.size_bytes", "0"), + resource.TestCheckNoResourceAttr(rsc, "url_parts.base_path"), + checkObjectStorage(client, &originId, func(o *cdn77.ObjectStorageOriginDetail) error { + return errors.Join( + acctest.EqualField("type", o.Type, origin.TypeObjectStorage), + acctest.EqualField("label", o.Label, "another label"), + acctest.NullFieldEqual("note", o.Note, "some note"), + acctest.EqualField("scheme", o.Scheme, "https"), + acctest.EqualField("host", o.Host, "us-1.cdn77-storage.com"), + acctest.NullFieldEqual("port", o.Port, 443), + acctest.EqualField("bucket_name", o.BucketName, anotherBucketName), + ) + }), + ), + }, + ) +} + +func TestAccOrigin_ObjectStorageResource_Import(t *testing.T) { + const rsc = "cdn77_origin_object_storage.os" + client := acctest.GetClient(t) + bucketName := "my-bucket-" + uuid.New().String() + var originId, clusterId, accessKeyId, accessKeySecret string + + acctest.Run(t, acctest.CheckOriginDestroyed(client, origin.TypeObjectStorage), + resource.TestStep{ + Config: acctest.Config(objectStoragesDataSourceConfig+`resource "cdn77_origin_object_storage" "os" { + label = "some label" + note = "some note" + acl = "private" + cluster_id = local.eu_cluster_id + bucket_name = "{bucketName}" + }`, "bucketName", bucketName), + Check: resource.ComposeAggregateTestCheckFunc( + acctest.CheckAndAssignAttr(rsc, "id", &originId), + acctest.CheckAndAssignAttr(rsc, "cluster_id", &clusterId), + acctest.CheckAndAssignAttr(rsc, "access_key_id", &accessKeyId), + acctest.CheckAndAssignAttr(rsc, "access_key_secret", &accessKeySecret), + ), + }, + resource.TestStep{ + ResourceName: rsc, + ImportState: true, + ImportStateIdFunc: func(*terraform.State) (string, error) { + return fmt.Sprintf("%s,private,%s,%s,%s", originId, clusterId, accessKeyId, accessKeySecret), nil + }, + ImportStateVerify: true, + }, + ) +} + +func TestAccOrigin_ObjectStorageDataSource_OnlyRequiredFields(t *testing.T) { + const nonExistingOriginId = "bcd7b5bb-a044-4611-82e4-3f3b2a3cda13" + const rsc = "data.cdn77_origin_object_storage.os" + const label = "random origin" + client := acctest.GetClient(t) + originBucketName := "my-bucket-" + uuid.New().String() + request := cdn77.OriginCreateObjectStorageJSONRequestBody{ + Acl: cdn77.AuthenticatedRead, + BucketName: originBucketName, + ClusterId: "842b5641-b641-4723-ac81-f8cc286e288f", + Label: label, + } + + response, err := client.OriginCreateObjectStorageWithResponse(context.Background(), request) + acctest.AssertResponseOk(t, "Failed to create Origin: %s", response, err) + + originId := response.JSON201.Id + scheme := string(response.JSON201.Scheme) + host := response.JSON201.Host + port := response.JSON201.Port + urlModel := shared.NewUrlModel(context.Background(), scheme, host, port, nullable.NewNullNullable[string]()) + originUrl := urlModel.Url.ValueString() + + t.Cleanup(func() { + acctest.MustDeleteOrigin(t, client, origin.TypeObjectStorage, originId) + }) + + acctest.Run(t, nil, + resource.TestStep{ + Config: acctest.Config(objectStorageDataSourceConfig, "id", nonExistingOriginId), + ExpectError: regexp.MustCompile(fmt.Sprintf(`.*?"%s".*?not found.*?`, nonExistingOriginId)), + }, + resource.TestStep{ + Config: acctest.Config(objectStorageDataSourceConfig, "id", originId), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(rsc, "id", originId), + resource.TestCheckResourceAttr(rsc, "label", label), + resource.TestCheckResourceAttr(rsc, "url", originUrl), + resource.TestCheckResourceAttr(rsc, "url_parts.scheme", scheme), + resource.TestCheckResourceAttr(rsc, "url_parts.host", host), + resource.TestCheckResourceAttr(rsc, "url_parts.port", "443"), + func(state *terraform.State) error { + if port, err := port.Get(); err == nil { + return resource.TestCheckResourceAttr(rsc, "url_parts.port", strconv.Itoa(port))(state) + } + + return resource.TestCheckNoResourceAttr(rsc, "url_parts.port")(state) + }, + resource.TestCheckResourceAttr(rsc, "bucket_name", originBucketName), + resource.TestCheckNoResourceAttr(rsc, "note"), + resource.TestCheckNoResourceAttr(rsc, "url_parts.base_path"), + resource.TestCheckNoResourceAttr(rsc, "acl"), + resource.TestCheckNoResourceAttr(rsc, "cluster_id"), + resource.TestCheckNoResourceAttr(rsc, "access_key_id"), + resource.TestCheckNoResourceAttr(rsc, "access_key_secret"), + ), + }, + ) +} + +func TestAccOrigin_ObjectStorageDataSource_AllFields(t *testing.T) { + const rsc = "data.cdn77_origin_object_storage.os" + const label = "random origin" + const note = "some note" + client := acctest.GetClient(t) + originBucketName := "my-bucket-" + uuid.New().String() + request := cdn77.OriginCreateObjectStorageJSONRequestBody{ + Acl: cdn77.AuthenticatedRead, + BucketName: originBucketName, + ClusterId: "842b5641-b641-4723-ac81-f8cc286e288f", + Label: label, + Note: nullable.NewNullableWithValue(note), + } + + response, err := client.OriginCreateObjectStorageWithResponse(context.Background(), request) + acctest.AssertResponseOk(t, "Failed to create Origin: %s", response, err) + + originId := response.JSON201.Id + scheme := string(response.JSON201.Scheme) + host := response.JSON201.Host + port := response.JSON201.Port + urlModel := shared.NewUrlModel(context.Background(), scheme, host, port, nullable.NewNullNullable[string]()) + originUrl := urlModel.Url.ValueString() + + t.Cleanup(func() { + acctest.MustDeleteOrigin(t, client, origin.TypeObjectStorage, originId) + }) + + acctest.Run(t, nil, resource.TestStep{ + Config: acctest.Config(objectStorageDataSourceConfig, "id", originId), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(rsc, "id", originId), + resource.TestCheckResourceAttr(rsc, "label", label), + resource.TestCheckResourceAttr(rsc, "note", note), + resource.TestCheckResourceAttr(rsc, "url", originUrl), + resource.TestCheckResourceAttr(rsc, "url_parts.scheme", scheme), + resource.TestCheckResourceAttr(rsc, "url_parts.host", host), + func(state *terraform.State) error { + if port, err := port.Get(); err == nil { + return resource.TestCheckResourceAttr(rsc, "url_parts.port", strconv.Itoa(port))(state) + } + + return resource.TestCheckNoResourceAttr(rsc, "url_parts.port")(state) + }, + resource.TestCheckResourceAttr(rsc, "bucket_name", originBucketName), + resource.TestCheckNoResourceAttr(rsc, "url_parts.base_path"), + resource.TestCheckNoResourceAttr(rsc, "acl"), + resource.TestCheckNoResourceAttr(rsc, "cluster_id"), + resource.TestCheckNoResourceAttr(rsc, "access_key_id"), + resource.TestCheckNoResourceAttr(rsc, "access_key_secret"), + ), + }) +} + +func checkObjectStorage( + client cdn77.ClientWithResponsesInterface, + originId *string, + fn func(o *cdn77.ObjectStorageOriginDetail) error, +) func(*terraform.State) error { + return func(*terraform.State) error { + response, err := client.OriginDetailObjectStorageWithResponse(context.Background(), *originId) + message := fmt.Sprintf("failed to get Origin[id=%s]: %%s", *originId) + + if err = acctest.CheckResponse(message, response, err); err != nil { + return err + } + + return fn(response.JSON200) + } +} + +const objectStoragesDataSourceConfig = ` +data "cdn77_object_storages" "all" { +} + +locals { + eu_cluster_id = one([for os in data.cdn77_object_storages.all.clusters : os.id if os.label == "EU"]) + us_cluster_id = one([for os in data.cdn77_object_storages.all.clusters : os.id if os.label == "US"]) +} +` + +const objectStorageDataSourceConfig = ` +data "cdn77_origin_object_storage" "os" { + id = "{id}" +} +` diff --git a/internal/provider/origin/schema.go b/internal/provider/origin/schema.go new file mode 100644 index 0000000..45c3c11 --- /dev/null +++ b/internal/provider/origin/schema.go @@ -0,0 +1,48 @@ +package origin + +import ( + "github.com/cdn77/terraform-provider-cdn77/internal/util" + "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/types" + "github.com/oapi-codegen/nullable" +) + +const ( + TypeAws = "aws" + TypeObjectStorage = "object-storage" + TypeUrl = "url" +) + +type SharedModel struct { + Id types.String `tfsdk:"id"` + Label types.String `tfsdk:"label"` + Note types.String `tfsdk:"note"` +} + +func NewSharedModel(id types.String, label string, note nullable.Nullable[string]) SharedModel { + return SharedModel{ + Id: id, + Label: types.StringValue(label), + Note: util.NullableToStringValue(note), + } +} + +func WithSharedSchemaAttrs(s schema.Schema) schema.Schema { + s.Attributes["id"] = schema.StringAttribute{ + Description: "Origin ID (UUID)", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + } + s.Attributes["label"] = schema.StringAttribute{ + Description: "The label helps you to identify your Origin", + Required: true, + } + s.Attributes["note"] = schema.StringAttribute{ + Description: "Optional note for the Origin", + Optional: true, + } + + return s +} diff --git a/internal/provider/origin_resource_schema.go b/internal/provider/origin/schema_deprecated.go similarity index 79% rename from internal/provider/origin_resource_schema.go rename to internal/provider/origin/schema_deprecated.go index e8a363e..02ef8f2 100644 --- a/internal/provider/origin_resource_schema.go +++ b/internal/provider/origin/schema_deprecated.go @@ -1,4 +1,4 @@ -package provider +package origin import ( "fmt" @@ -10,17 +10,33 @@ import ( "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" + "github.com/hashicorp/terraform-plugin-framework/types" ) -const ( - OriginTypeAws = "aws" - OriginTypeObjectStorage = "object-storage" - OriginTypeUrl = "url" -) +const DeprecatedOriginResourceType = "cdn77_origin" + +type DeprecatedOriginModel struct { + Id types.String `tfsdk:"id"` + Type types.String `tfsdk:"type"` + Label types.String `tfsdk:"label"` + Note types.String `tfsdk:"note"` + AwsAccessKeyId types.String `tfsdk:"aws_access_key_id"` + AwsAccessKeySecret types.String `tfsdk:"aws_access_key_secret"` + AwsRegion types.String `tfsdk:"aws_region"` + Acl types.String `tfsdk:"acl"` + ClusterId types.String `tfsdk:"cluster_id"` + AccessKeyId types.String `tfsdk:"access_key_id"` + AccessKeySecret types.String `tfsdk:"access_key_secret"` + BucketName types.String `tfsdk:"bucket_name"` + Scheme types.String `tfsdk:"scheme"` + Host types.String `tfsdk:"host"` + Port types.Int64 `tfsdk:"port"` + BaseDir types.String `tfsdk:"base_dir"` +} -var originTypes = []string{OriginTypeAws, OriginTypeObjectStorage, OriginTypeUrl} //nolint:gochecknoglobals +func CreateDeprecatedOriginResourceSchema() schema.Schema { + originTypes := []string{TypeAws, TypeObjectStorage, TypeUrl} -func CreateOriginResourceSchema() schema.Schema { return schema.Schema{ MarkdownDescription: "Origin resource allows you to manage your Origins", Attributes: map[string]schema.Attribute{ diff --git a/internal/provider/origin/url.go b/internal/provider/origin/url.go new file mode 100644 index 0000000..05793e9 --- /dev/null +++ b/internal/provider/origin/url.go @@ -0,0 +1,181 @@ +package origin + +import ( + "context" + "fmt" + "strings" + + "github.com/cdn77/cdn77-client-go/v2" + "github.com/cdn77/terraform-provider-cdn77/internal/provider/shared" + "github.com/cdn77/terraform-provider-cdn77/internal/util" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.ResourceWithConfigure = &UrlResource{} + _ resource.ResourceWithImportState = &UrlResource{} + _ resource.ResourceWithMoveState = &UrlResource{} +) + +type UrlResource struct { + *util.BaseResource +} + +func (r *UrlResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + diags := &resp.Diagnostics + var data UrlModel + + if diags.Append(req.Plan.Get(ctx, &data)...); diags.HasError() { + return + } + + const errMessage = "Failed to create URL Origin" + + scheme, host, port, basePath := data.UrlModel.Parts(ctx) + request := cdn77.OriginCreateUrlJSONRequestBody{ + Label: data.Label.ValueString(), + Note: util.StringValueToNullable(data.Note), + Scheme: cdn77.OriginScheme(scheme), + Host: host, + Port: port, + BaseDir: basePath, + } + + response, err := r.Client.OriginCreateUrlWithResponse(ctx, request) + if err != nil { + diags.AddError(errMessage, err.Error()) + + return + } + + util.ProcessResponse(diags, response, errMessage, response.JSON201, func(detail *cdn77.UrlOriginDetail) { + data.Id = types.StringValue(detail.Id) + + diags.Append(resp.State.Set(ctx, data)...) + }) +} + +func (r *UrlResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + diags := &resp.Diagnostics + var data UrlModel + + if diags.Append(req.Plan.Get(ctx, &data)...); diags.HasError() { + return + } + + const errMessage = "Failed to update URL Origin" + + scheme, host, port, basePath := data.UrlModel.Parts(ctx) + request := cdn77.OriginEditUrlJSONRequestBody{ + Label: data.Label.ValueStringPointer(), + Note: util.StringValueToNullable(data.Note), + Scheme: util.Pointer(cdn77.OriginScheme(scheme)), + Host: &host, + Port: port, + BaseDir: basePath, + } + + response, err := r.Client.OriginEditUrlWithResponse(ctx, data.Id.ValueString(), request) + if err != nil { + diags.AddError(errMessage, err.Error()) + + return + } + + util.ProcessEmptyResponse(diags, response, errMessage, func() { + diags.Append(resp.State.Set(ctx, data)...) + }) +} + +func (r *UrlResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + diags := &resp.Diagnostics + var data UrlModel + + if diags.Append(req.State.Get(ctx, &data)...); diags.HasError() { + return + } + + const errMessage = "Failed to delete URL Origin" + + response, err := r.Client.OriginDeleteUrlWithResponse(ctx, data.Id.ValueString()) + if err != nil { + diags.AddError(errMessage, err.Error()) + + return + } + + util.ValidateDeletionResponse(diags, response, errMessage) +} + +func (*UrlResource) ImportState( + ctx context.Context, + req resource.ImportStateRequest, + resp *resource.ImportStateResponse, +) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *UrlResource) MoveState(context.Context) []resource.StateMover { + deprecatedOriginSchema := CreateDeprecatedOriginResourceSchema() + + return []resource.StateMover{ + { + SourceSchema: &deprecatedOriginSchema, + StateMover: func(ctx context.Context, req resource.MoveStateRequest, resp *resource.MoveStateResponse) { + if req.SourceTypeName != DeprecatedOriginResourceType || + !strings.HasSuffix(req.SourceProviderAddress, "cdn77/cdn77") { + return + } + + diags := &resp.Diagnostics + var oldModel DeprecatedOriginModel + + if diags.Append(req.SourceState.Get(ctx, &oldModel)...); diags.HasError() { + return + } + + if oldModel.Type.ValueString() != TypeUrl { + diags.AddError( + "Unable to Move Resource State", + fmt.Sprintf( + "Only %q resources with type=%q can be moved to %q.\nLabel: %s\nType: %s\n", + DeprecatedOriginResourceType, + TypeUrl, + r.FullName(), + oldModel.Label.ValueString(), + oldModel.Type.ValueString(), + ), + ) + + return + } + + model := UrlModel{ + SharedModel: NewSharedModel( + oldModel.Id, + oldModel.Label.ValueString(), + util.StringValueToNullable(oldModel.Note), + ), + UrlModel: shared.NewUrlModel( + ctx, + oldModel.Scheme.ValueString(), + oldModel.Host.ValueString(), + util.Int64ValueToNullable[int](oldModel.Port), + util.StringValueToNullable(oldModel.BaseDir), + ), + } + + diags.Append(resp.TargetState.Set(ctx, model)...) + }, + }, + } +} + +var _ datasource.DataSourceWithConfigure = &UrlDataSource{} + +type UrlDataSource struct { + *util.BaseDataSource +} diff --git a/internal/provider/origin/url_reader.go b/internal/provider/origin/url_reader.go new file mode 100644 index 0000000..2b85708 --- /dev/null +++ b/internal/provider/origin/url_reader.go @@ -0,0 +1,47 @@ +package origin + +import ( + "context" + "fmt" + + "github.com/cdn77/cdn77-client-go/v2" + "github.com/cdn77/terraform-provider-cdn77/internal/provider/shared" + "github.com/hashicorp/terraform-plugin-framework/diag" +) + +type UrlReader struct{} + +func (*UrlReader) ErrMessage() string { + return "Failed to fetch URL Origin" +} + +func (*UrlReader) Fetch( + ctx context.Context, + client cdn77.ClientWithResponsesInterface, + model UrlModel, +) (*cdn77.OriginDetailUrlResponse, *cdn77.UrlOriginDetail, error) { + response, err := client.OriginDetailUrlWithResponse(ctx, model.Id.ValueString()) + if err != nil { + return nil, nil, err + } + + return response, response.JSON200, nil +} + +func (r *UrlReader) Process( + ctx context.Context, + model UrlModel, + detail *cdn77.UrlOriginDetail, + diags *diag.Diagnostics, +) UrlModel { + if detail.Type != TypeUrl { + diags.AddError(r.ErrMessage(), fmt.Sprintf("Origin with id=\"%s\" is not an URL Origin", detail.Id)) + + return model + } + + return UrlModel{ + SharedModel: NewSharedModel(model.Id, detail.Label, detail.Note), + UrlModel: shared.NewUrlModel(ctx, string(detail.Scheme), detail.Host, detail.Port, detail.BaseDir), + } +} diff --git a/internal/provider/origin/url_schema.go b/internal/provider/origin/url_schema.go new file mode 100644 index 0000000..ec781ee --- /dev/null +++ b/internal/provider/origin/url_schema.go @@ -0,0 +1,18 @@ +package origin + +import ( + "github.com/cdn77/terraform-provider-cdn77/internal/provider/shared" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" +) + +type UrlModel struct { + SharedModel + shared.UrlModel +} + +func CreateUrlResourceSchema() schema.Schema { + return WithSharedSchemaAttrs(shared.WithUrlSchemaAttrs(schema.Schema{ + MarkdownDescription: "URL Origin resource allows you to manage your custom URL Origins", + Attributes: map[string]schema.Attribute{}, + })) +} diff --git a/internal/provider/origin/url_test.go b/internal/provider/origin/url_test.go new file mode 100644 index 0000000..2594bf3 --- /dev/null +++ b/internal/provider/origin/url_test.go @@ -0,0 +1,283 @@ +package origin_test + +import ( + "context" + "errors" + "fmt" + "regexp" + "strconv" + "testing" + + "github.com/cdn77/cdn77-client-go/v2" + "github.com/cdn77/terraform-provider-cdn77/internal/acctest" + "github.com/cdn77/terraform-provider-cdn77/internal/provider/origin" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/oapi-codegen/nullable" +) + +func TestAccOrigin_UrlResource(t *testing.T) { + client := acctest.GetClient(t) + rsc := "cdn77_origin_url.url" + var originId string + + acctest.Run(t, acctest.CheckOriginDestroyed(client, origin.TypeUrl), + resource.TestStep{ + Config: `resource "cdn77_origin_url" "url" { + label = "some label" + url = "http://my-totally-random-custom-host.com" + url_parts = { + scheme = "http" + host = "my-totally-random-custom-host.com" + } + }`, + ExpectError: regexp.MustCompile(`(?s)Invalid Attribute Combination.*` + + `attributes specified when one \(and only one\) of \[url,url_parts\] is required`), + PlanOnly: true, + }, + resource.TestStep{ + Config: `resource "cdn77_origin_url" "url" { + label = "some label" + url = "http://my-totally-random-custom-host.com" + }`, + ConfigPlanChecks: acctest.ConfigPlanChecks(rsc, plancheck.ResourceActionCreate), + Check: resource.ComposeAggregateTestCheckFunc( + acctest.CheckAndAssignAttr(rsc, "id", &originId), + resource.TestCheckResourceAttr(rsc, "label", "some label"), + resource.TestCheckResourceAttr(rsc, "url", "http://my-totally-random-custom-host.com"), + resource.TestCheckResourceAttr(rsc, "url_parts.scheme", "http"), + resource.TestCheckResourceAttr(rsc, "url_parts.host", "my-totally-random-custom-host.com"), + resource.TestCheckNoResourceAttr(rsc, "note"), + resource.TestCheckNoResourceAttr(rsc, "url_parts.port"), + resource.TestCheckNoResourceAttr(rsc, "url_parts.base_path"), + checkUrl(client, &originId, func(o *cdn77.UrlOriginDetail) error { + return errors.Join( + acctest.EqualField("type", o.Type, origin.TypeUrl), + acctest.EqualField("label", o.Label, "some label"), + acctest.NullField("note", o.Note), + acctest.EqualField("scheme", o.Scheme, "http"), + acctest.EqualField("host", o.Host, "my-totally-random-custom-host.com"), + acctest.NullField("port", o.Port), + acctest.NullField("base_dir", o.BaseDir), + ) + }), + ), + }, + resource.TestStep{ + Config: `resource "cdn77_origin_url" "url" { + label = "some label" + url_parts = { + scheme = "http" + host = "my-totally-random-custom-host.com" + } + }`, + ConfigPlanChecks: acctest.ConfigPlanChecks(rsc, plancheck.ResourceActionNoop), + }, + resource.TestStep{ + Config: `resource "cdn77_origin_url" "url" { + label = "another label" + note = "some note" + url = "http://my-totally-random-custom-host.com:12345/some-dir" + }`, + ConfigPlanChecks: acctest.ConfigPlanChecks(rsc, plancheck.ResourceActionUpdate), + Check: resource.ComposeAggregateTestCheckFunc( + acctest.CheckAttr(rsc, "id", &originId), + resource.TestCheckResourceAttr(rsc, "label", "another label"), + resource.TestCheckResourceAttr(rsc, "note", "some note"), + resource.TestCheckResourceAttr(rsc, "url", "http://my-totally-random-custom-host.com:12345/some-dir"), + resource.TestCheckResourceAttr(rsc, "url_parts.scheme", "http"), + resource.TestCheckResourceAttr(rsc, "url_parts.host", "my-totally-random-custom-host.com"), + resource.TestCheckResourceAttr(rsc, "url_parts.port", "12345"), + resource.TestCheckResourceAttr(rsc, "url_parts.base_path", "some-dir"), + checkUrl(client, &originId, func(o *cdn77.UrlOriginDetail) error { + return errors.Join( + acctest.EqualField("type", o.Type, origin.TypeUrl), + acctest.EqualField("label", o.Label, "another label"), + acctest.NullFieldEqual("note", o.Note, "some note"), + acctest.EqualField("scheme", o.Scheme, "http"), + acctest.EqualField("host", o.Host, "my-totally-random-custom-host.com"), + acctest.NullFieldEqual("port", o.Port, 12345), + acctest.NullFieldEqual("base_dir", o.BaseDir, "some-dir"), + ) + }), + ), + }, + resource.TestStep{ + Config: `resource "cdn77_origin_url" "url" { + label = "another label" + note = "some note" + url_parts = { + scheme = "http" + host = "my-totally-random-custom-host.com" + port = 12345 + base_path = "some-dir" + } + }`, + ConfigPlanChecks: acctest.ConfigPlanChecks(rsc, plancheck.ResourceActionNoop), + }, + resource.TestStep{ + Config: `resource "cdn77_origin_url" "url" { + label = "another label" + url = "http://my-totally-random-custom-host.com" + }`, + ConfigPlanChecks: acctest.ConfigPlanChecks(rsc, plancheck.ResourceActionUpdate), + Check: resource.ComposeAggregateTestCheckFunc( + acctest.CheckAttr(rsc, "id", &originId), + resource.TestCheckResourceAttr(rsc, "label", "another label"), + resource.TestCheckResourceAttr(rsc, "url", "http://my-totally-random-custom-host.com"), + resource.TestCheckResourceAttr(rsc, "url_parts.scheme", "http"), + resource.TestCheckResourceAttr(rsc, "url_parts.host", "my-totally-random-custom-host.com"), + resource.TestCheckNoResourceAttr(rsc, "note"), + resource.TestCheckNoResourceAttr(rsc, "url_parts.port"), + resource.TestCheckNoResourceAttr(rsc, "url_parts.base_path"), + checkUrl(client, &originId, func(o *cdn77.UrlOriginDetail) error { + return errors.Join( + acctest.EqualField("type", o.Type, origin.TypeUrl), + acctest.EqualField("label", o.Label, "another label"), + acctest.NullField("note", o.Note), + acctest.EqualField("scheme", o.Scheme, "http"), + acctest.EqualField("host", o.Host, "my-totally-random-custom-host.com"), + acctest.NullField("port", o.Port), + acctest.NullField("base_dir", o.BaseDir), + ) + }), + ), + }, + ) +} + +func TestAccOrigin_UrlResource_Import(t *testing.T) { + client := acctest.GetClient(t) + rsc := "cdn77_origin_url.url" + var originId string + + acctest.Run(t, acctest.CheckOriginDestroyed(client, origin.TypeUrl), + resource.TestStep{ + Config: `resource "cdn77_origin_url" "url" { + label = "some label" + note = "some note" + url = "http://my-totally-random-custom-host.com" + }`, + Check: acctest.CheckAndAssignAttr(rsc, "id", &originId), + }, + resource.TestStep{ + ResourceName: rsc, + ImportState: true, + ImportStateIdFunc: func(*terraform.State) (string, error) { + return originId, nil + }, + ImportStateVerify: true, + }, + ) +} + +func TestAccOrigin_UrlDataSource_OnlyRequiredFields(t *testing.T) { + const nonExistingOriginId = "bcd7b5bb-a044-4611-82e4-3f3b2a3cda13" + const rsc = "data.cdn77_origin_url.url" + const label = "random origin" + const originUrl = "http://my-totally-random-custom-host.com" + const scheme = "http" + const host = "my-totally-random-custom-host.com" + client := acctest.GetClient(t) + request := cdn77.OriginCreateUrlJSONRequestBody{ + Label: label, + Scheme: scheme, + Host: host, + } + + response, err := client.OriginCreateUrlWithResponse(context.Background(), request) + acctest.AssertResponseOk(t, "Failed to create Origin: %s", response, err) + + originId := response.JSON201.Id + + t.Cleanup(func() { + acctest.MustDeleteOrigin(t, client, origin.TypeUrl, originId) + }) + + acctest.Run(t, nil, + resource.TestStep{ + Config: acctest.Config(urlDataSourceConfig, "id", nonExistingOriginId), + ExpectError: regexp.MustCompile(fmt.Sprintf(`.*?"%s".*?not found.*?`, nonExistingOriginId)), + }, + resource.TestStep{ + Config: acctest.Config(urlDataSourceConfig, "id", originId), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(rsc, "id", originId), + resource.TestCheckResourceAttr(rsc, "label", label), + resource.TestCheckResourceAttr(rsc, "url", originUrl), + resource.TestCheckResourceAttr(rsc, "url_parts.scheme", scheme), + resource.TestCheckResourceAttr(rsc, "url_parts.host", host), + resource.TestCheckNoResourceAttr(rsc, "note"), + resource.TestCheckNoResourceAttr(rsc, "url_parts.port"), + resource.TestCheckNoResourceAttr(rsc, "url_parts.base_path"), + ), + }, + ) +} + +func TestAccOrigin_UrlDataSource_AllFields(t *testing.T) { + const rsc = "data.cdn77_origin_url.url" + const label = "random origin" + const note = "some note" + const originUrl = "https://my-totally-random-custom-host.com:12345/some-dir" + const scheme = "https" + const host = "my-totally-random-custom-host.com" + const port = 12345 + const basePath = "some-dir" + client := acctest.GetClient(t) + request := cdn77.OriginCreateUrlJSONRequestBody{ + Label: label, + Note: nullable.NewNullableWithValue(note), + Scheme: scheme, + Host: host, + Port: nullable.NewNullableWithValue(port), + BaseDir: nullable.NewNullableWithValue(basePath), + } + + response, err := client.OriginCreateUrlWithResponse(context.Background(), request) + acctest.AssertResponseOk(t, "Failed to create Origin: %s", response, err) + + originId := response.JSON201.Id + + t.Cleanup(func() { + acctest.MustDeleteOrigin(t, client, origin.TypeUrl, originId) + }) + + acctest.Run(t, nil, resource.TestStep{ + Config: acctest.Config(urlDataSourceConfig, "id", originId), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(rsc, "id", originId), + resource.TestCheckResourceAttr(rsc, "label", label), + resource.TestCheckResourceAttr(rsc, "note", note), + resource.TestCheckResourceAttr(rsc, "url", originUrl), + resource.TestCheckResourceAttr(rsc, "url_parts.scheme", scheme), + resource.TestCheckResourceAttr(rsc, "url_parts.host", host), + resource.TestCheckResourceAttr(rsc, "url_parts.port", strconv.Itoa(port)), + resource.TestCheckResourceAttr(rsc, "url_parts.base_path", basePath), + ), + }) +} + +func checkUrl( + client cdn77.ClientWithResponsesInterface, + originId *string, + fn func(o *cdn77.UrlOriginDetail) error, +) func(*terraform.State) error { + return func(*terraform.State) error { + response, err := client.OriginDetailUrlWithResponse(context.Background(), *originId) + message := fmt.Sprintf("failed to get Origin[id=%s]: %%s", *originId) + + if err = acctest.CheckResponse(message, response, err); err != nil { + return err + } + + return fn(response.JSON200) + } +} + +const urlDataSourceConfig = ` +data "cdn77_origin_url" "url" { + id = "{id}" +} +` diff --git a/internal/provider/origin_config_validator.go b/internal/provider/origin_config_validator.go deleted file mode 100644 index 65bb900..0000000 --- a/internal/provider/origin_config_validator.go +++ /dev/null @@ -1,134 +0,0 @@ -package provider - -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/datasource" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" -) - -var ( - _ datasource.ConfigValidator = &OriginConfigValidator{} - _ resource.ConfigValidator = &OriginConfigValidator{} -) - -type OriginConfigValidator struct{} - -func NewOriginTypeConfigValidator() *OriginConfigValidator { - return &OriginConfigValidator{} -} - -func (v OriginConfigValidator) Description(ctx context.Context) string { - return v.MarkdownDescription(ctx) -} - -func (OriginConfigValidator) MarkdownDescription(_ context.Context) string { - return "Checks Origin configuration for all required/conflicting attributes" -} - -func (v OriginConfigValidator) ValidateDataSource( - ctx context.Context, - req datasource.ValidateConfigRequest, - resp *datasource.ValidateConfigResponse, -) { - resp.Diagnostics = v.Validate(ctx, req.Config) -} - -func (v OriginConfigValidator) ValidateResource( - ctx context.Context, - req resource.ValidateConfigRequest, - resp *resource.ValidateConfigResponse, -) { - resp.Diagnostics = v.Validate(ctx, req.Config) -} - -func (v OriginConfigValidator) Validate(ctx context.Context, config tfsdk.Config) diag.Diagnostics { - var data OriginModel - diags := config.Get(ctx, &data) - - originType := data.Type.ValueString() - awsAttributes := []attrNameAndValue{ - {name: "aws_access_key_id", value: data.AwsAccessKeyId}, - {name: "aws_access_key_secret", value: data.AwsAccessKeySecret}, - {name: "aws_region", value: data.AwsRegion}, - } - objectStorageAttributes := []attrNameAndValue{ - {name: "acl", value: data.Acl}, - {name: "cluster_id", value: data.ClusterId}, - {name: "bucket_name", value: data.BucketName}, - } - schemeAndHostAttributes := []attrNameAndValue{ - {name: "scheme", value: data.Scheme}, - {name: "host", value: data.Host}, - } - var conflictingAttributes, requiredAttributes []attrNameAndValue - - switch originType { - case OriginTypeAws: - conflictingAttributes = append(conflictingAttributes, objectStorageAttributes...) - requiredAttributes = schemeAndHostAttributes - case OriginTypeObjectStorage: - conflictingAttributes = append(conflictingAttributes, awsAttributes...) - conflictingAttributes = append(conflictingAttributes, schemeAndHostAttributes...) - conflictingAttributes = append(conflictingAttributes, attrNameAndValue{name: "port", value: data.Port}) - conflictingAttributes = append(conflictingAttributes, attrNameAndValue{name: "base_dir", value: data.BaseDir}) - requiredAttributes = objectStorageAttributes - case OriginTypeUrl: - conflictingAttributes = append(conflictingAttributes, awsAttributes...) - conflictingAttributes = append(conflictingAttributes, objectStorageAttributes...) - requiredAttributes = schemeAndHostAttributes - default: - addUnknownOriginTypeError(&diags, data) - - return diags - } - - diags.Append(v.doValidate(originType, conflictingAttributes, requiredAttributes)...) - - return diags -} - -func (OriginConfigValidator) doValidate( - originType string, - conflictingAttributes []attrNameAndValue, - requiredAttributes []attrNameAndValue, -) (diags diag.Diagnostics) { - for _, attribute := range conflictingAttributes { - if attribute.value.IsNull() { - continue - } - - diags.Append( - validatordiag.InvalidAttributeCombinationDiagnostic( - path.Root(attribute.name), - fmt.Sprintf(`Attribute "%s" can't be used with Origin type "%s"'`, attribute.name, originType), - ), - ) - } - - for _, attribute := range requiredAttributes { - if !attribute.value.IsNull() { - continue - } - - diags.Append( - validatordiag.InvalidAttributeCombinationDiagnostic( - path.Root(attribute.name), - fmt.Sprintf(`Attribute "%s" is required for Origin type "%s"'`, attribute.name, originType), - ), - ) - } - - return diags -} - -type attrNameAndValue struct { - name string - value attr.Value -} diff --git a/internal/provider/origin_data_reader.go b/internal/provider/origin_data_reader.go deleted file mode 100644 index 1bb7629..0000000 --- a/internal/provider/origin_data_reader.go +++ /dev/null @@ -1,174 +0,0 @@ -package provider - -import ( - "context" - - "github.com/cdn77/cdn77-client-go" - "github.com/cdn77/terraform-provider-cdn77/internal/util" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-framework/types" -) - -const missingOriginIdDetailMessage = "Origin ID is null, unknown or an empty string" - -type OriginModel struct { - Id types.String `tfsdk:"id"` - Type types.String `tfsdk:"type"` - Label types.String `tfsdk:"label"` - Note types.String `tfsdk:"note"` - AwsAccessKeyId types.String `tfsdk:"aws_access_key_id"` - AwsAccessKeySecret types.String `tfsdk:"aws_access_key_secret"` - AwsRegion types.String `tfsdk:"aws_region"` - Acl types.String `tfsdk:"acl"` - ClusterId types.String `tfsdk:"cluster_id"` - AccessKeyId types.String `tfsdk:"access_key_id"` - AccessKeySecret types.String `tfsdk:"access_key_secret"` - BucketName types.String `tfsdk:"bucket_name"` - Scheme types.String `tfsdk:"scheme"` - Host types.String `tfsdk:"host"` - Port types.Int64 `tfsdk:"port"` - BaseDir types.String `tfsdk:"base_dir"` -} - -type OriginDataReader struct { - ctx context.Context - client cdn77.ClientWithResponsesInterface - removeMissingResource bool -} - -func NewOriginDataSourceReader(ctx context.Context, client cdn77.ClientWithResponsesInterface) *OriginDataReader { - return &OriginDataReader{ctx: ctx, client: client, removeMissingResource: false} -} - -func NewOriginResourceReader(ctx context.Context, client cdn77.ClientWithResponsesInterface) *OriginDataReader { - return &OriginDataReader{ctx: ctx, client: client, removeMissingResource: true} -} - -func (d *OriginDataReader) Read(provider StateProvider, diags *diag.Diagnostics, state *tfsdk.State) { - var data OriginModel - if diags.Append(provider.Get(d.ctx, &data)...); diags.HasError() { - return - } - - if data.Id.ValueString() == "" { - diags.AddError("Can't fetch Origin without ID", missingOriginIdDetailMessage) - - return - } - - var ( - ok bool - statusCode int - ) - - const errMessage = "Failed to fetch Origin" - - switch data.Type.ValueString() { - case OriginTypeAws: - ok, statusCode = d.readAws(diags, errMessage, &data) - case OriginTypeObjectStorage: - ok, statusCode = d.readObjectStorage(diags, errMessage, &data) - case OriginTypeUrl: - ok, statusCode = d.readUrl(diags, errMessage, &data) - default: - addUnknownOriginTypeError(diags, data) - - return - } - - if ok { - diags.Append(state.Set(d.ctx, &data)...) - - return - } - - if d.removeMissingResource && maybeRemoveMissingResource(d.ctx, statusCode, data.Id.ValueString(), state) { - return - } -} - -func (d *OriginDataReader) readAws(diags *diag.Diagnostics, message string, data *OriginModel) (bool, int) { - response, err := d.client.OriginDetailAwsWithResponse(d.ctx, data.Id.ValueString()) - if err != nil { - diags.AddError(message, err.Error()) - - return false, 0 - } - - if !util.CheckResponse(diags, message, response, response.JSON404, response.JSONDefault) { - return false, response.StatusCode() - } - - *data = OriginModel{ - Id: data.Id, - Type: types.StringValue(OriginTypeAws), - Label: types.StringValue(response.JSON200.Label), - Note: util.NullableToStringValue(response.JSON200.Note), - AwsAccessKeyId: util.NullableToStringValue(response.JSON200.AwsAccessKeyId), - AwsAccessKeySecret: data.AwsAccessKeySecret, - AwsRegion: util.NullableToStringValue(response.JSON200.AwsRegion), - Scheme: types.StringValue(string(response.JSON200.Scheme)), - Host: types.StringValue(response.JSON200.Host), - Port: util.NullableIntToInt64Value(response.JSON200.Port), - BaseDir: util.NullableToStringValue(response.JSON200.BaseDir), - } - - return true, response.StatusCode() -} - -func (d *OriginDataReader) readObjectStorage(diags *diag.Diagnostics, message string, data *OriginModel) (bool, int) { - response, err := d.client.OriginDetailObjectStorageWithResponse(d.ctx, data.Id.ValueString()) - if err != nil { - diags.AddError(message, err.Error()) - - return false, 0 - } - - if !util.CheckResponse(diags, message, response, response.JSON404, response.JSONDefault) { - return false, response.StatusCode() - } - - *data = OriginModel{ - Id: data.Id, - Type: types.StringValue(OriginTypeObjectStorage), - Label: types.StringValue(response.JSON200.Label), - Note: util.NullableToStringValue(response.JSON200.Note), - Acl: data.Acl, - ClusterId: data.ClusterId, - AccessKeyId: data.AccessKeyId, - AccessKeySecret: data.AccessKeySecret, - BucketName: types.StringValue(response.JSON200.BucketName), - Scheme: types.StringValue(string(response.JSON200.Scheme)), - Host: types.StringValue(response.JSON200.Host), - Port: util.NullableIntToInt64Value(response.JSON200.Port), - } - - return true, response.StatusCode() -} - -func (d *OriginDataReader) readUrl(diags *diag.Diagnostics, message string, data *OriginModel) (bool, int) { - response, err := d.client.OriginDetailUrlWithResponse(d.ctx, data.Id.ValueString()) - if err != nil { - diags.AddError(message, err.Error()) - - return false, 0 - } - - if !util.CheckResponse(diags, message, response, response.JSON404, response.JSONDefault) { - return false, response.StatusCode() - } - - *data = OriginModel{ - Id: data.Id, - Type: types.StringValue(OriginTypeUrl), - Label: types.StringValue(response.JSON200.Label), - Note: util.NullableToStringValue(response.JSON200.Note), - Scheme: types.StringValue(string(response.JSON200.Scheme)), - Host: types.StringValue(response.JSON200.Host), - Port: util.NullableIntToInt64Value(response.JSON200.Port), - BaseDir: util.NullableToStringValue(response.JSON200.BaseDir), - } - - return true, response.StatusCode() -} diff --git a/internal/provider/origin_data_source.go b/internal/provider/origin_data_source.go deleted file mode 100644 index 030240c..0000000 --- a/internal/provider/origin_data_source.go +++ /dev/null @@ -1,43 +0,0 @@ -package provider - -import ( - "context" - - "github.com/cdn77/cdn77-client-go" - "github.com/cdn77/terraform-provider-cdn77/internal/util" - "github.com/hashicorp/terraform-plugin-framework/datasource" -) - -var _ datasource.DataSourceWithConfigure = &OriginDataSource{} - -func NewOriginDataSource() datasource.DataSource { - return &OriginDataSource{} -} - -type OriginDataSource struct { - client cdn77.ClientWithResponsesInterface -} - -func (*OriginDataSource) Metadata( - _ context.Context, - req datasource.MetadataRequest, - resp *datasource.MetadataResponse, -) { - resp.TypeName = req.ProviderTypeName + "_origin" -} - -func (*OriginDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = util.NewResourceDataSourceSchemaConverter("id", "type").Convert(CreateOriginResourceSchema()) -} - -func (d *OriginDataSource) Configure( - _ context.Context, - req datasource.ConfigureRequest, - resp *datasource.ConfigureResponse, -) { - resp.Diagnostics.Append(util.MaybeSetClient(req.ProviderData, &d.client)) -} - -func (d *OriginDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - NewOriginDataSourceReader(ctx, d.client).Read(&req.Config, &resp.Diagnostics, &resp.State) -} diff --git a/internal/provider/origin_data_source_test.go b/internal/provider/origin_data_source_test.go deleted file mode 100644 index feba512..0000000 --- a/internal/provider/origin_data_source_test.go +++ /dev/null @@ -1,396 +0,0 @@ -package provider_test - -import ( - "context" - "fmt" - "regexp" - "strconv" - "testing" - - "github.com/cdn77/cdn77-client-go" - "github.com/cdn77/terraform-provider-cdn77/internal/acctest" - "github.com/cdn77/terraform-provider-cdn77/internal/provider" - "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/oapi-codegen/nullable" -) - -func TestAccOriginDataSource_NonExistingOrigin(t *testing.T) { - const originId = "bcd7b5bb-a044-4611-82e4-3f3b2a3cda13" - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctest.GetProviderFactories(), - Steps: []resource.TestStep{ - { - Config: acctest.Config(originDataSourceConfigAws, "id", originId), - ExpectError: regexp.MustCompile(fmt.Sprintf(`.*?"%s".*?not found.*?`, originId)), - }, - { - Config: acctest.Config(originDataSourceConfigObjectStorage, "id", originId), - ExpectError: regexp.MustCompile(fmt.Sprintf(`.*?"%s".*?not found.*?`, originId)), - }, - { - Config: acctest.Config(originDataSourceConfigUrl, "id", originId), - ExpectError: regexp.MustCompile(fmt.Sprintf(`.*?"%s".*?not found.*?`, originId)), - }, - }, - }) -} - -func TestAccOriginDataSource_Aws_OnlyRequiredFields(t *testing.T) { - client := acctest.GetClient(t) - - const rsc = "data.cdn77_origin.aws" - const originHost = "my-totally-random-custom-host.com" - const originLabel = "random origin" - const originScheme = "https" - - request := cdn77.OriginAddAwsJSONRequestBody{ - Host: originHost, - Label: originLabel, - Scheme: originScheme, - } - response, err := client.OriginAddAwsWithResponse(context.Background(), request) - acctest.AssertResponseOk(t, "Failed to create Origin: %s", response, err) - - originId := response.JSON201.Id - - t.Cleanup(func() { - acctest.MustDeleteOrigin(t, client, provider.OriginTypeAws, originId) - }) - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctest.GetProviderFactories(), - Steps: []resource.TestStep{ - { - Config: acctest.Config(originDataSourceConfigAws, "id", originId), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr(rsc, "id", originId), - resource.TestCheckResourceAttr(rsc, "type", provider.OriginTypeAws), - resource.TestCheckResourceAttr(rsc, "label", originLabel), - resource.TestCheckResourceAttr(rsc, "scheme", originScheme), - resource.TestCheckResourceAttr(rsc, "host", originHost), - resource.TestCheckNoResourceAttr(rsc, "note"), - resource.TestCheckNoResourceAttr(rsc, "aws_access_key_id"), - resource.TestCheckNoResourceAttr(rsc, "aws_access_key_secret"), - resource.TestCheckNoResourceAttr(rsc, "aws_region"), - resource.TestCheckNoResourceAttr(rsc, "acl"), - resource.TestCheckNoResourceAttr(rsc, "cluster_id"), - resource.TestCheckNoResourceAttr(rsc, "bucket_name"), - resource.TestCheckNoResourceAttr(rsc, "access_key_id"), - resource.TestCheckNoResourceAttr(rsc, "access_key_secret"), - resource.TestCheckNoResourceAttr(rsc, "port"), - resource.TestCheckNoResourceAttr(rsc, "base_dir"), - ), - }, - }, - }) -} - -func TestAccOriginDataSource_Aws_AllFields(t *testing.T) { - client := acctest.GetClient(t) - - const rsc = "data.cdn77_origin.aws" - const originAwsKeyId = "someKeyId" - const originAwsKeySecret = "someKeySecret" - const originAwsRegion = "eu" - const originBaseDir = "some-dir" - const originHost = "my-totally-random-custom-host.com" - const originLabel = "random origin" - const originNote = "some note" - const originPort = 12345 - const originScheme = "https" - - request := cdn77.OriginAddAwsJSONRequestBody{ - AwsAccessKeyId: nullable.NewNullableWithValue(originAwsKeyId), - AwsAccessKeySecret: nullable.NewNullableWithValue(originAwsKeySecret), - AwsRegion: nullable.NewNullableWithValue(originAwsRegion), - BaseDir: nullable.NewNullableWithValue(originBaseDir), - Host: originHost, - Label: originLabel, - Note: nullable.NewNullableWithValue(originNote), - Port: nullable.NewNullableWithValue(originPort), - Scheme: originScheme, - } - response, err := client.OriginAddAwsWithResponse(context.Background(), request) - acctest.AssertResponseOk(t, "Failed to create Origin: %s", response, err) - - originId := response.JSON201.Id - - t.Cleanup(func() { - acctest.MustDeleteOrigin(t, client, provider.OriginTypeAws, originId) - }) - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctest.GetProviderFactories(), - Steps: []resource.TestStep{ - { - Config: acctest.Config(originDataSourceConfigAws, "id", originId), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr(rsc, "id", originId), - resource.TestCheckResourceAttr(rsc, "type", provider.OriginTypeAws), - resource.TestCheckResourceAttr(rsc, "label", originLabel), - resource.TestCheckResourceAttr(rsc, "note", originNote), - resource.TestCheckResourceAttr(rsc, "aws_access_key_id", originAwsKeyId), - resource.TestCheckResourceAttr(rsc, "aws_region", originAwsRegion), - resource.TestCheckResourceAttr(rsc, "host", originHost), - resource.TestCheckResourceAttr(rsc, "port", strconv.Itoa(originPort)), - resource.TestCheckResourceAttr(rsc, "base_dir", originBaseDir), - resource.TestCheckNoResourceAttr(rsc, "acl"), - resource.TestCheckNoResourceAttr(rsc, "cluster_id"), - resource.TestCheckNoResourceAttr(rsc, "bucket_name"), - resource.TestCheckNoResourceAttr(rsc, "access_key_id"), - resource.TestCheckNoResourceAttr(rsc, "access_key_secret"), - ), - }, - }, - }) -} - -func TestAccOriginDataSource_ObjectStorage_OnlyRequiredFields(t *testing.T) { - client := acctest.GetClient(t) - - const rsc = "data.cdn77_origin.os" - originBucketName := "my-bucket-" + uuid.New().String() - const originLabel = "random origin" - - request := cdn77.OriginAddObjectStorageJSONRequestBody{ - Acl: cdn77.AuthenticatedRead, - BucketName: originBucketName, - ClusterId: "842b5641-b641-4723-ac81-f8cc286e288f", - Label: originLabel, - } - response, err := client.OriginAddObjectStorageWithResponse(context.Background(), request) - acctest.AssertResponseOk(t, "Failed to create Origin: %s", response, err) - - originId := response.JSON201.Id - originScheme := string(response.JSON201.Scheme) - originHost := response.JSON201.Host - originPort := response.JSON201.Port - - t.Cleanup(func() { - acctest.MustDeleteOrigin(t, client, provider.OriginTypeObjectStorage, originId) - }) - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctest.GetProviderFactories(), - Steps: []resource.TestStep{ - { - Config: acctest.Config(originDataSourceConfigObjectStorage, "id", originId), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr(rsc, "id", originId), - resource.TestCheckResourceAttr(rsc, "type", provider.OriginTypeObjectStorage), - resource.TestCheckResourceAttr(rsc, "label", originLabel), - resource.TestCheckResourceAttr(rsc, "bucket_name", originBucketName), - resource.TestCheckResourceAttr(rsc, "scheme", originScheme), - resource.TestCheckResourceAttr(rsc, "host", originHost), - func(state *terraform.State) error { - if originPort.IsNull() { - return resource.TestCheckNoResourceAttr(rsc, "port")(state) - } - - return resource.TestCheckResourceAttr(rsc, "port", strconv.Itoa(originPort.MustGet()))(state) - }, - resource.TestCheckNoResourceAttr(rsc, "note"), - resource.TestCheckNoResourceAttr(rsc, "aws_access_key_id"), - resource.TestCheckNoResourceAttr(rsc, "aws_access_key_secret"), - resource.TestCheckNoResourceAttr(rsc, "aws_region"), - resource.TestCheckNoResourceAttr(rsc, "acl"), - resource.TestCheckNoResourceAttr(rsc, "cluster_id"), - resource.TestCheckNoResourceAttr(rsc, "access_key_id"), - resource.TestCheckNoResourceAttr(rsc, "access_key_secret"), - resource.TestCheckNoResourceAttr(rsc, "base_dir"), - ), - }, - }, - }) -} - -func TestAccOriginDataSource_ObjectStorage_AllFields(t *testing.T) { - client := acctest.GetClient(t) - - const rsc = "data.cdn77_origin.os" - originBucketName := "my-bucket-" + uuid.New().String() - const originLabel = "random origin" - const originNote = "some note" - - request := cdn77.OriginAddObjectStorageJSONRequestBody{ - Acl: cdn77.AuthenticatedRead, - BucketName: originBucketName, - ClusterId: "842b5641-b641-4723-ac81-f8cc286e288f", - Label: originLabel, - Note: nullable.NewNullableWithValue(originNote), - } - - response, err := client.OriginAddObjectStorageWithResponse(context.Background(), request) - acctest.AssertResponseOk(t, "Failed to create Origin: %s", response, err) - - originId := response.JSON201.Id - originScheme := string(response.JSON201.Scheme) - originHost := response.JSON201.Host - originPort := response.JSON201.Port - - t.Cleanup(func() { - acctest.MustDeleteOrigin(t, client, provider.OriginTypeObjectStorage, originId) - }) - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctest.GetProviderFactories(), - Steps: []resource.TestStep{ - { - Config: acctest.Config(originDataSourceConfigObjectStorage, "id", originId), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr(rsc, "id", originId), - resource.TestCheckResourceAttr(rsc, "type", provider.OriginTypeObjectStorage), - resource.TestCheckResourceAttr(rsc, "label", originLabel), - resource.TestCheckResourceAttr(rsc, "note", originNote), - resource.TestCheckResourceAttr(rsc, "bucket_name", originBucketName), - resource.TestCheckResourceAttr(rsc, "scheme", originScheme), - resource.TestCheckResourceAttr(rsc, "host", originHost), - func(state *terraform.State) error { - if originPort.IsNull() { - return resource.TestCheckNoResourceAttr(rsc, "port")(state) - } - - return resource.TestCheckResourceAttr(rsc, "port", strconv.Itoa(originPort.MustGet()))(state) - }, - resource.TestCheckNoResourceAttr(rsc, "aws_access_key_id"), - resource.TestCheckNoResourceAttr(rsc, "aws_access_key_secret"), - resource.TestCheckNoResourceAttr(rsc, "aws_region"), - resource.TestCheckNoResourceAttr(rsc, "acl"), - resource.TestCheckNoResourceAttr(rsc, "cluster_id"), - resource.TestCheckNoResourceAttr(rsc, "access_key_id"), - resource.TestCheckNoResourceAttr(rsc, "access_key_secret"), - resource.TestCheckNoResourceAttr(rsc, "base_dir"), - ), - }, - }, - }) -} - -func TestAccOriginDataSource_Url_OnlyRequiredFields(t *testing.T) { - client := acctest.GetClient(t) - - const rsc = "data.cdn77_origin.url" - const originHost = "my-totally-random-custom-host.com" - const originLabel = "random origin" - const originScheme = "https" - - request := cdn77.OriginAddUrlJSONRequestBody{ - Host: originHost, - Label: originLabel, - Scheme: originScheme, - } - response, err := client.OriginAddUrlWithResponse(context.Background(), request) - acctest.AssertResponseOk(t, "Failed to create Origin: %s", response, err) - - originId := response.JSON201.Id - - t.Cleanup(func() { - acctest.MustDeleteOrigin(t, client, provider.OriginTypeUrl, originId) - }) - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctest.GetProviderFactories(), - Steps: []resource.TestStep{ - { - Config: acctest.Config(originDataSourceConfigUrl, "id", originId), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr(rsc, "id", originId), - resource.TestCheckResourceAttr(rsc, "type", provider.OriginTypeUrl), - resource.TestCheckResourceAttr(rsc, "label", originLabel), - resource.TestCheckResourceAttr(rsc, "scheme", originScheme), - resource.TestCheckResourceAttr(rsc, "host", originHost), - resource.TestCheckNoResourceAttr(rsc, "note"), - resource.TestCheckNoResourceAttr(rsc, "aws_access_key_id"), - resource.TestCheckNoResourceAttr(rsc, "aws_access_key_secret"), - resource.TestCheckNoResourceAttr(rsc, "aws_region"), - resource.TestCheckNoResourceAttr(rsc, "acl"), - resource.TestCheckNoResourceAttr(rsc, "cluster_id"), - resource.TestCheckNoResourceAttr(rsc, "bucket_name"), - resource.TestCheckNoResourceAttr(rsc, "access_key_id"), - resource.TestCheckNoResourceAttr(rsc, "access_key_secret"), - resource.TestCheckNoResourceAttr(rsc, "port"), - resource.TestCheckNoResourceAttr(rsc, "base_dir"), - ), - }, - }, - }) -} - -func TestAccOriginDataSource_Url_AllFields(t *testing.T) { - client := acctest.GetClient(t) - - const rsc = "data.cdn77_origin.url" - const originBaseDir = "some-dir" - const originHost = "my-totally-random-custom-host.com" - const originLabel = "random origin" - const originNote = "some note" - const originPort = 12345 - const originScheme = "https" - - request := cdn77.OriginAddUrlJSONRequestBody{ - BaseDir: nullable.NewNullableWithValue(originBaseDir), - Host: originHost, - Label: originLabel, - Note: nullable.NewNullableWithValue(originNote), - Port: nullable.NewNullableWithValue(originPort), - Scheme: originScheme, - } - response, err := client.OriginAddUrlWithResponse(context.Background(), request) - acctest.AssertResponseOk(t, "Failed to create Origin: %s", response, err) - - originId := response.JSON201.Id - - t.Cleanup(func() { - acctest.MustDeleteOrigin(t, client, provider.OriginTypeUrl, originId) - }) - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctest.GetProviderFactories(), - Steps: []resource.TestStep{ - { - Config: acctest.Config(originDataSourceConfigUrl, "id", originId), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr(rsc, "id", originId), - resource.TestCheckResourceAttr(rsc, "type", provider.OriginTypeUrl), - resource.TestCheckResourceAttr(rsc, "label", originLabel), - resource.TestCheckResourceAttr(rsc, "note", originNote), - resource.TestCheckResourceAttr(rsc, "host", originHost), - resource.TestCheckResourceAttr(rsc, "port", strconv.Itoa(originPort)), - resource.TestCheckResourceAttr(rsc, "base_dir", originBaseDir), - resource.TestCheckNoResourceAttr(rsc, "aws_access_key_id"), - resource.TestCheckNoResourceAttr(rsc, "aws_access_key_secret"), - resource.TestCheckNoResourceAttr(rsc, "aws_region"), - resource.TestCheckNoResourceAttr(rsc, "acl"), - resource.TestCheckNoResourceAttr(rsc, "cluster_id"), - resource.TestCheckNoResourceAttr(rsc, "bucket_name"), - resource.TestCheckNoResourceAttr(rsc, "access_key_id"), - resource.TestCheckNoResourceAttr(rsc, "access_key_secret"), - ), - }, - }, - }) -} - -const originDataSourceConfigAws = ` -data "cdn77_origin" "aws" { - id = "{id}" - type = "aws" -} -` - -const originDataSourceConfigObjectStorage = ` -data "cdn77_origin" "os" { - id = "{id}" - type = "object-storage" -} -` - -const originDataSourceConfigUrl = ` -data "cdn77_origin" "url" { - id = "{id}" - type = "url" -} -` diff --git a/internal/provider/origin_resource.go b/internal/provider/origin_resource.go deleted file mode 100644 index 528a876..0000000 --- a/internal/provider/origin_resource.go +++ /dev/null @@ -1,487 +0,0 @@ -package provider - -import ( - "context" - "fmt" - "net/http" - "strings" - - "github.com/cdn77/cdn77-client-go" - "github.com/cdn77/terraform-provider-cdn77/internal/util" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -var ( - _ resource.ResourceWithConfigure = &OriginResource{} - _ resource.ResourceWithConfigValidators = &OriginResource{} - _ resource.ResourceWithImportState = &OriginResource{} -) - -func NewOriginResource() resource.Resource { - return &OriginResource{} -} - -type OriginResource struct { - client cdn77.ClientWithResponsesInterface -} - -func (*OriginResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_origin" -} - -func (*OriginResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = CreateOriginResourceSchema() -} - -func (r *OriginResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - resp.Diagnostics.Append(util.MaybeSetClient(req.ProviderData, &r.client)) -} - -func (*OriginResource) ConfigValidators(_ context.Context) []resource.ConfigValidator { - return []resource.ConfigValidator{NewOriginTypeConfigValidator()} -} - -func (r *OriginResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var data OriginModel - if resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...); resp.Diagnostics.HasError() { - return - } - - const errMessage = "Failed to create Origin" - - switch data.Type.ValueString() { - case OriginTypeAws: - if !r.createAws(ctx, &resp.Diagnostics, errMessage, &data) { - return - } - case OriginTypeObjectStorage: - if !r.createObjectStorage(ctx, &resp.Diagnostics, errMessage, &data) { - return - } - - case OriginTypeUrl: - if !r.createUrl(ctx, &resp.Diagnostics, errMessage, &data) { - return - } - default: - addUnknownOriginTypeError(&resp.Diagnostics, data) - - return - } - - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) -} - -func (r *OriginResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - NewOriginResourceReader(ctx, r.client).Read(&req.State, &resp.Diagnostics, &resp.State) -} - -//nolint:cyclop -func (r *OriginResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var data OriginModel - if resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...); resp.Diagnostics.HasError() { - return - } - - const errMessage = "Failed to update Origin" - - switch data.Type.ValueString() { - case OriginTypeAws: - if !r.updateAws(ctx, &resp.Diagnostics, errMessage, &data) { - return - } - case OriginTypeObjectStorage: - var stateData OriginModel - if resp.Diagnostics.Append(req.State.Get(ctx, &stateData)...); resp.Diagnostics.HasError() { - return - } - - if !r.updateObjectStorage(ctx, &resp.Diagnostics, errMessage, &data, &stateData) { - return - } - case OriginTypeUrl: - if !r.updateUrl(ctx, &resp.Diagnostics, errMessage, &data) { - return - } - default: - addUnknownOriginTypeError(&resp.Diagnostics, data) - - return - } - - if data.Port.IsUnknown() { - data.Port = types.Int64Null() - } - - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) -} - -func (r *OriginResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - var data OriginModel - if resp.Diagnostics.Append(req.State.Get(ctx, &data)...); resp.Diagnostics.HasError() { - return - } - - id := data.Id.ValueString() - if id == "" { - resp.Diagnostics.AddError("Can't delete Origin without ID", missingOriginIdDetailMessage) - - return - } - - const errMessage = "Failed to delete Origin" - - switch data.Type.ValueString() { - case OriginTypeAws: - r.deleteAws(ctx, &resp.Diagnostics, &resp.State, errMessage, id) - case OriginTypeObjectStorage: - r.deleteObjectStorage(ctx, &resp.Diagnostics, &resp.State, errMessage, id) - case OriginTypeUrl: - r.deleteUrl(ctx, &resp.Diagnostics, &resp.State, errMessage, id) - default: - addUnknownOriginTypeError(&resp.Diagnostics, data) - } -} - -func (*OriginResource) ImportState( - ctx context.Context, - req resource.ImportStateRequest, - resp *resource.ImportStateResponse, -) { - idParts := strings.Split(req.ID, ",") - if len(idParts) < 2 { - resp.Diagnostics.AddError( - "Invalid Import Identifier", - fmt.Sprintf( - `Expected one of: \n\t",url"\n\t",aws," \n\t`+ - `",object-storage,,,,")\nGot: %q`, - req.ID, - ), - ) - } - - id := idParts[0] - originType := idParts[1] - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("type"), originType)...) - - switch originType { - case OriginTypeAws: - if len(idParts) != 3 { - resp.Diagnostics.AddError( - "Invalid AWS Origin Import Identifier", - fmt.Sprintf(`Expected ",aws,"; got: %q`, req.ID), - ) - - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("aws_access_key_secret"), idParts[2])...) - case OriginTypeObjectStorage: - if len(idParts) != 6 { - resp.Diagnostics.AddError( - "Invalid Object Storage Origin Import Identifier", - fmt.Sprintf( - `Expected ",object-storage,,,,"; got: %q`, - req.ID, - ), - ) - - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("acl"), idParts[2])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("cluster_id"), idParts[3])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("access_key_id"), idParts[4])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("access_key_secret"), idParts[5])...) - case OriginTypeUrl: - if len(idParts) != 2 { - resp.Diagnostics.AddError( - "Invalid URL Origin Import Identifier", - fmt.Sprintf(`Expected ",url"; got: %q`, req.ID), - ) - - return - } - default: - addUnknownOriginTypeError(&resp.Diagnostics, OriginModel{Type: types.StringValue(originType)}) - } -} - -func (r *OriginResource) createAws( - ctx context.Context, - diags *diag.Diagnostics, - errMessage string, - data *OriginModel, -) bool { - request := cdn77.OriginAddAwsJSONRequestBody{ - AwsAccessKeyId: util.StringValueToNullable(data.AwsAccessKeyId), - AwsAccessKeySecret: util.StringValueToNullable(data.AwsAccessKeySecret), - AwsRegion: util.StringValueToNullable(data.AwsRegion), - BaseDir: util.StringValueToNullable(data.BaseDir), - Host: data.Host.ValueString(), - Label: data.Label.ValueString(), - Note: util.StringValueToNullable(data.Note), - Port: util.Int64ValueToNullable[int](data.Port), - Scheme: cdn77.OriginScheme(data.Scheme.ValueString()), - } - - response, err := r.client.OriginAddAwsWithResponse(ctx, request) - if err != nil { - diags.AddError(errMessage, err.Error()) - - return false - } - - if !util.CheckResponse(diags, errMessage, response, response.JSON422, response.JSONDefault) { - return false - } - - data.Id = types.StringValue(response.JSON201.Id) - data.AccessKeyId = types.StringNull() - data.AccessKeySecret = types.StringNull() - data.Port = util.NullableIntToInt64Value(response.JSON201.Port) - - return true -} - -func (r *OriginResource) createObjectStorage( - ctx context.Context, - diags *diag.Diagnostics, - errMessage string, - data *OriginModel, -) bool { - request := cdn77.OriginAddObjectStorageJSONRequestBody{ - Acl: cdn77.AclType(data.Acl.ValueString()), - BucketName: data.BucketName.ValueString(), - ClusterId: data.ClusterId.ValueString(), - Label: data.Label.ValueString(), - Note: util.StringValueToNullable(data.Note), - } - - response, err := r.client.OriginAddObjectStorageWithResponse(ctx, request) - if err != nil { - diags.AddError(errMessage, err.Error()) - - return false - } - - if !util.CheckResponse(diags, errMessage, response, response.JSON422, response.JSONDefault) { - return false - } - - data.Id = types.StringValue(response.JSON201.Id) - data.AccessKeyId = types.StringPointerValue(response.JSON201.AccessKeyId) - data.AccessKeySecret = types.StringPointerValue(response.JSON201.AccessSecret) - data.Scheme = types.StringValue(string(response.JSON201.Scheme)) - data.Host = types.StringValue(response.JSON201.Host) - data.Port = util.NullableIntToInt64Value(response.JSON201.Port) - - return true -} - -func (r *OriginResource) createUrl( - ctx context.Context, - diags *diag.Diagnostics, - errMessage string, - data *OriginModel, -) bool { - request := cdn77.OriginAddUrlJSONRequestBody{ - BaseDir: util.StringValueToNullable(data.BaseDir), - Host: data.Host.ValueString(), - Label: data.Label.ValueString(), - Note: util.StringValueToNullable(data.Note), - Port: util.Int64ValueToNullable[int](data.Port), - Scheme: cdn77.OriginScheme(data.Scheme.ValueString()), - } - - response, err := r.client.OriginAddUrlWithResponse(ctx, request) - if err != nil { - diags.AddError(errMessage, err.Error()) - - return false - } - - if !util.CheckResponse(diags, errMessage, response, response.JSON422, response.JSONDefault) { - return false - } - - data.Id = types.StringValue(response.JSON201.Id) - data.AccessKeyId = types.StringNull() - data.AccessKeySecret = types.StringNull() - data.Port = util.NullableIntToInt64Value(response.JSON201.Port) - - return true -} - -func (r *OriginResource) updateAws( - ctx context.Context, - diags *diag.Diagnostics, - errMessage string, - data *OriginModel, -) bool { - data.AccessKeyId = types.StringNull() - data.AccessKeySecret = types.StringNull() - - request := cdn77.OriginEditAwsJSONRequestBody{ - AwsAccessKeyId: util.StringValueToNullable(data.AwsAccessKeyId), - AwsAccessKeySecret: util.StringValueToNullable(data.AwsAccessKeySecret), - AwsRegion: util.StringValueToNullable(data.AwsRegion), - BaseDir: util.StringValueToNullable(data.BaseDir), - Host: data.Host.ValueStringPointer(), - Label: data.Label.ValueStringPointer(), - Note: util.StringValueToNullable(data.Note), - Port: util.Int64ValueToNullable[int](data.Port), - Scheme: data.Scheme.ValueStringPointer(), - } - - response, err := r.client.OriginEditAwsWithResponse(ctx, data.Id.ValueString(), request) - if err != nil { - diags.AddError(errMessage, err.Error()) - - return false - } - - return util.CheckResponse(diags, errMessage, response, response.JSON404, response.JSON422, response.JSONDefault) -} - -func (r *OriginResource) updateObjectStorage( - ctx context.Context, - diags *diag.Diagnostics, - errMessage string, - data *OriginModel, - stateData *OriginModel, -) bool { - data.Scheme = stateData.Scheme - data.Host = stateData.Host - data.Port = stateData.Port - - request := cdn77.OriginEditObjectStorageJSONRequestBody{ - Label: data.Label.ValueStringPointer(), - Note: util.StringValueToNullable(data.Note), - } - - response, err := r.client.OriginEditObjectStorageWithResponse(ctx, data.Id.ValueString(), request) - if err != nil { - diags.AddError(errMessage, err.Error()) - - return false - } - - return util.CheckResponse(diags, errMessage, response, response.JSON404, response.JSON422, response.JSONDefault) -} - -func (r *OriginResource) updateUrl( - ctx context.Context, - diags *diag.Diagnostics, - errMessage string, - data *OriginModel, -) bool { - data.AccessKeyId = types.StringNull() - data.AccessKeySecret = types.StringNull() - - request := cdn77.OriginEditUrlJSONRequestBody{ - BaseDir: util.StringValueToNullable(data.BaseDir), - Host: data.Host.ValueStringPointer(), - Label: data.Label.ValueStringPointer(), - Note: util.StringValueToNullable(data.Note), - Port: util.Int64ValueToNullable[int](data.Port), - Scheme: data.Scheme.ValueStringPointer(), - } - - response, err := r.client.OriginEditUrlWithResponse(ctx, data.Id.ValueString(), request) - if err != nil { - diags.AddError(errMessage, err.Error()) - - return false - } - - return util.CheckResponse(diags, errMessage, response, response.JSON404, response.JSON422, response.JSONDefault) -} - -func (r *OriginResource) deleteAws( - ctx context.Context, - diags *diag.Diagnostics, - state *tfsdk.State, - errMessage string, - id string, -) { - response, err := r.client.OriginDeleteAwsWithResponse(ctx, id) - if err != nil { - diags.AddError(errMessage, err.Error()) - - return - } - - if maybeRemoveMissingResource(ctx, response.StatusCode(), id, state) { - return - } - - util.CheckResponse(diags, errMessage, response, response.JSON404, response.JSON422, response.JSONDefault) -} - -func (r *OriginResource) deleteObjectStorage( - ctx context.Context, - diags *diag.Diagnostics, - state *tfsdk.State, - errMessage string, - id string, -) { - response, err := r.client.OriginDeleteObjectStorageWithResponse(ctx, id) - if err != nil { - diags.AddError(errMessage, err.Error()) - - return - } - - if maybeRemoveMissingResource(ctx, response.StatusCode(), id, state) { - return - } - - util.CheckResponse(diags, errMessage, response, response.JSON404, response.JSON422, response.JSONDefault) -} - -func (r *OriginResource) deleteUrl( - ctx context.Context, - diags *diag.Diagnostics, - state *tfsdk.State, - errMessage string, - id string, -) { - response, err := r.client.OriginDeleteUrlWithResponse(ctx, id) - if err != nil { - diags.AddError(errMessage, err.Error()) - - return - } - - if maybeRemoveMissingResource(ctx, response.StatusCode(), id, state) { - return - } - - util.CheckResponse(diags, errMessage, response, response.JSON404, response.JSON422, response.JSONDefault) -} - -func maybeRemoveMissingResource(ctx context.Context, statusCode int, id any, state *tfsdk.State) bool { - if statusCode == http.StatusNotFound { - tflog.Info(tflog.SetField(ctx, "id", id), "Resource not found; removing it from state") - state.RemoveResource(ctx) - - return true - } - - return false -} - -func addUnknownOriginTypeError(diags *diag.Diagnostics, data OriginModel) { - diags.AddError( - "Unknown Origin type", - fmt.Sprintf(`Got "%s", expected one of: %v`, data.Type.ValueString(), originTypes), - ) -} diff --git a/internal/provider/origin_resource_test.go b/internal/provider/origin_resource_test.go deleted file mode 100644 index 980291e..0000000 --- a/internal/provider/origin_resource_test.go +++ /dev/null @@ -1,1039 +0,0 @@ -package provider_test - -import ( - "context" - "errors" - "fmt" - "testing" - - "github.com/cdn77/cdn77-client-go" - "github.com/cdn77/terraform-provider-cdn77/internal/acctest" - "github.com/cdn77/terraform-provider-cdn77/internal/provider" - "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/plancheck" - "github.com/hashicorp/terraform-plugin-testing/terraform" -) - -func TestAccOriginResource_Aws(t *testing.T) { - client := acctest.GetClient(t) - var originId string - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctest.GetProviderFactories(), - CheckDestroy: checkOriginsDestroyed(client), - Steps: []resource.TestStep{ - { - Config: `resource "cdn77_origin" "aws" { - type = "aws" - label = "some label" - scheme = "http" - host = "my-totally-random-custom-host.com" - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("cdn77_origin.aws", plancheck.ResourceActionCreate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrWith("cdn77_origin.aws", "id", func(value string) error { - originId = value - - return acctest.NotEqual(value, "") - }), - resource.TestCheckResourceAttr("cdn77_origin.aws", "type", provider.OriginTypeAws), - resource.TestCheckResourceAttr("cdn77_origin.aws", "label", "some label"), - resource.TestCheckResourceAttr("cdn77_origin.aws", "scheme", "http"), - resource.TestCheckResourceAttr("cdn77_origin.aws", "host", "my-totally-random-custom-host.com"), - resource.TestCheckNoResourceAttr("cdn77_origin.aws", "note"), - resource.TestCheckNoResourceAttr("cdn77_origin.aws", "aws_access_key_id"), - resource.TestCheckNoResourceAttr("cdn77_origin.aws", "aws_access_key_secret"), - resource.TestCheckNoResourceAttr("cdn77_origin.aws", "aws_region"), - resource.TestCheckNoResourceAttr("cdn77_origin.aws", "port"), - resource.TestCheckNoResourceAttr("cdn77_origin.aws", "base_dir"), - checkAwsOrigin(client, &originId, func(o *cdn77.S3OriginDetail) error { - return errors.Join( - acctest.NullField("aws_access_key_id", o.AwsAccessKeyId), - acctest.NullField("aws_region", o.AwsRegion), - acctest.NullField("base_dir", o.BaseDir), - acctest.EqualField("host", o.Host, "my-totally-random-custom-host.com"), - acctest.EqualField("label", o.Label, "some label"), - acctest.NullField("note", o.Note), - acctest.NullField("port", o.Port), - acctest.EqualField("scheme", o.Scheme, "http"), - acctest.EqualField("type", o.Type, provider.OriginTypeAws), - ) - }), - ), - }, - { - Config: `resource "cdn77_origin" "aws" { - type = "aws" - label = "another label" - note = "some note" - scheme = "http" - host = "my-totally-random-custom-host.com" - port = 12345 - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("cdn77_origin.aws", plancheck.ResourceActionUpdate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrWith("cdn77_origin.aws", "id", func(value string) error { - return acctest.EqualField("id", value, originId) - }), - resource.TestCheckResourceAttr("cdn77_origin.aws", "type", provider.OriginTypeAws), - resource.TestCheckResourceAttr("cdn77_origin.aws", "label", "another label"), - resource.TestCheckResourceAttr("cdn77_origin.aws", "note", "some note"), - resource.TestCheckResourceAttr("cdn77_origin.aws", "scheme", "http"), - resource.TestCheckResourceAttr("cdn77_origin.aws", "host", "my-totally-random-custom-host.com"), - resource.TestCheckResourceAttr("cdn77_origin.aws", "port", "12345"), - resource.TestCheckNoResourceAttr("cdn77_origin.aws", "aws_access_key_id"), - resource.TestCheckNoResourceAttr("cdn77_origin.aws", "aws_access_key_secret"), - resource.TestCheckNoResourceAttr("cdn77_origin.aws", "aws_region"), - resource.TestCheckNoResourceAttr("cdn77_origin.aws", "base_dir"), - checkAwsOrigin(client, &originId, func(o *cdn77.S3OriginDetail) error { - return errors.Join( - acctest.NullField("aws_access_key_id", o.AwsAccessKeyId), - acctest.NullField("aws_region", o.AwsRegion), - acctest.NullField("base_dir", o.BaseDir), - acctest.EqualField("host", o.Host, "my-totally-random-custom-host.com"), - acctest.EqualField("label", o.Label, "another label"), - acctest.NullFieldEqual("note", o.Note, "some note"), - acctest.NullFieldEqual("port", o.Port, 12345), - acctest.EqualField("scheme", o.Scheme, "http"), - acctest.EqualField("type", o.Type, provider.OriginTypeAws), - ) - }), - ), - }, - { - Config: `resource "cdn77_origin" "aws" { - type = "aws" - label = "another label" - note = "some note" - aws_access_key_id = "keyid" - aws_access_key_secret = "keysecret" - aws_region = "eu" - scheme = "http" - host = "my-totally-random-custom-host.com" - port = 12345 - base_dir = "some-dir" - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("cdn77_origin.aws", plancheck.ResourceActionUpdate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrWith("cdn77_origin.aws", "id", func(value string) error { - return acctest.Equal(value, originId) - }), - resource.TestCheckResourceAttr("cdn77_origin.aws", "type", provider.OriginTypeAws), - resource.TestCheckResourceAttr("cdn77_origin.aws", "label", "another label"), - resource.TestCheckResourceAttr("cdn77_origin.aws", "note", "some note"), - resource.TestCheckResourceAttr("cdn77_origin.aws", "aws_access_key_id", "keyid"), - resource.TestCheckResourceAttrSet("cdn77_origin.aws", "aws_access_key_secret"), - resource.TestCheckResourceAttr("cdn77_origin.aws", "aws_region", "eu"), - resource.TestCheckResourceAttr("cdn77_origin.aws", "scheme", "http"), - resource.TestCheckResourceAttr("cdn77_origin.aws", "host", "my-totally-random-custom-host.com"), - resource.TestCheckResourceAttr("cdn77_origin.aws", "port", "12345"), - resource.TestCheckResourceAttr("cdn77_origin.aws", "base_dir", "some-dir"), - checkAwsOrigin(client, &originId, func(o *cdn77.S3OriginDetail) error { - return errors.Join( - acctest.NullFieldEqual("aws_access_key_id", o.AwsAccessKeyId, "keyid"), - acctest.NullFieldEqual("aws_region", o.AwsRegion, "eu"), - acctest.NullFieldEqual("base_dir", o.BaseDir, "some-dir"), - acctest.EqualField("host", o.Host, "my-totally-random-custom-host.com"), - acctest.EqualField("label", o.Label, "another label"), - acctest.NullFieldEqual("note", o.Note, "some note"), - acctest.NullFieldEqual("port", o.Port, 12345), - acctest.EqualField("scheme", o.Scheme, "http"), - acctest.EqualField("type", o.Type, provider.OriginTypeAws), - ) - }), - ), - }, - { - Config: `resource "cdn77_origin" "aws" { - type = "aws" - label = "another label" - scheme = "http" - host = "my-totally-random-custom-host.com" - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("cdn77_origin.aws", plancheck.ResourceActionUpdate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrWith("cdn77_origin.aws", "id", func(value string) error { - return acctest.EqualField("id", value, originId) - }), - resource.TestCheckResourceAttr("cdn77_origin.aws", "type", provider.OriginTypeAws), - resource.TestCheckResourceAttr("cdn77_origin.aws", "label", "another label"), - resource.TestCheckResourceAttr("cdn77_origin.aws", "scheme", "http"), - resource.TestCheckResourceAttr("cdn77_origin.aws", "host", "my-totally-random-custom-host.com"), - resource.TestCheckNoResourceAttr("cdn77_origin.aws", "note"), - resource.TestCheckNoResourceAttr("cdn77_origin.aws", "aws_access_key_id"), - resource.TestCheckNoResourceAttr("cdn77_origin.aws", "aws_access_key_secret"), - resource.TestCheckNoResourceAttr("cdn77_origin.aws", "aws_region"), - resource.TestCheckNoResourceAttr("cdn77_origin.aws", "port"), - resource.TestCheckNoResourceAttr("cdn77_origin.aws", "base_dir"), - checkAwsOrigin(client, &originId, func(o *cdn77.S3OriginDetail) error { - return errors.Join( - acctest.NullField("aws_access_key_id", o.AwsAccessKeyId), - acctest.NullField("aws_region", o.AwsRegion), - acctest.NullField("base_dir", o.BaseDir), - acctest.EqualField("host", o.Host, "my-totally-random-custom-host.com"), - acctest.EqualField("label", o.Label, "another label"), - acctest.NullField("note", o.Note), - acctest.NullField("port", o.Port), - acctest.EqualField("scheme", o.Scheme, "http"), - acctest.EqualField("type", o.Type, provider.OriginTypeAws), - ) - }), - ), - }, - { - Config: `resource "cdn77_origin" "aws" { - type = "url" - label = "another label" - note = "another note" - scheme = "http" - host = "my-totally-random-custom-host.com" - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("cdn77_origin.aws", plancheck.ResourceActionDestroyBeforeCreate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrWith("cdn77_origin.aws", "id", func(value string) error { - err := acctest.NotEqual(value, originId) - originId = value - - return err - }), - resource.TestCheckResourceAttr("cdn77_origin.aws", "type", provider.OriginTypeUrl), - resource.TestCheckResourceAttr("cdn77_origin.aws", "label", "another label"), - resource.TestCheckResourceAttr("cdn77_origin.aws", "note", "another note"), - resource.TestCheckResourceAttr("cdn77_origin.aws", "scheme", "http"), - resource.TestCheckResourceAttr("cdn77_origin.aws", "host", "my-totally-random-custom-host.com"), - resource.TestCheckNoResourceAttr("cdn77_origin.aws", "aws_access_key_id"), - resource.TestCheckNoResourceAttr("cdn77_origin.aws", "aws_access_key_secret"), - resource.TestCheckNoResourceAttr("cdn77_origin.aws", "aws_region"), - resource.TestCheckNoResourceAttr("cdn77_origin.aws", "port"), - resource.TestCheckNoResourceAttr("cdn77_origin.aws", "base_dir"), - checkUrlOrigin(client, &originId, func(o *cdn77.UrlOriginDetail) error { - return errors.Join( - acctest.NullField("base_dir", o.BaseDir), - acctest.EqualField("host", o.Host, "my-totally-random-custom-host.com"), - acctest.EqualField("label", o.Label, "another label"), - acctest.NullFieldEqual("note", o.Note, "another note"), - acctest.NullField("port", o.Port), - acctest.EqualField("scheme", o.Scheme, "http"), - acctest.EqualField("type", o.Type, provider.OriginTypeUrl), - ) - }), - ), - }, - }, - }) -} - -func TestAccOriginResourceImport_Aws(t *testing.T) { - client := acctest.GetClient(t) - rsc := "cdn77_origin.aws" - var originId string - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctest.GetProviderFactories(), - CheckDestroy: checkOriginsDestroyed(client), - Steps: []resource.TestStep{ - { - Config: `resource "cdn77_origin" "aws" { - type = "aws" - label = "some label" - note = "some note" - aws_access_key_id = "keyid" - aws_access_key_secret = "keysecret" - aws_region = "eu" - scheme = "http" - host = "my-totally-random-custom-host.com" - }`, - Check: resource.TestCheckResourceAttrWith(rsc, "id", func(value string) error { - originId = value - - return acctest.NotEqual(value, "") - }), - }, - { - ResourceName: rsc, - ImportState: true, - ImportStateIdFunc: func(*terraform.State) (string, error) { - return fmt.Sprintf("%s,aws,keysecret", originId), nil - }, - ImportStateVerify: true, - }, - }, - }) -} - -func TestAccOriginResource_ObjectStorage(t *testing.T) { - client := acctest.GetClient(t) - bucketName := "my-bucket-" + uuid.New().String() - anotherBucketName := "my-bucket-" + uuid.New().String() - var originId string - var clusterId string - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctest.GetProviderFactories(), - CheckDestroy: checkOriginsDestroyed(client), - Steps: []resource.TestStep{ - { - Config: acctest.Config(objectStoragesDataSourceConfig+`resource "cdn77_origin" "os" { - type = "object-storage" - label = "some label" - acl = "private" - cluster_id = local.eu_cluster_id - bucket_name = "{bucketName}" - }`, "bucketName", bucketName), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("cdn77_origin.os", plancheck.ResourceActionCreate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrWith("cdn77_origin.os", "id", func(value string) error { - originId = value - - return acctest.NotEqual(value, "") - }), - resource.TestCheckResourceAttr("cdn77_origin.os", "type", provider.OriginTypeObjectStorage), - resource.TestCheckResourceAttr("cdn77_origin.os", "label", "some label"), - resource.TestCheckResourceAttr("cdn77_origin.os", "acl", "private"), - resource.TestCheckResourceAttrWith("cdn77_origin.os", "cluster_id", func(value string) error { - clusterId = value - - return acctest.NotEqual(value, "") - }), - resource.TestCheckResourceAttr("cdn77_origin.os", "bucket_name", bucketName), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "access_key_id"), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "access_key_secret"), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "scheme"), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "host"), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "port"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "note"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "aws_access_key_id"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "aws_access_key_secret"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "aws_region"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "base_dir"), - checkObjectStorageOrigin(client, &originId, func(o *cdn77.ObjectStorageOriginDetail) error { - return errors.Join( - acctest.EqualField("bucket_name", o.BucketName, bucketName), - acctest.EqualField("label", o.Label, "some label"), - acctest.NullField("note", o.Note), - acctest.EqualField("type", o.Type, provider.OriginTypeObjectStorage), - ) - }), - ), - }, - { - Config: acctest.Config(objectStoragesDataSourceConfig+`resource "cdn77_origin" "os" { - type = "object-storage" - label = "another label" - note = "some note" - acl = "private" - cluster_id = local.eu_cluster_id - bucket_name = "{bucketName}" - }`, "bucketName", bucketName), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("cdn77_origin.os", plancheck.ResourceActionUpdate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrWith("cdn77_origin.os", "id", func(value string) error { - return acctest.Equal(value, originId) - }), - resource.TestCheckResourceAttr("cdn77_origin.os", "type", provider.OriginTypeObjectStorage), - resource.TestCheckResourceAttr("cdn77_origin.os", "label", "another label"), - resource.TestCheckResourceAttr("cdn77_origin.os", "note", "some note"), - resource.TestCheckResourceAttr("cdn77_origin.os", "acl", "private"), - resource.TestCheckResourceAttrWith("cdn77_origin.os", "cluster_id", func(value string) error { - return acctest.Equal(value, clusterId) - }), - resource.TestCheckResourceAttr("cdn77_origin.os", "bucket_name", bucketName), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "access_key_id"), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "access_key_secret"), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "scheme"), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "host"), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "port"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "aws_access_key_id"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "aws_access_key_secret"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "aws_region"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "base_dir"), - checkObjectStorageOrigin(client, &originId, func(o *cdn77.ObjectStorageOriginDetail) error { - return errors.Join( - acctest.EqualField("bucket_name", o.BucketName, bucketName), - acctest.EqualField("label", o.Label, "another label"), - acctest.NullFieldEqual("note", o.Note, "some note"), - acctest.EqualField("type", o.Type, provider.OriginTypeObjectStorage), - ) - }), - ), - }, - { - Config: acctest.Config(objectStoragesDataSourceConfig+`resource "cdn77_origin" "os" { - type = "object-storage" - label = "another label" - acl = "private" - cluster_id = local.eu_cluster_id - bucket_name = "{bucketName}" - }`, "bucketName", bucketName), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("cdn77_origin.os", plancheck.ResourceActionUpdate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrWith("cdn77_origin.os", "id", func(value string) error { - return acctest.Equal(value, originId) - }), - resource.TestCheckResourceAttr("cdn77_origin.os", "type", provider.OriginTypeObjectStorage), - resource.TestCheckResourceAttr("cdn77_origin.os", "label", "another label"), - resource.TestCheckResourceAttr("cdn77_origin.os", "acl", "private"), - resource.TestCheckResourceAttrWith("cdn77_origin.os", "cluster_id", func(value string) error { - return acctest.Equal(value, clusterId) - }), - resource.TestCheckResourceAttr("cdn77_origin.os", "bucket_name", bucketName), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "access_key_id"), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "access_key_secret"), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "scheme"), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "host"), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "port"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "note"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "aws_access_key_id"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "aws_access_key_secret"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "aws_region"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "base_dir"), - checkObjectStorageOrigin(client, &originId, func(o *cdn77.ObjectStorageOriginDetail) error { - return errors.Join( - acctest.EqualField("bucket_name", o.BucketName, bucketName), - acctest.EqualField("label", o.Label, "another label"), - acctest.NullField("note", o.Note), - acctest.EqualField("type", o.Type, provider.OriginTypeObjectStorage), - ) - }), - ), - }, - { - Config: acctest.Config(objectStoragesDataSourceConfig+`resource "cdn77_origin" "os" { - type = "object-storage" - label = "another label" - acl = "authenticated-read" - cluster_id = local.eu_cluster_id - bucket_name = "{bucketName}" - }`, "bucketName", bucketName), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("cdn77_origin.os", plancheck.ResourceActionDestroyBeforeCreate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrWith("cdn77_origin.os", "id", func(value string) error { - err := acctest.NotEqual(value, originId) - originId = value - - return err - }), - resource.TestCheckResourceAttr("cdn77_origin.os", "type", provider.OriginTypeObjectStorage), - resource.TestCheckResourceAttr("cdn77_origin.os", "label", "another label"), - resource.TestCheckResourceAttr("cdn77_origin.os", "acl", "authenticated-read"), - resource.TestCheckResourceAttrWith("cdn77_origin.os", "cluster_id", func(value string) error { - return acctest.Equal(value, clusterId) - }), - resource.TestCheckResourceAttr("cdn77_origin.os", "bucket_name", bucketName), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "access_key_id"), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "access_key_secret"), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "scheme"), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "host"), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "port"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "note"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "aws_access_key_id"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "aws_access_key_secret"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "aws_region"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "base_dir"), - checkObjectStorageOrigin(client, &originId, func(o *cdn77.ObjectStorageOriginDetail) error { - return errors.Join( - acctest.EqualField("bucket_name", o.BucketName, bucketName), - acctest.EqualField("label", o.Label, "another label"), - acctest.NullField("note", o.Note), - acctest.EqualField("type", o.Type, provider.OriginTypeObjectStorage), - ) - }), - ), - }, - { - Config: acctest.Config(objectStoragesDataSourceConfig+`resource "cdn77_origin" "os" { - type = "object-storage" - label = "another label" - acl = "authenticated-read" - cluster_id = local.us_cluster_id - bucket_name = "{bucketName}" - }`, "bucketName", bucketName), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("cdn77_origin.os", plancheck.ResourceActionDestroyBeforeCreate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrWith("cdn77_origin.os", "id", func(value string) error { - err := acctest.NotEqual(value, originId) - originId = value - - return err - }), - resource.TestCheckResourceAttr("cdn77_origin.os", "type", provider.OriginTypeObjectStorage), - resource.TestCheckResourceAttr("cdn77_origin.os", "label", "another label"), - resource.TestCheckResourceAttr("cdn77_origin.os", "acl", "authenticated-read"), - resource.TestCheckResourceAttrWith("cdn77_origin.os", "cluster_id", func(value string) error { - err := acctest.NotEqual(value, clusterId) - clusterId = value - - return err - }), - resource.TestCheckResourceAttr("cdn77_origin.os", "bucket_name", bucketName), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "access_key_id"), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "access_key_secret"), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "scheme"), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "host"), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "port"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "note"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "aws_access_key_id"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "aws_access_key_secret"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "aws_region"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "base_dir"), - checkObjectStorageOrigin(client, &originId, func(o *cdn77.ObjectStorageOriginDetail) error { - return errors.Join( - acctest.EqualField("bucket_name", o.BucketName, bucketName), - acctest.EqualField("label", o.Label, "another label"), - acctest.NullField("note", o.Note), - acctest.EqualField("type", o.Type, provider.OriginTypeObjectStorage), - ) - }), - ), - }, - { - Config: acctest.Config(objectStoragesDataSourceConfig+`resource "cdn77_origin" "os" { - type = "object-storage" - label = "another label" - acl = "authenticated-read" - cluster_id = local.us_cluster_id - bucket_name = "{bucketName}" - }`, "bucketName", anotherBucketName), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("cdn77_origin.os", plancheck.ResourceActionDestroyBeforeCreate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrWith("cdn77_origin.os", "id", func(value string) error { - err := acctest.NotEqual(value, originId) - originId = value - - return err - }), - resource.TestCheckResourceAttr("cdn77_origin.os", "type", provider.OriginTypeObjectStorage), - resource.TestCheckResourceAttr("cdn77_origin.os", "label", "another label"), - resource.TestCheckResourceAttr("cdn77_origin.os", "acl", "authenticated-read"), - resource.TestCheckResourceAttrWith("cdn77_origin.os", "cluster_id", func(value string) error { - return acctest.Equal(value, clusterId) - }), - resource.TestCheckResourceAttr("cdn77_origin.os", "bucket_name", anotherBucketName), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "access_key_id"), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "access_key_secret"), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "scheme"), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "host"), - resource.TestCheckResourceAttrSet("cdn77_origin.os", "port"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "note"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "aws_access_key_id"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "aws_access_key_secret"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "aws_region"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "base_dir"), - checkObjectStorageOrigin(client, &originId, func(o *cdn77.ObjectStorageOriginDetail) error { - return errors.Join( - acctest.EqualField("bucket_name", o.BucketName, anotherBucketName), - acctest.EqualField("label", o.Label, "another label"), - acctest.NullField("note", o.Note), - acctest.EqualField("type", o.Type, provider.OriginTypeObjectStorage), - ) - }), - ), - }, - { - Config: `resource "cdn77_origin" "os" { - type = "url" - label = "another label" - note = "another note" - scheme = "http" - host = "my-totally-random-custom-host.com" - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("cdn77_origin.os", plancheck.ResourceActionDestroyBeforeCreate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrWith("cdn77_origin.os", "id", func(value string) error { - err := acctest.NotEqual(value, originId) - originId = value - - return err - }), - resource.TestCheckResourceAttr("cdn77_origin.os", "type", provider.OriginTypeUrl), - resource.TestCheckResourceAttr("cdn77_origin.os", "label", "another label"), - resource.TestCheckResourceAttr("cdn77_origin.os", "note", "another note"), - resource.TestCheckResourceAttr("cdn77_origin.os", "scheme", "http"), - resource.TestCheckResourceAttr("cdn77_origin.os", "host", "my-totally-random-custom-host.com"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "aws_access_key_id"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "aws_access_key_secret"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "aws_region"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "acl"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "cluster_id"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "bucket_name"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "access_key_id"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "access_key_secret"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "port"), - resource.TestCheckNoResourceAttr("cdn77_origin.os", "base_dir"), - checkUrlOrigin(client, &originId, func(o *cdn77.UrlOriginDetail) error { - return errors.Join( - acctest.NullField("base_dir", o.BaseDir), - acctest.EqualField("host", o.Host, "my-totally-random-custom-host.com"), - acctest.EqualField("label", o.Label, "another label"), - acctest.NullFieldEqual("note", o.Note, "another note"), - acctest.NullField("port", o.Port), - acctest.EqualField("scheme", o.Scheme, "http"), - acctest.EqualField("type", o.Type, provider.OriginTypeUrl), - ) - }), - ), - }, - }, - }) -} - -func TestAccOriginResourceImport_ObjectStorage(t *testing.T) { - client := acctest.GetClient(t) - rsc := "cdn77_origin.os" - bucketName := "my-bucket-" + uuid.New().String() - var originId, clusterId, accessKeyId, accessKeySecret string - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctest.GetProviderFactories(), - CheckDestroy: checkOriginsDestroyed(client), - Steps: []resource.TestStep{ - { - Config: acctest.Config(objectStoragesDataSourceConfig+`resource "cdn77_origin" "os" { - type = "object-storage" - label = "some label" - note = "some note" - acl = "private" - cluster_id = local.eu_cluster_id - bucket_name = "{bucketName}" - }`, "bucketName", bucketName), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrWith(rsc, "id", func(value string) error { - originId = value - - return acctest.NotEqual(value, "") - }), - resource.TestCheckResourceAttrWith(rsc, "cluster_id", func(value string) error { - clusterId = value - - return acctest.NotEqual(value, "") - }), - resource.TestCheckResourceAttrWith(rsc, "access_key_id", func(value string) error { - accessKeyId = value - - return acctest.NotEqual(value, "") - }), - resource.TestCheckResourceAttrWith(rsc, "access_key_secret", func(value string) error { - accessKeySecret = value - - return acctest.NotEqual(value, "") - }), - ), - }, - { - ResourceName: rsc, - ImportState: true, - ImportStateIdFunc: func(*terraform.State) (string, error) { - return fmt.Sprintf( - "%s,object-storage,private,%s,%s,%s", - originId, clusterId, accessKeyId, accessKeySecret, - ), nil - }, - ImportStateVerify: true, - }, - }, - }) -} - -func TestAccOriginResource_Url(t *testing.T) { - client := acctest.GetClient(t) - var originId string - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctest.GetProviderFactories(), - CheckDestroy: checkOriginsDestroyed(client), - Steps: []resource.TestStep{ - { - Config: `resource "cdn77_origin" "url" { - type = "url" - label = "some label" - scheme = "http" - host = "my-totally-random-custom-host.com" - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("cdn77_origin.url", plancheck.ResourceActionCreate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrWith("cdn77_origin.url", "id", func(value string) error { - originId = value - - return acctest.NotEqual(value, "") - }), - resource.TestCheckResourceAttr("cdn77_origin.url", "type", provider.OriginTypeUrl), - resource.TestCheckResourceAttr("cdn77_origin.url", "label", "some label"), - resource.TestCheckResourceAttr("cdn77_origin.url", "scheme", "http"), - resource.TestCheckResourceAttr("cdn77_origin.url", "host", "my-totally-random-custom-host.com"), - resource.TestCheckNoResourceAttr("cdn77_origin.url", "note"), - resource.TestCheckNoResourceAttr("cdn77_origin.url", "aws_access_key_id"), - resource.TestCheckNoResourceAttr("cdn77_origin.url", "aws_access_key_secret"), - resource.TestCheckNoResourceAttr("cdn77_origin.url", "aws_region"), - resource.TestCheckNoResourceAttr("cdn77_origin.url", "port"), - resource.TestCheckNoResourceAttr("cdn77_origin.url", "base_dir"), - checkUrlOrigin(client, &originId, func(o *cdn77.UrlOriginDetail) error { - return errors.Join( - acctest.NullField("base_dir", o.BaseDir), - acctest.EqualField("host", o.Host, "my-totally-random-custom-host.com"), - acctest.EqualField("label", o.Label, "some label"), - acctest.NullField("note", o.Note), - acctest.NullField("port", o.Port), - acctest.EqualField("scheme", o.Scheme, "http"), - acctest.EqualField("type", o.Type, provider.OriginTypeUrl), - ) - }), - ), - }, - { - Config: `resource "cdn77_origin" "url" { - type = "url" - label = "another label" - note = "some note" - scheme = "http" - host = "my-totally-random-custom-host.com" - port = 12345 - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("cdn77_origin.url", plancheck.ResourceActionUpdate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrWith("cdn77_origin.url", "id", func(value string) error { - return acctest.EqualField("id", value, originId) - }), - resource.TestCheckResourceAttr("cdn77_origin.url", "type", provider.OriginTypeUrl), - resource.TestCheckResourceAttr("cdn77_origin.url", "label", "another label"), - resource.TestCheckResourceAttr("cdn77_origin.url", "note", "some note"), - resource.TestCheckResourceAttr("cdn77_origin.url", "scheme", "http"), - resource.TestCheckResourceAttr("cdn77_origin.url", "host", "my-totally-random-custom-host.com"), - resource.TestCheckResourceAttr("cdn77_origin.url", "port", "12345"), - resource.TestCheckNoResourceAttr("cdn77_origin.url", "aws_access_key_id"), - resource.TestCheckNoResourceAttr("cdn77_origin.url", "aws_access_key_secret"), - resource.TestCheckNoResourceAttr("cdn77_origin.url", "aws_region"), - resource.TestCheckNoResourceAttr("cdn77_origin.url", "base_dir"), - checkUrlOrigin(client, &originId, func(o *cdn77.UrlOriginDetail) error { - return errors.Join( - acctest.NullField("base_dir", o.BaseDir), - acctest.EqualField("host", o.Host, "my-totally-random-custom-host.com"), - acctest.EqualField("label", o.Label, "another label"), - acctest.NullFieldEqual("note", o.Note, "some note"), - acctest.NullFieldEqual("port", o.Port, 12345), - acctest.EqualField("scheme", o.Scheme, "http"), - acctest.EqualField("type", o.Type, provider.OriginTypeUrl), - ) - }), - ), - }, - { - Config: `resource "cdn77_origin" "url" { - type = "url" - label = "another label" - note = "some note" - scheme = "http" - host = "my-totally-random-custom-host.com" - port = 12345 - base_dir = "some-dir" - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("cdn77_origin.url", plancheck.ResourceActionUpdate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrWith("cdn77_origin.url", "id", func(value string) error { - return acctest.Equal(value, originId) - }), - resource.TestCheckResourceAttr("cdn77_origin.url", "type", provider.OriginTypeUrl), - resource.TestCheckResourceAttr("cdn77_origin.url", "label", "another label"), - resource.TestCheckResourceAttr("cdn77_origin.url", "note", "some note"), - resource.TestCheckResourceAttr("cdn77_origin.url", "scheme", "http"), - resource.TestCheckResourceAttr("cdn77_origin.url", "host", "my-totally-random-custom-host.com"), - resource.TestCheckResourceAttr("cdn77_origin.url", "port", "12345"), - resource.TestCheckResourceAttr("cdn77_origin.url", "base_dir", "some-dir"), - resource.TestCheckNoResourceAttr("cdn77_origin.url", "aws_access_key_id"), - resource.TestCheckNoResourceAttr("cdn77_origin.url", "aws_access_key_secret"), - resource.TestCheckNoResourceAttr("cdn77_origin.url", "aws_region"), - checkUrlOrigin(client, &originId, func(o *cdn77.UrlOriginDetail) error { - return errors.Join( - acctest.NullFieldEqual("base_dir", o.BaseDir, "some-dir"), - acctest.EqualField("host", o.Host, "my-totally-random-custom-host.com"), - acctest.EqualField("label", o.Label, "another label"), - acctest.NullFieldEqual("note", o.Note, "some note"), - acctest.NullFieldEqual("port", o.Port, 12345), - acctest.EqualField("scheme", o.Scheme, "http"), - acctest.EqualField("type", o.Type, provider.OriginTypeUrl), - ) - }), - ), - }, - { - Config: `resource "cdn77_origin" "url" { - type = "url" - label = "another label" - scheme = "http" - host = "my-totally-random-custom-host.com" - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("cdn77_origin.url", plancheck.ResourceActionUpdate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrWith("cdn77_origin.url", "id", func(value string) error { - return acctest.EqualField("id", value, originId) - }), - resource.TestCheckResourceAttr("cdn77_origin.url", "type", provider.OriginTypeUrl), - resource.TestCheckResourceAttr("cdn77_origin.url", "label", "another label"), - resource.TestCheckResourceAttr("cdn77_origin.url", "scheme", "http"), - resource.TestCheckResourceAttr("cdn77_origin.url", "host", "my-totally-random-custom-host.com"), - resource.TestCheckNoResourceAttr("cdn77_origin.url", "note"), - resource.TestCheckNoResourceAttr("cdn77_origin.url", "aws_access_key_id"), - resource.TestCheckNoResourceAttr("cdn77_origin.url", "aws_access_key_secret"), - resource.TestCheckNoResourceAttr("cdn77_origin.url", "aws_region"), - resource.TestCheckNoResourceAttr("cdn77_origin.url", "port"), - resource.TestCheckNoResourceAttr("cdn77_origin.url", "base_dir"), - checkUrlOrigin(client, &originId, func(o *cdn77.UrlOriginDetail) error { - return errors.Join( - acctest.NullField("base_dir", o.BaseDir), - acctest.EqualField("host", o.Host, "my-totally-random-custom-host.com"), - acctest.EqualField("label", o.Label, "another label"), - acctest.NullField("note", o.Note), - acctest.NullField("port", o.Port), - acctest.EqualField("scheme", o.Scheme, "http"), - acctest.EqualField("type", o.Type, provider.OriginTypeUrl), - ) - }), - ), - }, - { - Config: `resource "cdn77_origin" "url" { - type = "aws" - label = "another label" - note = "another note" - aws_access_key_id = "keyid" - aws_access_key_secret = "keysecret" - aws_region = "eu" - scheme = "http" - host = "my-totally-random-custom-host.com" - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("cdn77_origin.url", plancheck.ResourceActionDestroyBeforeCreate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrWith("cdn77_origin.url", "id", func(value string) error { - err := acctest.NotEqual(value, originId) - originId = value - - return err - }), - resource.TestCheckResourceAttr("cdn77_origin.url", "type", provider.OriginTypeAws), - resource.TestCheckResourceAttr("cdn77_origin.url", "label", "another label"), - resource.TestCheckResourceAttr("cdn77_origin.url", "note", "another note"), - resource.TestCheckResourceAttr("cdn77_origin.url", "aws_access_key_id", "keyid"), - resource.TestCheckResourceAttrSet("cdn77_origin.url", "aws_access_key_secret"), - resource.TestCheckResourceAttr("cdn77_origin.url", "aws_region", "eu"), - resource.TestCheckResourceAttr("cdn77_origin.url", "scheme", "http"), - resource.TestCheckResourceAttr("cdn77_origin.url", "host", "my-totally-random-custom-host.com"), - resource.TestCheckNoResourceAttr("cdn77_origin.url", "port"), - resource.TestCheckNoResourceAttr("cdn77_origin.url", "base_dir"), - checkAwsOrigin(client, &originId, func(o *cdn77.S3OriginDetail) error { - return errors.Join( - acctest.NullFieldEqual("aws_access_key_id", o.AwsAccessKeyId, "keyid"), - acctest.NullFieldEqual("aws_region", o.AwsRegion, "eu"), - acctest.NullField("base_dir", o.BaseDir), - acctest.EqualField("host", o.Host, "my-totally-random-custom-host.com"), - acctest.EqualField("label", o.Label, "another label"), - acctest.NullFieldEqual("note", o.Note, "another note"), - acctest.NullField("port", o.Port), - acctest.EqualField("scheme", o.Scheme, "http"), - acctest.EqualField("type", o.Type, provider.OriginTypeAws), - ) - }), - ), - }, - }, - }) -} - -func TestAccOriginResourceImport_Url(t *testing.T) { - client := acctest.GetClient(t) - rsc := "cdn77_origin.url" - var originId string - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctest.GetProviderFactories(), - CheckDestroy: checkOriginsDestroyed(client), - Steps: []resource.TestStep{ - { - Config: `resource "cdn77_origin" "url" { - type = "url" - label = "some label" - note = "some note" - scheme = "http" - host = "my-totally-random-custom-host.com" - }`, - Check: resource.TestCheckResourceAttrWith(rsc, "id", func(value string) error { - originId = value - - return acctest.NotEqual(value, "") - }), - }, - { - ResourceName: rsc, - ImportState: true, - ImportStateIdFunc: func(*terraform.State) (string, error) { - return fmt.Sprintf("%s,url", originId), nil - }, - ImportStateVerify: true, - }, - }, - }) -} - -func checkAwsOrigin( - client cdn77.ClientWithResponsesInterface, - originId *string, - fn func(o *cdn77.S3OriginDetail) error, -) func(*terraform.State) error { - return func(*terraform.State) error { - response, err := client.OriginDetailAwsWithResponse(context.Background(), *originId) - message := fmt.Sprintf("failed to get Origin[id=%s]: %%s", *originId) - - if err = acctest.CheckResponse(message, response, err); err != nil { - return err - } - - return fn(response.JSON200) - } -} - -func checkObjectStorageOrigin( - client cdn77.ClientWithResponsesInterface, - originId *string, - fn func(o *cdn77.ObjectStorageOriginDetail) error, -) func(*terraform.State) error { - return func(*terraform.State) error { - response, err := client.OriginDetailObjectStorageWithResponse(context.Background(), *originId) - message := fmt.Sprintf("failed to get Origin[id=%s]: %%s", *originId) - - if err = acctest.CheckResponse(message, response, err); err != nil { - return err - } - - return fn(response.JSON200) - } -} - -func checkUrlOrigin( - client cdn77.ClientWithResponsesInterface, - originId *string, - fn func(o *cdn77.UrlOriginDetail) error, -) func(*terraform.State) error { - return func(*terraform.State) error { - response, err := client.OriginDetailUrlWithResponse(context.Background(), *originId) - message := fmt.Sprintf("failed to get Origin[id=%s]: %%s", *originId) - - if err = acctest.CheckResponse(message, response, err); err != nil { - return err - } - - return fn(response.JSON200) - } -} - -func checkOriginsDestroyed(client cdn77.ClientWithResponsesInterface) func(*terraform.State) error { - return func(s *terraform.State) error { - for _, rs := range s.RootModule().Resources { - if rs.Type != "cdn77_origin" { - continue - } - - originId := rs.Primary.Attributes["id"] - - switch rs.Primary.Attributes["type"] { - case provider.OriginTypeAws: - response, err := client.OriginDetailAwsWithResponse(context.Background(), originId) - if err != nil { - return fmt.Errorf("failed to fetch Origin: %w", err) - } - - if response.JSON404 == nil { - return errors.New("expected origin to be deleted") - } - case provider.OriginTypeObjectStorage: - response, err := client.OriginDetailObjectStorageWithResponse(context.Background(), originId) - if err != nil { - return fmt.Errorf("failed to fetch Origin: %w", err) - } - - if response.JSON404 == nil { - return errors.New("expected origin to be deleted") - } - case provider.OriginTypeUrl: - response, err := client.OriginDetailUrlWithResponse(context.Background(), originId) - if err != nil { - return fmt.Errorf("failed to fetch Origin: %w", err) - } - - if response.JSON404 == nil { - return errors.New("expected origin to be deleted") - } - default: - return fmt.Errorf("unexpected Origin type: %s", rs.Primary.Attributes["type"]) - } - } - - return nil - } -} diff --git a/internal/provider/origins_data_source.go b/internal/provider/origins_data_source.go deleted file mode 100644 index 187c26d..0000000 --- a/internal/provider/origins_data_source.go +++ /dev/null @@ -1,133 +0,0 @@ -package provider - -import ( - "context" - "fmt" - "sort" - - "github.com/cdn77/cdn77-client-go" - "github.com/cdn77/terraform-provider-cdn77/internal/util" - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" -) - -type OriginsModel struct { - Origins []OriginModel `tfsdk:"origins"` -} - -var _ datasource.DataSourceWithConfigure = &OriginsDataSource{} - -func NewOriginsDataSource() datasource.DataSource { - return &OriginsDataSource{} -} - -type OriginsDataSource struct { - client cdn77.ClientWithResponsesInterface -} - -func (*OriginsDataSource) Metadata( - _ context.Context, - req datasource.MetadataRequest, - resp *datasource.MetadataResponse, -) { - resp.TypeName = req.ProviderTypeName + "_origins" -} - -func (*OriginsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - originSchema := util.NewResourceDataSourceSchemaConverter().Convert(CreateOriginResourceSchema()) - resp.Schema = schema.Schema{ - Attributes: map[string]schema.Attribute{ - "origins": schema.ListNestedAttribute{ - NestedObject: schema.NestedAttributeObject{Attributes: originSchema.Attributes}, - Computed: true, - Description: "List of all Origins", - }, - }, - Description: "Origins data source allows you to read all your Origins", - } -} - -func (d *OriginsDataSource) Configure( - _ context.Context, - req datasource.ConfigureRequest, - resp *datasource.ConfigureResponse, -) { - resp.Diagnostics.Append(util.MaybeSetClient(req.ProviderData, &d.client)) -} - -func (d *OriginsDataSource) Read(ctx context.Context, _ datasource.ReadRequest, resp *datasource.ReadResponse) { - const errMessage = "Failed to fetch list of all Origins" - - response, err := d.client.OriginListWithResponse(ctx) - if err != nil { - resp.Diagnostics.AddError(errMessage, err.Error()) - - return - } - - if !util.CheckResponse(&resp.Diagnostics, errMessage, response, response.JSONDefault) { - return - } - - origins := make([]OriginModel, 0, len(*response.JSON200)) - - for _, originUnion := range *response.JSON200 { - origin, err := originUnion.ValueByDiscriminator() - if err != nil { - resp.Diagnostics.AddError(errMessage, fmt.Sprintf("Failed to convert Origin to a specific type: %s", err)) - - return - } - - var originModel OriginModel - - switch o := origin.(type) { - case cdn77.S3OriginDetail: - originModel = OriginModel{ - Id: types.StringValue(o.Id), - Type: types.StringValue(OriginTypeAws), - Label: types.StringValue(o.Label), - Note: util.NullableToStringValue(o.Note), - AwsAccessKeyId: util.NullableToStringValue(o.AwsAccessKeyId), - AwsRegion: util.NullableToStringValue(o.AwsRegion), - Scheme: types.StringValue(string(o.Scheme)), - Host: types.StringValue(o.Host), - Port: util.NullableIntToInt64Value(o.Port), - BaseDir: util.NullableToStringValue(o.BaseDir), - } - case cdn77.ObjectStorageOriginDetail: - originModel = OriginModel{ - Id: types.StringValue(o.Id), - Type: types.StringValue(OriginTypeObjectStorage), - Label: types.StringValue(o.Label), - Note: util.NullableToStringValue(o.Note), - BucketName: types.StringValue(o.BucketName), - Scheme: types.StringValue(string(o.Scheme)), - Host: types.StringValue(o.Host), - Port: util.NullableIntToInt64Value(o.Port), - } - case cdn77.UrlOriginDetail: - originModel = OriginModel{ - Id: types.StringValue(o.Id), - Type: types.StringValue(OriginTypeUrl), - Label: types.StringValue(o.Label), - Note: util.NullableToStringValue(o.Note), - Scheme: types.StringValue(string(o.Scheme)), - Host: types.StringValue(o.Host), - Port: util.NullableIntToInt64Value(o.Port), - BaseDir: util.NullableToStringValue(o.BaseDir), - } - default: - continue - } - - origins = append(origins, originModel) - } - - sort.SliceStable(origins, func(i, j int) bool { - return origins[i].Id.ValueString() < origins[j].Id.ValueString() - }) - - resp.Diagnostics.Append(resp.State.Set(ctx, &OriginsModel{Origins: origins})...) -} diff --git a/internal/provider/origins_data_source_test.go b/internal/provider/origins_data_source_test.go deleted file mode 100644 index 68f7aa4..0000000 --- a/internal/provider/origins_data_source_test.go +++ /dev/null @@ -1,205 +0,0 @@ -package provider_test - -import ( - "context" - "fmt" - "sort" - "strconv" - "testing" - - "github.com/cdn77/cdn77-client-go" - "github.com/cdn77/terraform-provider-cdn77/internal/acctest" - "github.com/cdn77/terraform-provider-cdn77/internal/provider" - "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/oapi-codegen/nullable" -) - -func TestAccOriginsDataSource_All(t *testing.T) { - client := acctest.GetClient(t) - - const origin1Host = "my-totally-random-custom-host.com" - const origin1Label = "random origin" - const origin1Note = "some note" - const origin1Scheme = "https" - - request1 := cdn77.OriginAddUrlJSONRequestBody{ - Host: origin1Host, - Label: origin1Label, - Note: nullable.NewNullableWithValue(origin1Note), - Scheme: origin1Scheme, - } - response1, err := client.OriginAddUrlWithResponse(context.Background(), request1) - acctest.AssertResponseOk(t, "Failed to create Origin: %s", response1, err) - - origin1Id := response1.JSON201.Id - - t.Cleanup(func() { - acctest.MustDeleteOrigin(t, client, provider.OriginTypeUrl, origin1Id) - }) - - const origin2AwsKeyId = "someKeyId" - const origin2AwsKeySecret = "someKeySecret" - const origin2AwsRegion = "eu" - const origin2BaseDir = "some-dir" - const origin2Host = "some-other-totally-random-custom-host.com" - const origin2Label = "another origin" - const origin2Scheme = "http" - - request2 := cdn77.OriginAddAwsJSONRequestBody{ - AwsAccessKeyId: nullable.NewNullableWithValue(origin2AwsKeyId), - AwsAccessKeySecret: nullable.NewNullableWithValue(origin2AwsKeySecret), - AwsRegion: nullable.NewNullableWithValue(origin2AwsRegion), - BaseDir: nullable.NewNullableWithValue(origin2BaseDir), - Host: origin2Host, - Label: origin2Label, - Scheme: origin2Scheme, - } - response2, err := client.OriginAddAwsWithResponse(context.Background(), request2) - acctest.AssertResponseOk(t, "Failed to create Origin: %s", response2, err) - - origin2Id := response2.JSON201.Id - - t.Cleanup(func() { - acctest.MustDeleteOrigin(t, client, provider.OriginTypeAws, origin2Id) - }) - - origin3BucketName := "my-bucket-" + uuid.New().String() - const origin3Label = "yet another origin" - const origin3Note = "just a note" - - request := cdn77.OriginAddObjectStorageJSONRequestBody{ - Acl: cdn77.AuthenticatedRead, - BucketName: origin3BucketName, - ClusterId: "842b5641-b641-4723-ac81-f8cc286e288f", - Label: origin3Label, - Note: nullable.NewNullableWithValue(origin3Note), - } - - response, err := client.OriginAddObjectStorageWithResponse(context.Background(), request) - acctest.AssertResponseOk(t, "Failed to create Origin: %s", response, err) - - origin3Id := response.JSON201.Id - origin3Scheme := string(response.JSON201.Scheme) - origin3Host := response.JSON201.Host - origin3Port := response.JSON201.Port - - t.Cleanup(func() { - acctest.MustDeleteOrigin(t, client, provider.OriginTypeObjectStorage, origin3Id) - }) - - rsc := "data.cdn77_origins.all" - key := func(i int, k string) string { - return fmt.Sprintf("origins.%d.%s", i, k) - } - - originIdAndTestCheckFnFactory := []struct { - id string - factory func(i int) []resource.TestCheckFunc - }{ - {id: origin1Id, factory: func(i int) []resource.TestCheckFunc { - return []resource.TestCheckFunc{ - resource.TestCheckResourceAttr(rsc, key(i, "id"), origin1Id), - resource.TestCheckResourceAttr(rsc, key(i, "type"), provider.OriginTypeUrl), - resource.TestCheckResourceAttr(rsc, key(i, "label"), origin1Label), - resource.TestCheckResourceAttr(rsc, key(i, "note"), origin1Note), - resource.TestCheckResourceAttr(rsc, key(i, "scheme"), origin1Scheme), - resource.TestCheckResourceAttr(rsc, key(i, "host"), origin1Host), - resource.TestCheckNoResourceAttr(rsc, key(i, "aws_access_key_id")), - resource.TestCheckNoResourceAttr(rsc, key(i, "aws_access_key_secret")), - resource.TestCheckNoResourceAttr(rsc, key(i, "aws_region")), - resource.TestCheckNoResourceAttr(rsc, key(i, "acl")), - resource.TestCheckNoResourceAttr(rsc, key(i, "cluster_id")), - resource.TestCheckNoResourceAttr(rsc, key(i, "access_key_id")), - resource.TestCheckNoResourceAttr(rsc, key(i, "access_key_secret")), - resource.TestCheckNoResourceAttr(rsc, key(i, "port")), - resource.TestCheckNoResourceAttr(rsc, key(i, "base_dir")), - } - }}, - {id: origin2Id, factory: func(i int) []resource.TestCheckFunc { - return []resource.TestCheckFunc{ - resource.TestCheckResourceAttr(rsc, key(i, "id"), origin2Id), - resource.TestCheckResourceAttr(rsc, key(i, "type"), provider.OriginTypeAws), - resource.TestCheckResourceAttr(rsc, key(i, "label"), origin2Label), - resource.TestCheckResourceAttr(rsc, key(i, "aws_access_key_id"), origin2AwsKeyId), - resource.TestCheckResourceAttr(rsc, key(i, "aws_region"), origin2AwsRegion), - resource.TestCheckResourceAttr(rsc, key(i, "scheme"), origin2Scheme), - resource.TestCheckResourceAttr(rsc, key(i, "host"), origin2Host), - resource.TestCheckResourceAttr(rsc, key(i, "base_dir"), origin2BaseDir), - resource.TestCheckNoResourceAttr(rsc, key(i, "note")), - resource.TestCheckNoResourceAttr(rsc, key(i, "aws_access_key_secret")), - resource.TestCheckNoResourceAttr(rsc, key(i, "acl")), - resource.TestCheckNoResourceAttr(rsc, key(i, "cluster_id")), - resource.TestCheckNoResourceAttr(rsc, key(i, "access_key_id")), - resource.TestCheckNoResourceAttr(rsc, key(i, "access_key_secret")), - resource.TestCheckNoResourceAttr(rsc, key(i, "port")), - } - }}, - {id: origin3Id, factory: func(i int) []resource.TestCheckFunc { - return []resource.TestCheckFunc{ - resource.TestCheckResourceAttr(rsc, key(i, "id"), origin3Id), - resource.TestCheckResourceAttr(rsc, key(i, "type"), provider.OriginTypeObjectStorage), - resource.TestCheckResourceAttr(rsc, key(i, "label"), origin3Label), - resource.TestCheckResourceAttr(rsc, key(i, "note"), origin3Note), - resource.TestCheckResourceAttr(rsc, key(i, "bucket_name"), origin3BucketName), - resource.TestCheckResourceAttr(rsc, key(i, "scheme"), origin3Scheme), - resource.TestCheckResourceAttr(rsc, key(i, "host"), origin3Host), - func(state *terraform.State) error { - if origin3Port.IsNull() { - return resource.TestCheckNoResourceAttr(rsc, key(i, "port"))(state) - } - - port := strconv.Itoa(origin3Port.MustGet()) - - return resource.TestCheckResourceAttr(rsc, key(i, "port"), port)(state) - }, - resource.TestCheckNoResourceAttr(rsc, key(i, "aws_access_key_id")), - resource.TestCheckNoResourceAttr(rsc, key(i, "aws_access_key_secret")), - resource.TestCheckNoResourceAttr(rsc, key(i, "aws_region")), - resource.TestCheckNoResourceAttr(rsc, key(i, "acl")), - resource.TestCheckNoResourceAttr(rsc, key(i, "cluster_id")), - resource.TestCheckNoResourceAttr(rsc, key(i, "access_key_id")), - resource.TestCheckNoResourceAttr(rsc, key(i, "access_key_secret")), - resource.TestCheckNoResourceAttr(rsc, key(i, "base_dir")), - } - }}, - } - - sort.SliceStable(originIdAndTestCheckFnFactory, func(i, j int) bool { - return originIdAndTestCheckFnFactory[i].id < originIdAndTestCheckFnFactory[j].id - }) - - testCheckFns := []resource.TestCheckFunc{resource.TestCheckResourceAttr(rsc, "origins.#", "3")} - - for i, x := range originIdAndTestCheckFnFactory { - testCheckFns = append(testCheckFns, x.factory(i)...) - } - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctest.GetProviderFactories(), - Steps: []resource.TestStep{ - { - Config: originsDataSourceConfig, - Check: resource.ComposeAggregateTestCheckFunc(testCheckFns...), - }, - }, - }) -} - -func TestAccOriginsDataSource_Empty(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctest.GetProviderFactories(), - Steps: []resource.TestStep{ - { - Config: originsDataSourceConfig, - Check: resource.TestCheckResourceAttr("data.cdn77_origins.all", "origins.#", "0"), - }, - }, - }) -} - -const originsDataSourceConfig = ` -data "cdn77_origins" "all" { -} -` diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 9f32dd7..ab8f1fb 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -3,12 +3,15 @@ package provider import ( "context" "fmt" + "net" "net/http" "os" "strconv" "time" - "github.com/cdn77/cdn77-client-go" + "github.com/cdn77/cdn77-client-go/v2" + "github.com/cdn77/terraform-provider-cdn77/internal/mapping" + "github.com/cdn77/terraform-provider-cdn77/internal/provider/origin" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" @@ -177,21 +180,25 @@ func (*Cdn77Provider) getConfig( func (*Cdn77Provider) Resources(context.Context) []func() resource.Resource { return []func() resource.Resource{ - NewCdnResource, - NewOriginResource, - NewSslResource, + mapping.ResourceFactory(mapping.Cdn), + mapping.ResourceFactory(mapping.OriginAws), + mapping.ResourceFactory(mapping.OriginObjectStorage), + mapping.ResourceFactory(mapping.OriginUrl), + mapping.ResourceFactory(mapping.Ssl), } } func (*Cdn77Provider) DataSources(context.Context) []func() datasource.DataSource { return []func() datasource.DataSource{ - NewCdnDataSource, - NewCdnsDataSource, - NewObjectStoragesDataSource, - NewOriginDataSource, - NewOriginsDataSource, - NewSslDataSource, - NewSslsDataSource, + mapping.DataSourceFactory(mapping.Cdn), + mapping.DataSourceFactory(mapping.Cdns), + mapping.DataSourceFactory(mapping.ObjectStorages), + mapping.DataSourceFactory(mapping.OriginAws), + mapping.DataSourceFactory(mapping.OriginObjectStorage), + mapping.DataSourceFactory(mapping.OriginUrl), + origin.NewAllDataSource, + mapping.DataSourceFactory(mapping.Ssl), + mapping.DataSourceFactory(mapping.Ssls), } } @@ -203,15 +210,27 @@ func New(version string) func() provider.Provider { } } +type RoundTripperFunc func(*http.Request) (*http.Response, error) + +func (fn RoundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return fn(req) +} + func NewClient(endpoint string, token string, timeout time.Duration) (cdn77.ClientWithResponsesInterface, error) { - httpClient := &http.Client{} - if timeout > 0 { - httpClient.Timeout = timeout + dialer := &net.Dialer{} + transport := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: dialer.DialContext, + MaxIdleConns: 10, + MaxConnsPerHost: 10, + IdleConnTimeout: 30 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, } client, err := cdn77.NewClientWithResponses( endpoint, - cdn77.WithHTTPClient(httpClient), + cdn77.WithHTTPClient(&http.Client{Transport: transport, Timeout: timeout}), cdn77.WithRequestEditorFn(func(_ context.Context, req *http.Request) error { req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index 7dd057d..d69dbe1 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -35,8 +35,8 @@ func TestMain(m *testing.M) { return nil }, }) - resource.AddTestSweepers("cdn77_origin", &resource.Sweeper{ - Name: "cdn77_origin", + resource.AddTestSweepers("cdn77_origin_*", &resource.Sweeper{ + Name: "cdn77_origin_*", Dependencies: []string{"cdn77_cdn"}, F: func(_ string) error { client, err := acctest.GetClientErr() diff --git a/internal/provider/shared/url.go b/internal/provider/shared/url.go new file mode 100644 index 0000000..a071213 --- /dev/null +++ b/internal/provider/shared/url.go @@ -0,0 +1,319 @@ +package shared + +import ( + "context" + "fmt" + "net/url" + "regexp" + "slices" + "strconv" + "strings" + + "github.com/cdn77/terraform-provider-cdn77/internal/util" + "github.com/hashicorp/terraform-plugin-framework-validators/int32validator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" + "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" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/oapi-codegen/nullable" +) + +var urlBasePathRegexp = regexp.MustCompile(`^[a-zA-Z0-9.\-_\[\]+%*](/?[a-zA-Z0-9.\-_\[\]+%*])*$`) + +type UrlModel struct { + Url types.String `tfsdk:"url"` + UrlParts types.Object `tfsdk:"url_parts"` +} + +func (m UrlModel) Parts(ctx context.Context) ( + scheme string, + host string, + port nullable.Nullable[int], + basePath nullable.Nullable[string], +) { + var model UrlPartsModel + if diags := m.UrlParts.As(ctx, &model, basetypes.ObjectAsOptions{}); diags != nil { + panic(fmt.Sprintf("Failed to convert types.Object to UrlPartsModel: %v", diags)) + } + + return model.Scheme.ValueString(), + model.Host.ValueString(), + util.Int32ValueToNullable[int](model.Port), + util.StringValueToNullable(model.BasePath) +} + +type UrlPartsModel struct { + Scheme types.String `tfsdk:"scheme"` + Host types.String `tfsdk:"host"` + Port types.Int32 `tfsdk:"port"` + BasePath types.String `tfsdk:"base_path"` +} + +func NewUrlPartsModel( + scheme string, + host string, + port nullable.Nullable[int], + basePath nullable.Nullable[string], +) *UrlPartsModel { + return &UrlPartsModel{ + Scheme: types.StringValue(scheme), + Host: types.StringValue(host), + Port: util.NullableIntToInt32Value(port), + BasePath: util.NullableToStringValue(basePath), + } +} + +func NewUrlModel( + ctx context.Context, + scheme string, + host string, + port nullable.Nullable[int], + basePath nullable.Nullable[string], +) UrlModel { + urlParts := NewUrlPartsModel(scheme, host, port, basePath) + urlPartsTypes := map[string]attr.Type{ + "scheme": urlParts.Scheme.Type(ctx), + "host": urlParts.Host.Type(ctx), + "port": urlParts.Port.Type(ctx), + "base_path": urlParts.BasePath.Type(ctx), + } + + urlPartsObject, diags := types.ObjectValueFrom(ctx, urlPartsTypes, urlParts) + if diags != nil { + panic(fmt.Sprintf("Failed to convert UrlPartsModel to types.Object: %v", diags)) + } + + return UrlModel{Url: urlFromParts(urlParts), UrlParts: urlPartsObject} +} + +func WithUrlSchemaAttrs(s schema.Schema) schema.Schema { + return withMaybeComputableUrlSchemaAttrs(s, false) +} + +func WithComputedUrlSchemaAttrs(s schema.Schema) schema.Schema { + return withMaybeComputableUrlSchemaAttrs(s, true) +} + +func withMaybeComputableUrlSchemaAttrs(s schema.Schema, computed bool) schema.Schema { + urlOrSourceValidator := stringvalidator.ExactlyOneOf(path.MatchRoot("url"), path.MatchRoot("url_parts")) + s.Attributes["url"] = schema.StringAttribute{ + Description: `Absolute URL of this resource. Alternative to the attribute "url_parts".`, + Optional: util.If(computed, false, true), + Computed: true, + Validators: util.If( + computed, + []validator.String{UrlStringValidator{}}, + []validator.String{UrlStringValidator{}, urlOrSourceValidator}, + ), + PlanModifiers: []planmodifier.String{UrlAndUrlPartsPlanModifier{}, stringplanmodifier.UseStateForUnknown()}, + } + s.Attributes["url_parts"] = schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "scheme": schema.StringAttribute{ + Description: `URL scheme; can be either "http" or "https"`, + Required: util.If(computed, false, true), + Computed: computed, + Validators: []validator.String{stringvalidator.OneOf("http", "https")}, + }, + "host": schema.StringAttribute{ + Description: "Network host; can be a domain name or an IP address", + Required: util.If(computed, false, true), + Computed: computed, + }, + "port": schema.Int32Attribute{ + Description: "Port number between 1 and 65535 (if not specified, default scheme port is used)", + Optional: true, + Validators: []validator.Int32{int32validator.Between(1, 65535)}, + }, + "base_path": schema.StringAttribute{ + Description: "Path to the directory where the content is stored", + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtMost(255), + stringvalidator.RegexMatches( + urlBasePathRegexp, + "mustn't begin or end with a slash "+ + "and must consist only of alphanumeric and following characters: .-_[]+%*/", + ), + }, + }, + }, + Optional: util.If(computed, false, true), + Computed: true, + Description: "Set of attributes describing the resource URL. Alternative to the attribute \"url\".", + Validators: util.If(computed, nil, []validator.Object{urlOrSourceValidator.(validator.Object)}), + PlanModifiers: []planmodifier.Object{UrlAndUrlPartsPlanModifier{}, objectplanmodifier.UseStateForUnknown()}, + } + + return s +} + +type UrlAndUrlPartsPlanModifier struct{} + +func (UrlAndUrlPartsPlanModifier) Description(context.Context) string { + return "sets the URL if URL parts are set and vice versa" +} + +func (m UrlAndUrlPartsPlanModifier) MarkdownDescription(ctx context.Context) string { + return m.Description(ctx) +} + +func (UrlAndUrlPartsPlanModifier) PlanModifyString( + ctx context.Context, + req planmodifier.StringRequest, + resp *planmodifier.StringResponse, +) { + if !req.PlanValue.IsUnknown() { + return + } + + diags := resp.Diagnostics + var urlParts *UrlPartsModel + partsPath := req.Path.ParentPath().AtName("url_parts") + + if diags.Append(req.Plan.GetAttribute(ctx, partsPath, urlParts)...); diags.HasError() { + return + } + + resp.PlanValue = urlFromParts(urlParts) +} + +func (UrlAndUrlPartsPlanModifier) PlanModifyObject( + ctx context.Context, + req planmodifier.ObjectRequest, + resp *planmodifier.ObjectResponse, +) { + if !req.PlanValue.IsUnknown() { + return + } + + diags := resp.Diagnostics + var urlValue types.String + + if diags.Append(req.Plan.GetAttribute(ctx, req.Path.ParentPath().AtName("url"), &urlValue)...); diags.HasError() { + return + } + + if urlValue.IsUnknown() { + return + } + + u, err := url.Parse(urlValue.ValueString()) + if err != nil { + diags.AddError("Invalid URL", fmt.Sprintf("Failed to parse URL: %s", err)) + + return + } + + port := nullable.NewNullNullable[int]() + if i, err := strconv.Atoi(u.Port()); err == nil { + port.Set(i) + } + + u.Path = strings.TrimLeft(u.Path, "/") + basePath := util.If(u.Path == "", nullable.NewNullNullable[string](), nullable.NewNullableWithValue[string](u.Path)) + urlParts := NewUrlPartsModel(u.Scheme, u.Hostname(), port, basePath) + + if diags.Append(req.Plan.SetAttribute(ctx, req.Path, urlParts)...); diags.HasError() { + return + } + + diags.Append(req.Plan.GetAttribute(ctx, req.Path, &resp.PlanValue)...) +} + +type UrlStringValidator struct{} + +func (UrlStringValidator) Description(context.Context) string { + return "validates that a string contains valid scheme, host and optionally port and base path" +} + +func (v UrlStringValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +//nolint:cyclop +func (UrlStringValidator) ValidateString( + _ context.Context, + req validator.StringRequest, + resp *validator.StringResponse, +) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + diags := &resp.Diagnostics + addErr := func(summary, detail string) { + diags.AddAttributeError(req.Path, summary, detail) + } + + u, err := url.Parse(req.ConfigValue.ValueString()) + if err != nil { + addErr("Invalid URL", fmt.Sprintf("failed to parse URL: %s", err.Error())) + + return + } + + if !slices.Contains([]string{"http", "https"}, u.Scheme) { + detail := fmt.Sprintf(`URL must containt either "http" or "https" scheme, got: %q`, u.Scheme) + + addErr("Invalid URL Scheme", detail) + } + + if u.Hostname() == "" { + addErr("Missing URL Host", "URL must contain a hostname (either a domain name or an IP address)") + } + + port := u.Port() + if port != "" { + if p, portErr := strconv.ParseInt(u.Port(), 10, 32); portErr != nil || p < 1 || p > 65535 { + addErr("Invalid URL Port", "URL port must be an integer between 1 and 65535") + } + } + + if u.Path != "" { + basePath := u.Path[1:] + if len(basePath) > 255 || !urlBasePathRegexp.MatchString(strings.TrimLeft(basePath, "/")) { + requirements := strings.Join( + []string{ + "- be at most 255 characters", + "- not end with a slash (the / character)", + "- consist only of alphanumeric and following characters: .-_[]+%*/", + }, + "\n\t", + ) + detail := fmt.Sprintf("URL path must adhere to the following requirements:\n\t%s", requirements) + + addErr("Invalid URL Path", detail) + } + } + + if u.User != nil || u.RawQuery != "" || u.Fragment != "" { + addErr("Invalid URL", "URL must not contain user information, query parameters or fragments") + } +} + +func urlFromParts(urlParts *UrlPartsModel) types.String { + var urlString strings.Builder + + urlString.WriteString(urlParts.Scheme.ValueString()) + urlString.WriteString("://") + urlString.WriteString(urlParts.Host.ValueString()) + + if !urlParts.Port.IsNull() && !urlParts.Port.IsUnknown() { + urlString.WriteRune(':') + urlString.WriteString(strconv.Itoa(int(urlParts.Port.ValueInt32()))) + } + + if !urlParts.BasePath.IsNull() && !urlParts.BasePath.IsUnknown() { + urlString.WriteRune('/') + urlString.WriteString(urlParts.BasePath.ValueString()) + } + + return types.StringValue(urlString.String()) +} diff --git a/internal/provider/ssl/all.go b/internal/provider/ssl/all.go new file mode 100644 index 0000000..b1cebce --- /dev/null +++ b/internal/provider/ssl/all.go @@ -0,0 +1,69 @@ +package ssl + +import ( + "cmp" + "context" + "slices" + + "github.com/cdn77/cdn77-client-go/v2" + "github.com/cdn77/terraform-provider-cdn77/internal/util" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type AllModel struct { + Ssls []BaseModel `tfsdk:"ssls"` +} + +var _ datasource.DataSourceWithConfigure = &AllDataSource{} + +type AllDataSource struct { + *util.BaseDataSource +} + +func (d *AllDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + d.BaseDataSource.Schema(ctx, req, resp) + + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "ssls": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{Attributes: resp.Schema.Attributes}, + Computed: true, + Description: "List of all SSLs", + }, + }, + Description: "SSLs data source allows you to read all your SSL certificates and keys", + } +} + +func (d *AllDataSource) Read(ctx context.Context, _ datasource.ReadRequest, resp *datasource.ReadResponse) { + const errMessage = "Failed to fetch list of all SSLs" + + diags := &resp.Diagnostics + + response, err := d.Client.SslSniListWithResponse(ctx) + if err != nil { + diags.AddError(errMessage, err.Error()) + + return + } + + util.ProcessResponse(diags, response, errMessage, response.JSON200, func(list *cdn77.SslList) { + ssls := make([]BaseModel, 0, len(*list)) + + for _, ssl := range *list { + data := BaseModel{Id: types.StringValue(ssl.Id)} + if readSslDetails(ctx, diags, &data, &ssl); diags.HasError() { + return + } + + ssls = append(ssls, data) + } + + slices.SortStableFunc(ssls, func(a, b BaseModel) int { + return cmp.Compare(a.Id.ValueString(), b.Id.ValueString()) + }) + diags.Append(resp.State.Set(ctx, AllModel{Ssls: ssls})...) + }) +} diff --git a/internal/provider/ssls_data_source_test.go b/internal/provider/ssl/all_test.go similarity index 65% rename from internal/provider/ssls_data_source_test.go rename to internal/provider/ssl/all_test.go index 64255aa..2d3efe8 100644 --- a/internal/provider/ssls_data_source_test.go +++ b/internal/provider/ssl/all_test.go @@ -1,4 +1,4 @@ -package provider_test +package ssl_test import ( "fmt" @@ -6,16 +6,16 @@ import ( "testing" "github.com/cdn77/terraform-provider-cdn77/internal/acctest" + "github.com/cdn77/terraform-provider-cdn77/internal/acctest/testdata" "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) -func TestAccSslsDataSource_All(t *testing.T) { +func TestAccSslAllDataSource(t *testing.T) { + const rsc = "data.cdn77_ssls.all" client := acctest.GetClient(t) + ssl1Id := acctest.MustAddSslWithCleanup(t, client, testdata.SslCert1, testdata.SslKey) + ssl2Id := acctest.MustAddSslWithCleanup(t, client, testdata.SslCert2, testdata.SslKey) - ssl1Id := acctest.MustAddSslWithCleanup(t, client, sslTestCert1, sslTestKey) - ssl2Id := acctest.MustAddSslWithCleanup(t, client, sslTestCert2, sslTestKey) - - rsc := "data.cdn77_ssls.all" key := func(i int, k string) string { return fmt.Sprintf("ssls.%d.%s", i, k) } @@ -26,23 +26,21 @@ func TestAccSslsDataSource_All(t *testing.T) { {id: ssl1Id, factory: func(i int) []resource.TestCheckFunc { return []resource.TestCheckFunc{ resource.TestCheckResourceAttr(rsc, key(i, "id"), ssl1Id), - resource.TestCheckResourceAttr(rsc, key(i, "certificate"), sslTestCert1), + resource.TestCheckResourceAttr(rsc, key(i, "certificate"), testdata.SslCert1), resource.TestCheckResourceAttr(rsc, key(i, "subjects.#"), "2"), resource.TestCheckTypeSetElemAttr(rsc, key(i, "subjects.*"), "cdn.example.com"), resource.TestCheckTypeSetElemAttr(rsc, key(i, "subjects.*"), "other.mycdn.cz"), resource.TestCheckResourceAttr(rsc, key(i, "expires_at"), "2051-09-02 12:19:20"), - resource.TestCheckNoResourceAttr(rsc, key(i, "private_key")), } }}, {id: ssl2Id, factory: func(i int) []resource.TestCheckFunc { return []resource.TestCheckFunc{ resource.TestCheckResourceAttr(rsc, key(i, "id"), ssl2Id), - resource.TestCheckResourceAttr(rsc, key(i, "certificate"), sslTestCert2), + resource.TestCheckResourceAttr(rsc, key(i, "certificate"), testdata.SslCert2), resource.TestCheckResourceAttr(rsc, key(i, "subjects.#"), "1"), resource.TestCheckTypeSetElemAttr(rsc, key(i, "subjects.*"), "mycdn.cz"), resource.TestCheckResourceAttr(rsc, key(i, "expires_at"), "2051-09-03 07:51:29"), - resource.TestCheckNoResourceAttr(rsc, key(i, "private_key")), } }}, @@ -58,26 +56,16 @@ func TestAccSslsDataSource_All(t *testing.T) { testCheckFns = append(testCheckFns, x.factory(i)...) } - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctest.GetProviderFactories(), - Steps: []resource.TestStep{ - { - Config: sslsDataSourceConfig, - Check: resource.ComposeAggregateTestCheckFunc(testCheckFns...), - }, - }, + acctest.Run(t, nil, resource.TestStep{ + Config: sslsDataSourceConfig, + Check: resource.ComposeAggregateTestCheckFunc(testCheckFns...), }) } -func TestAccSslsDataSource_Empty(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctest.GetProviderFactories(), - Steps: []resource.TestStep{ - { - Config: sslsDataSourceConfig, - Check: resource.TestCheckResourceAttr("data.cdn77_ssls.all", "ssls.#", "0"), - }, - }, +func TestAccSslAllDataSource_Empty(t *testing.T) { + acctest.Run(t, nil, resource.TestStep{ + Config: sslsDataSourceConfig, + Check: resource.TestCheckResourceAttr("data.cdn77_ssls.all", "ssls.#", "0"), }) } diff --git a/internal/provider/ssl/reader.go b/internal/provider/ssl/reader.go new file mode 100644 index 0000000..f672fa3 --- /dev/null +++ b/internal/provider/ssl/reader.go @@ -0,0 +1,42 @@ +package ssl + +import ( + "context" + "time" + + "github.com/cdn77/cdn77-client-go/v2" + "github.com/cdn77/terraform-provider-cdn77/internal/util" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type Reader struct{} + +func (*Reader) ErrMessage() string { + return "Failed to fetch SSL" +} + +func (*Reader) Fetch( + ctx context.Context, + client cdn77.ClientWithResponsesInterface, + model Model, +) (*cdn77.SslSniDetailResponse, *cdn77.Ssl, error) { + response, err := client.SslSniDetailWithResponse(ctx, model.Id.ValueString()) + if err != nil { + return nil, nil, err + } + + return response, response.JSON200, nil +} + +func (*Reader) Process(ctx context.Context, model Model, ssl *cdn77.Ssl, diags *diag.Diagnostics) Model { + readSslDetails(ctx, diags, &model.BaseModel, ssl) + + return model +} + +func readSslDetails(ctx context.Context, diags *diag.Diagnostics, model *BaseModel, ssl *cdn77.Ssl) { + model.Certificate = types.StringValue(ssl.Certificate) + model.Subjects = util.SetValueFrom(ctx, diags, types.StringType, ssl.Cnames) + model.ExpiresAt = types.StringValue(ssl.ExpiresAt.Format(time.DateTime)) +} diff --git a/internal/provider/ssl_resource_schema.go b/internal/provider/ssl/schema.go similarity index 64% rename from internal/provider/ssl_resource_schema.go rename to internal/provider/ssl/schema.go index 90b5da8..7216479 100644 --- a/internal/provider/ssl_resource_schema.go +++ b/internal/provider/ssl/schema.go @@ -1,4 +1,4 @@ -package provider +package ssl import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -7,7 +7,31 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) -func CreateSslResourceSchema() schema.Schema { +type Model struct { + BaseModel + + PrivateKey types.String `tfsdk:"private_key"` +} + +type BaseModel struct { + Id types.String `tfsdk:"id"` + Certificate types.String `tfsdk:"certificate"` + Subjects types.Set `tfsdk:"subjects"` + ExpiresAt types.String `tfsdk:"expires_at"` +} + +func CreateResourceSchema() schema.Schema { + s := CreateBaseResourceSchema() + s.Attributes["private_key"] = schema.StringAttribute{ + Description: "Private key associated with the certificate", + Required: true, + Sensitive: true, + } + + return s +} + +func CreateBaseResourceSchema() schema.Schema { return schema.Schema{ Description: "SSL resource allows you to managed your SSL certificates and keys", Attributes: map[string]schema.Attribute{ @@ -20,11 +44,6 @@ func CreateSslResourceSchema() schema.Schema { Description: "SNI certificate", Required: true, }, - "private_key": schema.StringAttribute{ - Description: "Private key associated with the certificate", - Required: true, - Sensitive: true, - }, "subjects": schema.SetAttribute{ ElementType: types.StringType, Computed: true, diff --git a/internal/provider/ssl/ssl.go b/internal/provider/ssl/ssl.go new file mode 100644 index 0000000..731b3f6 --- /dev/null +++ b/internal/provider/ssl/ssl.go @@ -0,0 +1,148 @@ +package ssl + +import ( + "context" + "encoding/base64" + "fmt" + "strings" + + "github.com/cdn77/cdn77-client-go/v2" + "github.com/cdn77/terraform-provider-cdn77/internal/util" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.ResourceWithConfigure = &Resource{} + _ resource.ResourceWithImportState = &Resource{} +) + +type Resource struct { + *util.BaseResource +} + +func (r *Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + diags := &resp.Diagnostics + var data Model + + if diags.Append(req.Plan.Get(ctx, &data)...); diags.HasError() { + return + } + + request := cdn77.SslSniAddJSONRequestBody{ + Certificate: data.Certificate.ValueString(), + PrivateKey: data.PrivateKey.ValueString(), + } + + const errMessage = "Failed to create SSL" + + response, err := r.Client.SslSniAddWithResponse(ctx, request) + if err != nil { + diags.AddError(errMessage, err.Error()) + + return + } + + util.ProcessResponse(diags, response, errMessage, response.JSON201, func(ssl *cdn77.Ssl) { + data.Id = types.StringValue(ssl.Id) + if readSslDetails(ctx, diags, &data.BaseModel, ssl); diags.HasError() { + return + } + + diags.Append(resp.State.Set(ctx, data)...) + }) +} + +func (r *Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + diags := &resp.Diagnostics + var data Model + + if diags.Append(req.Plan.Get(ctx, &data)...); diags.HasError() { + return + } + + request := cdn77.SslSniEditJSONRequestBody{ + Certificate: data.Certificate.ValueString(), + PrivateKey: data.PrivateKey.ValueStringPointer(), + } + + const errMessage = "Failed to update SSL" + + response, err := r.Client.SslSniEditWithResponse(ctx, data.Id.ValueString(), request) + if err != nil { + diags.AddError(errMessage, err.Error()) + + return + } + + util.ProcessResponse(diags, response, errMessage, response.JSON200, func(ssl *cdn77.Ssl) { + if readSslDetails(ctx, diags, &data.BaseModel, ssl); diags.HasError() { + return + } + + diags.Append(resp.State.Set(ctx, data)...) + }) +} + +func (r *Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + diags := &resp.Diagnostics + var data Model + + if diags.Append(req.State.Get(ctx, &data)...); diags.HasError() { + return + } + + const errMessage = "Failed to delete SSL" + + response, err := r.Client.SslSniDeleteWithResponse(ctx, data.Id.ValueString()) + if err != nil { + diags.AddError(errMessage, err.Error()) + + return + } + + util.ValidateDeletionResponse(diags, response, errMessage) +} + +func (*Resource) ImportState( + ctx context.Context, + req resource.ImportStateRequest, + resp *resource.ImportStateResponse, +) { + idParts := strings.Split(req.ID, ",") + + if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + resp.Diagnostics.AddError( + "Unexpected Import Identifier", + fmt.Sprintf( + "Expected import identifier with format: ,. "+ + " must be the whole PEM file (including headers) encoded via base64. Got: %q", + req.ID, + ), + ) + + return + } + + id, privateKeyBase64 := idParts[0], idParts[1] + + privateKey, err := base64.StdEncoding.DecodeString(privateKeyBase64) + if err != nil { + resp.Diagnostics.AddError( + "Invalid Private Key", + "Private Key must be base64 encoded key (including the PEM headers)") + + return + } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("private_key"), string(privateKey))...) +} + +var _ datasource.DataSourceWithConfigure = &DataSource{} + +type DataSource struct { + *util.BaseDataSource +} diff --git a/internal/provider/ssl/ssl_test.go b/internal/provider/ssl/ssl_test.go new file mode 100644 index 0000000..eb3cf7d --- /dev/null +++ b/internal/provider/ssl/ssl_test.go @@ -0,0 +1,185 @@ +package ssl_test + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "regexp" + "sort" + "testing" + "time" + + "github.com/cdn77/cdn77-client-go/v2" + "github.com/cdn77/terraform-provider-cdn77/internal/acctest" + "github.com/cdn77/terraform-provider-cdn77/internal/acctest/testdata" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccSslResource(t *testing.T) { + const rsc = "cdn77_ssl.crt" + client := acctest.GetClient(t) + var sslId string + + acctest.Run(t, checkSslsDestroyed(client), + resource.TestStep{ + Config: acctest.Config(resourceConfig, "cert", testdata.SslCert1, "key", testdata.SslKey), + ConfigPlanChecks: acctest.ConfigPlanChecks(rsc, plancheck.ResourceActionCreate), + Check: resource.ComposeAggregateTestCheckFunc( + acctest.CheckAndAssignAttr(rsc, "id", &sslId), + resource.TestCheckResourceAttr(rsc, "certificate", testdata.SslCert1), + resource.TestCheckResourceAttr(rsc, "private_key", testdata.SslKey), + resource.TestCheckResourceAttr(rsc, "subjects.#", "2"), + resource.TestCheckTypeSetElemAttr(rsc, "subjects.*", "cdn.example.com"), + resource.TestCheckTypeSetElemAttr(rsc, "subjects.*", "other.mycdn.cz"), + resource.TestCheckResourceAttr(rsc, "expires_at", "2051-09-02 12:19:20"), + + checkSsl(client, &sslId, func(o *cdn77.Ssl) error { + sort.Stable(sort.StringSlice(o.Cnames)) + + return errors.Join( + acctest.EqualField("certificate", o.Certificate, testdata.SslCert1), + acctest.EqualField("expires_at", o.ExpiresAt.Format(time.DateTime), "2051-09-02 12:19:20"), + acctest.EqualField("len(cnames)", len(o.Cnames), 2), + acctest.EqualField("cnames.0", o.Cnames[0], "cdn.example.com"), + acctest.EqualField("cnames.1", o.Cnames[1], "other.mycdn.cz"), + ) + }), + ), + }, + resource.TestStep{ + Config: acctest.Config(resourceConfig, "cert", testdata.SslCert2, "key", testdata.SslKey), + ConfigPlanChecks: acctest.ConfigPlanChecks(rsc, plancheck.ResourceActionUpdate), + Check: resource.ComposeAggregateTestCheckFunc( + acctest.CheckAttr(rsc, "id", &sslId), + resource.TestCheckResourceAttr(rsc, "certificate", testdata.SslCert2), + resource.TestCheckResourceAttr(rsc, "private_key", testdata.SslKey), + resource.TestCheckResourceAttr(rsc, "subjects.#", "1"), + resource.TestCheckTypeSetElemAttr(rsc, "subjects.*", "mycdn.cz"), + resource.TestCheckResourceAttr(rsc, "expires_at", "2051-09-03 07:51:29"), + + checkSsl(client, &sslId, func(o *cdn77.Ssl) error { + sort.Stable(sort.StringSlice(o.Cnames)) + + return errors.Join( + acctest.EqualField("certificate", o.Certificate, testdata.SslCert2), + acctest.EqualField("expires_at", o.ExpiresAt.Format(time.DateTime), "2051-09-03 07:51:29"), + acctest.EqualField("len(cnames)", len(o.Cnames), 1), + acctest.EqualField("cnames.0", o.Cnames[0], "mycdn.cz"), + ) + }), + ), + }, + ) +} + +func TestAccSslResource_Import(t *testing.T) { + client := acctest.GetClient(t) + rsc := "cdn77_ssl.crt" + var sslId string + + acctest.Run(t, checkSslsDestroyed(client), + resource.TestStep{ + Config: acctest.Config(resourceConfig, "cert", testdata.SslCert1, "key", testdata.SslKey), + Check: acctest.CheckAndAssignAttr(rsc, "id", &sslId), + }, + resource.TestStep{ + ResourceName: rsc, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: func(*terraform.State) (string, error) { + privateKeyBase64 := base64.StdEncoding.EncodeToString([]byte(testdata.SslKey)) + + return fmt.Sprintf("%s,%s", sslId, privateKeyBase64), nil + }, + }, + ) +} + +func TestAccSslDataSource(t *testing.T) { + const nonExistingSslId = "ae4f471f-029a-4a5c-b9bd-27ea28815de0" + client := acctest.GetClient(t) + sslId := acctest.MustAddSslWithCleanup(t, client, testdata.SslCert1, testdata.SslKey) + + acctest.Run(t, nil, + resource.TestStep{ + Config: acctest.Config(dataSourceConfig, "id", nonExistingSslId), + ExpectError: regexp.MustCompile( + fmt.Sprintf(`SNI certificate with id "%s" was not\s+found.`, nonExistingSslId), + ), + }, + resource.TestStep{ + Config: acctest.Config(dataSourceConfig, "id", sslId), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.cdn77_ssl.ipsum", "id", sslId), + resource.TestCheckResourceAttr("data.cdn77_ssl.ipsum", "certificate", testdata.SslCert1), + resource.TestCheckResourceAttr("data.cdn77_ssl.ipsum", "subjects.#", "2"), + resource.TestCheckTypeSetElemAttr("data.cdn77_ssl.ipsum", "subjects.*", "cdn.example.com"), + resource.TestCheckTypeSetElemAttr("data.cdn77_ssl.ipsum", "subjects.*", "other.mycdn.cz"), + resource.TestCheckResourceAttr("data.cdn77_ssl.ipsum", "expires_at", "2051-09-02 12:19:20"), + resource.TestCheckNoResourceAttr("data.cdn77_ssl.ipsum", "private_key"), + ), + }, + ) +} + +func checkSsl( + client cdn77.ClientWithResponsesInterface, + sslId *string, + fn func(o *cdn77.Ssl) error, +) func(*terraform.State) error { + return func(*terraform.State) error { + response, err := client.SslSniDetailWithResponse(context.Background(), *sslId) + message := fmt.Sprintf("failed to get SSL[id=%s]: %%s", *sslId) + + if err = acctest.CheckResponse(message, response, err); err != nil { + return err + } + + return fn(response.JSON200) + } +} + +func checkSslsDestroyed(client cdn77.ClientWithResponsesInterface) resource.TestCheckFunc { + return func(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "cdn77_ssl" { + continue + } + + response, err := client.SslSniDetailWithResponse(context.Background(), rs.Primary.Attributes["id"]) + if err != nil { + return fmt.Errorf("failed to fetch SSL: %w", err) + } + + if response.JSON404 == nil { + return errors.New("expected SSL to be deleted") + } + } + + return nil + } +} + +const resourceConfig = ` +resource "cdn77_ssl" "crt" { + certificate = trimspace( + <,. "+ - " must be the whole PEM file (including headers) encoded via base64. Got: %q", - req.ID, - ), - ) - - return - } - - id, privateKeyBase64 := idParts[0], idParts[1] - - privateKey, err := base64.StdEncoding.DecodeString(privateKeyBase64) - if err != nil { - resp.Diagnostics.AddError( - "Invalid Private Key", - "Private Key must be base64 encoded key (including the PEM headers)") - - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("private_key"), string(privateKey))...) -} diff --git a/internal/provider/ssl_resource_test.go b/internal/provider/ssl_resource_test.go deleted file mode 100644 index 0da8e74..0000000 --- a/internal/provider/ssl_resource_test.go +++ /dev/null @@ -1,175 +0,0 @@ -package provider_test - -import ( - "context" - "encoding/base64" - "errors" - "fmt" - "sort" - "testing" - "time" - - "github.com/cdn77/cdn77-client-go" - "github.com/cdn77/terraform-provider-cdn77/internal/acctest" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/plancheck" - "github.com/hashicorp/terraform-plugin-testing/terraform" -) - -func TestAccSslResource(t *testing.T) { - client := acctest.GetClient(t) - var sslId string - - rsc := "cdn77_ssl.crt" - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctest.GetProviderFactories(), - CheckDestroy: checkSslsDestroyed(client), - Steps: []resource.TestStep{ - { - Config: acctest.Config(SslResourceConfig, "cert", sslTestCert1, "key", sslTestKey), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction(rsc, plancheck.ResourceActionCreate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrWith(rsc, "id", func(value string) error { - sslId = value - - return acctest.NotEqual(value, "") - }), - resource.TestCheckResourceAttr(rsc, "certificate", sslTestCert1), - resource.TestCheckResourceAttr(rsc, "private_key", sslTestKey), - resource.TestCheckResourceAttr(rsc, "subjects.#", "2"), - resource.TestCheckTypeSetElemAttr(rsc, "subjects.*", "cdn.example.com"), - resource.TestCheckTypeSetElemAttr(rsc, "subjects.*", "other.mycdn.cz"), - resource.TestCheckResourceAttr(rsc, "expires_at", "2051-09-02 12:19:20"), - - checkSsl(client, &sslId, func(o *cdn77.Ssl) error { - sort.Stable(sort.StringSlice(o.Cnames)) - - return errors.Join( - acctest.EqualField("certificate", o.Certificate, sslTestCert1), - acctest.EqualField("expires_at", o.ExpiresAt.Format(time.DateTime), "2051-09-02 12:19:20"), - acctest.EqualField("len(cnames)", len(o.Cnames), 2), - acctest.EqualField("cnames.0", o.Cnames[0], "cdn.example.com"), - acctest.EqualField("cnames.1", o.Cnames[1], "other.mycdn.cz"), - ) - }), - ), - }, - { - Config: acctest.Config(SslResourceConfig, "cert", sslTestCert2, "key", sslTestKey), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction(rsc, plancheck.ResourceActionUpdate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrWith(rsc, "id", func(value string) error { - return acctest.Equal(value, sslId) - }), - resource.TestCheckResourceAttr(rsc, "certificate", sslTestCert2), - resource.TestCheckResourceAttr(rsc, "private_key", sslTestKey), - resource.TestCheckResourceAttr(rsc, "subjects.#", "1"), - resource.TestCheckTypeSetElemAttr(rsc, "subjects.*", "mycdn.cz"), - resource.TestCheckResourceAttr(rsc, "expires_at", "2051-09-03 07:51:29"), - - checkSsl(client, &sslId, func(o *cdn77.Ssl) error { - sort.Stable(sort.StringSlice(o.Cnames)) - - return errors.Join( - acctest.EqualField("certificate", o.Certificate, sslTestCert2), - acctest.EqualField("expires_at", o.ExpiresAt.Format(time.DateTime), "2051-09-03 07:51:29"), - acctest.EqualField("len(cnames)", len(o.Cnames), 1), - acctest.EqualField("cnames.0", o.Cnames[0], "mycdn.cz"), - ) - }), - ), - }, - }, - }) -} - -func TestAccSslResourceImport(t *testing.T) { - client := acctest.GetClient(t) - rsc := "cdn77_ssl.crt" - var sslId string - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctest.GetProviderFactories(), - CheckDestroy: checkSslsDestroyed(client), - Steps: []resource.TestStep{ - { - Config: acctest.Config(SslResourceConfig, "cert", sslTestCert1, "key", sslTestKey), - Check: resource.TestCheckResourceAttrWith(rsc, "id", func(value string) error { - sslId = value - - return acctest.NotEqual(value, "") - }), - }, - { - ResourceName: rsc, - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: func(*terraform.State) (string, error) { - return fmt.Sprintf("%s,%s", sslId, base64.StdEncoding.EncodeToString([]byte(sslTestKey))), nil - }, - }, - }, - }) -} - -func checkSsl( - client cdn77.ClientWithResponsesInterface, - sslId *string, - fn func(o *cdn77.Ssl) error, -) func(*terraform.State) error { - return func(*terraform.State) error { - response, err := client.SslSniDetailWithResponse(context.Background(), *sslId) - message := fmt.Sprintf("failed to get SSL[id=%s]: %%s", *sslId) - - if err = acctest.CheckResponse(message, response, err); err != nil { - return err - } - - return fn(response.JSON200) - } -} - -func checkSslsDestroyed(client cdn77.ClientWithResponsesInterface) func(*terraform.State) error { - return func(s *terraform.State) error { - for _, rs := range s.RootModule().Resources { - if rs.Type != "cdn77_ssl" { - continue - } - - response, err := client.SslSniDetailWithResponse(context.Background(), rs.Primary.Attributes["id"]) - if err != nil { - return fmt.Errorf("failed to fetch SSL: %w", err) - } - - if response.JSON404 == nil { - return errors.New("expected SSL to be deleted") - } - } - - return nil - } -} - -const SslResourceConfig = ` -resource "cdn77_ssl" "crt" { - certificate = trimspace( - <= 0; i-- { + tField := tResponse.Field(i) + + name, ok := strings.CutPrefix(tField.Name, "JSON") + if !ok { + continue + } + + if status, err := strconv.Atoi(name); err != nil && name != "Default" || status < 300 { + continue + } + + vField := vResponse.Field(i) + if vField.IsNil() { continue } var detail string - switch m := errMessage.(type) { + switch m := vField.Interface().(type) { case *cdn77.Errors: - detail = buildResponseErrMessage(response.StatusCode(), m.Errors, nil) + detail = buildResponseErrMessage(response, m.Errors, nil) case *cdn77.FieldErrors: - detail = buildResponseErrMessage(response.StatusCode(), m.Errors, m.Fields) + detail = buildResponseErrMessage(response, m.Errors, m.Fields) default: - panic(fmt.Sprintf(`unexpected error response type "%T" (HTTP %d)`, errMessage, response.StatusCode())) + detail = fmt.Sprintf( + "Unexpected error response type \"%T\"\nHTTP %d %s\n\n%s\n", + vField.Interface(), + response.StatusCode(), + http.StatusText(response.StatusCode()), + response.Bytes(), + ) } - diags.AddError(message, detail) + diags.AddError(errMessage, detail) return false } @@ -84,7 +161,14 @@ func CheckResponse(diags *diag.Diagnostics, message string, response StatusCodeP return true } -func buildResponseErrMessage(statusCode int, errs []string, fields map[string][]string) string { +func unexpectedApiError(diags *diag.Diagnostics, response Response, errMessage string) { + code := response.StatusCode() + detail := fmt.Sprintf("Unexpected API response\nHTTP %d %s\n\n%s\n", code, http.StatusText(code), response.Bytes()) + + diags.AddError(errMessage, detail) +} + +func buildResponseErrMessage(response Response, errs []string, fields map[string][]string) string { var fieldsMessage string if fieldsCount := len(fields); fieldsCount != 0 { fieldsMessage = "\n\nFollowing fields have some errors:\n" @@ -101,10 +185,14 @@ func buildResponseErrMessage(statusCode int, errs []string, fields map[string][] } } - httpErr := fmt.Sprintf("HTTP %d %s", statusCode, http.StatusText(statusCode)) + httpErr := fmt.Sprintf("HTTP %d %s", response.StatusCode(), http.StatusText(response.StatusCode())) switch len(errs) { case 0: + if fieldsMessage == "" { + return fmt.Sprintf("Received unexpected API response:\n\t%s\n%s", httpErr, response.Bytes()) + } + return fmt.Sprintf("Received unexpected API response:\n\t%s%s", httpErr, fieldsMessage) case 1: return fmt.Sprintf("Received unexpected API error:\n\t%s%s\n\n%s", errs[0], fieldsMessage, httpErr) diff --git a/internal/util/client.go b/internal/util/client.go index 5268f5d..c045d4b 100644 --- a/internal/util/client.go +++ b/internal/util/client.go @@ -3,7 +3,7 @@ package util import ( "fmt" - "github.com/cdn77/cdn77-client-go" + "github.com/cdn77/cdn77-client-go/v2" "github.com/hashicorp/terraform-plugin-framework/diag" ) diff --git a/internal/util/collections.go b/internal/util/collections.go index c819a24..bfabaa8 100644 --- a/internal/util/collections.go +++ b/internal/util/collections.go @@ -4,9 +4,11 @@ import ( "context" "fmt" + "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/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) func StringSetToSlice(ctx context.Context, diags *diag.Diagnostics, attrPath path.Path, s types.Set) ([]string, bool) { @@ -87,3 +89,21 @@ func StringMapToMap( return stringMap, true } + +func SetValueFrom(ctx context.Context, diags *diag.Diagnostics, elemType attr.Type, elements any) basetypes.SetValue { + set, ds := types.SetValueFrom(ctx, elemType, elements) + if ds != nil { + diags.Append(ds...) + } + + return set +} + +func MapValueFrom(ctx context.Context, diags *diag.Diagnostics, elemType attr.Type, elements any) basetypes.MapValue { + m, ds := types.MapValueFrom(ctx, elemType, elements) + if ds != nil { + diags.Append(ds...) + } + + return m +} diff --git a/internal/util/provider.go b/internal/util/provider.go new file mode 100644 index 0000000..833c21c --- /dev/null +++ b/internal/util/provider.go @@ -0,0 +1,86 @@ +package util + +import ( + "context" + "strings" + + "github.com/cdn77/cdn77-client-go/v2" + "github.com/hashicorp/terraform-plugin-framework/datasource" + ds_schema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource" + rsc_schema "github.com/hashicorp/terraform-plugin-framework/resource/schema" +) + +type BaseResource struct { + name string + schemaProvider func() rsc_schema.Schema + reader Reader + + providerTypeName string + Client cdn77.ClientWithResponsesInterface +} + +func NewBaseResource(name string, schemaProvider func() rsc_schema.Schema, reader Reader) *BaseResource { + return &BaseResource{name: name, schemaProvider: schemaProvider, reader: reader} +} + +func (r *BaseResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + r.providerTypeName = req.ProviderTypeName + resp.TypeName = strings.Join([]string{r.providerTypeName, r.name}, "_") +} + +func (r *BaseResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = r.schemaProvider() +} + +func (r *BaseResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + resp.Diagnostics.Append(MaybeSetClient(req.ProviderData, &r.Client)) +} + +func (r *BaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + r.reader.RemoveMissingResource().Read(ctx, r.Client, &req.State, &resp.State, &resp.Diagnostics) +} + +func (r *BaseResource) FullName() string { + return r.providerTypeName + "_" + r.name +} + +type BaseDataSource struct { + name string + schemaProvider func() ds_schema.Schema + reader Reader + + Client cdn77.ClientWithResponsesInterface +} + +func NewBaseDataSource(name string, schemaProvider func() ds_schema.Schema, reader Reader) *BaseDataSource { + return &BaseDataSource{name: name, schemaProvider: schemaProvider, reader: reader} +} + +func (d *BaseDataSource) Metadata( + _ context.Context, + req datasource.MetadataRequest, + resp *datasource.MetadataResponse, +) { + resp.TypeName = strings.Join([]string{req.ProviderTypeName, d.name}, "_") +} + +func (d *BaseDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = d.schemaProvider() +} + +func (d *BaseDataSource) Configure( + _ context.Context, + req datasource.ConfigureRequest, + resp *datasource.ConfigureResponse, +) { + resp.Diagnostics.Append(MaybeSetClient(req.ProviderData, &d.Client)) +} + +func (d *BaseDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + d.reader.Read(ctx, d.Client, &req.Config, &resp.State, &resp.Diagnostics) +} + +func (d *BaseDataSource) Reader() Reader { + return d.reader +} diff --git a/internal/util/reader.go b/internal/util/reader.go new file mode 100644 index 0000000..9edcaea --- /dev/null +++ b/internal/util/reader.go @@ -0,0 +1,96 @@ +package util + +import ( + "context" + "net/http" + + "github.com/cdn77/cdn77-client-go/v2" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +type Reader interface { + RemoveMissingResource() Reader + Read( + ctx context.Context, + client cdn77.ClientWithResponsesInterface, + reqStateProvider StateProvider, + respState *tfsdk.State, + diags *diag.Diagnostics, + ) + Fill(ctx context.Context, client cdn77.ClientWithResponsesInterface, model *any) diag.Diagnostics +} + +type StateProvider interface { + Get(ctx context.Context, target any) diag.Diagnostics +} + +type GenericReader[M any, R Response, Rok any] interface { + ErrMessage() string + Fetch(ctx context.Context, client cdn77.ClientWithResponsesInterface, model M) (R, *Rok, error) + Process(ctx context.Context, model M, detail *Rok, diags *diag.Diagnostics) M +} + +type UniversalReader[M any, R Response, Rok any] struct { + reader GenericReader[M, R, Rok] + removeMissingResource bool +} + +func NewUniversalReader[M any, R Response, Rok any](reader GenericReader[M, R, Rok]) *UniversalReader[M, R, Rok] { + return &UniversalReader[M, R, Rok]{reader: reader} +} + +func (r UniversalReader[M, R, Rok]) RemoveMissingResource() Reader { + return UniversalReader[M, R, Rok]{reader: r.reader, removeMissingResource: true} +} + +func (r UniversalReader[M, R, Rok]) Read( + ctx context.Context, + client cdn77.ClientWithResponsesInterface, + reqStateProvider StateProvider, + respState *tfsdk.State, + diags *diag.Diagnostics, +) { + var model M + if diags.Append(reqStateProvider.Get(ctx, &model)...); diags.HasError() { + return + } + + response, responseOk, err := r.reader.Fetch(ctx, client, model) + if err != nil { + diags.AddError(r.reader.ErrMessage(), err.Error()) + + return + } + + if r.removeMissingResource && response.StatusCode() == http.StatusNotFound { + respState.RemoveResource(ctx) + + return + } + + ProcessResponse(diags, response, r.reader.ErrMessage(), responseOk, func(detail *Rok) { + if model = r.reader.Process(ctx, model, detail, diags); diags.HasError() { + return + } + + diags.Append(respState.Set(ctx, model)...) + }) +} + +func (r UniversalReader[M, R, Rok]) Fill( + ctx context.Context, + client cdn77.ClientWithResponsesInterface, + model *any, +) (diags diag.Diagnostics) { + response, responseOk, err := r.reader.Fetch(ctx, client, (*model).(M)) + if err != nil { + return diag.Diagnostics{diag.NewErrorDiagnostic(r.reader.ErrMessage(), err.Error())} + } + + ProcessResponse(&diags, response, r.reader.ErrMessage(), responseOk, func(detail *Rok) { + *model = r.reader.Process(ctx, (*model).(M), detail, &diags) + }) + + return diags +} diff --git a/internal/util/schema.go b/internal/util/schema.go index dfc2827..33d22bf 100644 --- a/internal/util/schema.go +++ b/internal/util/schema.go @@ -90,6 +90,17 @@ func (c *ResourceDataSourceSchemaConverter) convertAttributes( DeprecationMessage: rscAttr.DeprecationMessage, Validators: If(isRequired, rscAttr.Validators, nil), } + case rsc_schema.Int32Attribute: + dsAttr = ds_schema.Int32Attribute{ + CustomType: rscAttr.CustomType, + Required: isRequired, + Computed: !isRequired, + Sensitive: rscAttr.Sensitive, + Description: rscAttr.Description, + MarkdownDescription: rscAttr.MarkdownDescription, + DeprecationMessage: rscAttr.DeprecationMessage, + Validators: If(isRequired, rscAttr.Validators, nil), + } case rsc_schema.SetAttribute: dsAttr = ds_schema.SetAttribute{ ElementType: rscAttr.ElementType, diff --git a/internal/util/util.go b/internal/util/util.go index ea92341..64effb9 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -7,3 +7,7 @@ func If[T any](condition bool, ifTrueValue T, elseValue T) T { //nolint:revive / return elseValue } + +func Pointer[T any](v T) *T { + return &v +}