Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 2.10.0

ENHANCEMENTS:

- 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.

## 2.9.0

ENHANCEMENTS:
Expand Down
23 changes: 23 additions & 0 deletions docs/actions/request.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ TerraCurl request action
- `key_file` (String) Path to a file on local disk that contains the PEM-encoded private key for which the authentication certificate was issued
- `max_retry` (Number) Maximum number of tries until it is marked as failed
- `request_body` (String) A request body to attach to the API call
- `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.
- `request_multipart` (Attributes) Multipart form request body. The provider sets `Content-Type` with a generated boundary. (see [below for nested schema](#nestedatt--request_multipart))
- `request_parameters` (Map of String) Map of parameters to attach to the API call
- `retry_interval` (Number) Time in seconds between each retry attempt
- `skip_tls_verify` (Boolean) Set this to true to disable verification of the server's TLS certificate
Expand All @@ -43,3 +45,24 @@ Required:

- `password` (String) Password for HTTP Digest authentication.
- `username` (String) Username for HTTP Digest authentication.


<a id="nestedatt--request_multipart"></a>
### Nested Schema for `request_multipart`

Required:

- `parts` (Attributes List) Multipart form parts. (see [below for nested schema](#nestedatt--request_multipart--parts))

<a id="nestedatt--request_multipart--parts"></a>
### Nested Schema for `request_multipart.parts`

Required:

- `name` (String) Form field name.

Optional:

- `content_type` (String) Optional Content-Type for this part. Defaults to `text/plain` for value parts and `application/octet-stream` for file parts.
- `file_path` (String) Path to a file on local disk for this form field.
- `value` (String) Text form field value.
23 changes: 23 additions & 0 deletions docs/data-sources/request.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ TerraCurl request data source
- `key_file` (String) Path to a file on local disk that contains the PEM-encoded private key for which the authentication certificate was issued
- `max_retry` (Number) Maximum number of tries until it is marked as failed
- `request_body` (String) A request body to attach to the API call
- `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.
- `request_multipart` (Attributes) Multipart form request body. The provider sets `Content-Type` with a generated boundary. (see [below for nested schema](#nestedatt--request_multipart))
- `request_parameters` (Map of String) Map of parameters to attach to the API call
- `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.
- `retry_interval` (Number) Interval between each attempt
Expand All @@ -53,3 +55,24 @@ Required:

- `password` (String, Sensitive) Password for HTTP Digest authentication.
- `username` (String, Sensitive) Username for HTTP Digest authentication.


<a id="nestedatt--request_multipart"></a>
### Nested Schema for `request_multipart`

Required:

- `parts` (Attributes List) Multipart form parts. (see [below for nested schema](#nestedatt--request_multipart--parts))

<a id="nestedatt--request_multipart--parts"></a>
### Nested Schema for `request_multipart.parts`

Required:

- `name` (String) Form field name.

Optional:

- `content_type` (String) Optional Content-Type for this part. Defaults to `text/plain` for value parts and `application/octet-stream` for file parts.
- `file_path` (String) Path to a file on local disk for this form field.
- `value` (String) Text form field value.
71 changes: 71 additions & 0 deletions docs/ephemeral-resources/request.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ TerraCurl request ephemeral resource
- `close_max_retry` (Number) Maximum number of tries until it is marked as failed
- `close_method` (String) HTTP method to use in the API call
- `close_request_body` (String) A request body to attach to the API call
- `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.
- `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))
- `close_request_parameters` (Map of String) Map of parameters to attach to the API call
- `close_response_codes` (List of String) A list of expected response codes
- `close_retry_interval` (Number) Interval between each attempt
Expand All @@ -56,13 +58,17 @@ TerraCurl request ephemeral resource
- `renew_max_retry` (Number) Maximum number of tries until it is marked as failed
- `renew_method` (String) HTTP method to use in the API call
- `renew_request_body` (String) A request body to attach to the API call
- `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.
- `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))
- `renew_request_parameters` (Map of String) Map of parameters to attach to the API call
- `renew_response_codes` (List of String) A list of expected response codes
- `renew_retry_interval` (Number) Interval between each attempt
- `renew_skip_tls_verify` (Boolean) Set this to true to disable verification of the server's TLS certificate
- `renew_timeout` (Number) Time in seconds before each request times out. Defaults to 10
- `renew_url` (String) Api endpoint to call
- `request_body` (String) A request body to attach to the API call
- `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.
- `request_multipart` (Attributes) Multipart form request body. The provider sets `Content-Type` with a generated boundary. (see [below for nested schema](#nestedatt--request_multipart))
- `request_parameters` (Map of String) Map of parameters to attach to the API call
- `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.
- `retry_interval` (Number) Interval between each attempt
Expand Down Expand Up @@ -94,6 +100,28 @@ Required:
- `username` (String, Sensitive) Username for HTTP Digest authentication.


<a id="nestedatt--close_request_multipart"></a>
### Nested Schema for `close_request_multipart`

Required:

- `parts` (Attributes List) Multipart form parts. (see [below for nested schema](#nestedatt--close_request_multipart--parts))

<a id="nestedatt--close_request_multipart--parts"></a>
### Nested Schema for `close_request_multipart.parts`

Required:

- `name` (String) Form field name.

Optional:

- `content_type` (String) Optional Content-Type for this part. Defaults to `text/plain` for value parts and `application/octet-stream` for file parts.
- `file_path` (String) Path to a file on local disk for this form field.
- `value` (String) Text form field value.



<a id="nestedatt--digest_auth"></a>
### Nested Schema for `digest_auth`

Expand All @@ -110,3 +138,46 @@ Required:

- `password` (String, Sensitive) Password for HTTP Digest authentication.
- `username` (String, Sensitive) Username for HTTP Digest authentication.


<a id="nestedatt--renew_request_multipart"></a>
### Nested Schema for `renew_request_multipart`

Required:

- `parts` (Attributes List) Multipart form parts. (see [below for nested schema](#nestedatt--renew_request_multipart--parts))

<a id="nestedatt--renew_request_multipart--parts"></a>
### Nested Schema for `renew_request_multipart.parts`

Required:

- `name` (String) Form field name.

Optional:

- `content_type` (String) Optional Content-Type for this part. Defaults to `text/plain` for value parts and `application/octet-stream` for file parts.
- `file_path` (String) Path to a file on local disk for this form field.
- `value` (String) Text form field value.



<a id="nestedatt--request_multipart"></a>
### Nested Schema for `request_multipart`

Required:

- `parts` (Attributes List) Multipart form parts. (see [below for nested schema](#nestedatt--request_multipart--parts))

<a id="nestedatt--request_multipart--parts"></a>
### Nested Schema for `request_multipart.parts`

Required:

- `name` (String) Form field name.

Optional:

- `content_type` (String) Optional Content-Type for this part. Defaults to `text/plain` for value parts and `application/octet-stream` for file parts.
- `file_path` (String) Path to a file on local disk for this form field.
- `value` (String) Text form field value.
182 changes: 182 additions & 0 deletions docs/guides/file_and_multipart_bodies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
---
page_title: "File and Multipart Request Bodies"
subcategory: "Guides"
description: |-
Load request bodies from disk with request_body_file and build multipart/form-data payloads with request_multipart across TerraCurl surfaces.
---

# File and Multipart Request Bodies

TerraCurl supports two complementary ways to send non-JSON request bodies without embedding large or binary payloads in Terraform configuration or state:

| Mechanism | Use case | Stored in state |
|-----------|----------|-----------------|
| `request_body_file` | Raw binary or text body (for example a ZIP deploy) | File path only |
| `request_multipart` | `multipart/form-data` with text fields and file parts | Part metadata and paths only |

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.

## Mutual exclusion

For each HTTP operation, configure **one** body source:

- `request_body` or `request_body_wo` (resources only for `_wo`)
- `request_body_file`
- `request_multipart`

The provider validates this at plan time.

## File-based request bodies

Use `request_body_file` when the API expects a raw payload such as a ZIP archive, protobuf message, or plain-text document.

```terraform
resource "terracurl_request" "kudu_deploy" {
name = "kudu-deploy"
url = "https://myapp.scm.azurewebsites.net/api/zipdeploy?isAsync=true"
method = "PUT"

request_body_file = "${path.module}/app.zip"

headers = {
Content-Type = "application/zip"
}

response_codes = [200, 202]
skip_read = true
skip_destroy = true
}
```

Behavior:

- The provider reads the file with `os.ReadFile` at HTTP call time.
- Only the **path** is stored in Terraform state, not the file bytes (same pattern as `cert_file`).
- Changing the path triggers replace via `RequiresReplace`.
- Content changes at the same path do **not** automatically trigger replace. Taint or change the path when the on-disk file changes.

Set `Content-Type` in `headers` when the API requires a specific media type.

## Multipart form bodies

Use `request_multipart` for APIs that accept `multipart/form-data` uploads (similar to `curl -F name=John -F photo=@john.jpg`).

```terraform
resource "terracurl_request" "upload" {
name = "upload"
url = "https://api.example.com/upload"
method = "POST"

request_multipart = {
parts = [
{ name = "name", value = "John" },
{ name = "photo", file_path = "${path.module}/john.jpg", content_type = "image/jpeg" },
]
}

response_codes = [200]
skip_read = true
skip_destroy = true
}
```

Each part requires:

| Attribute | Required | Description |
|-----------|----------|-------------|
| `name` | Yes | Form field name |
| `value` | One of `value` or `file_path` | Text field value |
| `file_path` | One of `value` or `file_path` | Path to a file on disk |
| `content_type` | No | Defaults to `text/plain` for value parts and `application/octet-stream` for file parts |

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.

## Data source example

```terraform
data "terracurl_request" "upload_status" {
name = "upload-status"
url = "https://api.example.com/upload"
method = "POST"
response_codes = ["200"]

request_multipart = {
parts = [
{ name = "name", value = "John" },
{ name = "photo", file_path = "${path.module}/john.jpg" },
]
}
}
```

## Action example

```terraform
action "terracurl_request" "notify" {
url = "https://api.example.com/notify"
method = "POST"

request_body_file = "${path.module}/payload.bin"

headers = {
Content-Type = "application/octet-stream"
}

response_codes = [200, 204]
}
```

## Ephemeral resource example

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.

```terraform
ephemeral "terracurl_request" "session" {
name = "session"
url = "https://api.example.com/open"
method = "POST"
response_codes = ["201"]

skip_renew = false
renew_url = "https://api.example.com/renew"
renew_method = "PUT"
renew_request_body_file = "${path.module}/renew-payload.json"
renew_response_codes = ["200"]

skip_close = false
close_url = "https://api.example.com/close"
close_method = "DELETE"
close_request_multipart = {
parts = [
{ name = "token", value = "placeholder" },
]
}
close_response_codes = ["204"]
}
```

## Resource read and destroy variants

On `terracurl_request` resources:

- `read_request_body_file` / `read_request_multipart` for read calls
- `destroy_request_body_file` / `destroy_request_multipart` for destroy calls

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.

## Destroy templating with multipart

`{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.

See the [Destroy Response Templating guide](destroy_templating) for placeholder syntax.

## Limitations

- File contents are loaded fully into memory at request time (same as embedding bytes in configuration).
- The provider does not sniff MIME types from file extensions; set `content_type` explicitly when needed.
- Write-only multipart parts are not supported (multipart configuration is already metadata-only in state).

## Related guides

- [Write-Only Headers and Request Bodies](write_only) — inline secret bodies without persisting values in state
- [Destroy Response Templating](destroy_templating) — inject create response values into destroy configuration
Loading
Loading