From f8d77a1bd401d084b9a9baa9c01d16a8902779b6 Mon Sep 17 00:00:00 2001 From: AlejandroFabianCampos Date: Fri, 25 Sep 2026 16:02:32 -0300 Subject: [PATCH] fix: provider fixes for 0.3.0 --- CHANGELOG.md | 20 +++- README.md | 10 +- docs/data-sources/chains.md | 6 +- docs/data-sources/endpoint.md | 2 +- docs/data-sources/endpoint_urls.md | 1 + docs/data-sources/endpoints.md | 10 +- docs/index.md | 2 +- docs/resources/endpoint.md | 15 +-- docs/resources/endpoint_domain_mask.md | 6 +- docs/resources/endpoint_ip.md | 6 +- docs/resources/endpoint_jwt.md | 6 +- docs/resources/endpoint_method_rate_limit.md | 6 +- docs/resources/endpoint_rate_limits.md | 6 +- docs/resources/endpoint_referrer.md | 6 +- docs/resources/endpoint_request_filter.md | 6 +- docs/resources/endpoint_token.md | 6 +- .../quicknode_chains/data-source.tf | 6 +- .../quicknode_endpoint_urls/data-source.tf | 1 + examples/provider/provider.tf | 2 +- .../resources/quicknode_endpoint/resource.tf | 11 +- .../resource.tf | 6 +- .../quicknode_endpoint_ip/resource.tf | 6 +- .../quicknode_endpoint_jwt/resource.tf | 6 +- .../resource.tf | 6 +- .../resource.tf | 6 +- .../quicknode_endpoint_referrer/resource.tf | 6 +- .../resource.tf | 6 +- .../quicknode_endpoint_token/resource.tf | 6 +- internal/client/errors.go | 6 + internal/client/security_test.go | 13 ++ internal/provider/acceptance_test.go | 112 ++++++++++++------ internal/provider/endpoint_data_source.go | 2 +- internal/provider/endpoint_resource.go | 14 +-- internal/provider/endpoints_data_source.go | 6 +- internal/provider/security_entry_resource.go | 21 +++- 35 files changed, 241 insertions(+), 115 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b62c93..255fdae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,24 @@ # Changelog -## 0.2.0 (Unreleased) +## 0.3.0 (Unreleased) + +BREAKING CHANGES: + +* `quicknode_endpoint.status` and `quicknode_endpoint.multichain` are required. Their + defaults applied on import too, so importing a paused or multichain endpoint without + declaring them planned to resume it or turn multichain off. +* `tags` on `data.quicknode_endpoint` and `data.quicknode_endpoints`, and the + `labels`, `networks`, `statuses` and `tag_labels` filters on `data.quicknode_endpoints`, + are sets instead of lists. + +BUG FIXES: + +* Importing an IP address, domain mask or referrer while its security toggle is off + reports the disabled toggle. The Admin API hides those entries while it is off. +* Creating one that already exists while its toggle is off explains that it is hidden + and how to import it. + +## 0.2.0 (September 25, 2026) BREAKING CHANGES: diff --git a/README.md b/README.md index d1f6cad..4ccb707 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ terraform { required_providers { quicknode = { source = "quicknode/quicknode" - version = "~> 0.1" + version = "~> 0.3.0" } } } @@ -28,9 +28,11 @@ terraform { provider "quicknode" {} resource "quicknode_endpoint" "payments" { - chain = "eth" - network = "mainnet" - label = "payments-prod" + chain = "eth" + network = "mainnet" + label = "payments-prod" + status = "active" + multichain = false } ``` diff --git a/docs/data-sources/chains.md b/docs/data-sources/chains.md index 8065bb5..3f851b1 100644 --- a/docs/data-sources/chains.md +++ b/docs/data-sources/chains.md @@ -25,8 +25,10 @@ locals { } resource "quicknode_endpoint" "primary" { - chain = local.ethereum.slug - network = one([for network in local.ethereum.networks : network.slug if network.chain_id == 1]) + chain = local.ethereum.slug + network = one([for network in local.ethereum.networks : network.slug if network.chain_id == 1]) + multichain = false + status = "active" } ``` diff --git a/docs/data-sources/endpoint.md b/docs/data-sources/endpoint.md index 7201be6..ee91e15 100644 --- a/docs/data-sources/endpoint.md +++ b/docs/data-sources/endpoint.md @@ -52,7 +52,7 @@ resource "quicknode_endpoint_ip" "office" { - `safe_wss_url` (String) The WebSocket URL with the auth token replaced by `REPLACE_WITH_TOKEN`, or null on chains without WebSocket support. - `security_options` (Attributes) Which security mechanisms the endpoint enforces. (see [below for nested schema](#nestedatt--security_options)) - `status` (String) `active` or `paused`. -- `tags` (List of String) Tag labels applied to the endpoint. +- `tags` (Set of String) Tag labels applied to the endpoint. - `tokens` (Attributes List) Auth tokens for the endpoint. Values land in Terraform state, so keep state encrypted and remote. (see [below for nested schema](#nestedatt--tokens)) - `wss_url_with_token` (String, Sensitive) The working WebSocket endpoint, or null on chains without WebSocket support. diff --git a/docs/data-sources/endpoint_urls.md b/docs/data-sources/endpoint_urls.md index bcb0573..fb7634a 100644 --- a/docs/data-sources/endpoint_urls.md +++ b/docs/data-sources/endpoint_urls.md @@ -23,6 +23,7 @@ resource "quicknode_endpoint" "api" { chain = "eth" network = "mainnet" multichain = true + status = "active" } # depends_on defers the read to the apply, so it sees multichain enabled. diff --git a/docs/data-sources/endpoints.md b/docs/data-sources/endpoints.md index 66f05a2..2f1801c 100644 --- a/docs/data-sources/endpoints.md +++ b/docs/data-sources/endpoints.md @@ -41,11 +41,11 @@ output "production_networks" { ### Optional -- `labels` (List of String) Keep only endpoints carrying these labels. -- `networks` (List of String) Keep only endpoints on these networks, for example `mainnet` or `base-sepolia`. +- `labels` (Set of String) Keep only endpoints carrying these labels. +- `networks` (Set of String) Keep only endpoints on these networks, for example `mainnet` or `base-sepolia`. - `search` (String) Match against the endpoint's subdomain or label. -- `statuses` (List of String) Keep only endpoints with these statuses: `active` or `paused`. -- `tag_labels` (List of String) Keep only endpoints carrying these tags. +- `statuses` (Set of String) Keep only endpoints with these statuses: `active` or `paused`. +- `tag_labels` (Set of String) Keep only endpoints carrying these tags. ### Read-Only @@ -68,4 +68,4 @@ Read-Only: - `safe_http_url` (String) The HTTPS URL with the auth token replaced by `REPLACE_WITH_TOKEN`. Safe to log or display. The list route carries no usable token, so read `data.quicknode_endpoint` for a working URL. - `safe_wss_url` (String) The WebSocket URL with the auth token replaced by `REPLACE_WITH_TOKEN`, or null on chains without WebSocket support. - `status` (String) `active` or `paused`. -- `tags` (List of String) Tag labels applied to the endpoint. +- `tags` (Set of String) Tag labels applied to the endpoint. diff --git a/docs/index.md b/docs/index.md index 63813c2..e4cb67d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -26,7 +26,7 @@ terraform { required_providers { quicknode = { source = "quicknode/quicknode" - version = "~> 0.1" + version = "~> 0.3.0" } } } diff --git a/docs/resources/endpoint.md b/docs/resources/endpoint.md index 464adc5..375b2da 100644 --- a/docs/resources/endpoint.md +++ b/docs/resources/endpoint.md @@ -20,11 +20,12 @@ The resource does not track the endpoint's tokens or the URLs that carry them, b ```terraform resource "quicknode_endpoint" "payments" { - chain = "eth" - network = "mainnet" - label = "payments-prod" - status = "active" - tags = ["prod", "payments"] + chain = "eth" + network = "mainnet" + label = "payments-prod" + status = "active" + multichain = false + tags = ["prod", "payments"] # Each toggle decides whether a mechanism is enforced. The entries it applies # to are separate resources, such as quicknode_endpoint_ip. A toggle left out @@ -65,15 +66,15 @@ output "payments_rpc_url" { ### Required - `chain` (String) Chain slug, for example `eth`, `base`, `arb`, `sol`. Slugs are often abbreviations of the chain's name; read `data.quicknode_chains` for the full list. +- `multichain` (Boolean) Whether the endpoint serves more than one network. Required, so importing an endpoint never changes which networks it serves. - `network` (String) Network slug, for example `mainnet`, `base-sepolia`, `arbitrum-mainnet`. +- `status` (String) `active` or `paused`. Required, so importing an endpoint never changes whether it serves traffic. ### Optional - `ip_custom_header` (String) Name of the header the endpoint reads the caller's IP address from, for example `X-Real-IP`. Set it when calls arrive through a proxy, so IP restrictions see the original caller's address and not the proxy's. - `label` (String) Descriptive label for the endpoint. Labels are not unique and do not identify the endpoint. Removing the attribute clears the label. -- `multichain` (Boolean) Whether the endpoint serves more than one network. - `security_options` (Attributes) Which security mechanisms the endpoint enforces. Each toggle only decides whether a mechanism is applied; the entries it applies to are separate resources, such as `quicknode_endpoint_ip`. A toggle left out of the configuration keeps whatever value the endpoint already has. (see [below for nested schema](#nestedatt--security_options)) -- `status` (String) `active` or `paused`. - `tags` (Set of String) Tag labels applied to the endpoint. Omitting the attribute removes every tag the provider finds on the endpoint. ### Read-Only diff --git a/docs/resources/endpoint_domain_mask.md b/docs/resources/endpoint_domain_mask.md index 53bafcb..b047858 100644 --- a/docs/resources/endpoint_domain_mask.md +++ b/docs/resources/endpoint_domain_mask.md @@ -17,8 +17,10 @@ The entry only takes effect once `security_options.domain_masks` is enabled on t ```terraform resource "quicknode_endpoint" "api" { - chain = "eth" - network = "mainnet" + chain = "eth" + network = "mainnet" + multichain = false + status = "active" security_options = { domain_masks = true diff --git a/docs/resources/endpoint_ip.md b/docs/resources/endpoint_ip.md index e8f9425..8cd03d5 100644 --- a/docs/resources/endpoint_ip.md +++ b/docs/resources/endpoint_ip.md @@ -17,8 +17,10 @@ The entry only takes effect once `security_options.ips` is enabled on the endpoi ```terraform resource "quicknode_endpoint" "api" { - chain = "eth" - network = "mainnet" + chain = "eth" + network = "mainnet" + multichain = false + status = "active" security_options = { ips = true diff --git a/docs/resources/endpoint_jwt.md b/docs/resources/endpoint_jwt.md index cfc5f38..f331393 100644 --- a/docs/resources/endpoint_jwt.md +++ b/docs/resources/endpoint_jwt.md @@ -17,8 +17,10 @@ The key only takes effect once `security_options.jwts` is enabled on the endpoin ```terraform resource "quicknode_endpoint" "api" { - chain = "eth" - network = "mainnet" + chain = "eth" + network = "mainnet" + multichain = false + status = "active" security_options = { jwts = true diff --git a/docs/resources/endpoint_method_rate_limit.md b/docs/resources/endpoint_method_rate_limit.md index 82c9237..efc5d09 100644 --- a/docs/resources/endpoint_method_rate_limit.md +++ b/docs/resources/endpoint_method_rate_limit.md @@ -17,8 +17,10 @@ Use it to keep a handful of expensive calls, such as `eth_getLogs` over wide blo ```terraform resource "quicknode_endpoint" "api" { - chain = "eth" - network = "mainnet" + chain = "eth" + network = "mainnet" + multichain = false + status = "active" } # Keep a few expensive calls from consuming the endpoint's whole budget. diff --git a/docs/resources/endpoint_rate_limits.md b/docs/resources/endpoint_rate_limits.md index 2963c4d..e56e94b 100644 --- a/docs/resources/endpoint_rate_limits.md +++ b/docs/resources/endpoint_rate_limits.md @@ -17,8 +17,10 @@ Each bucket the Quicknode plan sets is reported under `plan_default`. A bucket s ```terraform resource "quicknode_endpoint" "api" { - chain = "eth" - network = "mainnet" + chain = "eth" + network = "mainnet" + multichain = false + status = "active" } # A bucket set here overrides the plan default. A bucket left out keeps it, and diff --git a/docs/resources/endpoint_referrer.md b/docs/resources/endpoint_referrer.md index 9f4b50e..fd9060d 100644 --- a/docs/resources/endpoint_referrer.md +++ b/docs/resources/endpoint_referrer.md @@ -17,8 +17,10 @@ The entry only takes effect once `security_options.referrers` is enabled on the ```terraform resource "quicknode_endpoint" "api" { - chain = "eth" - network = "mainnet" + chain = "eth" + network = "mainnet" + multichain = false + status = "active" security_options = { referrers = true diff --git a/docs/resources/endpoint_request_filter.md b/docs/resources/endpoint_request_filter.md index 208cee8..b1cf0f4 100644 --- a/docs/resources/endpoint_request_filter.md +++ b/docs/resources/endpoint_request_filter.md @@ -17,8 +17,10 @@ The Admin API turns filtering on when a filter exists and off when the last one ```terraform resource "quicknode_endpoint" "api" { - chain = "eth" - network = "mainnet" + chain = "eth" + network = "mainnet" + multichain = false + status = "active" } # Anything outside the set is rejected. The Admin API turns filtering on once a diff --git a/docs/resources/endpoint_token.md b/docs/resources/endpoint_token.md index 6065cdb..4ccfdb1 100644 --- a/docs/resources/endpoint_token.md +++ b/docs/resources/endpoint_token.md @@ -17,8 +17,10 @@ Quicknode generates the value, so the resource takes no input beyond the endpoin ```terraform resource "quicknode_endpoint" "api" { - chain = "eth" - network = "mainnet" + chain = "eth" + network = "mainnet" + multichain = false + status = "active" security_options = { tokens = true diff --git a/examples/data-sources/quicknode_chains/data-source.tf b/examples/data-sources/quicknode_chains/data-source.tf index d4fbac4..0c0b5c9 100644 --- a/examples/data-sources/quicknode_chains/data-source.tf +++ b/examples/data-sources/quicknode_chains/data-source.tf @@ -7,6 +7,8 @@ locals { } resource "quicknode_endpoint" "primary" { - chain = local.ethereum.slug - network = one([for network in local.ethereum.networks : network.slug if network.chain_id == 1]) + chain = local.ethereum.slug + network = one([for network in local.ethereum.networks : network.slug if network.chain_id == 1]) + multichain = false + status = "active" } diff --git a/examples/data-sources/quicknode_endpoint_urls/data-source.tf b/examples/data-sources/quicknode_endpoint_urls/data-source.tf index d87b7a7..c4abf5e 100644 --- a/examples/data-sources/quicknode_endpoint_urls/data-source.tf +++ b/examples/data-sources/quicknode_endpoint_urls/data-source.tf @@ -2,6 +2,7 @@ resource "quicknode_endpoint" "api" { chain = "eth" network = "mainnet" multichain = true + status = "active" } # depends_on defers the read to the apply, so it sees multichain enabled. diff --git a/examples/provider/provider.tf b/examples/provider/provider.tf index 95fa69e..89d00d1 100644 --- a/examples/provider/provider.tf +++ b/examples/provider/provider.tf @@ -2,7 +2,7 @@ terraform { required_providers { quicknode = { source = "quicknode/quicknode" - version = "~> 0.1" + version = "~> 0.3.0" } } } diff --git a/examples/resources/quicknode_endpoint/resource.tf b/examples/resources/quicknode_endpoint/resource.tf index 08294a4..7e4ee60 100644 --- a/examples/resources/quicknode_endpoint/resource.tf +++ b/examples/resources/quicknode_endpoint/resource.tf @@ -1,9 +1,10 @@ resource "quicknode_endpoint" "payments" { - chain = "eth" - network = "mainnet" - label = "payments-prod" - status = "active" - tags = ["prod", "payments"] + chain = "eth" + network = "mainnet" + label = "payments-prod" + status = "active" + multichain = false + tags = ["prod", "payments"] # Each toggle decides whether a mechanism is enforced. The entries it applies # to are separate resources, such as quicknode_endpoint_ip. A toggle left out diff --git a/examples/resources/quicknode_endpoint_domain_mask/resource.tf b/examples/resources/quicknode_endpoint_domain_mask/resource.tf index 8c491aa..75eae08 100644 --- a/examples/resources/quicknode_endpoint_domain_mask/resource.tf +++ b/examples/resources/quicknode_endpoint_domain_mask/resource.tf @@ -1,6 +1,8 @@ resource "quicknode_endpoint" "api" { - chain = "eth" - network = "mainnet" + chain = "eth" + network = "mainnet" + multichain = false + status = "active" security_options = { domain_masks = true diff --git a/examples/resources/quicknode_endpoint_ip/resource.tf b/examples/resources/quicknode_endpoint_ip/resource.tf index fc0eb5e..60ac43e 100644 --- a/examples/resources/quicknode_endpoint_ip/resource.tf +++ b/examples/resources/quicknode_endpoint_ip/resource.tf @@ -1,6 +1,8 @@ resource "quicknode_endpoint" "api" { - chain = "eth" - network = "mainnet" + chain = "eth" + network = "mainnet" + multichain = false + status = "active" security_options = { ips = true diff --git a/examples/resources/quicknode_endpoint_jwt/resource.tf b/examples/resources/quicknode_endpoint_jwt/resource.tf index affbfd0..3df12a7 100644 --- a/examples/resources/quicknode_endpoint_jwt/resource.tf +++ b/examples/resources/quicknode_endpoint_jwt/resource.tf @@ -1,6 +1,8 @@ resource "quicknode_endpoint" "api" { - chain = "eth" - network = "mainnet" + chain = "eth" + network = "mainnet" + multichain = false + status = "active" security_options = { jwts = true diff --git a/examples/resources/quicknode_endpoint_method_rate_limit/resource.tf b/examples/resources/quicknode_endpoint_method_rate_limit/resource.tf index 2d2760b..d43fb1f 100644 --- a/examples/resources/quicknode_endpoint_method_rate_limit/resource.tf +++ b/examples/resources/quicknode_endpoint_method_rate_limit/resource.tf @@ -1,6 +1,8 @@ resource "quicknode_endpoint" "api" { - chain = "eth" - network = "mainnet" + chain = "eth" + network = "mainnet" + multichain = false + status = "active" } # Keep a few expensive calls from consuming the endpoint's whole budget. diff --git a/examples/resources/quicknode_endpoint_rate_limits/resource.tf b/examples/resources/quicknode_endpoint_rate_limits/resource.tf index 60b568b..382332c 100644 --- a/examples/resources/quicknode_endpoint_rate_limits/resource.tf +++ b/examples/resources/quicknode_endpoint_rate_limits/resource.tf @@ -1,6 +1,8 @@ resource "quicknode_endpoint" "api" { - chain = "eth" - network = "mainnet" + chain = "eth" + network = "mainnet" + multichain = false + status = "active" } # A bucket set here overrides the plan default. A bucket left out keeps it, and diff --git a/examples/resources/quicknode_endpoint_referrer/resource.tf b/examples/resources/quicknode_endpoint_referrer/resource.tf index 635197d..5f3d92b 100644 --- a/examples/resources/quicknode_endpoint_referrer/resource.tf +++ b/examples/resources/quicknode_endpoint_referrer/resource.tf @@ -1,6 +1,8 @@ resource "quicknode_endpoint" "api" { - chain = "eth" - network = "mainnet" + chain = "eth" + network = "mainnet" + multichain = false + status = "active" security_options = { referrers = true diff --git a/examples/resources/quicknode_endpoint_request_filter/resource.tf b/examples/resources/quicknode_endpoint_request_filter/resource.tf index ceb23cc..1633eb1 100644 --- a/examples/resources/quicknode_endpoint_request_filter/resource.tf +++ b/examples/resources/quicknode_endpoint_request_filter/resource.tf @@ -1,6 +1,8 @@ resource "quicknode_endpoint" "api" { - chain = "eth" - network = "mainnet" + chain = "eth" + network = "mainnet" + multichain = false + status = "active" } # Anything outside the set is rejected. The Admin API turns filtering on once a diff --git a/examples/resources/quicknode_endpoint_token/resource.tf b/examples/resources/quicknode_endpoint_token/resource.tf index 72cdd96..4e89ecd 100644 --- a/examples/resources/quicknode_endpoint_token/resource.tf +++ b/examples/resources/quicknode_endpoint_token/resource.tf @@ -1,6 +1,8 @@ resource "quicknode_endpoint" "api" { - chain = "eth" - network = "mainnet" + chain = "eth" + network = "mainnet" + multichain = false + status = "active" security_options = { tokens = true diff --git a/internal/client/errors.go b/internal/client/errors.go index a0bee44..b615007 100644 --- a/internal/client/errors.go +++ b/internal/client/errors.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "net/http" + "strings" ) // Error carries an Admin API failure. The Admin API can report a failure either @@ -27,6 +28,11 @@ func IsNotFound(err error) bool { return errors.As(err, &apiErr) && apiErr.Status == http.StatusNotFound } +func IsAlreadyExists(err error) bool { + var apiErr *Error + return errors.As(err, &apiErr) && strings.Contains(apiErr.Message, "ALREADY_EXISTS") +} + // IsUnauthorized reports the two ways the Admin API rejects a key: an invalid // key, and a key on a plan without Admin API access. func IsUnauthorized(err error) bool { diff --git a/internal/client/security_test.go b/internal/client/security_test.go index 7be6c67..a3a14f6 100644 --- a/internal/client/security_test.go +++ b/internal/client/security_test.go @@ -202,6 +202,19 @@ func TestAddEndpointIPRejectsEnvelopeError(t *testing.T) { } } +func TestIsAlreadyExists(t *testing.T) { + duplicate := statusError("add endpoint ip", http.StatusBadRequest, + []byte(`{"error":"IP_ADDRESS_ALREADY_EXISTS: Request failed upstream, please check your parameters and try again."}`)) + if !IsAlreadyExists(duplicate) { + t.Error("the live duplicate-IP response has to be recognised") + } + + other := statusError("add endpoint ip", http.StatusBadRequest, []byte(`{"error":"INVALID_IP_ADDRESS"}`)) + if IsAlreadyExists(other) { + t.Error("an unrelated 400 must not be treated as a duplicate") + } +} + // TestEmptyWritesAreSkipped covers the configuration that manages none of the // toggles or buckets. An empty body is a pointless call at best, so the client // makes none at all. diff --git a/internal/provider/acceptance_test.go b/internal/provider/acceptance_test.go index 1134700..8cac628 100644 --- a/internal/provider/acceptance_test.go +++ b/internal/provider/acceptance_test.go @@ -70,9 +70,11 @@ func testAccCheckEndpointsDestroyed(state *terraform.State) error { func endpointConfig(label string) string { return fmt.Sprintf(` resource "quicknode_endpoint" "test" { - chain = %q - network = %q - label = %q + chain = %q + network = %q + label = %q + multichain = false + status = "active" } `, acceptanceChain, acceptanceNetwork, label) } @@ -116,9 +118,11 @@ func TestAccEndpoint_securityOptions(t *testing.T) { withOptions := func(cors bool) string { return fmt.Sprintf(` resource "quicknode_endpoint" "test" { - chain = %q - network = %q - label = "tfacc-security-options" + chain = %q + network = %q + label = "tfacc-security-options" + multichain = false + status = "active" security_options = { cors = %t @@ -152,9 +156,11 @@ resource "quicknode_endpoint" "test" { func TestAccEndpointIP_importByValue(t *testing.T) { const config = ` resource "quicknode_endpoint" "test" { - chain = "eth" - network = "ethereum-sepolia" - label = "tfacc-ip" + chain = "eth" + network = "ethereum-sepolia" + label = "tfacc-ip" + multichain = false + status = "active" security_options = { ips = true @@ -200,9 +206,11 @@ func TestAccRequestFilter_updatesInPlace(t *testing.T) { withMethods := func(methods string) string { return fmt.Sprintf(` resource "quicknode_endpoint" "test" { - chain = %q - network = %q - label = "tfacc-request-filter" + chain = %q + network = %q + label = "tfacc-request-filter" + multichain = false + status = "active" } resource "quicknode_endpoint_request_filter" "test" { @@ -241,9 +249,11 @@ func TestAccRateLimits_dropReturnsPlanDefault(t *testing.T) { withBuckets := func(buckets string) string { return fmt.Sprintf(` resource "quicknode_endpoint" "test" { - chain = %q - network = %q - label = "tfacc-rate-limits" + chain = %q + network = %q + label = "tfacc-rate-limits" + multichain = false + status = "active" } resource "quicknode_endpoint_rate_limits" "test" { @@ -281,9 +291,11 @@ func TestAccMethodRateLimit_lifecycle(t *testing.T) { withRate := func(rate int, enabled bool) string { return fmt.Sprintf(` resource "quicknode_endpoint" "test" { - chain = %q - network = %q - label = "tfacc-method-rate-limit" + chain = %q + network = %q + label = "tfacc-method-rate-limit" + multichain = false + status = "active" } resource "quicknode_endpoint_method_rate_limit" "test" { @@ -322,9 +334,11 @@ resource "quicknode_endpoint_method_rate_limit" "test" { func TestAccEndpointDataSource_byLabel(t *testing.T) { const config = ` resource "quicknode_endpoint" "test" { - chain = "eth" - network = "ethereum-sepolia" - label = "tfacc-data-source" + chain = "eth" + network = "ethereum-sepolia" + label = "tfacc-data-source" + multichain = false + status = "active" } data "quicknode_endpoint" "test" { @@ -404,8 +418,10 @@ func endpointIDForImport(suffix func(*terraform.State) (string, error)) func(*te func TestAccEndpoint_labelClearedByRemoval(t *testing.T) { unlabelled := fmt.Sprintf(` resource "quicknode_endpoint" "test" { - chain = %q - network = %q + chain = %q + network = %q + multichain = false + status = "active" } `, acceptanceChain, acceptanceNetwork) @@ -434,9 +450,10 @@ func TestAccEndpoint_tagsStatusAndHeader(t *testing.T) { withAttributes := func(body string) string { return fmt.Sprintf(` resource "quicknode_endpoint" "test" { - chain = %q - network = %q - label = "tfacc-attributes" + chain = %q + network = %q + label = "tfacc-attributes" + multichain = false %s } `, acceptanceChain, acceptanceNetwork, body) @@ -473,7 +490,7 @@ resource "quicknode_endpoint" "test" { ), }, { - Config: withAttributes(""), + Config: withAttributes(` status = "active"`), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("quicknode_endpoint.test", "tags.#", "0"), resource.TestCheckNoResourceAttr("quicknode_endpoint.test", "ip_custom_header"), @@ -486,9 +503,11 @@ resource "quicknode_endpoint" "test" { func TestAccEndpointToken_lifecycle(t *testing.T) { config := fmt.Sprintf(` resource "quicknode_endpoint" "test" { - chain = %q - network = %q - label = "tfacc-token" + chain = %q + network = %q + label = "tfacc-token" + multichain = false + status = "active" } resource "quicknode_endpoint_token" "test" { @@ -529,9 +548,11 @@ resource "quicknode_endpoint_token" "test" { func TestAccEndpointJWT_importByName(t *testing.T) { config := fmt.Sprintf(` resource "quicknode_endpoint" "test" { - chain = %q - network = %q - label = "tfacc-jwt" + chain = %q + network = %q + label = "tfacc-jwt" + multichain = false + status = "active" security_options = { jwts = true @@ -579,9 +600,11 @@ EOT func TestAccSecurityEntries_importByValue(t *testing.T) { config := fmt.Sprintf(` resource "quicknode_endpoint" "test" { - chain = %q - network = %q - label = "tfacc-entries" + chain = %q + network = %q + label = "tfacc-entries" + multichain = false + status = "active" security_options = { domain_masks = true @@ -641,9 +664,11 @@ resource "quicknode_endpoint_referrer" "test" { func TestAccSecurityEntry_survivesDisabledToggle(t *testing.T) { config := fmt.Sprintf(` resource "quicknode_endpoint" "test" { - chain = %q - network = %q - label = "tfacc-disabled-toggle" + chain = %q + network = %q + label = "tfacc-disabled-toggle" + multichain = false + status = "active" security_options = { ips = false @@ -668,6 +693,14 @@ resource "quicknode_endpoint_ip" "test" { resource.TestCheckResourceAttr("quicknode_endpoint.test", "security_options.ips", "false"), ), }, + { + ResourceName: "quicknode_endpoint_ip.test", + ImportState: true, + ImportStateIdFunc: endpointIDForImport(func(*terraform.State) (string, error) { + return "203.0.113.9", nil + }), + ExpectError: regexp.MustCompile(`IP address filtering is disabled on the endpoint`), + }, }, }) } @@ -683,6 +716,7 @@ resource "quicknode_endpoint" "test" { network = %q label = "tfacc-urls" multichain = %t + status = "active" } data "quicknode_endpoint_urls" "test" { diff --git a/internal/provider/endpoint_data_source.go b/internal/provider/endpoint_data_source.go index b8f6cf2..2146d66 100644 --- a/internal/provider/endpoint_data_source.go +++ b/internal/provider/endpoint_data_source.go @@ -79,7 +79,7 @@ func (d *endpointDataSource) Schema(_ context.Context, _ datasource.SchemaReques "network": schema.StringAttribute{Computed: true, MarkdownDescription: "Network slug."}, "status": schema.StringAttribute{Computed: true, MarkdownDescription: "`active` or `paused`."}, "multichain": computedBool("Whether the endpoint serves more than one network."), - "tags": schema.ListAttribute{ + "tags": schema.SetAttribute{ Computed: true, ElementType: types.StringType, MarkdownDescription: "Tag labels applied to the endpoint.", diff --git a/internal/provider/endpoint_resource.go b/internal/provider/endpoint_resource.go index cf32f89..907812b 100644 --- a/internal/provider/endpoint_resource.go +++ b/internal/provider/endpoint_resource.go @@ -10,9 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -86,17 +84,13 @@ func (r *endpointResource) Schema(_ context.Context, _ resource.SchemaRequest, r Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, }, "status": schema.StringAttribute{ - Optional: true, - Computed: true, - Default: stringdefault.StaticString(statusActive), - MarkdownDescription: "`active` or `paused`.", + Required: true, + MarkdownDescription: "`active` or `paused`. Required, so importing an endpoint never changes whether it serves traffic.", Validators: []validator.String{stringvalidator.OneOf(statusActive, statusPaused)}, }, "multichain": schema.BoolAttribute{ - Optional: true, - Computed: true, - Default: booldefault.StaticBool(false), - MarkdownDescription: "Whether the endpoint serves more than one network.", + Required: true, + MarkdownDescription: "Whether the endpoint serves more than one network. Required, so importing an endpoint never changes which networks it serves.", }, "tags": schema.SetAttribute{ Optional: true, diff --git a/internal/provider/endpoints_data_source.go b/internal/provider/endpoints_data_source.go index 62268fb..ac179af 100644 --- a/internal/provider/endpoints_data_source.go +++ b/internal/provider/endpoints_data_source.go @@ -52,8 +52,8 @@ func (d *endpointsDataSource) Metadata(_ context.Context, req datasource.Metadat } func (d *endpointsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - filter := func(description string) schema.ListAttribute { - return schema.ListAttribute{ + filter := func(description string) schema.SetAttribute { + return schema.SetAttribute{ Optional: true, ElementType: types.StringType, MarkdownDescription: description, @@ -99,7 +99,7 @@ func (d *endpointsDataSource) Schema(_ context.Context, _ datasource.SchemaReque "dedicated": schema.BoolAttribute{Computed: true, MarkdownDescription: "Whether the endpoint runs on dedicated infrastructure."}, "flat_rate": schema.BoolAttribute{Computed: true, MarkdownDescription: "Whether the endpoint is billed at a flat rate."}, "multichain": schema.BoolAttribute{Computed: true, MarkdownDescription: "Whether the endpoint serves more than one network."}, - "tags": schema.ListAttribute{ + "tags": schema.SetAttribute{ Computed: true, ElementType: types.StringType, MarkdownDescription: "Tag labels applied to the endpoint.", diff --git a/internal/provider/security_entry_resource.go b/internal/provider/security_entry_resource.go index 10f670e..7068883 100644 --- a/internal/provider/security_entry_resource.go +++ b/internal/provider/security_entry_resource.go @@ -156,7 +156,7 @@ func (r *securityEntryResource) Create(ctx context.Context, req resource.CreateR entry, err := r.kind.add(r.client, ctx, endpointID.ValueString(), value.ValueString()) if err != nil { - resp.Diagnostics.AddError("Could not add the "+r.kind.noun, err.Error()) + resp.Diagnostics.AddError("Could not add the "+r.kind.noun, err.Error()+r.hiddenEntryHint(ctx, err, endpointID.ValueString(), value.ValueString())) return } @@ -166,6 +166,18 @@ func (r *securityEntryResource) Create(ctx context.Context, req resource.CreateR resp.Diagnostics.Append(warnToggleDisabled(ctx, r.client, endpointID.ValueString(), r.kind.toggle, r.kind.subject)...) } +func (r *securityEntryResource) hiddenEntryHint(ctx context.Context, err error, endpointID, value string) string { + if !client.IsAlreadyExists(err) { + return "" + } + security, readErr := r.client.GetEndpointSecurity(ctx, endpointID) + if readErr != nil || securityToggleEnabled(security.Options, r.kind.toggle) { + return "" + } + return fmt.Sprintf("\n\nThe %s already exists on endpoint %s, but security_options.%s is false, and the Admin API hides its entries while it is off. "+ + "Enable it, then import the entry with \"terraform import
%s/%s\".", r.kind.noun, endpointID, r.kind.toggle, endpointID, value) +} + func (r *securityEntryResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var entryID, endpointID types.String resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("id"), &entryID)...) @@ -243,6 +255,13 @@ func (r *securityEntryResource) ImportState(ctx context.Context, req resource.Im } switch len(matches) { case 0: + if !securityToggleEnabled(security.Options, r.kind.toggle) { + resp.Diagnostics.AddError( + r.kind.subject+" is disabled on the endpoint", + fmt.Sprintf("Endpoint %s has security_options.%s set to false, and the Admin API hides its %s entries while it is off. Enable it and import again.", endpointID, r.kind.toggle, r.kind.noun), + ) + return + } resp.Diagnostics.AddError( "No matching "+r.kind.noun, fmt.Sprintf("Endpoint %s has no %s entry with the value %q.", endpointID, r.kind.noun, value),