Skip to content

Commit 69b0e63

Browse files
Add write-only headers and request bodies (Closes #115, #116)
Introduce headers_wo and request_body_wo on terracurl_request with version attributes, private-state snapshots for read/destroy, and Terraform 1.11+ support. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 8505913 commit 69b0e63

17 files changed

Lines changed: 945 additions & 134 deletions

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 2.8.0
2+
3+
ENHANCEMENTS:
4+
5+
- Add write-only headers and request bodies on `terracurl_request` resources with `_wo_version` companions and private-state snapshots for read/destroy. Requires Terraform 1.11+. Closes #115, #116.
6+
17
## 2.7.0
28

39
ENHANCEMENTS:

docs/guides/default_headers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ Provider headers win on key collision. This ensures a fresh provider token repla
8080
## Limitations
8181

8282
- Tokens set only in resource `headers` or `destroy_headers` are still persisted in state. For destroy-only refresh, move auth to `default_headers` or run `terraform apply` before destroy to update state.
83-
- Write-only resource headers (see issue #115) are a separate follow-up to avoid persisting secrets in state entirely.
83+
- Write-only resource headers (see the [Write-Only Headers and Request Bodies guide](write_only)) avoid persisting secrets in state entirely.
8484
- `default_headers` is marked sensitive in the provider schema and will not appear in plan output.
8585

8686
## Workaround without default_headers

docs/guides/digest_auth.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ If credentials are set only in `destroy_digest_auth`, those values are persisted
8787

8888
- HTTP Basic authentication is not built in; set a static `Authorization: Basic ...` header manually or use provider `default_headers`.
8989
- NTLM, Kerberos, and other enterprise proxy authentication mechanisms remain unsupported. See the [HTTP Proxy Support guide](proxy).
90-
- Digest credentials configured on resources are stored in Terraform state (marked sensitive). Write-only credentials are tracked separately in issue #115.
90+
- Digest credentials configured on resources are stored in Terraform state (marked sensitive). For credentials that must not appear in state, use write-only headers or provider `default_headers` where applicable. See the [Write-Only Headers and Request Bodies guide](write_only).
9191
- Custom `realm` or algorithm tuning is not exposed unless a real API requires it.
9292

9393
## Digest vs default_headers

docs/guides/write_only.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
page_title: "Write-Only Headers and Request Bodies"
3+
subcategory: "Guides"
4+
description: |-
5+
Use write-only headers and request bodies on terracurl_request resources to send secrets without persisting them in Terraform state.
6+
---
7+
8+
# Write-Only Headers and Request Bodies
9+
10+
Terraform 1.11+ supports **write-only arguments**: values supplied in configuration that are used during apply but are **not stored in plan or state JSON**. TerraCurl exposes this for `terracurl_request` resource headers and request bodies on create, read, and destroy operations.
11+
12+
Use write-only attributes when secrets must not appear in `terraform show`, remote state, or version control–backed state files — even when marked `sensitive`.
13+
14+
## Requirements
15+
16+
- Terraform **1.11 or later**
17+
- `terracurl_request` **resource only** (write-only is not available on data sources, actions, or ephemeral resources)
18+
19+
## Create example
20+
21+
```terraform
22+
resource "terracurl_request" "example" {
23+
name = "example"
24+
url = "https://api.example.com/resource"
25+
method = "PUT"
26+
27+
headers_wo = {
28+
X-Vault-Token = var.vault_token
29+
}
30+
headers_wo_version = 1
31+
32+
request_body_wo = jsonencode({
33+
password = var.password
34+
})
35+
request_body_wo_version = 1
36+
37+
response_codes = [200]
38+
skip_read = true
39+
}
40+
```
41+
42+
Do **not** set both `headers` and `headers_wo` (or `request_body` and `request_body_wo`) on the same operation. TerraCurl rejects conflicting pairs at plan time.
43+
44+
## Per-operation attributes
45+
46+
| Operation | Headers | Body | Version attributes |
47+
|-----------|---------|------|--------------------|
48+
| Create | `headers_wo` | `request_body_wo` | `headers_wo_version`, `request_body_wo_version` |
49+
| Read | `read_headers_wo` | `read_request_body_wo` | `read_headers_wo_version`, `read_request_body_wo_version` |
50+
| Destroy | `destroy_headers_wo` | `destroy_request_body_wo` | `destroy_headers_wo_version`, `destroy_request_body_wo_version` |
51+
52+
Version attributes are stored in state (not write-only). Increment a `_wo_version` when you change the corresponding write-only value so TerraCurl refreshes the snapshotted credentials on update.
53+
54+
## Read and destroy without config access
55+
56+
Terraform does not pass resource configuration to Read or Delete RPCs. TerraCurl **snapshots** read/destroy write-only values into provider private state during Create (and refreshes the snapshot on Update when a `_wo_version` changes). Private state is not shown in plan output, but it is persisted with the resource.
57+
58+
For rotating destroy credentials without storing them in resource state, consider [provider `default_headers`](default_headers) instead.
59+
60+
## Updating write-only values
61+
62+
TerraCurl does not re-issue the create HTTP call on in-place update. To change a write-only body or header after initial apply:
63+
64+
1. Update the write-only value in configuration
65+
2. Increment the matching `_wo_version` attribute
66+
3. Run `terraform apply`
67+
68+
If the API must be called again with the new payload, trigger replacement through other attribute changes or taint the resource.
69+
70+
## Comparison with other options
71+
72+
| Approach | In state? | In plan? | Destroy refresh |
73+
|----------|-----------|----------|-----------------|
74+
| `headers` / `request_body` | Yes (sensitive still stored) | Hidden if sensitive | From state (can go stale) |
75+
| `headers_wo` / `request_body_wo` | No | No | From private snapshot |
76+
| Provider `default_headers` | No (provider config) | No | Re-evaluated every run |
77+
78+
## Limitations
79+
80+
- Resource-only; use `default_headers` or sensitive attributes on data sources, actions, and ephemeral resources
81+
- Private state snapshot is not zero-persistence storage
82+
- Digest credentials remain stateful today; write-only digest auth is future work

docs/index.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,45 @@ resource "terracurl_request" "example" {
163163
}
164164
```
165165

166+
## Write-Only Headers and Request Bodies
167+
168+
TerraCurl supports write-only `headers_wo` and `request_body_wo` attributes on `terracurl_request` resources (Terraform 1.11+). Values are sent on HTTP calls but are not stored in Terraform state.
169+
170+
See the [Write-Only Headers and Request Bodies guide](guides/write_only) for version attributes, read/destroy private-state behavior, and comparison with `default_headers`.
171+
172+
```terraform
173+
# Write-only headers and bodies require Terraform 1.11+.
174+
#
175+
# Values are used for HTTP calls but not stored in Terraform state.
176+
# Increment *_wo_version when changing a write-only value.
177+
178+
resource "terracurl_request" "example" {
179+
name = "example"
180+
url = "https://httpbin.org/put"
181+
method = "PUT"
182+
183+
headers_wo = {
184+
Authorization = "Bearer ${var.api_token}"
185+
}
186+
headers_wo_version = 1
187+
188+
request_body_wo = jsonencode({
189+
password = var.password
190+
})
191+
request_body_wo_version = 1
192+
193+
response_codes = [200]
194+
skip_read = true
195+
196+
destroy_url = "https://httpbin.org/delete"
197+
destroy_method = "DELETE"
198+
destroy_response_codes = [200]
199+
200+
destroy_headers_wo = {
201+
Authorization = "Bearer ${var.api_token}"
202+
}
203+
destroy_headers_wo_version = 1
204+
}
205+
```
206+
166207
## Limitations

docs/resources/request.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ When `skip_read` is `false` and `read_url`, `read_method`, and `read_response_co
2525

2626
### Optional
2727

28+
> **NOTE**: [Write-only arguments](https://developer.hashicorp.com/terraform/language/resources/ephemeral#write-only-arguments) are supported in Terraform 1.11 and later.
29+
2830
- `ca_cert_directory` (String) Path to a directory on local disk that contains one or more certificate files that will be used to validate the certificate presented by the server
2931
- `ca_cert_file` (String) Path to a file on local disk that will be used to validate the certificate presented by the server
3032
- `cert_file` (String) Path to a file on local disk that contains the PEM-encoded certificate to present to the server
@@ -33,10 +35,14 @@ When `skip_read` is `false` and `read_url`, `read_method`, and `read_response_co
3335
- `destroy_cert_file` (String) Path to a file on local disk that contains the PEM-encoded certificate to present to the server for the destroy call
3436
- `destroy_digest_auth` (Attributes, Sensitive) HTTP Digest authentication credentials for the destroy request. Overrides provider `default_digest_auth` when configured. (see [below for nested schema](#nestedatt--destroy_digest_auth))
3537
- `destroy_headers` (Map of String) Map of headers to attach to the destroy API call. Host (case-insensitive) overrides the HTTP Host header sent on the wire, independent of the URL hostname.
38+
- `destroy_headers_wo` (Map of String, Sensitive, [Write-only](https://developer.hashicorp.com/terraform/language/resources/ephemeral#write-only-arguments)) Write-only headers for the destroy call. Snapshotted in provider private state for use during destroy.
39+
- `destroy_headers_wo_version` (Number) Increment to trigger refreshing snapshotted `destroy_headers_wo` values.
3640
- `destroy_key_file` (String) Path to a file on local disk that contains the PEM-encoded private key for which the authentication certificate was issued for the destroy call
3741
- `destroy_max_retry` (Number) Maximum number of tries until it is marked as failed for the destroy call
3842
- `destroy_method` (String) Destroy HTTP method to use in the API call
3943
- `destroy_request_body` (String) A request body to attach to the destroy API call
44+
- `destroy_request_body_wo` (String, Sensitive, [Write-only](https://developer.hashicorp.com/terraform/language/resources/ephemeral#write-only-arguments)) Write-only request body for the destroy call. Not stored in Terraform state.
45+
- `destroy_request_body_wo_version` (Number) Increment to trigger applying an updated `destroy_request_body_wo` value.
4046
- `destroy_request_parameters` (Map of String) Map of parameters to attach to the destroy API call
4147
- `destroy_response_codes` (List of String) A list of expected response codes for the destroy call
4248
- `destroy_retry_interval` (Number) Interval between each attempt for the destroy call
@@ -45,6 +51,8 @@ When `skip_read` is `false` and `read_url`, `read_method`, and `read_response_co
4551
- `destroy_url` (String) Destroy API endpoint to call
4652
- `digest_auth` (Attributes, Sensitive) HTTP Digest authentication credentials for the create request. Overrides provider `default_digest_auth` when configured. (see [below for nested schema](#nestedatt--digest_auth))
4753
- `headers` (Map of String) Map of headers to attach to the API call. Host (case-insensitive) overrides the HTTP Host header sent on the wire, independent of the URL hostname.
54+
- `headers_wo` (Map of String, Sensitive, [Write-only](https://developer.hashicorp.com/terraform/language/resources/ephemeral#write-only-arguments)) Write-only headers for the create call. Not stored in Terraform state. Requires Terraform 1.11 or later.
55+
- `headers_wo_version` (Number) Increment to trigger applying updated `headers_wo` values.
4856
- `ignore_response_fields` (List of String) List of JSON fields to ignore during drift detection.
4957
- `key_file` (String) Path to a file on local disk that contains the PEM-encoded private key for which the authentication certificate was issued
5058
- `max_retry` (Number) Maximum number of tries until it is marked as failed
@@ -53,14 +61,20 @@ When `skip_read` is `false` and `read_url`, `read_method`, and `read_response_co
5361
- `read_cert_file` (String) Path to a PEM-encoded certificate for the read request (TLS).
5462
- `read_digest_auth` (Attributes, Sensitive) HTTP Digest authentication credentials for the read request. Overrides provider `default_digest_auth` when configured. (see [below for nested schema](#nestedatt--read_digest_auth))
5563
- `read_headers` (Map of String) Map of headers for the read request. Host (case-insensitive) overrides the HTTP Host header sent on the wire, independent of the URL hostname.
64+
- `read_headers_wo` (Map of String, Sensitive, [Write-only](https://developer.hashicorp.com/terraform/language/resources/ephemeral#write-only-arguments)) Write-only headers for the read call. Snapshotted in provider private state for drift detection.
65+
- `read_headers_wo_version` (Number) Increment to trigger refreshing snapshotted `read_headers_wo` values.
5666
- `read_key_file` (String) Path to a PEM-encoded private key for the read request (TLS).
5767
- `read_method` (String) HTTP method for reading resource state. Required if `skip_read` is false.
5868
- `read_parameters` (Map of String) Optional request parameters to add to the URL
5969
- `read_request_body` (String) Optional request body to use for the read request.
70+
- `read_request_body_wo` (String, Sensitive, [Write-only](https://developer.hashicorp.com/terraform/language/resources/ephemeral#write-only-arguments)) Write-only request body for the read call. Snapshotted in provider private state for drift detection.
71+
- `read_request_body_wo_version` (Number) Increment to trigger refreshing snapshotted `read_request_body_wo` values.
6072
- `read_response_codes` (List of String) Expected response codes for the read request. Required if `skip_read` is false.
6173
- `read_skip_tls_verify` (Boolean) Skip TLS verification for the read request.
6274
- `read_url` (String) API endpoint for reading resource state. Required if `skip_read` is false.
6375
- `request_body` (String) A request body to attach to the API call
76+
- `request_body_wo` (String, Sensitive, [Write-only](https://developer.hashicorp.com/terraform/language/resources/ephemeral#write-only-arguments)) Write-only request body for the create call. Not stored in Terraform state. Requires Terraform 1.11 or later.
77+
- `request_body_wo_version` (Number) Increment to trigger applying an updated `request_body_wo` value.
6478
- `request_parameters` (Map of String) Map of parameters to attach to the API call
6579
- `response_sensitive` (Boolean) Set to `true` to treat the response as sensitive. When enabled, the response body is written to `sensitive_response` (a sensitive attribute) and `response` is left empty so that secret values are not displayed in plan output. Defaults to `false` to preserve existing behavior.
6680
- `retry_interval` (Number) Interval between each attempt
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Write-only headers and bodies require Terraform 1.11+.
2+
#
3+
# Values are used for HTTP calls but not stored in Terraform state.
4+
# Increment *_wo_version when changing a write-only value.
5+
6+
resource "terracurl_request" "example" {
7+
name = "example"
8+
url = "https://httpbin.org/put"
9+
method = "PUT"
10+
11+
headers_wo = {
12+
Authorization = "Bearer ${var.api_token}"
13+
}
14+
headers_wo_version = 1
15+
16+
request_body_wo = jsonencode({
17+
password = var.password
18+
})
19+
request_body_wo_version = 1
20+
21+
response_codes = [200]
22+
skip_read = true
23+
24+
destroy_url = "https://httpbin.org/delete"
25+
destroy_method = "DELETE"
26+
destroy_response_codes = [200]
27+
28+
destroy_headers_wo = {
29+
Authorization = "Bearer ${var.api_token}"
30+
}
31+
destroy_headers_wo_version = 1
32+
}

0 commit comments

Comments
 (0)