From 25a3a02b66345ab197267732a42ce842d80726ed Mon Sep 17 00:00:00 2001 From: AlejandroFabianCampos Date: Wed, 23 Sep 2026 10:06:36 -0500 Subject: [PATCH 1/6] fix: read domain masks from the field the security route returns --- api/admin/admin.gen.go | 12 ++++++------ api/admin/openapi.json | 2 +- api/admin/patches.json | 19 +++++++++++++++++++ internal/client/security.go | 2 +- internal/client/security_test.go | 2 +- 5 files changed, 28 insertions(+), 9 deletions(-) diff --git a/api/admin/admin.gen.go b/api/admin/admin.gen.go index 3e2bfd7..ca1f0b7 100644 --- a/api/admin/admin.gen.go +++ b/api/admin/admin.gen.go @@ -9248,8 +9248,8 @@ type GetV0EndpointsByIdSecurityResponse struct { Data *struct { // DomainMasks An array of domain mask entries; null if none configured DomainMasks *[]struct { - DomainMask *string `json:"domain_mask,omitempty"` - Id *string `json:"id,omitempty"` + Domain *string `json:"domain,omitempty"` + Id *string `json:"id,omitempty"` } `json:"domain_masks,omitempty"` // Ips An array of allowed IP entries; null if none configured @@ -9341,8 +9341,8 @@ func (r GetV0EndpointsByIdSecurityResponse) GetJSON200() *struct { Data *struct { // DomainMasks An array of domain mask entries; null if none configured DomainMasks *[]struct { - DomainMask *string `json:"domain_mask,omitempty"` - Id *string `json:"id,omitempty"` + Domain *string `json:"domain,omitempty"` + Id *string `json:"id,omitempty"` } `json:"domain_masks,omitempty"` // Ips An array of allowed IP entries; null if none configured @@ -15130,8 +15130,8 @@ func ParseGetV0EndpointsByIdSecurityResponse(rsp *http.Response) (*GetV0Endpoint Data *struct { // DomainMasks An array of domain mask entries; null if none configured DomainMasks *[]struct { - DomainMask *string `json:"domain_mask,omitempty"` - Id *string `json:"id,omitempty"` + Domain *string `json:"domain,omitempty"` + Id *string `json:"id,omitempty"` } `json:"domain_masks,omitempty"` // Ips An array of allowed IP entries; null if none configured diff --git a/api/admin/openapi.json b/api/admin/openapi.json index 5c32d00..be2f577 100644 --- a/api/admin/openapi.json +++ b/api/admin/openapi.json @@ -2875,7 +2875,7 @@ "description": "An array of domain mask entries; null if none configured", "items": { "properties": { - "domain_mask": { + "domain": { "type": "string" }, "id": { diff --git a/api/admin/patches.json b/api/admin/patches.json index 130b8bb..b1921f3 100644 --- a/api/admin/patches.json +++ b/api/admin/patches.json @@ -331,5 +331,24 @@ } } } + }, + { + "op": "set", + "path": "/paths/~1v0~1endpoints~1{id}~1security/get/responses/200/content/application~1json/schema/properties/data/properties/domain_masks", + "value": { + "description": "An array of domain mask entries; null if none configured", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "domain": { + "type": "string" + } + } + } + } } ] diff --git a/internal/client/security.go b/internal/client/security.go index c7553a6..a06ecfa 100644 --- a/internal/client/security.go +++ b/internal/client/security.go @@ -117,7 +117,7 @@ func (c *Client) GetEndpointSecurity(ctx context.Context, endpointID string) (*E } if data.DomainMasks != nil { for _, raw := range *data.DomainMasks { - security.DomainMasks = append(security.DomainMasks, SecurityEntry{ID: deref(raw.Id), Value: deref(raw.DomainMask)}) + security.DomainMasks = append(security.DomainMasks, SecurityEntry{ID: deref(raw.Id), Value: deref(raw.Domain)}) } } if data.Referrers != nil { diff --git a/internal/client/security_test.go b/internal/client/security_test.go index 672ac8a..4fc0f35 100644 --- a/internal/client/security_test.go +++ b/internal/client/security_test.go @@ -17,7 +17,7 @@ const securityBody = `{"data":{ "hsts":true,"cors":false,"requestFilters":true,"responseLogging":false, "ipCustomHeader":{"value":"X-Real-IP"}}, "ips":[{"id":"ip-1","ip":"203.0.113.7"}], -"domain_masks":[{"id":"dm-1","domain_mask":"rpc.example.com"}], +"domain_masks":[{"id":"dm-1","domain":"rpc.example.com"}], "referrers":[{"id":"rf-1","referrer":"https://example.com"}], "jwts":[{"id":"jwt-1","name":"signer","kid":"kid-1","public_key":"-----BEGIN PUBLIC KEY-----"}], "request_filters":[{"id":"filter-1","method":["eth_call","eth_getLogs"]}], From 95ca55935e54a09b4fb1bcc0d256a24e15068afb Mon Sep 17 00:00:00 2001 From: AlejandroFabianCampos Date: Wed, 23 Sep 2026 10:06:40 -0500 Subject: [PATCH 2/6] fix: keep the endpoint label when the attribute is removed --- internal/provider/endpoint_resource.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/internal/provider/endpoint_resource.go b/internal/provider/endpoint_resource.go index abd4871..15bc861 100644 --- a/internal/provider/endpoint_resource.go +++ b/internal/provider/endpoint_resource.go @@ -89,8 +89,11 @@ func (r *endpointResource) Schema(_ context.Context, _ resource.SchemaRequest, r PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, }, "label": schema.StringAttribute{ - Optional: true, - MarkdownDescription: "Descriptive label for the endpoint. Labels are not unique and are not used to identify the endpoint.", + Optional: true, + Computed: true, + MarkdownDescription: "Descriptive label for the endpoint. Labels are not unique and do not identify the endpoint. " + + "Quicknode has no route for clearing a label once set, so removing the attribute leaves the current label in place and Terraform stops tracking it.", + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, }, "status": schema.StringAttribute{ Optional: true, @@ -215,8 +218,8 @@ func (r *endpointResource) Create(ctx context.Context, req resource.CreateReques return } - if label := plan.Label.ValueString(); label != "" { - if err := r.client.SetEndpointLabel(ctx, created.ID, label); err != nil { + if !plan.Label.IsUnknown() && !plan.Label.IsNull() { + if err := r.client.SetEndpointLabel(ctx, created.ID, plan.Label.ValueString()); err != nil { resp.Diagnostics.AddError( "Created the endpoint but could not set its label", fmt.Sprintf("Endpoint %s exists and is tracked in state. %s", created.ID, err.Error()), @@ -298,7 +301,7 @@ func (r *endpointResource) Update(ctx context.Context, req resource.UpdateReques id := state.ID.ValueString() plan.ID = state.ID - if !plan.Label.Equal(state.Label) { + if !plan.Label.IsUnknown() && !plan.Label.Equal(state.Label) { if err := r.client.SetEndpointLabel(ctx, id, plan.Label.ValueString()); err != nil { resp.Diagnostics.AddError("Could not update the endpoint label", err.Error()) return @@ -366,6 +369,9 @@ func (r *endpointResource) readInto(ctx context.Context, id string, model *endpo return } model.ID = types.StringValue(endpoint.ID) + if model.Label.IsUnknown() { + model.Label = stringOrNull(endpoint.Label) + } applyEndpointURLs(endpoint, model) tokens, tokenDiags := tokenList(endpoint.Tokens) From 308007874e54e1ac6aedbd246b3399d263e8be76 Mon Sep 17 00:00:00 2001 From: AlejandroFabianCampos Date: Wed, 23 Sep 2026 10:10:31 -0500 Subject: [PATCH 3/6] fix: send the key id the JWT signing key route requires --- docs/resources/endpoint_jwt.md | 10 ++++------ .../resources/quicknode_endpoint_jwt/resource.tf | 8 +++----- internal/provider/jwt_resource.go | 12 ++++++------ 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/docs/resources/endpoint_jwt.md b/docs/resources/endpoint_jwt.md index 1c4f161..9a7fb46 100644 --- a/docs/resources/endpoint_jwt.md +++ b/docs/resources/endpoint_jwt.md @@ -29,14 +29,12 @@ resource "quicknode_endpoint" "api" { resource "quicknode_endpoint_jwt" "signer" { endpoint_id = quicknode_endpoint.api.id name = "signer" + kid = "signer-2026-01" public_key = file("${path.module}/signer.pub.pem") } -# Put the generated kid in the header of the tokens signed with the matching -# private key. -output "jwt_kid" { - value = quicknode_endpoint_jwt.signer.kid -} +# Tokens signed with the matching private key carry the same kid in their +# header, which is how Quicknode picks the key to verify them with. ``` @@ -45,13 +43,13 @@ output "jwt_kid" { ### Required - `endpoint_id` (String) Endpoint the signing key belongs to. +- `kid` (String) Key id for this signing key. Put the same value in the `kid` header of the tokens signed with the matching private key. Changing it replaces the entry, because the Admin API has no route to edit one in place. - `name` (String) Name for the key, used to tell several keys apart on one endpoint. - `public_key` (String) PEM-encoded public key that signed tokens are verified against. Changing it replaces the entry, because the Admin API has no route to edit one in place. ### Read-Only - `id` (String) Entry id assigned by Quicknode. -- `kid` (String) Key id Quicknode assigns. Put it in the `kid` header of the tokens signed with the matching private key. ## Import diff --git a/examples/resources/quicknode_endpoint_jwt/resource.tf b/examples/resources/quicknode_endpoint_jwt/resource.tf index f402cfe..affbfd0 100644 --- a/examples/resources/quicknode_endpoint_jwt/resource.tf +++ b/examples/resources/quicknode_endpoint_jwt/resource.tf @@ -11,11 +11,9 @@ resource "quicknode_endpoint" "api" { resource "quicknode_endpoint_jwt" "signer" { endpoint_id = quicknode_endpoint.api.id name = "signer" + kid = "signer-2026-01" public_key = file("${path.module}/signer.pub.pem") } -# Put the generated kid in the header of the tokens signed with the matching -# private key. -output "jwt_kid" { - value = quicknode_endpoint_jwt.signer.kid -} +# Tokens signed with the matching private key carry the same kid in their +# header, which is how Quicknode picks the key to verify them with. diff --git a/internal/provider/jwt_resource.go b/internal/provider/jwt_resource.go index f7cef49..50d5d0f 100644 --- a/internal/provider/jwt_resource.go +++ b/internal/provider/jwt_resource.go @@ -68,9 +68,10 @@ func (r *jwtResource) Schema(_ context.Context, _ resource.SchemaRequest, resp * Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, }, "kid": schema.StringAttribute{ - Computed: true, - MarkdownDescription: "Key id Quicknode assigns. Put it in the `kid` header of the tokens signed with the matching private key.", - PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + Required: true, + MarkdownDescription: "Key id for this signing key. Put the same value in the `kid` header of the tokens signed with the matching private key. Changing it replaces the entry, because the Admin API has no route to edit one in place.", + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, }, }, } @@ -97,6 +98,7 @@ func (r *jwtResource) Create(ctx context.Context, req resource.CreateRequest, re created, err := r.client.AddJWT(ctx, plan.EndpointID.ValueString(), client.JWT{ Name: plan.Name.ValueString(), + KID: plan.KID.ValueString(), PublicKey: plan.PublicKey.ValueString(), }) if err != nil { @@ -105,7 +107,6 @@ func (r *jwtResource) Create(ctx context.Context, req resource.CreateRequest, re } plan.ID = types.StringValue(created.ID) - plan.KID = types.StringValue(created.KID) resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) resp.Diagnostics.Append(warnToggleDisabled(ctx, r.client, plan.EndpointID.ValueString(), "jwts", "JWT authentication")...) } @@ -142,8 +143,7 @@ func (r *jwtResource) Read(ctx context.Context, req resource.ReadRequest, resp * resp.State.RemoveResource(ctx) } -// Update exists only to satisfy the interface. Every attribute replaces the -// resource, so Terraform never calls it. +// Update never runs: every attribute replaces the resource. func (r *jwtResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { } From 7794e5472a31d84581b8af7ac7453df93251df24 Mon Sep 17 00:00:00 2001 From: AlejandroFabianCampos Date: Wed, 23 Sep 2026 10:10:43 -0500 Subject: [PATCH 4/6] fix: keep security entries in state while a disabled toggle hides them --- internal/provider/jwt_resource.go | 6 ++++++ internal/provider/security_entry_resource.go | 15 ++++++++++----- internal/provider/token_resource.go | 14 +++++++++----- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/internal/provider/jwt_resource.go b/internal/provider/jwt_resource.go index 50d5d0f..59bad75 100644 --- a/internal/provider/jwt_resource.go +++ b/internal/provider/jwt_resource.go @@ -140,6 +140,12 @@ func (r *jwtResource) Read(ctx context.Context, req resource.ReadRequest, resp * resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) return } + // The security route omits a list entirely while its toggle is disabled, + // so an entry that cannot be seen has not necessarily been deleted. + // Dropping it from state here would have the next apply create a duplicate. + if !securityToggleEnabled(security.Options, "jwts") { + return + } resp.State.RemoveResource(ctx) } diff --git a/internal/provider/security_entry_resource.go b/internal/provider/security_entry_resource.go index 8cb4732..d32600f 100644 --- a/internal/provider/security_entry_resource.go +++ b/internal/provider/security_entry_resource.go @@ -73,7 +73,7 @@ var securityEntryKinds = []securityEntryKind{ toggle: "referrers", noun: "referrer", subject: "Referrer filtering", - description: "A referrer allowed to call a Quicknode endpoint. Referrer checks suit browser traffic, where the header is set by the browser rather than by the caller.", + description: "A referrer allowed to call a Quicknode endpoint. Referrer checks suit browser traffic, where the browser sets the header and the caller cannot.", valueDoc: "Referrer URL allowed to call the endpoint, for example `https://app.example.com`.", add: func(c *client.Client, ctx context.Context, endpointID, value string) (*client.SecurityEntry, error) { return c.AddReferrer(ctx, endpointID, value) @@ -191,11 +191,16 @@ func (r *securityEntryResource) Read(ctx context.Context, req resource.ReadReque resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root(r.kind.attribute), types.StringValue(entry.Value))...) return } + // The security route omits a list entirely while its toggle is disabled, + // so an entry that cannot be seen has not necessarily been deleted. + // Dropping it from state here would have the next apply create a duplicate. + if !securityToggleEnabled(security.Options, r.kind.toggle) { + return + } resp.State.RemoveResource(ctx) } -// Update exists only to satisfy the interface. Every attribute replaces the -// resource, so Terraform never calls it. +// Update never runs: every attribute replaces the resource. func (r *securityEntryResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { } @@ -212,8 +217,8 @@ func (r *securityEntryResource) Delete(ctx context.Context, req resource.DeleteR } } -// ImportState takes "/", so the address is what the -// operator already knows rather than an id that only exists in the API. +// ImportState takes "/". The address is the value itself, +// so nothing has to be looked up first. func (r *securityEntryResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { endpointID, value, found := strings.Cut(req.ID, "/") if !found || endpointID == "" || value == "" { diff --git a/internal/provider/token_resource.go b/internal/provider/token_resource.go index 223abe2..37146c8 100644 --- a/internal/provider/token_resource.go +++ b/internal/provider/token_resource.go @@ -117,11 +117,16 @@ func (r *endpointTokenResource) Read(ctx context.Context, req resource.ReadReque resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) return } + // The security route omits a list entirely while its toggle is disabled, + // so an entry that cannot be seen has not necessarily been deleted. + // Dropping it from state here would have the next apply create a duplicate. + if !securityToggleEnabled(security.Options, "tokens") { + return + } resp.State.RemoveResource(ctx) } -// Update exists only to satisfy the interface. The endpoint replaces the -// resource and the value is generated, so Terraform never calls it. +// Update never runs: the endpoint replaces the resource and Quicknode generates the value. func (r *endpointTokenResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { } @@ -137,9 +142,8 @@ func (r *endpointTokenResource) Delete(ctx context.Context, req resource.DeleteR } } -// ImportState takes "/". Tokens are addressed by id -// rather than by value, so importing one does not put the credential on a -// command line or into a shell history. +// ImportState takes "/". Addressing a token by id keeps +// the credential off the command line and out of shell history. func (r *endpointTokenResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { endpointID, tokenID, found := strings.Cut(req.ID, "/") if !found || endpointID == "" || tokenID == "" { From e1247b06795423d32cf9cb63744dfff21c64bb17 Mon Sep 17 00:00:00 2001 From: AlejandroFabianCampos Date: Wed, 23 Sep 2026 10:10:47 -0500 Subject: [PATCH 5/6] feat: cover the untested resources and endpoint attributes with acceptance tests --- internal/provider/acceptance_test.go | 310 ++++++++++++++++++++++++++- 1 file changed, 307 insertions(+), 3 deletions(-) diff --git a/internal/provider/acceptance_test.go b/internal/provider/acceptance_test.go index 9caf897..398ff6e 100644 --- a/internal/provider/acceptance_test.go +++ b/internal/provider/acceptance_test.go @@ -111,7 +111,7 @@ func TestAccEndpoint_lifecycle(t *testing.T) { // TestAccEndpoint_securityOptions checks the boolean-read, string-write // asymmetry end to end, and that a toggle the configuration does not manage -// keeps its value rather than being reset. +// keeps its value. func TestAccEndpoint_securityOptions(t *testing.T) { withOptions := func(cors bool) string { return fmt.Sprintf(` @@ -137,7 +137,7 @@ resource "quicknode_endpoint" "test" { Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("quicknode_endpoint.test", "security_options.cors", "false"), // tokens is unmanaged here, so it has to keep the value a - // new endpoint is created with rather than being turned off. + // new endpoint is created with. resource.TestCheckResourceAttr("quicknode_endpoint.test", "security_options.tokens", "true"), ), }, @@ -233,7 +233,7 @@ resource "quicknode_endpoint_request_filter" "test" { // TestAccRateLimits_dropReturnsPlanDefault covers the behaviour the patch route // cannot express: removing a bucket from the configuration has to delete the -// override rather than leave it in place. +// override. func TestAccRateLimits_dropReturnsPlanDefault(t *testing.T) { withBuckets := func(buckets string) string { return fmt.Sprintf(` @@ -366,3 +366,307 @@ func TestAccChainsDataSource(t *testing.T) { }, }) } + +// jwtAcceptancePublicKey is a throwaway RSA public key, generated for these +// tests and used nowhere else. The matching private key was discarded. +const jwtAcceptancePublicKey = `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvxK0qszzlluhCeocaEdM +RRoXn+2tmIyi/ToD8YmXQd9KhqHJLs2zT6WAEctEr3AetcJmWpwyXEaMzSn6aoIn +SBmic1PeGCyecISo2wt/RkbdoWYJe48T4BOAwH/ljOztYjldfiVKINNTR8975K1Z +uo6QOo//grLrZOzqU7BTG8r7MqyX8eAh/W3MEthhpFEIlLCOyxL0HMwOsfbTYG9/ +EvmvmBTqnXfU653cFnAWjtHBVO3YOcp/CmaU+4pqF+Pu+prGajLIekzZnjIx0W6j +mY6xngVlYGHdIj1qCcvETrSRaz6JemJpBseFNaOUe7AYDl2Ne5UPnkdgDTZUOuj5 +/QIDAQAB +-----END PUBLIC KEY----- +` + +// endpointIDForImport addresses a child resource by the endpoint it belongs to, +// which is how every one of them is imported. +func endpointIDForImport(suffix func(*terraform.State) (string, error)) func(*terraform.State) (string, error) { + return func(state *terraform.State) (string, error) { + endpoint := state.RootModule().Resources["quicknode_endpoint.test"] + if endpoint == nil { + return "", fmt.Errorf("the endpoint is not in state") + } + tail, err := suffix(state) + if err != nil { + return "", err + } + return endpoint.Primary.ID + "/" + tail, nil + } +} + +// TestAccEndpoint_labelSurvivesRemoval covers the one attribute Quicknode has +// no route to clear. Dropping it from the configuration has to leave the +// endpoint's label alone and settle into an empty plan. +func TestAccEndpoint_labelSurvivesRemoval(t *testing.T) { + unlabelled := fmt.Sprintf(` +resource "quicknode_endpoint" "test" { + chain = %q + network = %q +} +`, acceptanceChain, acceptanceNetwork) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: protoV6ProviderFactories, + CheckDestroy: testAccCheckEndpointsDestroyed, + Steps: []resource.TestStep{ + { + Config: endpointConfig("tfacc-label"), + Check: resource.TestCheckResourceAttr("quicknode_endpoint.test", "label", "tfacc-label"), + }, + { + Config: unlabelled, + Check: resource.TestCheckResourceAttr("quicknode_endpoint.test", "label", "tfacc-label"), + }, + }, + }) +} + +// TestAccEndpoint_tagsStatusAndHeader exercises the attributes that reconcile +// against the API on update: tags are added and removed one at a time, the +// status is paused and resumed, and clearing the custom IP header is a delete +// call of its own. +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" +%s +} +`, acceptanceChain, acceptanceNetwork, body) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: protoV6ProviderFactories, + CheckDestroy: testAccCheckEndpointsDestroyed, + Steps: []resource.TestStep{ + { + Config: withAttributes(` tags = ["tfacc-one", "tfacc-two"] + status = "paused" + ip_custom_header = "X-Real-IP"`), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("quicknode_endpoint.test", "tags.#", "2"), + resource.TestCheckTypeSetElemAttr("quicknode_endpoint.test", "tags.*", "tfacc-one"), + resource.TestCheckTypeSetElemAttr("quicknode_endpoint.test", "tags.*", "tfacc-two"), + resource.TestCheckResourceAttr("quicknode_endpoint.test", "status", "paused"), + resource.TestCheckResourceAttr("quicknode_endpoint.test", "ip_custom_header", "X-Real-IP"), + ), + }, + { + // One tag stays, one goes and one arrives, so the reconcile has + // to issue both an add and a remove in the same apply. + Config: withAttributes(` tags = ["tfacc-two", "tfacc-three"] + status = "active" + ip_custom_header = "X-Forwarded-For"`), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("quicknode_endpoint.test", "tags.#", "2"), + resource.TestCheckTypeSetElemAttr("quicknode_endpoint.test", "tags.*", "tfacc-three"), + resource.TestCheckResourceAttr("quicknode_endpoint.test", "status", "active"), + resource.TestCheckResourceAttr("quicknode_endpoint.test", "ip_custom_header", "X-Forwarded-For"), + ), + }, + { + Config: withAttributes(""), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("quicknode_endpoint.test", "tags.#", "0"), + resource.TestCheckNoResourceAttr("quicknode_endpoint.test", "ip_custom_header"), + ), + }, + }, + }) +} + +func TestAccEndpointToken_lifecycle(t *testing.T) { + config := fmt.Sprintf(` +resource "quicknode_endpoint" "test" { + chain = %q + network = %q + label = "tfacc-token" +} + +resource "quicknode_endpoint_token" "test" { + endpoint_id = quicknode_endpoint.test.id +} +`, acceptanceChain, acceptanceNetwork) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: protoV6ProviderFactories, + CheckDestroy: testAccCheckEndpointsDestroyed, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("quicknode_endpoint_token.test", "id"), + resource.TestCheckResourceAttrSet("quicknode_endpoint_token.test", "token"), + ), + }, + { + // The endpoint is created carrying one token and is not read + // again during the apply that adds the second, so the count on + // the endpoint only settles on the next refresh. + Config: config, + Check: resource.TestCheckResourceAttr("quicknode_endpoint.test", "tokens.#", "2"), + }, + { + ResourceName: "quicknode_endpoint_token.test", + ImportState: true, + ImportStateIdFunc: endpointIDForImport(func(state *terraform.State) (string, error) { + return state.RootModule().Resources["quicknode_endpoint_token.test"].Primary.ID, nil + }), + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccEndpointJWT_importByName(t *testing.T) { + config := fmt.Sprintf(` +resource "quicknode_endpoint" "test" { + chain = %q + network = %q + label = "tfacc-jwt" + + security_options = { + jwts = true + } +} + +resource "quicknode_endpoint_jwt" "test" { + endpoint_id = quicknode_endpoint.test.id + name = "tfacc-signer" + kid = "tfacc-kid" + public_key = <<-EOT +%s +EOT +} +`, acceptanceChain, acceptanceNetwork, jwtAcceptancePublicKey) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: protoV6ProviderFactories, + CheckDestroy: testAccCheckEndpointsDestroyed, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("quicknode_endpoint_jwt.test", "id"), + resource.TestCheckResourceAttr("quicknode_endpoint_jwt.test", "kid", "tfacc-kid"), + resource.TestCheckResourceAttr("quicknode_endpoint_jwt.test", "name", "tfacc-signer"), + ), + }, + { + ResourceName: "quicknode_endpoint_jwt.test", + ImportState: true, + ImportStateIdFunc: endpointIDForImport(func(*terraform.State) (string, error) { + return "tfacc-signer", nil + }), + ImportStateVerify: true, + }, + }, + }) +} + +// TestAccSecurityEntries_importByValue covers the two allowlists the IP test +// does not, on one endpoint, since each kind shares an implementation and +// differs only in its routes. +func TestAccSecurityEntries_importByValue(t *testing.T) { + config := fmt.Sprintf(` +resource "quicknode_endpoint" "test" { + chain = %q + network = %q + label = "tfacc-entries" + + security_options = { + domain_masks = true + referrers = true + } +} + +resource "quicknode_endpoint_domain_mask" "test" { + endpoint_id = quicknode_endpoint.test.id + domain = "tfacc.example.com" +} + +resource "quicknode_endpoint_referrer" "test" { + endpoint_id = quicknode_endpoint.test.id + referrer = "https://tfacc.example.com" +} +`, acceptanceChain, acceptanceNetwork) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: protoV6ProviderFactories, + CheckDestroy: testAccCheckEndpointsDestroyed, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("quicknode_endpoint_domain_mask.test", "domain", "tfacc.example.com"), + resource.TestCheckResourceAttr("quicknode_endpoint_referrer.test", "referrer", "https://tfacc.example.com"), + resource.TestCheckResourceAttr("quicknode_endpoint.test", "security_options.domain_masks", "true"), + resource.TestCheckResourceAttr("quicknode_endpoint.test", "security_options.referrers", "true"), + ), + }, + { + ResourceName: "quicknode_endpoint_domain_mask.test", + ImportState: true, + ImportStateIdFunc: endpointIDForImport(func(*terraform.State) (string, error) { + return "tfacc.example.com", nil + }), + ImportStateVerify: true, + }, + { + ResourceName: "quicknode_endpoint_referrer.test", + ImportState: true, + ImportStateIdFunc: endpointIDForImport(func(*terraform.State) (string, error) { + return "https://tfacc.example.com", nil + }), + ImportStateVerify: true, + }, + }, + }) +} + +// TestAccSecurityEntry_survivesDisabledToggle covers the read hazard behind the +// order the documentation recommends. The security route omits a list entirely +// while its toggle is disabled, so an entry added before the toggle is turned +// on has to stay in state and leave the plan empty. +func TestAccSecurityEntry_survivesDisabledToggle(t *testing.T) { + config := fmt.Sprintf(` +resource "quicknode_endpoint" "test" { + chain = %q + network = %q + label = "tfacc-disabled-toggle" + + security_options = { + ips = false + } +} + +resource "quicknode_endpoint_ip" "test" { + endpoint_id = quicknode_endpoint.test.id + ip = "203.0.113.9" +} +`, acceptanceChain, acceptanceNetwork) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: protoV6ProviderFactories, + CheckDestroy: testAccCheckEndpointsDestroyed, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("quicknode_endpoint_ip.test", "ip", "203.0.113.9"), + resource.TestCheckResourceAttr("quicknode_endpoint.test", "security_options.ips", "false"), + ), + }, + }, + }) +} From e319cc4f697274c7614ec79ad44f3911d59b29af Mon Sep 17 00:00:00 2001 From: AlejandroFabianCampos Date: Wed, 23 Sep 2026 10:10:51 -0500 Subject: [PATCH 6/6] fix: tighten the provider's prose and trim the repeated phrasing --- CHANGELOG.md | 28 +++++++++---------- README.md | 4 +-- docs/data-sources/chains.md | 2 +- docs/data-sources/endpoint.md | 4 +-- docs/data-sources/endpoints.md | 4 +-- docs/index.md | 2 +- docs/resources/endpoint.md | 12 ++++---- docs/resources/endpoint_ip.md | 2 +- docs/resources/endpoint_rate_limits.md | 6 ++-- docs/resources/endpoint_referrer.md | 4 +-- docs/resources/endpoint_token.md | 4 +-- .../quicknode_chains/data-source.tf | 2 +- .../quicknode_endpoint_ip/resource.tf | 2 +- .../quicknode_endpoint_token/import.sh | 4 +-- internal/client/client.go | 10 +++---- internal/client/client_test.go | 6 ++-- internal/client/list.go | 2 +- internal/client/ratelimits.go | 8 +++--- internal/client/ratelimits_test.go | 5 ++-- internal/client/security.go | 4 +-- internal/client/security_test.go | 2 +- internal/client/transport.go | 2 +- internal/provider/endpoint_data_source.go | 2 +- internal/provider/endpoint_resource.go | 18 ++++++------ internal/provider/endpoints_data_source.go | 2 +- internal/provider/rate_limits_resource.go | 14 +++++----- internal/provider/security_options.go | 6 ++-- internal/provider/slug_validation.go | 6 ++-- internal/provider/slug_validation_test.go | 4 +-- templates/index.md.tmpl | 2 +- 30 files changed, 85 insertions(+), 88 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e97dd0..9528f32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,18 +19,16 @@ FEATURES: NOTES: -* Endpoint URLs with the credential removed are named `safe_http_url` and - `safe_wss_url`, and carry the literal `TOKEN` where the credential belongs - rather than having it cut out. The real URL's shape survives, including any - path suffix the chain appends after the token, so - `replace(..., "TOKEN", token)` reproduces a working address on every chain. - -* `quicknode_endpoint` gains `security_options`, which decides what the endpoint - enforces, and `ip_custom_header` for endpoints behind a proxy. A toggle left - out of the configuration keeps whatever value the endpoint already has. -* Allowlist entries are imported by value rather than by the id the API - assigned, so `terraform import quicknode_endpoint_ip.office 652052/203.0.113.7` - needs nothing looked up first. -* Adding an allowlist entry while its toggle is disabled warns rather than - fails. Building an allowlist before enabling enforcement is the safe order for - an endpoint already serving traffic. +* `safe_http_url` and `safe_wss_url` carry the literal `TOKEN` where the credential + belongs, so `replace(url, "TOKEN", token)` rebuilds a working address on every chain. +* `quicknode_endpoint.security_options` decides what the endpoint enforces. A toggle + left out of the configuration keeps the value the endpoint already has. +* `quicknode_endpoint.ip_custom_header` names the header an endpoint behind a proxy + reads the caller's IP address from. +* Allowlist entries are imported by value, as `652052/203.0.113.7`. +* Adding an allowlist entry while its toggle is disabled warns and succeeds, so an + allowlist can be built before enforcement is turned on. +* `quicknode_endpoint.label` cannot be cleared once set, so removing the attribute + leaves the endpoint's label in place. +* `quicknode_endpoint_jwt` takes `kid` as an input. The Admin API requires it when + the signing key is registered. diff --git a/README.md b/README.md index 58c3aa4..d1f6cad 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,8 @@ resource "quicknode_endpoint" "payments" { ``` Authentication uses a Quicknode [API key](https://www.quicknode.com/docs/admin-api), -available on paid plans. Set `QUICKNODE_API_KEY` in the environment rather than -writing it into a configuration file. +available on paid plans. Set `QUICKNODE_API_KEY` in the environment; do not +write it into a configuration file. The provider covers endpoints, the security mechanisms they enforce and who is allowed past them, RPC method filtering, and rate limits both endpoint-wide and diff --git a/docs/data-sources/chains.md b/docs/data-sources/chains.md index 13a9524..8065bb5 100644 --- a/docs/data-sources/chains.md +++ b/docs/data-sources/chains.md @@ -19,7 +19,7 @@ Chain slugs are abbreviations that often differ from the chain's name: Ethereum data "quicknode_chains" "all" {} # Chain slugs are abbreviations that often differ from the chain's name, so look -# one up rather than hardcoding it. +# one up instead of hardcoding it. locals { ethereum = one([for chain in data.quicknode_chains.all.chains : chain if chain.slug == "eth"]) } diff --git a/docs/data-sources/endpoint.md b/docs/data-sources/endpoint.md index b44cf3c..6e9e865 100644 --- a/docs/data-sources/endpoint.md +++ b/docs/data-sources/endpoint.md @@ -4,14 +4,14 @@ page_title: "quicknode_endpoint Data Source - quicknode" subcategory: "" description: |- One endpoint that already exists on the account, looked up by id or by label. Use it to wire a Terraform configuration into an endpoint created elsewhere without importing it. - Labels are not unique, so a label matching more than one endpoint is an error rather than an arbitrary pick. + Labels are not unique, so a label that matches more than one endpoint is an error. --- # quicknode_endpoint (Data Source) One endpoint that already exists on the account, looked up by `id` or by `label`. Use it to wire a Terraform configuration into an endpoint created elsewhere without importing it. -Labels are not unique, so a label matching more than one endpoint is an error rather than an arbitrary pick. +Labels are not unique, so a label that matches more than one endpoint is an error. ## Example Usage diff --git a/docs/data-sources/endpoints.md b/docs/data-sources/endpoints.md index 57d3828..b399188 100644 --- a/docs/data-sources/endpoints.md +++ b/docs/data-sources/endpoints.md @@ -4,14 +4,14 @@ page_title: "quicknode_endpoints Data Source - quicknode" subcategory: "" description: |- Endpoints on the account, optionally filtered. Several values in one filter match any of them, and several filters must all match. - Rows carry what the list route returns, which is less than data.quicknode_endpoint: no tokens, no security settings and no rate limits. Every page is walked, so the result is the whole account rather than the first screen. + Rows carry what the list route returns, which is less than data.quicknode_endpoint: no tokens, no security settings and no rate limits. Every page is walked, so the result covers the whole account. --- # quicknode_endpoints (Data Source) Endpoints on the account, optionally filtered. Several values in one filter match any of them, and several filters must all match. -Rows carry what the list route returns, which is less than `data.quicknode_endpoint`: no tokens, no security settings and no rate limits. Every page is walked, so the result is the whole account rather than the first screen. +Rows carry what the list route returns, which is less than `data.quicknode_endpoint`: no tokens, no security settings and no rate limits. Every page is walked, so the result covers the whole account. ## Example Usage diff --git a/docs/index.md b/docs/index.md index ba9e7f8..9f536f7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,7 +12,7 @@ rules, and rate limits, through `terraform plan` and `terraform apply`. ## Authentication The provider authenticates with a Quicknode [API key](https://www.quicknode.com/docs/admin-api), -available on paid plans. Set it in the environment rather than in a +available on paid plans. Set it in the environment, not in a configuration file: ```sh diff --git a/docs/resources/endpoint.md b/docs/resources/endpoint.md index 7834f02..e96df81 100644 --- a/docs/resources/endpoint.md +++ b/docs/resources/endpoint.md @@ -4,14 +4,14 @@ page_title: "quicknode_endpoint Resource - quicknode" subcategory: "" description: |- A Quicknode RPC endpoint on a chain and network. - Pass http_url_with_token to anything that needs to make RPC calls. safe_http_url and safe_wss_url carry the literal TOKEN where the credential belongs, so they are safe to log or display while keeping the real URL's shape, including any path suffix the chain appends. Substitute a token into one rather than assembling a URL from parts. + Pass http_url_with_token to anything that needs to make RPC calls. safe_http_url and safe_wss_url carry the literal TOKEN where the credential belongs, so they are safe to log or display while keeping the real URL's shape, including any path suffix the chain appends. --- # quicknode_endpoint (Resource) A Quicknode RPC endpoint on a chain and network. -Pass `http_url_with_token` to anything that needs to make RPC calls. `safe_http_url` and `safe_wss_url` carry the literal `TOKEN` where the credential belongs, so they are safe to log or display while keeping the real URL's shape, including any path suffix the chain appends. Substitute a token into one rather than assembling a URL from parts. +Pass `http_url_with_token` to anything that needs to make RPC calls. `safe_http_url` and `safe_wss_url` carry the literal `TOKEN` where the credential belongs, so they are safe to log or display while keeping the real URL's shape, including any path suffix the chain appends. ## Example Usage @@ -56,13 +56,13 @@ output "payments_rpc_url_redacted" { ### Required -- `chain` (String) Chain slug, for example `eth`, `base`, `arb`, `sol`. Slugs are often abbreviations rather than the chain's name; read `data.quicknode_chains` for the full list. +- `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. - `network` (String) Network slug, for example `mainnet`, `base-sepolia`, `arbitrum-mainnet`. ### 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 match the original caller rather than the proxy. -- `label` (String) Descriptive label for the endpoint. Labels are not unique and are not used to identify the endpoint. +- `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. Quicknode has no route for clearing a label once set, so removing the attribute leaves the current label in place and Terraform stops tracking it. - `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`. @@ -93,7 +93,7 @@ Optional: Read-Only: - `request_filters` (Boolean) Whether RPC method filtering is applied. Read-only: the Admin API turns this on when a `quicknode_endpoint_request_filter` exists and off when the last one is removed. -- `response_logging` (Boolean) Whether responses are logged for the endpoint. Read-only: it is set by the account's plan rather than per endpoint. +- `response_logging` (Boolean) Whether responses are logged for the endpoint. Read-only: the account's plan sets it and it cannot be changed per endpoint. diff --git a/docs/resources/endpoint_ip.md b/docs/resources/endpoint_ip.md index 06a2db2..65e976e 100644 --- a/docs/resources/endpoint_ip.md +++ b/docs/resources/endpoint_ip.md @@ -32,7 +32,7 @@ resource "quicknode_endpoint_ip" "office" { # Entries may be added before the toggle is enabled, which is the safe order for # an endpoint already serving traffic. Adding one while security_options.ips is -# false produces a warning rather than an error. +# false produces a warning and not an error. resource "quicknode_endpoint_ip" "vpn" { endpoint_id = quicknode_endpoint.api.id ip = "198.51.100.0/24" diff --git a/docs/resources/endpoint_rate_limits.md b/docs/resources/endpoint_rate_limits.md index 4bcc436..f036413 100644 --- a/docs/resources/endpoint_rate_limits.md +++ b/docs/resources/endpoint_rate_limits.md @@ -4,14 +4,14 @@ page_title: "quicknode_endpoint_rate_limits Resource - quicknode" subcategory: "" description: |- Endpoint-wide request rate limits, one resource per endpoint. - Each bucket the Quicknode plan sets is reported under plan_default. A bucket set here overrides the plan default; a bucket left out keeps the plan default, and removing one that was set returns that bucket to the plan default rather than leaving the override in place. + Each bucket the Quicknode plan sets is reported under plan_default. A bucket set here overrides the plan default; a bucket left out keeps the plan default, and removing one that was set clears the override, returning that bucket to the plan default. --- # quicknode_endpoint_rate_limits (Resource) Endpoint-wide request rate limits, one resource per endpoint. -Each bucket the Quicknode plan sets is reported under `plan_default`. A bucket set here overrides the plan default; a bucket left out keeps the plan default, and removing one that was set returns that bucket to the plan default rather than leaving the override in place. +Each bucket the Quicknode plan sets is reported under `plan_default`. A bucket set here overrides the plan default; a bucket left out keeps the plan default, and removing one that was set clears the override, returning that bucket to the plan default. ## Example Usage @@ -50,7 +50,7 @@ output "plan_allows_per_second" { ### Read-Only -- `id` (String) Same as `endpoint_id`. Rate limits are a property of the endpoint rather than a separate object. +- `id` (String) Same as `endpoint_id`. Rate limits are a property of the endpoint, not a separate object. - `plan_default` (Attributes) What the account's Quicknode plan allows, before any override set here. A bucket the plan does not limit is reported as `-1`. (see [below for nested schema](#nestedatt--plan_default)) diff --git a/docs/resources/endpoint_referrer.md b/docs/resources/endpoint_referrer.md index 1341e57..d61e4b4 100644 --- a/docs/resources/endpoint_referrer.md +++ b/docs/resources/endpoint_referrer.md @@ -3,13 +3,13 @@ page_title: "quicknode_endpoint_referrer Resource - quicknode" subcategory: "" description: |- - A referrer allowed to call a Quicknode endpoint. Referrer checks suit browser traffic, where the header is set by the browser rather than by the caller. + A referrer allowed to call a Quicknode endpoint. Referrer checks suit browser traffic, where the browser sets the header and the caller cannot. The entry only takes effect once security_options.referrers is enabled on the endpoint. Entries can be added before the toggle is turned on, which is the safe order for an endpoint already serving traffic. --- # quicknode_endpoint_referrer (Resource) -A referrer allowed to call a Quicknode endpoint. Referrer checks suit browser traffic, where the header is set by the browser rather than by the caller. +A referrer allowed to call a Quicknode endpoint. Referrer checks suit browser traffic, where the browser sets the header and the caller cannot. The entry only takes effect once `security_options.referrers` is enabled on the endpoint. Entries can be added before the toggle is turned on, which is the safe order for an endpoint already serving traffic. diff --git a/docs/resources/endpoint_token.md b/docs/resources/endpoint_token.md index b5622de..1cbad37 100644 --- a/docs/resources/endpoint_token.md +++ b/docs/resources/endpoint_token.md @@ -56,7 +56,7 @@ Import is supported using the following syntax: The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: ```shell -# Tokens are imported by id rather than by value, as "/", -# so the credential stays out of shell history. +# Tokens are imported by id, as "/", which keeps the +# credential out of shell history. terraform import quicknode_endpoint_token.indexer 652052/d3312bd2-c1a2-4d89-865f-11c99fa3863a ``` diff --git a/examples/data-sources/quicknode_chains/data-source.tf b/examples/data-sources/quicknode_chains/data-source.tf index 98d5283..d4fbac4 100644 --- a/examples/data-sources/quicknode_chains/data-source.tf +++ b/examples/data-sources/quicknode_chains/data-source.tf @@ -1,7 +1,7 @@ data "quicknode_chains" "all" {} # Chain slugs are abbreviations that often differ from the chain's name, so look -# one up rather than hardcoding it. +# one up instead of hardcoding it. locals { ethereum = one([for chain in data.quicknode_chains.all.chains : chain if chain.slug == "eth"]) } diff --git a/examples/resources/quicknode_endpoint_ip/resource.tf b/examples/resources/quicknode_endpoint_ip/resource.tf index e31e4af..fc0eb5e 100644 --- a/examples/resources/quicknode_endpoint_ip/resource.tf +++ b/examples/resources/quicknode_endpoint_ip/resource.tf @@ -14,7 +14,7 @@ resource "quicknode_endpoint_ip" "office" { # Entries may be added before the toggle is enabled, which is the safe order for # an endpoint already serving traffic. Adding one while security_options.ips is -# false produces a warning rather than an error. +# false produces a warning and not an error. resource "quicknode_endpoint_ip" "vpn" { endpoint_id = quicknode_endpoint.api.id ip = "198.51.100.0/24" diff --git a/examples/resources/quicknode_endpoint_token/import.sh b/examples/resources/quicknode_endpoint_token/import.sh index 3ca2f41..e7caa3a 100644 --- a/examples/resources/quicknode_endpoint_token/import.sh +++ b/examples/resources/quicknode_endpoint_token/import.sh @@ -1,3 +1,3 @@ -# Tokens are imported by id rather than by value, as "/", -# so the credential stays out of shell history. +# Tokens are imported by id, as "/", which keeps the +# credential out of shell history. terraform import quicknode_endpoint_token.indexer 652052/d3312bd2-c1a2-4d89-865f-11c99fa3863a diff --git a/internal/client/client.go b/internal/client/client.go index ef56856..a9da6f1 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -17,7 +17,7 @@ const DefaultBaseURL = "https://api.quicknode.com" // URLTokenPlaceholder stands in for the auth token in SafeHTTPURL and // SafeWSSURL. It keeps the shape of the real URL, including any path suffix the // chain appends, so the token's position stays visible and a caller can -// substitute one rather than guess where it goes. +// substitute one without guessing where it goes. const URLTokenPlaceholder = "TOKEN" type Client struct { @@ -51,7 +51,7 @@ func WithMaxRetries(maxRetries int) Option { // WithRequestsPerSecond throttles outbound calls. A terraform apply over a large // workspace bursts many Admin API calls at once, so the provider paces itself -// rather than relying on the API to reject the excess. +// and does not lean on the API to reject the excess. func WithRequestsPerSecond(requestsPerSecond int) Option { return func(o *options) { if requestsPerSecond > 0 { @@ -127,7 +127,7 @@ type Endpoint struct { } // EndpointToken is one of an endpoint's auth tokens. An endpoint can carry -// several, which is why this is a list rather than a single value. +// several, so this is a list. type EndpointToken struct { ID string Value string @@ -397,8 +397,8 @@ func (e *Endpoint) setURLs(httpURL, wssURL string) { // RedactEndpointURL replaces the credential in an endpoint URL with // URLTokenPlaceholder. The Admin API returns URLs shaped // https://.quiknode.pro/[/], and the suffix differs by -// chain, so the placeholder is substituted in position rather than the token -// being cut out. The result keeps the real URL's shape and is safe to log. +// chain, so the placeholder goes in the token's position and nothing is cut +// out. The result keeps the real URL's shape and is safe to log. func RedactEndpointURL(raw string) string { if raw == "" { return "" diff --git a/internal/client/client_test.go b/internal/client/client_test.go index d8f6df0..a6f762c 100644 --- a/internal/client/client_test.go +++ b/internal/client/client_test.go @@ -52,9 +52,9 @@ func TestRedactEndpointURL(t *testing.T) { } } -// TestRedactedURLKeepsItsShape is the point of the placeholder: substituting a -// token has to reproduce the original URL exactly, including a suffix the -// chain appends after the token. +// TestRedactedURLKeepsItsShape checks what the placeholder exists for: +// substituting a token has to reproduce the original URL exactly, including a +// suffix the chain appends after the token. func TestRedactedURLKeepsItsShape(t *testing.T) { for _, raw := range []string{ "https://polished-damp-grass.hype-testnet.quiknode.pro/abc123/evm", diff --git a/internal/client/list.go b/internal/client/list.go index 759ccb1..c95ad46 100644 --- a/internal/client/list.go +++ b/internal/client/list.go @@ -117,7 +117,7 @@ func (c *Client) ListEndpoints(ctx context.Context, filter EndpointFilter) ([]En } // FindEndpointByLabel resolves a label to a single endpoint. Labels are not -// unique, so more than one match is an error rather than an arbitrary pick. +// unique, so more than one match is an error. func (c *Client) FindEndpointByLabel(ctx context.Context, label string) (*EndpointSummary, error) { const operation = "find endpoint by label" diff --git a/internal/client/ratelimits.go b/internal/client/ratelimits.go index 3c00d67..2c93e02 100644 --- a/internal/client/ratelimits.go +++ b/internal/client/ratelimits.go @@ -85,8 +85,8 @@ func (c *Client) GetRateLimits(ctx context.Context, endpointID string) ([]RateLi return limits, nil } -// SetRateLimits writes the buckets the caller manages. Managing none is a no-op -// rather than an empty write. +// SetRateLimits writes the buckets the caller manages. Managing none sends no +// request at all. func (c *Client) SetRateLimits(ctx context.Context, endpointID string, overrides RateLimitOverrides) error { const operation = "update endpoint rate limits" @@ -211,8 +211,8 @@ func (c *Client) AddMethodRateLimit(ctx context.Context, endpointID string, limi } // UpdateMethodRateLimit edits a limiter in place. The route takes the whole -// object rather than a delta, and it does not accept interval, so a changed -// interval has to replace the limiter. +// object, not a delta, and it does not accept interval, so a changed interval +// has to replace the limiter. func (c *Client) UpdateMethodRateLimit(ctx context.Context, endpointID, limiterID string, limiter MethodRateLimit) error { const operation = "update endpoint method rate limit" diff --git a/internal/client/ratelimits_test.go b/internal/client/ratelimits_test.go index df78ee0..2b72c63 100644 --- a/internal/client/ratelimits_test.go +++ b/internal/client/ratelimits_test.go @@ -51,7 +51,7 @@ func TestGetRateLimitsKeepsSourceAndID(t *testing.T) { // TestSetRateLimitsOmitsUnmanagedBuckets guards the same hazard the security // toggles have: a bucket the configuration does not set must stay out of the -// body rather than being sent as a zero. +// body, since sending a zero would throttle it to nothing. func TestSetRateLimitsOmitsUnmanagedBuckets(t *testing.T) { var captured map[string]any @@ -136,8 +136,7 @@ func TestListMethodRateLimits(t *testing.T) { } // TestUpdateMethodRateLimitSendsWholeObject records that the update route -// replaces the limiter rather than merging a delta, and that it takes no -// interval. +// replaces the limiter whole, merges no delta, and takes no interval. func TestUpdateMethodRateLimitSendsWholeObject(t *testing.T) { var captured map[string]any diff --git a/internal/client/security.go b/internal/client/security.go index a06ecfa..f1c508e 100644 --- a/internal/client/security.go +++ b/internal/client/security.go @@ -155,8 +155,8 @@ func (c *Client) GetEndpointSecurity(ctx context.Context, endpointID string) (*E // SetSecurityOptions writes the settable toggles. The read path reports them as // booleans and the write path takes the strings "enabled" and "disabled", so -// the conversion happens here rather than in every caller. A patch that manages -// nothing is a no-op rather than an empty write. +// the conversion happens here once, for every caller. A patch that manages +// nothing sends no request at all. func (c *Client) SetSecurityOptions(ctx context.Context, endpointID string, patch SecurityOptionsPatch) error { const operation = "update endpoint security options" diff --git a/internal/client/security_test.go b/internal/client/security_test.go index 4fc0f35..7be6c67 100644 --- a/internal/client/security_test.go +++ b/internal/client/security_test.go @@ -70,7 +70,7 @@ func TestGetEndpointSecurity(t *testing.T) { // TestSetSecurityOptionsSendsStrings guards the asymmetry between the two // halves of the API: reads report the toggles as booleans and the write takes // the strings "enabled" and "disabled". A toggle the configuration does not -// manage has to stay out of the body entirely rather than being sent as false. +// manage has to stay out of the body entirely, since false would turn it off. func TestSetSecurityOptionsSendsStrings(t *testing.T) { var captured map[string]any diff --git a/internal/client/transport.go b/internal/client/transport.go index e044813..09ee0a3 100644 --- a/internal/client/transport.go +++ b/internal/client/transport.go @@ -20,7 +20,7 @@ const ( // retryTransport retries throttled and transient responses. A terraform apply // over a large workspace bursts many Admin API calls at once, so the provider -// backs off rather than surfacing a 429 as a resource failure. +// backs off instead of surfacing a 429 as a resource failure. type retryTransport struct { base http.RoundTripper apiKey string diff --git a/internal/provider/endpoint_data_source.go b/internal/provider/endpoint_data_source.go index b94166d..a20c8f1 100644 --- a/internal/provider/endpoint_data_source.go +++ b/internal/provider/endpoint_data_source.go @@ -63,7 +63,7 @@ func (d *endpointDataSource) Schema(_ context.Context, _ datasource.SchemaReques } resp.Schema = schema.Schema{ MarkdownDescription: "One endpoint that already exists on the account, looked up by `id` or by `label`. Use it to wire a Terraform configuration into an endpoint created elsewhere without importing it.\n\n" + - "Labels are not unique, so a label matching more than one endpoint is an error rather than an arbitrary pick.", + "Labels are not unique, so a label that matches more than one endpoint is an error.", Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ Optional: true, diff --git a/internal/provider/endpoint_resource.go b/internal/provider/endpoint_resource.go index 15bc861..b8878f5 100644 --- a/internal/provider/endpoint_resource.go +++ b/internal/provider/endpoint_resource.go @@ -71,7 +71,7 @@ func (r *endpointResource) Schema(_ context.Context, _ resource.SchemaRequest, r MarkdownDescription: "A Quicknode RPC endpoint on a chain and network.\n\n" + "Pass `http_url_with_token` to anything that needs to make RPC calls. " + "`safe_http_url` and `safe_wss_url` carry the literal `TOKEN` where the credential belongs, so they are safe to log or display " + - "while keeping the real URL's shape, including any path suffix the chain appends. Substitute a token into one rather than assembling a URL from parts.", + "while keeping the real URL's shape, including any path suffix the chain appends.", Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ Computed: true, @@ -80,7 +80,7 @@ func (r *endpointResource) Schema(_ context.Context, _ resource.SchemaRequest, r }, "chain": schema.StringAttribute{ Required: true, - MarkdownDescription: "Chain slug, for example `eth`, `base`, `arb`, `sol`. Slugs are often abbreviations rather than the chain's name; read `data.quicknode_chains` for the full list.", + MarkdownDescription: "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.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, }, "network": schema.StringAttribute{ @@ -138,7 +138,7 @@ func (r *endpointResource) Schema(_ context.Context, _ resource.SchemaRequest, r "security_options": securityOptionsSchema(), "ip_custom_header": schema.StringAttribute{ Optional: true, - MarkdownDescription: "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 match the original caller rather than the proxy.", + MarkdownDescription: "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.", Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, }, "tokens": schema.ListNestedAttribute{ @@ -176,8 +176,8 @@ func (r *endpointResource) Configure(_ context.Context, req resource.ConfigureRe r.chains = data.Chains } -// ModifyPlan rejects an unknown chain or network before anything is created, -// rather than letting the Admin API reject it partway through an apply. +// ModifyPlan rejects an unknown chain or network at plan time, before an apply +// is already partway through creating something. func (r *endpointResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { if req.Plan.Raw.IsNull() { return @@ -384,8 +384,8 @@ func (r *endpointResource) readInto(ctx context.Context, id string, model *endpo } // applySecurity writes the settable toggles and the custom IP header. The -// previous header is needed because clearing the attribute has to become a -// delete rather than an empty write. +// previous header is needed because clearing the attribute becomes a delete +// call, not an empty write. func (r *endpointResource) applySecurity(ctx context.Context, id string, options types.Object, header, previousHeader types.String) diag.Diagnostics { patch, diags := securityOptionsPatch(ctx, options) if diags.HasError() { @@ -447,8 +447,8 @@ func applyEndpoint(endpoint *client.Endpoint, state *endpointResourceModel) diag return diags } -// applyEndpointURLs maps empty URLs to null so that a chain without WebSocket -// support reports safe_wss_url as absent rather than as an empty string. +// applyEndpointURLs maps empty URLs to null, so a chain without WebSocket +// support reports safe_wss_url as absent. func applyEndpointURLs(endpoint *client.Endpoint, model *endpointResourceModel) { model.SafeHTTPURL = stringOrNull(endpoint.SafeHTTPURL) model.SafeWSSURL = stringOrNull(endpoint.SafeWSSURL) diff --git a/internal/provider/endpoints_data_source.go b/internal/provider/endpoints_data_source.go index 50c9197..1545b22 100644 --- a/internal/provider/endpoints_data_source.go +++ b/internal/provider/endpoints_data_source.go @@ -62,7 +62,7 @@ func (d *endpointsDataSource) Schema(_ context.Context, _ datasource.SchemaReque resp.Schema = schema.Schema{ MarkdownDescription: "Endpoints on the account, optionally filtered. Several values in one filter match any of them, and several filters must all match.\n\n" + "Rows carry what the list route returns, which is less than `data.quicknode_endpoint`: no tokens, no security settings and no rate limits. " + - "Every page is walked, so the result is the whole account rather than the first screen.", + "Every page is walked, so the result covers the whole account.", Attributes: map[string]schema.Attribute{ "search": schema.StringAttribute{ Optional: true, diff --git a/internal/provider/rate_limits_resource.go b/internal/provider/rate_limits_resource.go index bd56d18..1282dcb 100644 --- a/internal/provider/rate_limits_resource.go +++ b/internal/provider/rate_limits_resource.go @@ -60,11 +60,11 @@ func (r *rateLimitsResource) Schema(_ context.Context, _ resource.SchemaRequest, resp.Schema = schema.Schema{ MarkdownDescription: "Endpoint-wide request rate limits, one resource per endpoint.\n\n" + "Each bucket the Quicknode plan sets is reported under `plan_default`. A bucket set here overrides the plan default; " + - "a bucket left out keeps the plan default, and removing one that was set returns that bucket to the plan default rather than leaving the override in place.", + "a bucket left out keeps the plan default, and removing one that was set clears the override, returning that bucket to the plan default.", Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ Computed: true, - MarkdownDescription: "Same as `endpoint_id`. Rate limits are a property of the endpoint rather than a separate object.", + MarkdownDescription: "Same as `endpoint_id`. Rate limits are a property of the endpoint, not a separate object.", PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, }, "endpoint_id": schema.StringAttribute{ @@ -164,9 +164,9 @@ func (r *rateLimitsResource) Update(ctx context.Context, req resource.UpdateRequ endpointID := state.EndpointID.ValueString() plan.ID = state.ID - // A bucket dropped from the configuration has to lose its override, which - // is a delete rather than a write: the patch route has no way to say - // "return this bucket to the plan default". + // A bucket dropped from the configuration has to lose its override. The + // patch route cannot express "return this bucket to the plan default", so + // it takes a delete. dropped := map[string]bool{ client.BucketRPS: plan.RPS.IsNull() && !state.RPS.IsNull(), client.BucketRPM: plan.RPM.IsNull() && !state.RPM.IsNull(), @@ -193,8 +193,8 @@ func (r *rateLimitsResource) Update(ctx context.Context, req resource.UpdateRequ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) } -// Delete returns every bucket to the plan default. Nothing is torn down, -// because the limits belong to the endpoint rather than to a separate object. +// Delete returns every bucket to the plan default. The limits belong to the +// endpoint, so nothing is torn down. func (r *rateLimitsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { var state rateLimitsResourceModel resp.Diagnostics.Append(req.State.Get(ctx, &state)...) diff --git a/internal/provider/security_options.go b/internal/provider/security_options.go index 34fc8ee..c009d9f 100644 --- a/internal/provider/security_options.go +++ b/internal/provider/security_options.go @@ -66,7 +66,7 @@ func securityOptionsSchema() schema.SingleNestedAttribute { }, "response_logging": schema.BoolAttribute{ Computed: true, - MarkdownDescription: "Whether responses are logged for the endpoint. Read-only: it is set by the account's plan rather than per endpoint.", + MarkdownDescription: "Whether responses are logged for the endpoint. Read-only: the account's plan sets it and it cannot be changed per endpoint.", }, }, } @@ -148,8 +148,8 @@ func securityToggleEnabled(options client.SecurityOptions, name string) bool { // warnToggleDisabled reports an entry the endpoint is not enforcing. The API // accepts it either way, and building an allowlist before enabling the toggle -// is the safe order for an endpoint already serving traffic, so this is a -// warning rather than an error. +// is the safe order for an endpoint already serving traffic, so this warns +// and does not fail. func warnToggleDisabled(ctx context.Context, quicknode *client.Client, endpointID, toggle, subject string) diag.Diagnostics { var diags diag.Diagnostics diff --git a/internal/provider/slug_validation.go b/internal/provider/slug_validation.go index 952d176..c4356f6 100644 --- a/internal/provider/slug_validation.go +++ b/internal/provider/slug_validation.go @@ -16,7 +16,7 @@ type slugProblem struct { Detail string } -// validateChainNetwork matches slugs exactly rather than case-insensitively. +// validateChainNetwork matches slugs exactly, casing included. // The Admin API echoes back its own casing, and chain and network force // replacement, so accepting "ETH" for "eth" would let Read overwrite the // configured value on the next refresh and leave every subsequent plan @@ -42,7 +42,7 @@ func validateChainNetwork(chains []client.Chain, wantChain, wantNetwork string) return &slugProblem{ Attribute: "network", Summary: "Network slug differs in case", - Detail: fmt.Sprintf("Use %q rather than %q. Quicknode reports its own casing back, and network forces replacement, so a mismatch would make every plan propose a replacement.", network.Slug, wantNetwork), + Detail: fmt.Sprintf("Use %q, not %q. Quicknode reports its own casing back, and network forces replacement, so a mismatch would make every plan propose a replacement.", network.Slug, wantNetwork), } } } @@ -60,7 +60,7 @@ func validateChainNetwork(chains []client.Chain, wantChain, wantNetwork string) return &slugProblem{ Attribute: "chain", Summary: "Chain slug differs in case", - Detail: fmt.Sprintf("Use %q rather than %q. Quicknode reports its own casing back, and chain forces replacement, so a mismatch would make every plan propose a replacement.", chain.Slug, wantChain), + Detail: fmt.Sprintf("Use %q, not %q. Quicknode reports its own casing back, and chain forces replacement, so a mismatch would make every plan propose a replacement.", chain.Slug, wantChain), } } chainSlugs = append(chainSlugs, chain.Slug) diff --git a/internal/provider/slug_validation_test.go b/internal/provider/slug_validation_test.go index e0ffbed..49b6f58 100644 --- a/internal/provider/slug_validation_test.go +++ b/internal/provider/slug_validation_test.go @@ -30,11 +30,11 @@ func TestValidateChainNetwork(t *testing.T) { {name: "exact match on a qualified network", chain: "base", network: "base-sepolia"}, { name: "chain case mismatch is rejected", chain: "ETH", network: "mainnet", - wantAttr: "chain", wantDetails: `Use "eth" rather than "ETH"`, + wantAttr: "chain", wantDetails: `Use "eth", not "ETH"`, }, { name: "network case mismatch is rejected", chain: "base", network: "Base-Sepolia", - wantAttr: "network", wantDetails: `Use "base-sepolia" rather than "Base-Sepolia"`, + wantAttr: "network", wantDetails: `Use "base-sepolia", not "Base-Sepolia"`, }, { name: "chain name instead of slug", chain: "ethereum", network: "mainnet", diff --git a/templates/index.md.tmpl b/templates/index.md.tmpl index 7194dbb..5468fe3 100644 --- a/templates/index.md.tmpl +++ b/templates/index.md.tmpl @@ -12,7 +12,7 @@ rules, and rate limits, through `terraform plan` and `terraform apply`. ## Authentication The provider authenticates with a Quicknode [API key](https://www.quicknode.com/docs/admin-api), -available on paid plans. Set it in the environment rather than in a +available on paid plans. Set it in the environment, not in a configuration file: ```sh