diff --git a/README.md b/README.md
index a6d1c30c..cc1903ec 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,10 @@
# Terraform Provider: HTTP
The HTTP provider interacts with generic HTTP servers.
-It provides a data source that issues an HTTP request exposing the response headers and body
-for use within a Terraform deployment.
+It provides:
+
+- a data source (`http`) that issues an HTTP request exposing the response headers and body.
+- a resource (`http`) that performs an HTTP request during apply and stores response data in state.
## Documentation, questions and discussions
diff --git a/docs/cdktf/python/resources/http.md b/docs/cdktf/python/resources/http.md
new file mode 100644
index 00000000..9a4a93ca
--- /dev/null
+++ b/docs/cdktf/python/resources/http.md
@@ -0,0 +1,233 @@
+---
+page_title: "http Resource - terraform-provider-http"
+subcategory: ""
+description: |-
+ The http resource makes an HTTP request to the given URL and exports
+ information about the response.
+ The given URL may be either an http or https URL. This resource
+ will issue a warning if the result is not UTF-8 encoded.
+ ~> Important Although https URLs can be used, there is currently no
+ mechanism to authenticate the remote server except for general verification of
+ the server certificate's chain of trust. Data retrieved from servers not under
+ your control should be treated as untrustworthy.
+ By default, there are no retries. Configuring the retry block will result in
+ retries if an error is returned by the client (e.g., connection errors) or if
+ a 5xx-range (except 501) status code is received. For further details see
+ go-retryablehttp https://pkg.go.dev/github.com/hashicorp/go-retryablehttp.
+---
+
+
+
+# http (Resource)
+
+The `http` resource makes an HTTP request to the given URL and exports
+information about the response.
+
+The given URL may be either an `http` or `https` URL. This resource
+will issue a warning if the result is not UTF-8 encoded.
+
+~> **Important** Although `https` URLs can be used, there is currently no
+mechanism to authenticate the remote server except for general verification of
+the server certificate's chain of trust. Data retrieved from servers not under
+your control should be treated as untrustworthy.
+
+By default, there are no retries. Configuring the retry block will result in
+retries if an error is returned by the client (e.g., connection errors) or if
+a 5xx-range (except 501) status code is received. For further details see
+[go-retryablehttp](https://pkg.go.dev/github.com/hashicorp/go-retryablehttp).
+
+## Example Usage
+
+```python
+# DO NOT EDIT. Code generated by 'cdktf convert' - Please report bugs at https://cdk.tf/bug
+from constructs import Construct
+from cdktf import TerraformStack
+#
+# Provider bindings are generated by running `cdktf get`.
+# See https://cdk.tf/provider-generation for more details.
+#
+from imports.http. import ResourceHttp
+class MyConvertedCode(TerraformStack):
+ def __init__(self, scope, name):
+ super().__init__(scope, name)
+ ResourceHttp(self, "example",
+ request_headers=[{
+ "Accept": "application/json"
+ }
+ ],
+ url="https://checkpoint-api.hashicorp.com/v1/check/terraform"
+ )
+ ResourceHttp(self, "example_head",
+ method="HEAD",
+ url="https://checkpoint-api.hashicorp.com/v1/check/terraform"
+ )
+ ResourceHttp(self, "example_post",
+ method="POST",
+ request_body="request body",
+ url="https://checkpoint-api.hashicorp.com/v1/check/terraform"
+ )
+```
+
+## Controlling when the request is sent
+
+Use the resource argument `when` to control whether the HTTP request is executed during apply operations or only during destroy:
+
+- `apply` (default): request is executed during create and update.
+- `destroy`: request is executed only during resource destruction.
+
+```python
+# DO NOT EDIT. Code generated by 'cdktf convert' - Please report bugs at https://cdk.tf/bug
+from constructs import Construct
+from cdktf import TerraformStack
+from imports.http. import ResourceHttp
+class MyConvertedCode(TerraformStack):
+ def __init__(self, scope, name):
+ super().__init__(scope, name)
+ ResourceHttp(self, "cleanup",
+ method="DELETE",
+ url="https://api.example.com/cleanup",
+ when="destroy"
+ )
+```
+
+## Usage with Postcondition
+
+Note: Pre/postconditions validate values during apply. They are only meaningful when the resource executes the HTTP request during apply, i.e., when `when = "apply"` (default). If `when = "destroy"`, these conditions will not evaluate against a fresh request result.
+
+```python
+# DO NOT EDIT. Code generated by 'cdktf convert' - Please report bugs at https://cdk.tf/bug
+from constructs import Construct
+from cdktf import TerraformSelf, Fn, TerraformStack
+#
+# Provider bindings are generated by running `cdktf get`.
+# See https://cdk.tf/provider-generation for more details.
+#
+from imports.http. import ResourceHttp
+class MyConvertedCode(TerraformStack):
+ def __init__(self, scope, name):
+ super().__init__(scope, name)
+ ResourceHttp(self, "example",
+ lifecycle={
+ "postcondition": [{
+ "condition": Fn.contains([201, 204],
+ TerraformSelf.get_any("status_code")),
+ "error_message": "Status code invalid"
+ }
+ ]
+ },
+ request_headers=[{
+ "Accept": "application/json"
+ }
+ ],
+ url="https://checkpoint-api.hashicorp.com/v1/check/terraform",
+ when="apply"
+ )
+```
+
+## Usage with Precondition
+
+Note: Pre/postconditions validate values during apply. They are only meaningful when the resource executes the HTTP request during apply, i.e., when `when = "apply"` (default). If `when = "destroy"`, these conditions will not evaluate against a fresh request result.
+
+```python
+# DO NOT EDIT. Code generated by 'cdktf convert' - Please report bugs at https://cdk.tf/bug
+from constructs import Construct
+from cdktf import Fn, TerraformStack
+#
+# Provider bindings are generated by running `cdktf get`.
+# See https://cdk.tf/provider-generation for more details.
+#
+from imports.http. import ResourceHttp
+class MyConvertedCode(TerraformStack):
+ def __init__(self, scope, name):
+ super().__init__(scope, name)
+ http_example = ResourceHttp(self, "example",
+ request_headers=[{
+ "Accept": "application/json"
+ }
+ ],
+ url="https://checkpoint-api.hashicorp.com/v1/check/terraform",
+ when="apply"
+ )
+ http_example.add_override("lifecycle.precondition", [{
+ "condition": Fn.contains([200, 201, 204], http_example.status_code),
+ "error_message": "Unexpected status code"
+ }
+ ])
+```
+
+## Usage with Provisioner
+
+[Failure Behaviour](https://www.terraform.io/language/resources/provisioners/syntax#failure-behavior)
+can be leveraged within a provisioner in order to raise an error and stop applying.
+
+```python
+# DO NOT EDIT. Code generated by 'cdktf convert' - Please report bugs at https://cdk.tf/bug
+from constructs import Construct
+from cdktf import Fn, TerraformStack
+#
+# Provider bindings are generated by running `cdktf get`.
+# See https://cdk.tf/provider-generation for more details.
+#
+from imports.null.resource import Resource
+from imports.http. import ResourceHttp
+class MyConvertedCode(TerraformStack):
+ def __init__(self, scope, name):
+ super().__init__(scope, name)
+ http_example = ResourceHttp(self, "example",
+ request_headers=[{
+ "Accept": "application/json"
+ }
+ ],
+ url="https://checkpoint-api.hashicorp.com/v1/check/terraform"
+ )
+ null_provider_resource_example = Resource(self, "example_1",
+ provisioners=[{
+ "type": "local-exec",
+ "command": Fn.contains([201, 204], http_example.status_code)
+ }
+ ]
+ )
+ # This allows the Terraform resource name to match the original name. You can remove the call if you don't need them to match.
+ null_provider_resource_example.override_logical_id("example")
+```
+
+
+## Schema
+
+### Required
+
+- `url` (String) The URL for the request. Supported schemes are `http` and `https`.
+
+### Optional
+
+- `ca_cert_pem` (String) Certificate Authority (CA) in [PEM (RFC 1421)](https://datatracker.ietf.org/doc/html/rfc1421) format.
+- `client_cert_pem` (String) Client certificate in [PEM (RFC 1421)](https://datatracker.ietf.org/doc/html/rfc1421) format.
+- `client_key_pem` (String) Client key in [PEM (RFC 1421)](https://datatracker.ietf.org/doc/html/rfc1421) format.
+- `insecure` (Boolean) Disables verification of the server's certificate chain and hostname. Defaults to `false`
+- `method` (String) The HTTP Method for the request. Allowed methods are a subset of methods defined in [RFC7231](https://datatracker.ietf.org/doc/html/rfc7231#section-4.3) namely, `GET`, `HEAD`, and `POST`. `POST` support is only intended for read-only URLs, such as submitting a search.
+- `request_body` (String) The request body as a string.
+- `request_headers` (Map of String) A map of request header field names and values.
+- `request_timeout_ms` (Number) The request timeout in milliseconds.
+- `retry` (Block, Optional) Retry request configuration. By default there are no retries. Configuring this block will result in retries if an error is returned by the client (e.g., connection errors) or if a 5xx-range (except 501) status code is received. For further details see [go-retryablehttp](https://pkg.go.dev/github.com/hashicorp/go-retryablehttp). (see [below for nested schema](#nestedblock--retry))
+- `when` (String) When to send the HTTP request. Valid values are `apply` (default) and `destroy`. When set to `apply`, the request is sent during resource creation and updates. When set to `destroy`, the request is only sent during resource destruction.
+
+### Read-Only
+
+- `body` (String, Deprecated) The response body returned as a string. **NOTE**: This is deprecated, use `response_body` instead.
+- `id` (String) The URL used for the request.
+- `response_body` (String) The response body returned as a string.
+- `response_body_base64` (String) The response body encoded as base64 (standard) as defined in [RFC 4648](https://datatracker.ietf.org/doc/html/rfc4648#section-4).
+- `response_headers` (Map of String) A map of response header field names and values. Duplicate headers are concatenated according to [RFC2616](https://www.rfc-editor.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2).
+- `status_code` (Number) The HTTP response status code.
+
+
+### Nested Schema for `retry`
+
+Optional:
+
+- `attempts` (Number) The number of times the request is to be retried. For example, if 2 is specified, the request will be tried a maximum of 3 times.
+- `max_delay_ms` (Number) The maximum delay between retry requests in milliseconds.
+- `min_delay_ms` (Number) The minimum delay between retry requests in milliseconds.
+
+
+
diff --git a/docs/cdktf/typescript/resources/http.md b/docs/cdktf/typescript/resources/http.md
new file mode 100644
index 00000000..e38be427
--- /dev/null
+++ b/docs/cdktf/typescript/resources/http.md
@@ -0,0 +1,257 @@
+---
+page_title: "http Resource - terraform-provider-http"
+subcategory: ""
+description: |-
+ The http resource makes an HTTP request to the given URL and exports
+ information about the response.
+ The given URL may be either an http or https URL. This resource
+ will issue a warning if the result is not UTF-8 encoded.
+ ~> Important Although https URLs can be used, there is currently no
+ mechanism to authenticate the remote server except for general verification of
+ the server certificate's chain of trust. Data retrieved from servers not under
+ your control should be treated as untrustworthy.
+ By default, there are no retries. Configuring the retry block will result in
+ retries if an error is returned by the client (e.g., connection errors) or if
+ a 5xx-range (except 501) status code is received. For further details see
+ go-retryablehttp https://pkg.go.dev/github.com/hashicorp/go-retryablehttp.
+---
+
+
+
+# http (Resource)
+
+The `http` resource makes an HTTP request to the given URL and exports
+information about the response.
+
+The given URL may be either an `http` or `https` URL. This resource
+will issue a warning if the result is not UTF-8 encoded.
+
+~> **Important** Although `https` URLs can be used, there is currently no
+mechanism to authenticate the remote server except for general verification of
+the server certificate's chain of trust. Data retrieved from servers not under
+your control should be treated as untrustworthy.
+
+By default, there are no retries. Configuring the retry block will result in
+retries if an error is returned by the client (e.g., connection errors) or if
+a 5xx-range (except 501) status code is received. For further details see
+[go-retryablehttp](https://pkg.go.dev/github.com/hashicorp/go-retryablehttp).
+
+## Example Usage
+
+```typescript
+// DO NOT EDIT. Code generated by 'cdktf convert' - Please report bugs at https://cdk.tf/bug
+import { Construct } from "constructs";
+import { TerraformStack } from "cdktf";
+/*
+ * Provider bindings are generated by running `cdktf get`.
+ * See https://cdk.tf/provider-generation for more details.
+ */
+import { ResourceHttp } from "./.gen/providers/http/";
+class MyConvertedCode extends TerraformStack {
+ constructor(scope: Construct, name: string) {
+ super(scope, name);
+ new ResourceHttp(this, "example", {
+ requestHeaders: [
+ {
+ Accept: "application/json",
+ },
+ ],
+ url: "https://checkpoint-api.hashicorp.com/v1/check/terraform",
+ });
+ new ResourceHttp(this, "example_head", {
+ method: "HEAD",
+ url: "https://checkpoint-api.hashicorp.com/v1/check/terraform",
+ });
+ new ResourceHttp(this, "example_post", {
+ method: "POST",
+ requestBody: "request body",
+ url: "https://checkpoint-api.hashicorp.com/v1/check/terraform",
+ });
+ }
+}
+
+```
+
+## Controlling when the request is sent
+
+Use the resource argument `when` to control whether the HTTP request is executed during apply operations or only during destroy:
+
+- `apply` (default): request is executed during create and update.
+- `destroy`: request is executed only during resource destruction.
+
+```typescript
+// DO NOT EDIT. Code generated by 'cdktf convert' - Please report bugs at https://cdk.tf/bug
+import { Construct } from "constructs";
+import { TerraformStack } from "cdktf";
+import { ResourceHttp } from "./.gen/providers/http/";
+class MyConvertedCode extends TerraformStack {
+ constructor(scope: Construct, name: string) {
+ super(scope, name);
+ new ResourceHttp(this, "cleanup", {
+ method: "DELETE",
+ url: "https://api.example.com/cleanup",
+ when: "destroy",
+ });
+ }
+}
+
+```
+
+## Usage with Postcondition
+
+Note: Pre/postconditions validate values during apply. They are only meaningful when the resource executes the HTTP request during apply, i.e., when `when = "apply"` (default). If `when = "destroy"`, these conditions will not evaluate against a fresh request result.
+
+```typescript
+// DO NOT EDIT. Code generated by 'cdktf convert' - Please report bugs at https://cdk.tf/bug
+import { Construct } from "constructs";
+import { TerraformSelf, Fn, TerraformStack } from "cdktf";
+/*
+ * Provider bindings are generated by running `cdktf get`.
+ * See https://cdk.tf/provider-generation for more details.
+ */
+import { ResourceHttp } from "./.gen/providers/http/";
+class MyConvertedCode extends TerraformStack {
+ constructor(scope: Construct, name: string) {
+ super(scope, name);
+ new ResourceHttp(this, "example", {
+ lifecycle: {
+ postcondition: [
+ {
+ condition: Fn.contains(
+ [201, 204],
+ TerraformSelf.getAny("status_code")
+ ),
+ errorMessage: "Status code invalid",
+ },
+ ],
+ },
+ requestHeaders: [
+ {
+ Accept: "application/json",
+ },
+ ],
+ url: "https://checkpoint-api.hashicorp.com/v1/check/terraform",
+ when: "apply",
+ });
+ }
+}
+
+```
+
+## Usage with Precondition
+
+Note: Pre/postconditions validate values during apply. They are only meaningful when the resource executes the HTTP request during apply, i.e., when `when = "apply"` (default). If `when = "destroy"`, these conditions will not evaluate against a fresh request result.
+
+```typescript
+// DO NOT EDIT. Code generated by 'cdktf convert' - Please report bugs at https://cdk.tf/bug
+import { Construct } from "constructs";
+import { Fn, TerraformStack } from "cdktf";
+/*
+ * Provider bindings are generated by running `cdktf get`.
+ * See https://cdk.tf/provider-generation for more details.
+ */
+import { ResourceHttp } from "./.gen/providers/http/";
+class MyConvertedCode extends TerraformStack {
+ constructor(scope: Construct, name: string) {
+ super(scope, name);
+ const httpExample = new ResourceHttp(this, "example", {
+ requestHeaders: [
+ {
+ Accept: "application/json",
+ },
+ ],
+ url: "https://checkpoint-api.hashicorp.com/v1/check/terraform",
+ when: "apply",
+ });
+ httpExample.addOverride("lifecycle.precondition", [
+ {
+ condition: Fn.contains([200, 201, 204], httpExample.statusCode),
+ errorMessage: "Unexpected status code",
+ },
+ ]);
+ }
+}
+
+```
+
+## Usage with Provisioner
+
+[Failure Behaviour](https://www.terraform.io/language/resources/provisioners/syntax#failure-behavior)
+can be leveraged within a provisioner in order to raise an error and stop applying.
+
+```typescript
+// DO NOT EDIT. Code generated by 'cdktf convert' - Please report bugs at https://cdk.tf/bug
+import { Construct } from "constructs";
+import { Fn, TerraformStack } from "cdktf";
+/*
+ * Provider bindings are generated by running `cdktf get`.
+ * See https://cdk.tf/provider-generation for more details.
+ */
+import { Resource } from "./.gen/providers/null/resource";
+import { ResourceHttp } from "./.gen/providers/http/";
+class MyConvertedCode extends TerraformStack {
+ constructor(scope: Construct, name: string) {
+ super(scope, name);
+ const httpExample = new ResourceHttp(this, "example", {
+ requestHeaders: [
+ {
+ Accept: "application/json",
+ },
+ ],
+ url: "https://checkpoint-api.hashicorp.com/v1/check/terraform",
+ });
+ const nullProviderResourceExample = new Resource(this, "example_1", {
+ provisioners: [
+ {
+ type: "local-exec",
+ command: Fn.contains([201, 204], httpExample.statusCode),
+ },
+ ],
+ });
+ /*This allows the Terraform resource name to match the original name. You can remove the call if you don't need them to match.*/
+ nullProviderResourceExample.overrideLogicalId("example");
+ }
+}
+
+```
+
+
+## Schema
+
+### Required
+
+- `url` (String) The URL for the request. Supported schemes are `http` and `https`.
+
+### Optional
+
+- `caCertPem` (String) Certificate Authority (CA) in [PEM (RFC 1421)](https://datatracker.ietf.org/doc/html/rfc1421) format.
+- `client_cert_pem` (String) Client certificate in [PEM (RFC 1421)](https://datatracker.ietf.org/doc/html/rfc1421) format.
+- `client_key_pem` (String) Client key in [PEM (RFC 1421)](https://datatracker.ietf.org/doc/html/rfc1421) format.
+- `insecure` (Boolean) Disables verification of the server's certificate chain and hostname. Defaults to `false`
+- `method` (String) The HTTP Method for the request. Allowed methods are a subset of methods defined in [RFC7231](https://datatracker.ietf.org/doc/html/rfc7231#section-4.3) namely, `GET`, `HEAD`, and `POST`. `POST` support is only intended for read-only URLs, such as submitting a search.
+- `requestBody` (String) The request body as a string.
+- `requestHeaders` (Map of String) A map of request header field names and values.
+- `requestTimeoutMs` (Number) The request timeout in milliseconds.
+- `retry` (Block, Optional) Retry request configuration. By default there are no retries. Configuring this block will result in retries if an error is returned by the client (e.g., connection errors) or if a 5xx-range (except 501) status code is received. For further details see [go-retryablehttp](https://pkg.go.dev/github.com/hashicorp/go-retryablehttp). (see [below for nested schema](#nestedblock--retry))
+- `when` (String) When to send the HTTP request. Valid values are `apply` (default) and `destroy`. When set to `apply`, the request is sent during resource creation and updates. When set to `destroy`, the request is only sent during resource destruction.
+
+### Read-Only
+
+- `body` (String, Deprecated) The response body returned as a string. **NOTE**: This is deprecated, use `responseBody` instead.
+- `id` (String) The URL used for the request.
+- `responseBody` (String) The response body returned as a string.
+- `responseBodyBase64` (String) The response body encoded as base64 (standard) as defined in [RFC 4648](https://datatracker.ietf.org/doc/html/rfc4648#section-4).
+- `responseHeaders` (Map of String) A map of response header field names and values. Duplicate headers are concatenated according to [RFC2616](https://www.rfc-editor.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2).
+- `statusCode` (Number) The HTTP response status code.
+
+
+### Nested Schema for `retry`
+
+Optional:
+
+- `attempts` (Number) The number of times the request is to be retried. For example, if 2 is specified, the request will be tried a maximum of 3 times.
+- `maxDelayMs` (Number) The maximum delay between retry requests in milliseconds.
+- `minDelayMs` (Number) The minimum delay between retry requests in milliseconds.
+
+
+
diff --git a/docs/resources/http.md b/docs/resources/http.md
new file mode 100644
index 00000000..32e464b2
--- /dev/null
+++ b/docs/resources/http.md
@@ -0,0 +1,250 @@
+---
+page_title: "http Resource - terraform-provider-http"
+subcategory: ""
+description: |-
+ The http resource makes an HTTP request to the given URL and exports
+ information about the response.
+ The given URL may be either an http or https URL. This resource
+ will issue a warning if the result is not UTF-8 encoded.
+ ~> Important Although https URLs can be used, there is currently no
+ mechanism to authenticate the remote server except for general verification of
+ the server certificate's chain of trust. Data retrieved from servers not under
+ your control should be treated as untrustworthy.
+ By default, there are no retries. Configuring the retry block will result in
+ retries if an error is returned by the client (e.g., connection errors) or if
+ a 5xx-range (except 501) status code is received. For further details see
+ go-retryablehttp https://pkg.go.dev/github.com/hashicorp/go-retryablehttp.
+---
+
+# http (Resource)
+
+The `http` resource makes an HTTP request to the given URL and exports
+information about the response.
+
+The given URL may be either an `http` or `https` URL. This resource
+will issue a warning if the result is not UTF-8 encoded.
+
+~> **Important** Although `https` URLs can be used, there is currently no
+mechanism to authenticate the remote server except for general verification of
+the server certificate's chain of trust. Data retrieved from servers not under
+your control should be treated as untrustworthy.
+
+By default, there are no retries. Configuring the retry block will result in
+retries if an error is returned by the client (e.g., connection errors) or if
+a 5xx-range (except 501) status code is received. For further details see
+[go-retryablehttp](https://pkg.go.dev/github.com/hashicorp/go-retryablehttp).
+
+## Example Usage
+
+```terraform
+# The following example shows how to issue an HTTP request supplying
+# an optional request header.
+resource "http" "example" {
+ url = "https://checkpoint-api.hashicorp.com/v1/check/terraform"
+
+ # Optional request headers
+ request_headers = {
+ Accept = "application/json"
+ }
+}
+
+# The following example shows how to issue an HTTP HEAD request.
+resource "http" "example_head" {
+ url = "https://checkpoint-api.hashicorp.com/v1/check/terraform"
+ method = "HEAD"
+}
+
+# The following example shows how to issue an HTTP POST request
+# supplying an optional request body.
+resource "http" "example_post" {
+ url = "https://checkpoint-api.hashicorp.com/v1/check/terraform"
+ method = "POST"
+
+ # Optional request body
+ request_body = "request body"
+}
+```
+
+## Controlling when the request is sent
+
+Use the resource argument `when` to control whether the HTTP request is executed during apply operations or only during destroy:
+
+- `apply` (default): request is executed during create and update.
+- `destroy`: request is executed only during resource destruction.
+
+```terraform
+# Example 1: HTTP request on apply (default behavior)
+resource "http" "example_apply" {
+ url = "https://httpbin.org/get"
+
+ request_headers = {
+ Accept = "application/json"
+ }
+
+ # This is the default behavior - request is sent during apply
+ when = "apply"
+}
+
+# Example 2: HTTP request only on destroy
+resource "http" "example_destroy" {
+ url = "https://httpbin.org/delete"
+ method = "DELETE"
+
+ request_headers = {
+ Accept = "application/json"
+ }
+
+ # Request is only sent during resource destruction
+ when = "destroy"
+}
+
+# Example 3: Default behavior (no when attribute specified)
+resource "http" "example_default" {
+ url = "https://httpbin.org/get"
+
+ request_headers = {
+ Accept = "application/json"
+ }
+
+ # No "when" attribute specified - defaults to "apply"
+}
+
+output "example_apply_status_code" {
+ value = http.example_apply.status_code
+}
+
+output "example_destroy_status_code" {
+ value = http.example_destroy.status_code
+}
+
+output "example_default_status_code" {
+ value = http.example_default.status_code
+}
+```
+
+## Usage with Postcondition
+
+Note: Pre/postconditions validate values during apply. They are only meaningful when the resource executes the HTTP request during apply, i.e., when `when = "apply"` (default). If `when = "destroy"`, these conditions will not evaluate against a fresh request result.
+
+[Precondition and Postcondition](https://www.terraform.io/language/expressions/custom-conditions)
+checks are available with Terraform v1.2.0 and later.
+
+```terraform
+resource "http" "example_postcondition" {
+ url = "https://checkpoint-api.hashicorp.com/v1/check/terraform"
+
+ request_headers = {
+ Accept = "application/json"
+ }
+
+ # Ensure the request runs at apply for postconditions
+ when = "apply"
+
+ lifecycle {
+ postcondition {
+ condition = contains([201, 204], self.status_code)
+ error_message = "Status code invalid"
+ }
+ }
+}
+
+output "postcondition_status_code" {
+ value = http.example_postcondition.status_code
+}
+```
+
+## Usage with Precondition
+
+Note: Pre/postconditions validate values during apply. They are only meaningful when the resource executes the HTTP request during apply, i.e., when `when = "apply"` (default). If `when = "destroy"`, these conditions will not evaluate against a fresh request result.
+
+[Precondition and Postcondition](https://www.terraform.io/language/expressions/custom-conditions)
+checks are available with Terraform v1.2.0 and later.
+
+```terraform
+resource "http" "example_precondition" {
+ url = "https://checkpoint-api.hashicorp.com/v1/check/terraform"
+
+ request_headers = {
+ Accept = "application/json"
+ }
+
+ # Ensure the request runs at apply for preconditions
+ when = "apply"
+
+ lifecycle {
+ precondition {
+ condition = contains([200, 201, 204], self.status_code)
+ error_message = "Unexpected status code"
+ }
+ }
+}
+
+output "precondition_status_code" {
+ value = http.example_precondition.status_code
+}
+```
+
+## Usage with Provisioner
+
+[Failure Behaviour](https://www.terraform.io/language/resources/provisioners/syntax#failure-behavior)
+can be leveraged within a provisioner in order to raise an error and stop applying.
+
+```terraform
+resource "http" "example" {
+ url = "https://checkpoint-api.hashicorp.com/v1/check/terraform"
+
+ request_headers = {
+ Accept = "application/json"
+ }
+}
+
+resource "null_resource" "example" {
+ # On success, this will attempt to execute the true command in the
+ # shell environment running terraform.
+ # On failure, this will attempt to execute the false command in the
+ # shell environment running terraform.
+ provisioner "local-exec" {
+ command = contains([201, 204], http.example.status_code)
+ }
+}
+```
+
+
+## Schema
+
+### Required
+
+- `url` (String) The URL for the request. Supported schemes are `http` and `https`.
+
+### Optional
+
+- `ca_cert_pem` (String) Certificate Authority (CA) in [PEM (RFC 1421)](https://datatracker.ietf.org/doc/html/rfc1421) format.
+- `client_cert_pem` (String) Client certificate in [PEM (RFC 1421)](https://datatracker.ietf.org/doc/html/rfc1421) format.
+- `client_key_pem` (String) Client key in [PEM (RFC 1421)](https://datatracker.ietf.org/doc/html/rfc1421) format.
+- `insecure` (Boolean) Disables verification of the server's certificate chain and hostname. Defaults to `false`
+- `method` (String) The HTTP Method for the request. Allowed methods are a subset of methods defined in [RFC7231](https://datatracker.ietf.org/doc/html/rfc7231#section-4.3) namely, `GET`, `HEAD`, and `POST`. `POST` support is only intended for read-only URLs, such as submitting a search.
+- `request_body` (String) The request body as a string.
+- `request_headers` (Map of String) A map of request header field names and values.
+- `request_timeout_ms` (Number) The request timeout in milliseconds.
+- `retry` (Block, Optional) Retry request configuration. By default there are no retries. Configuring this block will result in retries if an error is returned by the client (e.g., connection errors) or if a 5xx-range (except 501) status code is received. For further details see [go-retryablehttp](https://pkg.go.dev/github.com/hashicorp/go-retryablehttp). (see [below for nested schema](#nestedblock--retry))
+- `when` (String) When to send the HTTP request. Valid values are `apply` (default) and `destroy`. When set to `apply`, the request is sent during resource creation and updates. When set to `destroy`, the request is only sent during resource destruction.
+
+### Read-Only
+
+- `body` (String, Deprecated) The response body returned as a string. **NOTE**: This is deprecated, use `response_body` instead.
+- `id` (String) The URL used for the request.
+- `response_body` (String) The response body returned as a string.
+- `response_body_base64` (String) The response body encoded as base64 (standard) as defined in [RFC 4648](https://datatracker.ietf.org/doc/html/rfc4648#section-4).
+- `response_headers` (Map of String) A map of response header field names and values. Duplicate headers are concatenated according to [RFC2616](https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2).
+- `status_code` (Number) The HTTP response status code.
+
+
+### Nested Schema for `retry`
+
+Optional:
+
+- `attempts` (Number) The number of times the request is to be retried. For example, if 2 is specified, the request will be tried a maximum of 3 times.
+- `max_delay_ms` (Number) The maximum delay between retry requests in milliseconds.
+- `min_delay_ms` (Number) The minimum delay between retry requests in milliseconds.
+
+
diff --git a/examples/resources/http/postcondition.tf b/examples/resources/http/postcondition.tf
new file mode 100644
index 00000000..00209649
--- /dev/null
+++ b/examples/resources/http/postcondition.tf
@@ -0,0 +1,23 @@
+resource "http" "example_postcondition" {
+ url = "https://checkpoint-api.hashicorp.com/v1/check/terraform"
+
+ request_headers = {
+ Accept = "application/json"
+ }
+
+ # Ensure the request runs at apply for postconditions
+ when = "apply"
+
+ lifecycle {
+ postcondition {
+ condition = contains([201, 204], self.status_code)
+ error_message = "Status code invalid"
+ }
+ }
+}
+
+output "postcondition_status_code" {
+ value = http.example_postcondition.status_code
+}
+
+
diff --git a/examples/resources/http/precondition.tf b/examples/resources/http/precondition.tf
new file mode 100644
index 00000000..d96e2ede
--- /dev/null
+++ b/examples/resources/http/precondition.tf
@@ -0,0 +1,23 @@
+resource "http" "example_precondition" {
+ url = "https://checkpoint-api.hashicorp.com/v1/check/terraform"
+
+ request_headers = {
+ Accept = "application/json"
+ }
+
+ # Ensure the request runs at apply for preconditions
+ when = "apply"
+
+ lifecycle {
+ precondition {
+ condition = contains([200, 201, 204], self.status_code)
+ error_message = "Unexpected status code"
+ }
+ }
+}
+
+output "precondition_status_code" {
+ value = http.example_precondition.status_code
+}
+
+
diff --git a/examples/resources/http/provisioner.tf b/examples/resources/http/provisioner.tf
new file mode 100644
index 00000000..a8cfad21
--- /dev/null
+++ b/examples/resources/http/provisioner.tf
@@ -0,0 +1,17 @@
+resource "http" "example" {
+ url = "https://checkpoint-api.hashicorp.com/v1/check/terraform"
+
+ request_headers = {
+ Accept = "application/json"
+ }
+}
+
+resource "null_resource" "example" {
+ # On success, this will attempt to execute the true command in the
+ # shell environment running terraform.
+ # On failure, this will attempt to execute the false command in the
+ # shell environment running terraform.
+ provisioner "local-exec" {
+ command = contains([201, 204], http.example.status_code)
+ }
+}
diff --git a/examples/resources/http/resource.tf b/examples/resources/http/resource.tf
new file mode 100644
index 00000000..26504bf6
--- /dev/null
+++ b/examples/resources/http/resource.tf
@@ -0,0 +1,26 @@
+# The following example shows how to issue an HTTP request supplying
+# an optional request header.
+resource "http" "example" {
+ url = "https://checkpoint-api.hashicorp.com/v1/check/terraform"
+
+ # Optional request headers
+ request_headers = {
+ Accept = "application/json"
+ }
+}
+
+# The following example shows how to issue an HTTP HEAD request.
+resource "http" "example_head" {
+ url = "https://checkpoint-api.hashicorp.com/v1/check/terraform"
+ method = "HEAD"
+}
+
+# The following example shows how to issue an HTTP POST request
+# supplying an optional request body.
+resource "http" "example_post" {
+ url = "https://checkpoint-api.hashicorp.com/v1/check/terraform"
+ method = "POST"
+
+ # Optional request body
+ request_body = "request body"
+}
diff --git a/examples/resources/http/when_attribute.tf b/examples/resources/http/when_attribute.tf
new file mode 100644
index 00000000..cd0d95bd
--- /dev/null
+++ b/examples/resources/http/when_attribute.tf
@@ -0,0 +1,49 @@
+# Example 1: HTTP request on apply (default behavior)
+resource "http" "example_apply" {
+ url = "https://httpbin.org/get"
+
+ request_headers = {
+ Accept = "application/json"
+ }
+
+ # This is the default behavior - request is sent during apply
+ when = "apply"
+}
+
+# Example 2: HTTP request only on destroy
+resource "http" "example_destroy" {
+ url = "https://httpbin.org/delete"
+ method = "DELETE"
+
+ request_headers = {
+ Accept = "application/json"
+ }
+
+ # Request is only sent during resource destruction
+ when = "destroy"
+}
+
+# Example 3: Default behavior (no when attribute specified)
+resource "http" "example_default" {
+ url = "https://httpbin.org/get"
+
+ request_headers = {
+ Accept = "application/json"
+ }
+
+ # No "when" attribute specified - defaults to "apply"
+}
+
+output "example_apply_status_code" {
+ value = http.example_apply.status_code
+}
+
+output "example_destroy_status_code" {
+ value = http.example_destroy.status_code
+}
+
+output "example_default_status_code" {
+ value = http.example_default.status_code
+}
+
+
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index aa8a3f46..381bb185 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -30,7 +30,9 @@ func (p *httpProvider) Configure(context.Context, provider.ConfigureRequest, *pr
}
func (p *httpProvider) Resources(context.Context) []func() resource.Resource {
- return nil
+ return []func() resource.Resource{
+ NewHttpResource,
+ }
}
func (p *httpProvider) DataSources(context.Context) []func() datasource.DataSource {
diff --git a/internal/provider/resource_http.go b/internal/provider/resource_http.go
new file mode 100644
index 00000000..9a1008ed
--- /dev/null
+++ b/internal/provider/resource_http.go
@@ -0,0 +1,577 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+ "unicode/utf8"
+
+ "github.com/hashicorp/go-retryablehttp"
+ "github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
+ "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ rs "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/tfsdk"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+ "golang.org/x/net/http/httpproxy"
+)
+
+var _ resource.Resource = (*httpResource)(nil)
+
+func NewHttpResource() resource.Resource {
+ return &httpResource{}
+}
+
+type httpResource struct{}
+
+func (r *httpResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
+ // Resource name matches the data source name intentionally.
+ resp.TypeName = "http"
+}
+
+func (r *httpResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = rs.Schema{
+ Description: `
+The ` + "`http`" + ` resource makes an HTTP request to the given URL and exports
+information about the response.
+
+The given URL may be either an ` + "`http`" + ` or ` + "`https`" + ` URL. This resource
+will issue a warning if the result is not UTF-8 encoded.
+
+~> **Important** Although ` + "`https`" + ` URLs can be used, there is currently no
+mechanism to authenticate the remote server except for general verification of
+the server certificate's chain of trust. Data retrieved from servers not under
+your control should be treated as untrustworthy.
+
+By default, there are no retries. Configuring the retry block will result in
+retries if an error is returned by the client (e.g., connection errors) or if
+a 5xx-range (except 501) status code is received. For further details see
+[go-retryablehttp](https://pkg.go.dev/github.com/hashicorp/go-retryablehttp).
+`,
+
+ Attributes: map[string]rs.Attribute{
+ "id": rs.StringAttribute{
+ Description: "The URL used for the request.",
+ Computed: true,
+ },
+
+ "url": rs.StringAttribute{
+ Description: "The URL for the request. Supported schemes are `http` and `https`.",
+ Required: true,
+ },
+
+ "method": rs.StringAttribute{
+ Description: "The HTTP Method for the request. " +
+ "Allowed methods are a subset of methods defined in [RFC7231](https://datatracker.ietf.org/doc/html/rfc7231#section-4.3) namely, " +
+ "`GET`, `HEAD`, and `POST`. `POST` support is only intended for read-only URLs, such as submitting a search.",
+ Optional: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf([]string{
+ http.MethodGet,
+ http.MethodPost,
+ http.MethodHead,
+ }...),
+ },
+ },
+
+ "request_headers": rs.MapAttribute{
+ Description: "A map of request header field names and values.",
+ ElementType: types.StringType,
+ Optional: true,
+ },
+
+ "request_body": rs.StringAttribute{
+ Description: "The request body as a string.",
+ Optional: true,
+ },
+
+ "request_timeout_ms": rs.Int64Attribute{
+ Description: "The request timeout in milliseconds.",
+ Optional: true,
+ Validators: []validator.Int64{
+ int64validator.AtLeast(1),
+ },
+ },
+
+ "response_body": rs.StringAttribute{
+ Description: "The response body returned as a string.",
+ Computed: true,
+ },
+
+ "body": rs.StringAttribute{
+ Description: "The response body returned as a string. " +
+ "**NOTE**: This is deprecated, use `response_body` instead.",
+ Computed: true,
+ DeprecationMessage: "Use response_body instead",
+ },
+
+ "response_body_base64": rs.StringAttribute{
+ Description: "The response body encoded as base64 (standard) as defined in [RFC 4648](https://datatracker.ietf.org/doc/html/rfc4648#section-4).",
+ Computed: true,
+ },
+
+ "ca_cert_pem": rs.StringAttribute{
+ Description: "Certificate Authority (CA) " +
+ "in [PEM (RFC 1421)](https://datatracker.ietf.org/doc/html/rfc1421) format.",
+ Optional: true,
+ Validators: []validator.String{
+ stringvalidator.ConflictsWith(path.MatchRoot("insecure")),
+ },
+ },
+
+ "client_cert_pem": rs.StringAttribute{
+ Description: "Client certificate " +
+ "in [PEM (RFC 1421)](https://datatracker.ietf.org/doc/html/rfc1421) format.",
+ Optional: true,
+ Validators: []validator.String{
+ stringvalidator.AlsoRequires(path.MatchRoot("client_key_pem")),
+ },
+ },
+
+ "client_key_pem": rs.StringAttribute{
+ Description: "Client key " +
+ "in [PEM (RFC 1421)](https://datatracker.ietf.org/doc/html/rfc1421) format.",
+ Optional: true,
+ Validators: []validator.String{
+ stringvalidator.AlsoRequires(path.MatchRoot("client_cert_pem")),
+ },
+ },
+
+ "insecure": rs.BoolAttribute{
+ Description: "Disables verification of the server's certificate chain and hostname. Defaults to `false`",
+ Optional: true,
+ },
+
+ "when": rs.StringAttribute{
+ Description: "When to send the HTTP request. Valid values are `apply` (default) and `destroy`. " +
+ "When set to `apply`, the request is sent during resource creation and updates. " +
+ "When set to `destroy`, the request is only sent during resource destruction.",
+ Optional: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf([]string{
+ "apply",
+ "destroy",
+ }...),
+ },
+ },
+
+ "response_headers": rs.MapAttribute{
+ Description: `A map of response header field names and values.` +
+ ` Duplicate headers are concatenated according to [RFC2616](https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2).`,
+ ElementType: types.StringType,
+ Computed: true,
+ },
+
+ "status_code": rs.Int64Attribute{
+ Description: `The HTTP response status code.`,
+ Computed: true,
+ },
+ },
+
+ Blocks: map[string]rs.Block{
+ "retry": rs.SingleNestedBlock{
+ Description: "Retry request configuration. By default there are no retries. Configuring this block will result in " +
+ "retries if an error is returned by the client (e.g., connection errors) or if a 5xx-range (except 501) status code is received. " +
+ "For further details see [go-retryablehttp](https://pkg.go.dev/github.com/hashicorp/go-retryablehttp).",
+ Attributes: map[string]rs.Attribute{
+ "attempts": rs.Int64Attribute{
+ Description: "The number of times the request is to be retried. For example, if 2 is specified, the request will be tried a maximum of 3 times.",
+ Optional: true,
+ Validators: []validator.Int64{
+ int64validator.AtLeast(0),
+ },
+ },
+ "min_delay_ms": rs.Int64Attribute{
+ Description: "The minimum delay between retry requests in milliseconds.",
+ Optional: true,
+ Validators: []validator.Int64{
+ int64validator.AtLeast(0),
+ },
+ },
+ "max_delay_ms": rs.Int64Attribute{
+ Description: "The maximum delay between retry requests in milliseconds.",
+ Optional: true,
+ Validators: []validator.Int64{
+ int64validator.AtLeast(0),
+ int64validator.AtLeastSumOf(path.MatchRelative().AtParent().AtName("min_delay_ms")),
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func (r *httpResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+}
+
+func (r *httpResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var model modelV1
+ diags := req.Plan.Get(ctx, &model)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Only perform request if "when" is set to "apply" (default behavior when not specified)
+ whenValue := "apply"
+ if !model.When.IsNull() && !model.When.IsUnknown() {
+ whenValue = model.When.ValueString()
+ }
+
+ if whenValue == "apply" {
+ if err := r.performRequest(ctx, &model, &resp.Diagnostics); err != nil {
+ return
+ }
+ } else {
+ // Set default values for computed fields when not making request
+ model.ID = types.StringValue(model.URL.ValueString())
+
+ // Create an empty map for response headers
+ emptyHeaders := make(map[string]attr.Value)
+ model.ResponseHeaders = types.MapValueMust(types.StringType, emptyHeaders)
+
+ model.ResponseBody = types.StringValue("")
+ model.Body = types.StringValue("")
+ model.ResponseBodyBase64 = types.StringValue("")
+ model.StatusCode = types.Int64Value(0)
+ }
+
+ diags = resp.State.Set(ctx, model)
+ resp.Diagnostics.Append(diags...)
+}
+
+func (r *httpResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var model modelV1
+ diags := req.State.Get(ctx, &model)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // No HTTP request is performed during read operations
+ // Ensure computed fields are properly set if they're null/unknown
+ if model.ID.IsNull() || model.ID.IsUnknown() {
+ model.ID = types.StringValue(model.URL.ValueString())
+ }
+ if model.ResponseHeaders.IsNull() || model.ResponseHeaders.IsUnknown() {
+ emptyHeaders := make(map[string]attr.Value)
+ model.ResponseHeaders = types.MapValueMust(types.StringType, emptyHeaders)
+ }
+ if model.ResponseBody.IsNull() || model.ResponseBody.IsUnknown() {
+ model.ResponseBody = types.StringValue("")
+ }
+ if model.Body.IsNull() || model.Body.IsUnknown() {
+ model.Body = types.StringValue("")
+ }
+ if model.ResponseBodyBase64.IsNull() || model.ResponseBodyBase64.IsUnknown() {
+ model.ResponseBodyBase64 = types.StringValue("")
+ }
+ if model.StatusCode.IsNull() || model.StatusCode.IsUnknown() {
+ model.StatusCode = types.Int64Value(0)
+ }
+
+ diags = resp.State.Set(ctx, model)
+ resp.Diagnostics.Append(diags...)
+}
+
+func (r *httpResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ // Preserve computed fields across updates; reflect config changes and optionally perform request
+ var plan, state modelV1
+ var diags diag.Diagnostics
+
+ // Read desired configuration from plan
+ d := req.Plan.Get(ctx, &plan)
+ resp.Diagnostics.Append(d...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Read prior state to retain computed fields when needed
+ d = req.State.Get(ctx, &state)
+ resp.Diagnostics.Append(d...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ whenValue := "apply"
+ if !plan.When.IsNull() && !plan.When.IsUnknown() {
+ whenValue = plan.When.ValueString()
+ }
+
+ // Begin with desired config (plan)
+ model := plan
+
+ if whenValue == "apply" {
+ if err := r.performRequest(ctx, &model, &resp.Diagnostics); err != nil {
+ return
+ }
+ } else {
+ // Keep previous computed fields when not issuing a request
+ model.ID = state.ID
+ model.ResponseHeaders = state.ResponseHeaders
+ model.ResponseBody = state.ResponseBody
+ model.Body = state.Body
+ model.ResponseBodyBase64 = state.ResponseBodyBase64
+ model.StatusCode = state.StatusCode
+ }
+
+ diags = resp.State.Set(ctx, model)
+ resp.Diagnostics.Append(diags...)
+}
+
+func (r *httpResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var model modelV1
+ diags := req.State.Get(ctx, &model)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Only perform request if "when" is set to "destroy"
+ whenValue := "apply"
+ if !model.When.IsNull() && !model.When.IsUnknown() {
+ whenValue = model.When.ValueString()
+ }
+
+ if whenValue == "destroy" {
+ if err := r.performRequest(ctx, &model, &resp.Diagnostics); err != nil {
+ return
+ }
+ }
+}
+
+func (r *httpResource) performRequest(ctx context.Context, model *modelV1, diags *diag.Diagnostics) error {
+ requestURL := model.URL.ValueString()
+ method := model.Method.ValueString()
+ requestHeaders := model.RequestHeaders
+
+ if method == "" {
+ method = http.MethodGet
+ }
+
+ caCertificate := model.CaCertificate
+
+ tr, ok := http.DefaultTransport.(*http.Transport)
+ if !ok {
+ diags.AddError(
+ "Error configuring http transport",
+ "Error http: Can't configure http transport.",
+ )
+ return fmt.Errorf("transport clone")
+ }
+
+ clonedTr := tr.Clone()
+
+ clonedTr.Proxy = func(req *http.Request) (*url.URL, error) {
+ return httpproxy.FromEnvironment().ProxyFunc()(req.URL)
+ }
+
+ if clonedTr.TLSClientConfig == nil {
+ clonedTr.TLSClientConfig = &tls.Config{}
+ }
+
+ if !model.Insecure.IsNull() {
+ if clonedTr.TLSClientConfig == nil {
+ clonedTr.TLSClientConfig = &tls.Config{}
+ }
+ clonedTr.TLSClientConfig.InsecureSkipVerify = model.Insecure.ValueBool()
+ }
+
+ // Use `ca_cert_pem` cert pool
+ if !caCertificate.IsNull() {
+ caCertPool := x509.NewCertPool()
+ if ok := caCertPool.AppendCertsFromPEM([]byte(caCertificate.ValueString())); !ok {
+ diags.AddError(
+ "Error configuring TLS client",
+ "Error tls: Can't add the CA certificate to certificate pool. Only PEM encoded certificates are supported.",
+ )
+ return fmt.Errorf("bad ca cert")
+ }
+
+ if clonedTr.TLSClientConfig == nil {
+ clonedTr.TLSClientConfig = &tls.Config{}
+ }
+ clonedTr.TLSClientConfig.RootCAs = caCertPool
+ }
+
+ if !model.ClientCert.IsNull() && !model.ClientKey.IsNull() {
+ cert, err := tls.X509KeyPair([]byte(model.ClientCert.ValueString()), []byte(model.ClientKey.ValueString()))
+ if err != nil {
+ diags.AddError(
+ "error creating x509 key pair",
+ fmt.Sprintf("error creating x509 key pair from provided pem blocks\n\nError: %s", err),
+ )
+ return err
+ }
+ clonedTr.TLSClientConfig.Certificates = []tls.Certificate{cert}
+ }
+
+ var retry retryModel
+ if !model.Retry.IsNull() && !model.Retry.IsUnknown() {
+ if d := model.Retry.As(ctx, &retry, basetypes.ObjectAsOptions{}); d.HasError() {
+ diags.Append(d...)
+ return fmt.Errorf("retry decode")
+ }
+ }
+
+ retryClient := retryablehttp.NewClient()
+ retryClient.HTTPClient.Transport = clonedTr
+
+ var timeout time.Duration
+
+ if model.RequestTimeout.ValueInt64() > 0 {
+ timeout = time.Duration(model.RequestTimeout.ValueInt64()) * time.Millisecond
+ retryClient.HTTPClient.Timeout = timeout
+ }
+
+ retryClient.Logger = levelledLogger{ctx}
+ retryClient.RetryMax = int(retry.Attempts.ValueInt64())
+
+ if !retry.MinDelay.IsNull() && !retry.MinDelay.IsUnknown() && retry.MinDelay.ValueInt64() >= 0 {
+ retryClient.RetryWaitMin = time.Duration(retry.MinDelay.ValueInt64()) * time.Millisecond
+ }
+
+ if !retry.MaxDelay.IsNull() && !retry.MaxDelay.IsUnknown() && retry.MaxDelay.ValueInt64() >= 0 {
+ retryClient.RetryWaitMax = time.Duration(retry.MaxDelay.ValueInt64()) * time.Millisecond
+ }
+
+ request, err := retryablehttp.NewRequestWithContext(ctx, method, requestURL, nil)
+ if err != nil {
+ diags.AddError(
+ "Error creating request",
+ fmt.Sprintf("Error creating request: %s", err),
+ )
+ return err
+ }
+
+ if !model.RequestBody.IsNull() {
+ err = request.SetBody(strings.NewReader(model.RequestBody.ValueString()))
+
+ if err != nil {
+ diags.AddError(
+ "Error Setting Request Body",
+ "An unexpected error occurred while setting the request body: "+err.Error(),
+ )
+
+ return err
+ }
+ }
+
+ for name, value := range requestHeaders.Elements() {
+ var header string
+ d := tfsdk.ValueAs(ctx, value, &header)
+ diags.Append(d...)
+ if diags.HasError() {
+ return fmt.Errorf("header decode")
+ }
+
+ request.Header.Set(name, header)
+ if strings.ToLower(name) == "host" {
+ request.Host = header
+ }
+ }
+
+ response, err := retryClient.Do(request)
+ if err != nil {
+ target := &url.Error{}
+ if errors.As(err, &target) {
+ if target.Timeout() {
+ detail := fmt.Sprintf("timeout error: %s", err)
+
+ if timeout > 0 {
+ detail = fmt.Sprintf("request exceeded the specified timeout: %s, err: %s", timeout.String(), err)
+ }
+
+ diags.AddError(
+ "Error making request",
+ detail,
+ )
+ return err
+ }
+ }
+
+ diags.AddError(
+ "Error making request",
+ fmt.Sprintf("Error making request: %s", err),
+ )
+ return err
+ }
+
+ defer response.Body.Close()
+
+ bytes, err := io.ReadAll(response.Body)
+ if err != nil {
+ diags.AddError(
+ "Error reading response body",
+ fmt.Sprintf("Error reading response body: %s", err),
+ )
+ return err
+ }
+
+ if !utf8.Valid(bytes) {
+ diags.AddWarning(
+ "Response body is not recognized as UTF-8",
+ "Terraform may not properly handle the response_body if the contents are binary.",
+ )
+ }
+
+ responseBody := string(bytes)
+ responseBodyBase64Std := base64.StdEncoding.EncodeToString(bytes)
+
+ responseHeaders := make(map[string]string)
+ for k, v := range response.Header {
+ // Concatenate according to RFC9110 https://www.rfc-editor.org/rfc/rfc9110.html#section-5.2
+ responseHeaders[k] = strings.Join(v, ", ")
+ }
+
+ respHeadersState, d := types.MapValueFrom(ctx, types.StringType, responseHeaders)
+ diags.Append(d...)
+ if diags.HasError() {
+ return fmt.Errorf("headers state")
+ }
+
+ model.ID = types.StringValue(requestURL)
+ model.ResponseHeaders = respHeadersState
+ model.ResponseBody = types.StringValue(responseBody)
+ model.Body = types.StringValue(responseBody)
+ model.ResponseBodyBase64 = types.StringValue(responseBodyBase64Std)
+ model.StatusCode = types.Int64Value(int64(response.StatusCode))
+
+ return nil
+}
+
+type modelV1 struct {
+ ID types.String `tfsdk:"id"`
+ URL types.String `tfsdk:"url"`
+ Method types.String `tfsdk:"method"`
+ RequestHeaders types.Map `tfsdk:"request_headers"`
+ RequestBody types.String `tfsdk:"request_body"`
+ RequestTimeout types.Int64 `tfsdk:"request_timeout_ms"`
+ Retry types.Object `tfsdk:"retry"`
+ When types.String `tfsdk:"when"`
+ ResponseHeaders types.Map `tfsdk:"response_headers"`
+ CaCertificate types.String `tfsdk:"ca_cert_pem"`
+ ClientCert types.String `tfsdk:"client_cert_pem"`
+ ClientKey types.String `tfsdk:"client_key_pem"`
+ Insecure types.Bool `tfsdk:"insecure"`
+ ResponseBody types.String `tfsdk:"response_body"`
+ Body types.String `tfsdk:"body"`
+ ResponseBodyBase64 types.String `tfsdk:"response_body_base64"`
+ StatusCode types.Int64 `tfsdk:"status_code"`
+}
diff --git a/internal/provider/resource_http_test.go b/internal/provider/resource_http_test.go
new file mode 100644
index 00000000..66c8bf47
--- /dev/null
+++ b/internal/provider/resource_http_test.go
@@ -0,0 +1,302 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "regexp"
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/hashicorp/terraform-plugin-testing/tfversion"
+)
+
+func TestResource_200(t *testing.T) {
+ testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/plain")
+ w.Header().Set("X-Single", "foobar")
+ w.Header().Add("X-Double", "1")
+ w.Header().Add("X-Double", "2")
+ _, _ = w.Write([]byte("1.0.0"))
+ }))
+ defer testServer.Close()
+
+ resource.ParallelTest(t, resource.TestCase{
+ ProtoV5ProviderFactories: protoV5ProviderFactories(),
+ Steps: []resource.TestStep{
+ {
+ Config: fmt.Sprintf(`
+ resource "http" "http_test" {
+ url = "%s"
+ }`, testServer.URL),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("http.http_test", "response_body", "1.0.0"),
+ resource.TestCheckResourceAttr("http.http_test", "response_headers.Content-Type", "text/plain"),
+ resource.TestCheckResourceAttr("http.http_test", "response_headers.X-Single", "foobar"),
+ resource.TestCheckResourceAttr("http.http_test", "response_headers.X-Double", "1, 2"),
+ resource.TestCheckResourceAttr("http.http_test", "status_code", "200"),
+ ),
+ },
+ },
+ })
+}
+
+func TestResource_200_SlashInPath(t *testing.T) {
+ testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/plain")
+ _, _ = w.Write([]byte("1.0.0"))
+ }))
+ defer testServer.Close()
+
+ resource.ParallelTest(t, resource.TestCase{
+ ProtoV5ProviderFactories: protoV5ProviderFactories(),
+ Steps: []resource.TestStep{
+ {
+ Config: fmt.Sprintf(`
+ resource "http" "http_test" {
+ url = "%s/200"
+ }`, testServer.URL),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("http.http_test", "response_body", "1.0.0"),
+ resource.TestCheckResourceAttr("http.http_test", "response_headers.Content-Type", "text/plain"),
+ resource.TestCheckResourceAttr("http.http_test", "status_code", "200"),
+ ),
+ },
+ },
+ })
+}
+
+func TestResource_404(t *testing.T) {
+ testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/plain")
+ w.WriteHeader(http.StatusNotFound)
+ }))
+ defer testServer.Close()
+
+ resource.ParallelTest(t, resource.TestCase{
+ ProtoV5ProviderFactories: protoV5ProviderFactories(),
+ Steps: []resource.TestStep{
+ {
+ Config: fmt.Sprintf(`
+ resource "http" "http_test" {
+ url = "%s"
+ }`, testServer.URL),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("http.http_test", "response_body", ""),
+ resource.TestCheckResourceAttr("http.http_test", "status_code", "404"),
+ ),
+ },
+ },
+ })
+}
+
+func TestResource_withAuthorizationRequestHeader_200(t *testing.T) {
+ testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Header.Get("Authorization") == "Zm9vOmJhcg==" {
+ w.Header().Set("Content-Type", "text/plain")
+ _, _ = w.Write([]byte("1.0.0"))
+ } else {
+ w.WriteHeader(http.StatusForbidden)
+ }
+ }))
+ defer testServer.Close()
+
+ resource.ParallelTest(t, resource.TestCase{
+ ProtoV5ProviderFactories: protoV5ProviderFactories(),
+ Steps: []resource.TestStep{
+ {
+ Config: fmt.Sprintf(`
+ resource "http" "http_test" {
+ url = "%s"
+
+ request_headers = {
+ "Authorization" = "Zm9vOmJhcg=="
+ }
+ }`, testServer.URL),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("http.http_test", "response_body", "1.0.0"),
+ resource.TestCheckResourceAttr("http.http_test", "status_code", "200"),
+ ),
+ },
+ },
+ })
+}
+
+func TestResource_POST_200(t *testing.T) {
+ testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method == "POST" {
+ w.Header().Set("Content-Type", "text/plain")
+ _, _ = w.Write([]byte("created"))
+ } else {
+ w.WriteHeader(http.StatusMethodNotAllowed)
+ }
+ }))
+ defer testServer.Close()
+
+ resource.ParallelTest(t, resource.TestCase{
+ ProtoV5ProviderFactories: protoV5ProviderFactories(),
+ Steps: []resource.TestStep{
+ {
+ Config: fmt.Sprintf(`
+ resource "http" "http_test" {
+ url = "%s"
+ method = "POST"
+
+ request_body = "request body"
+ }`, testServer.URL),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("http.http_test", "response_body", "created"),
+ resource.TestCheckResourceAttr("http.http_test", "status_code", "200"),
+ ),
+ },
+ },
+ })
+}
+
+func TestResource_Provisioner(t *testing.T) {
+ testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/plain")
+ }))
+ defer testServer.Close()
+
+ resource.ParallelTest(t, resource.TestCase{
+ ProtoV5ProviderFactories: protoV5ProviderFactories(),
+ ExternalProviders: map[string]resource.ExternalProvider{
+ "null": {
+ VersionConstraint: "3.1.1",
+ Source: "hashicorp/null",
+ },
+ },
+ Steps: []resource.TestStep{
+ {
+ Config: fmt.Sprintf(`
+ resource "http" "http_test" {
+ url = "%s"
+ }
+ resource "null_resource" "example" {
+ provisioner "local-exec" {
+ command = contains([201, 204], http.http_test.status_code)
+ }
+ }`, testServer.URL),
+ ExpectError: regexp.MustCompile(`Error running command 'false': exit status 1. Output:`),
+ },
+ {
+ Config: fmt.Sprintf(`
+ resource "http" "http_test" {
+ url = "%s"
+ }
+ resource "null_resource" "example" {
+ provisioner "local-exec" {
+ command = contains([200], http.http_test.status_code)
+ }
+ }`, testServer.URL),
+ Check: resource.TestCheckResourceAttr("http.http_test", "status_code", "200"),
+ },
+ },
+ })
+}
+
+func TestResource_x509cert_skip_on_tf_014(t *testing.T) {
+ testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/x-x509-ca-cert")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte("pem"))
+ }))
+ defer testServer.Close()
+
+ resource.ParallelTest(t, resource.TestCase{
+ TerraformVersionChecks: []tfversion.TerraformVersionCheck{
+ tfversion.SkipBetween(tfversion.Version0_14_0, tfversion.Version0_15_0),
+ },
+ ProtoV5ProviderFactories: protoV5ProviderFactories(),
+ Steps: []resource.TestStep{
+ {
+ Config: fmt.Sprintf(`
+ resource "http" "http_test" {
+ url = "%s/x509-ca-cert/200"
+ }`, testServer.URL),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("http.http_test", "response_body", "pem"),
+ resource.TestCheckResourceAttr("http.http_test", "status_code", "200"),
+ ),
+ },
+ },
+ })
+}
+
+func TestResource_WhenAttribute_Apply(t *testing.T) {
+ testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/plain")
+ _, _ = w.Write([]byte("test response"))
+ }))
+ defer testServer.Close()
+
+ resource.ParallelTest(t, resource.TestCase{
+ ProtoV5ProviderFactories: protoV5ProviderFactories(),
+ Steps: []resource.TestStep{
+ {
+ Config: fmt.Sprintf(`
+ resource "http" "http_test" {
+ url = "%s"
+ when = "apply"
+ }`, testServer.URL),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("http.http_test", "response_body", "test response"),
+ resource.TestCheckResourceAttr("http.http_test", "status_code", "200"),
+ ),
+ },
+ },
+ })
+}
+
+func TestResource_WhenAttribute_Destroy(t *testing.T) {
+ testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/plain")
+ _, _ = w.Write([]byte("test response"))
+ }))
+ defer testServer.Close()
+
+ resource.ParallelTest(t, resource.TestCase{
+ ProtoV5ProviderFactories: protoV5ProviderFactories(),
+ Steps: []resource.TestStep{
+ {
+ Config: fmt.Sprintf(`
+ resource "http" "http_test" {
+ url = "%s"
+ when = "destroy"
+ }`, testServer.URL),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("http.http_test", "response_body", ""),
+ resource.TestCheckResourceAttr("http.http_test", "status_code", "0"),
+ ),
+ },
+ },
+ })
+}
+
+func TestResource_WhenAttribute_Default(t *testing.T) {
+ testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/plain")
+ _, _ = w.Write([]byte("test response"))
+ }))
+ defer testServer.Close()
+
+ resource.ParallelTest(t, resource.TestCase{
+ ProtoV5ProviderFactories: protoV5ProviderFactories(),
+ Steps: []resource.TestStep{
+ {
+ Config: fmt.Sprintf(`
+ resource "http" "http_test" {
+ url = "%s"
+ }`, testServer.URL),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("http.http_test", "response_body", "test response"),
+ resource.TestCheckResourceAttr("http.http_test", "status_code", "200"),
+ ),
+ },
+ },
+ })
+}
diff --git a/templates/resources/http.md.tmpl b/templates/resources/http.md.tmpl
new file mode 100644
index 00000000..3ee0838d
--- /dev/null
+++ b/templates/resources/http.md.tmpl
@@ -0,0 +1,52 @@
+---
+page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}"
+subcategory: ""
+description: |-
+{{ .Description | plainmarkdown | trimspace | prefixlines " " }}
+---
+
+# {{.Name}} ({{.Type}})
+
+{{ .Description | trimspace }}
+
+## Example Usage
+
+{{ tffile "examples/resources/http/resource.tf" }}
+
+## Controlling when the request is sent
+
+Use the resource argument `when` to control whether the HTTP request is executed during apply operations or only during destroy:
+
+- `apply` (default): request is executed during create and update.
+- `destroy`: request is executed only during resource destruction.
+
+{{ tffile "examples/resources/http/when_attribute.tf" }}
+
+## Usage with Postcondition
+
+Note: Pre/postconditions validate values during apply. They are only meaningful when the resource executes the HTTP request during apply, i.e., when `when = "apply"` (default). If `when = "destroy"`, these conditions will not evaluate against a fresh request result.
+
+[Precondition and Postcondition](https://www.terraform.io/language/expressions/custom-conditions)
+checks are available with Terraform v1.2.0 and later.
+
+{{ tffile "examples/resources/http/postcondition.tf" }}
+
+## Usage with Precondition
+
+Note: Pre/postconditions validate values during apply. They are only meaningful when the resource executes the HTTP request during apply, i.e., when `when = "apply"` (default). If `when = "destroy"`, these conditions will not evaluate against a fresh request result.
+
+[Precondition and Postcondition](https://www.terraform.io/language/expressions/custom-conditions)
+checks are available with Terraform v1.2.0 and later.
+
+{{ tffile "examples/resources/http/precondition.tf" }}
+
+## Usage with Provisioner
+
+[Failure Behaviour](https://www.terraform.io/language/resources/provisioners/syntax#failure-behavior)
+can be leveraged within a provisioner in order to raise an error and stop applying.
+
+{{ tffile "examples/resources/http/provisioner.tf" }}
+
+{{ .SchemaMarkdown | trimspace }}
+
+