Skip to content

Commit 6a3542a

Browse files
Add file and multipart request body support across all surfaces.
Introduce request_body_file and request_multipart for resources, data sources, actions, and ephemeral resources so binary payloads and form uploads are read from disk at request time without storing bytes in state. Closes #43 and #84. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 330082d commit 6a3542a

27 files changed

Lines changed: 2218 additions & 59 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 2.10.0
2+
3+
ENHANCEMENTS:
4+
5+
- Add `request_body_file` and `request_multipart` request body support across `terracurl_request` resources, data sources, actions, and ephemeral resources. File bytes are read at request time and are not stored in Terraform state. Multipart bodies set `Content-Type` with a generated boundary. Closes #43 and #84.
6+
17
## 2.9.0
28

39
ENHANCEMENTS:

docs/actions/request.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ TerraCurl request action
3131
- `key_file` (String) Path to a file on local disk that contains the PEM-encoded private key for which the authentication certificate was issued
3232
- `max_retry` (Number) Maximum number of tries until it is marked as failed
3333
- `request_body` (String) A request body to attach to the API call
34+
- `request_body_file` (String) Path to a file on local disk to use as the request body. File bytes are read at request time and are not stored in Terraform state.
35+
- `request_multipart` (Attributes) Multipart form request body. The provider sets `Content-Type` with a generated boundary. (see [below for nested schema](#nestedatt--request_multipart))
3436
- `request_parameters` (Map of String) Map of parameters to attach to the API call
3537
- `retry_interval` (Number) Time in seconds between each retry attempt
3638
- `skip_tls_verify` (Boolean) Set this to true to disable verification of the server's TLS certificate
@@ -43,3 +45,24 @@ Required:
4345

4446
- `password` (String) Password for HTTP Digest authentication.
4547
- `username` (String) Username for HTTP Digest authentication.
48+
49+
50+
<a id="nestedatt--request_multipart"></a>
51+
### Nested Schema for `request_multipart`
52+
53+
Required:
54+
55+
- `parts` (Attributes List) Multipart form parts. (see [below for nested schema](#nestedatt--request_multipart--parts))
56+
57+
<a id="nestedatt--request_multipart--parts"></a>
58+
### Nested Schema for `request_multipart.parts`
59+
60+
Required:
61+
62+
- `name` (String) Form field name.
63+
64+
Optional:
65+
66+
- `content_type` (String) Optional Content-Type for this part. Defaults to `text/plain` for value parts and `application/octet-stream` for file parts.
67+
- `file_path` (String) Path to a file on local disk for this form field.
68+
- `value` (String) Text form field value.

docs/data-sources/request.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ TerraCurl request data source
3232
- `key_file` (String) Path to a file on local disk that contains the PEM-encoded private key for which the authentication certificate was issued
3333
- `max_retry` (Number) Maximum number of tries until it is marked as failed
3434
- `request_body` (String) A request body to attach to the API call
35+
- `request_body_file` (String) Path to a file on local disk to use as the request body. File bytes are read at request time and are not stored in Terraform state.
36+
- `request_multipart` (Attributes) Multipart form request body. The provider sets `Content-Type` with a generated boundary. (see [below for nested schema](#nestedatt--request_multipart))
3537
- `request_parameters` (Map of String) Map of parameters to attach to the API call
3638
- `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.
3739
- `retry_interval` (Number) Interval between each attempt
@@ -53,3 +55,24 @@ Required:
5355

5456
- `password` (String, Sensitive) Password for HTTP Digest authentication.
5557
- `username` (String, Sensitive) Username for HTTP Digest authentication.
58+
59+
60+
<a id="nestedatt--request_multipart"></a>
61+
### Nested Schema for `request_multipart`
62+
63+
Required:
64+
65+
- `parts` (Attributes List) Multipart form parts. (see [below for nested schema](#nestedatt--request_multipart--parts))
66+
67+
<a id="nestedatt--request_multipart--parts"></a>
68+
### Nested Schema for `request_multipart.parts`
69+
70+
Required:
71+
72+
- `name` (String) Form field name.
73+
74+
Optional:
75+
76+
- `content_type` (String) Optional Content-Type for this part. Defaults to `text/plain` for value parts and `application/octet-stream` for file parts.
77+
- `file_path` (String) Path to a file on local disk for this form field.
78+
- `value` (String) Text form field value.

docs/ephemeral-resources/request.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ TerraCurl request ephemeral resource
3636
- `close_max_retry` (Number) Maximum number of tries until it is marked as failed
3737
- `close_method` (String) HTTP method to use in the API call
3838
- `close_request_body` (String) A request body to attach to the API call
39+
- `close_request_body_file` (String) Path to a file on local disk to use as the request body. File bytes are read at request time and are not stored in Terraform state.
40+
- `close_request_multipart` (Attributes) Multipart form request body. The provider sets `Content-Type` with a generated boundary. (see [below for nested schema](#nestedatt--close_request_multipart))
3941
- `close_request_parameters` (Map of String) Map of parameters to attach to the API call
4042
- `close_response_codes` (List of String) A list of expected response codes
4143
- `close_retry_interval` (Number) Interval between each attempt
@@ -56,13 +58,17 @@ TerraCurl request ephemeral resource
5658
- `renew_max_retry` (Number) Maximum number of tries until it is marked as failed
5759
- `renew_method` (String) HTTP method to use in the API call
5860
- `renew_request_body` (String) A request body to attach to the API call
61+
- `renew_request_body_file` (String) Path to a file on local disk to use as the request body. File bytes are read at request time and are not stored in Terraform state.
62+
- `renew_request_multipart` (Attributes) Multipart form request body. The provider sets `Content-Type` with a generated boundary. (see [below for nested schema](#nestedatt--renew_request_multipart))
5963
- `renew_request_parameters` (Map of String) Map of parameters to attach to the API call
6064
- `renew_response_codes` (List of String) A list of expected response codes
6165
- `renew_retry_interval` (Number) Interval between each attempt
6266
- `renew_skip_tls_verify` (Boolean) Set this to true to disable verification of the server's TLS certificate
6367
- `renew_timeout` (Number) Time in seconds before each request times out. Defaults to 10
6468
- `renew_url` (String) Api endpoint to call
6569
- `request_body` (String) A request body to attach to the API call
70+
- `request_body_file` (String) Path to a file on local disk to use as the request body. File bytes are read at request time and are not stored in Terraform state.
71+
- `request_multipart` (Attributes) Multipart form request body. The provider sets `Content-Type` with a generated boundary. (see [below for nested schema](#nestedatt--request_multipart))
6672
- `request_parameters` (Map of String) Map of parameters to attach to the API call
6773
- `response_sensitive` (Boolean) Set to `true` to treat response bodies as sensitive. When enabled, response bodies are written to the corresponding `sensitive_*` attributes and the non-sensitive attributes are left empty so secret values are not displayed in plan output. Defaults to `false` to preserve existing behavior.
6874
- `retry_interval` (Number) Interval between each attempt
@@ -94,6 +100,28 @@ Required:
94100
- `username` (String, Sensitive) Username for HTTP Digest authentication.
95101

96102

103+
<a id="nestedatt--close_request_multipart"></a>
104+
### Nested Schema for `close_request_multipart`
105+
106+
Required:
107+
108+
- `parts` (Attributes List) Multipart form parts. (see [below for nested schema](#nestedatt--close_request_multipart--parts))
109+
110+
<a id="nestedatt--close_request_multipart--parts"></a>
111+
### Nested Schema for `close_request_multipart.parts`
112+
113+
Required:
114+
115+
- `name` (String) Form field name.
116+
117+
Optional:
118+
119+
- `content_type` (String) Optional Content-Type for this part. Defaults to `text/plain` for value parts and `application/octet-stream` for file parts.
120+
- `file_path` (String) Path to a file on local disk for this form field.
121+
- `value` (String) Text form field value.
122+
123+
124+
97125
<a id="nestedatt--digest_auth"></a>
98126
### Nested Schema for `digest_auth`
99127

@@ -110,3 +138,46 @@ Required:
110138

111139
- `password` (String, Sensitive) Password for HTTP Digest authentication.
112140
- `username` (String, Sensitive) Username for HTTP Digest authentication.
141+
142+
143+
<a id="nestedatt--renew_request_multipart"></a>
144+
### Nested Schema for `renew_request_multipart`
145+
146+
Required:
147+
148+
- `parts` (Attributes List) Multipart form parts. (see [below for nested schema](#nestedatt--renew_request_multipart--parts))
149+
150+
<a id="nestedatt--renew_request_multipart--parts"></a>
151+
### Nested Schema for `renew_request_multipart.parts`
152+
153+
Required:
154+
155+
- `name` (String) Form field name.
156+
157+
Optional:
158+
159+
- `content_type` (String) Optional Content-Type for this part. Defaults to `text/plain` for value parts and `application/octet-stream` for file parts.
160+
- `file_path` (String) Path to a file on local disk for this form field.
161+
- `value` (String) Text form field value.
162+
163+
164+
165+
<a id="nestedatt--request_multipart"></a>
166+
### Nested Schema for `request_multipart`
167+
168+
Required:
169+
170+
- `parts` (Attributes List) Multipart form parts. (see [below for nested schema](#nestedatt--request_multipart--parts))
171+
172+
<a id="nestedatt--request_multipart--parts"></a>
173+
### Nested Schema for `request_multipart.parts`
174+
175+
Required:
176+
177+
- `name` (String) Form field name.
178+
179+
Optional:
180+
181+
- `content_type` (String) Optional Content-Type for this part. Defaults to `text/plain` for value parts and `application/octet-stream` for file parts.
182+
- `file_path` (String) Path to a file on local disk for this form field.
183+
- `value` (String) Text form field value.
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
---
2+
page_title: "File and Multipart Request Bodies"
3+
subcategory: "Guides"
4+
description: |-
5+
Load request bodies from disk with request_body_file and build multipart/form-data payloads with request_multipart across TerraCurl surfaces.
6+
---
7+
8+
# File and Multipart Request Bodies
9+
10+
TerraCurl supports two complementary ways to send non-JSON request bodies without embedding large or binary payloads in Terraform configuration or state:
11+
12+
| Mechanism | Use case | Stored in state |
13+
|-----------|----------|-----------------|
14+
| `request_body_file` | Raw binary or text body (for example a ZIP deploy) | File path only |
15+
| `request_multipart` | `multipart/form-data` with text fields and file parts | Part metadata and paths only |
16+
17+
These attributes are available on **resources**, **data sources**, **actions**, and **ephemeral resources**. On `terracurl_request` resources, matching `read_*` and `destroy_*` variants exist for read and destroy operations.
18+
19+
## Mutual exclusion
20+
21+
For each HTTP operation, configure **one** body source:
22+
23+
- `request_body` or `request_body_wo` (resources only for `_wo`)
24+
- `request_body_file`
25+
- `request_multipart`
26+
27+
The provider validates this at plan time.
28+
29+
## File-based request bodies
30+
31+
Use `request_body_file` when the API expects a raw payload such as a ZIP archive, protobuf message, or plain-text document.
32+
33+
```terraform
34+
resource "terracurl_request" "kudu_deploy" {
35+
name = "kudu-deploy"
36+
url = "https://myapp.scm.azurewebsites.net/api/zipdeploy?isAsync=true"
37+
method = "PUT"
38+
39+
request_body_file = "${path.module}/app.zip"
40+
41+
headers = {
42+
Content-Type = "application/zip"
43+
}
44+
45+
response_codes = [200, 202]
46+
skip_read = true
47+
skip_destroy = true
48+
}
49+
```
50+
51+
Behavior:
52+
53+
- The provider reads the file with `os.ReadFile` at HTTP call time.
54+
- Only the **path** is stored in Terraform state, not the file bytes (same pattern as `cert_file`).
55+
- Changing the path triggers replace via `RequiresReplace`.
56+
- Content changes at the same path do **not** automatically trigger replace. Taint or change the path when the on-disk file changes.
57+
58+
Set `Content-Type` in `headers` when the API requires a specific media type.
59+
60+
## Multipart form bodies
61+
62+
Use `request_multipart` for APIs that accept `multipart/form-data` uploads (similar to `curl -F name=John -F photo=@john.jpg`).
63+
64+
```terraform
65+
resource "terracurl_request" "upload" {
66+
name = "upload"
67+
url = "https://api.example.com/upload"
68+
method = "POST"
69+
70+
request_multipart = {
71+
parts = [
72+
{ name = "name", value = "John" },
73+
{ name = "photo", file_path = "${path.module}/john.jpg", content_type = "image/jpeg" },
74+
]
75+
}
76+
77+
response_codes = [200]
78+
skip_read = true
79+
skip_destroy = true
80+
}
81+
```
82+
83+
Each part requires:
84+
85+
| Attribute | Required | Description |
86+
|-----------|----------|-------------|
87+
| `name` | Yes | Form field name |
88+
| `value` | One of `value` or `file_path` | Text field value |
89+
| `file_path` | One of `value` or `file_path` | Path to a file on disk |
90+
| `content_type` | No | Defaults to `text/plain` for value parts and `application/octet-stream` for file parts |
91+
92+
The provider builds the body with Go's `mime/multipart` package and sets `Content-Type: multipart/form-data; boundary=...` on the outbound request. If you also set `Content-Type` in `headers`, the provider-generated multipart header **wins** for that operation.
93+
94+
## Data source example
95+
96+
```terraform
97+
data "terracurl_request" "upload_status" {
98+
name = "upload-status"
99+
url = "https://api.example.com/upload"
100+
method = "POST"
101+
response_codes = ["200"]
102+
103+
request_multipart = {
104+
parts = [
105+
{ name = "name", value = "John" },
106+
{ name = "photo", file_path = "${path.module}/john.jpg" },
107+
]
108+
}
109+
}
110+
```
111+
112+
## Action example
113+
114+
```terraform
115+
action "terracurl_request" "notify" {
116+
url = "https://api.example.com/notify"
117+
method = "POST"
118+
119+
request_body_file = "${path.module}/payload.bin"
120+
121+
headers = {
122+
Content-Type = "application/octet-stream"
123+
}
124+
125+
response_codes = [200, 204]
126+
}
127+
```
128+
129+
## Ephemeral resource example
130+
131+
Ephemeral resources support `request_body_file` / `request_multipart` on open, plus `renew_*` and `close_*` variants. Renew and close file paths and multipart metadata are snapshotted to private state at open and re-read from disk at renew/close time.
132+
133+
```terraform
134+
ephemeral "terracurl_request" "session" {
135+
name = "session"
136+
url = "https://api.example.com/open"
137+
method = "POST"
138+
response_codes = ["201"]
139+
140+
skip_renew = false
141+
renew_url = "https://api.example.com/renew"
142+
renew_method = "PUT"
143+
renew_request_body_file = "${path.module}/renew-payload.json"
144+
renew_response_codes = ["200"]
145+
146+
skip_close = false
147+
close_url = "https://api.example.com/close"
148+
close_method = "DELETE"
149+
close_request_multipart = {
150+
parts = [
151+
{ name = "token", value = "placeholder" },
152+
]
153+
}
154+
close_response_codes = ["204"]
155+
}
156+
```
157+
158+
## Resource read and destroy variants
159+
160+
On `terracurl_request` resources:
161+
162+
- `read_request_body_file` / `read_request_multipart` for read calls
163+
- `destroy_request_body_file` / `destroy_request_multipart` for destroy calls
164+
165+
Write-only string bodies (`*_request_body_wo`) remain available for inline secrets. File and multipart attributes are already path/metadata-only in state, so write-only variants are not provided for them.
166+
167+
## Destroy templating with multipart
168+
169+
`{response.<path>}` placeholders in `destroy_request_multipart` part `value` fields are substituted at destroy time from the stored create response. File parts (`file_path`) are not templated.
170+
171+
See the [Destroy Response Templating guide](destroy_templating) for placeholder syntax.
172+
173+
## Limitations
174+
175+
- File contents are loaded fully into memory at request time (same as embedding bytes in configuration).
176+
- The provider does not sniff MIME types from file extensions; set `content_type` explicitly when needed.
177+
- Write-only multipart parts are not supported (multipart configuration is already metadata-only in state).
178+
179+
## Related guides
180+
181+
- [Write-Only Headers and Request Bodies](write_only) — inline secret bodies without persisting values in state
182+
- [Destroy Response Templating](destroy_templating) — inject create response values into destroy configuration

0 commit comments

Comments
 (0)