diff --git a/docs/ephemeral-resources/http.md b/docs/ephemeral-resources/http.md new file mode 100644 index 00000000..8b7d7024 --- /dev/null +++ b/docs/ephemeral-resources/http.md @@ -0,0 +1,75 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "http Ephemeral Resource - terraform-provider-http" +subcategory: "" +description: |- + The http ephemeral resource makes an HTTP GET 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 (Ephemeral Resource) + +The `http` ephemeral resource makes an HTTP GET 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). + + + + +## 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)) + +### 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/internal/provider/data_source_http.go b/internal/provider/data_source_http.go index 0ab7b095..813b545c 100644 --- a/internal/provider/data_source_http.go +++ b/internal/provider/data_source_http.go @@ -22,6 +22,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/tfsdk" @@ -209,12 +210,22 @@ a 5xx-range (except 501) status code is received. For further details see func (d *httpDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { var model modelV0 - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) + resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) if resp.Diagnostics.HasError() { return } + resp.Diagnostics.Append(doRequest(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) +} + +func doRequest(ctx context.Context, model *modelV0) diag.Diagnostics { + var diags diag.Diagnostics + requestURL := model.URL.ValueString() method := model.Method.ValueString() requestHeaders := model.RequestHeaders @@ -227,11 +238,11 @@ func (d *httpDataSource) Read(ctx context.Context, req datasource.ReadRequest, r tr, ok := http.DefaultTransport.(*http.Transport) if !ok { - resp.Diagnostics.AddError( + diags.AddError( "Error configuring http transport", "Error http: Can't configure http transport.", ) - return + return diags } // Prevent issues with multiple data source configurations modifying the shared transport. @@ -257,11 +268,11 @@ func (d *httpDataSource) Read(ctx context.Context, req datasource.ReadRequest, r if !caCertificate.IsNull() { caCertPool := x509.NewCertPool() if ok := caCertPool.AppendCertsFromPEM([]byte(caCertificate.ValueString())); !ok { - resp.Diagnostics.AddError( + diags.AddError( "Error configuring TLS client", "Error tls: Can't add the CA certificate to certificate pool. Only PEM encoded certificates are supported.", ) - return + return diags } if clonedTr.TLSClientConfig == nil { @@ -273,11 +284,11 @@ func (d *httpDataSource) Read(ctx context.Context, req datasource.ReadRequest, r if !model.ClientCert.IsNull() && !model.ClientKey.IsNull() { cert, err := tls.X509KeyPair([]byte(model.ClientCert.ValueString()), []byte(model.ClientKey.ValueString())) if err != nil { - resp.Diagnostics.AddError( + diags.AddError( "error creating x509 key pair", fmt.Sprintf("error creating x509 key pair from provided pem blocks\n\nError: %s", err), ) - return + return diags } clonedTr.TLSClientConfig.Certificates = []tls.Certificate{cert} } @@ -286,9 +297,9 @@ func (d *httpDataSource) Read(ctx context.Context, req datasource.ReadRequest, r if !model.Retry.IsNull() && !model.Retry.IsUnknown() { diags = model.Retry.As(ctx, &retry, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return + diags.Append(diags...) + if diags.HasError() { + return diags } } @@ -314,34 +325,32 @@ func (d *httpDataSource) Read(ctx context.Context, req datasource.ReadRequest, r } request, err := retryablehttp.NewRequestWithContext(ctx, method, requestURL, nil) - if err != nil { - resp.Diagnostics.AddError( + diags.AddError( "Error creating request", fmt.Sprintf("Error creating request: %s", err), ) - return + return diags } if !model.RequestBody.IsNull() { err = request.SetBody(strings.NewReader(model.RequestBody.ValueString())) - if err != nil { - resp.Diagnostics.AddError( + diags.AddError( "Error Setting Request Body", "An unexpected error occurred while setting the request body: "+err.Error(), ) - return + return diags } } for name, value := range requestHeaders.Elements() { var header string diags = tfsdk.ValueAs(ctx, value, &header) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return + diags.Append(diags...) + if diags.HasError() { + return diags } request.Header.Set(name, header) @@ -361,34 +370,34 @@ func (d *httpDataSource) Read(ctx context.Context, req datasource.ReadRequest, r detail = fmt.Sprintf("request exceeded the specified timeout: %s, err: %s", timeout.String(), err) } - resp.Diagnostics.AddError( + diags.AddError( "Error making request", detail, ) - return + return diags } } - resp.Diagnostics.AddError( + diags.AddError( "Error making request", fmt.Sprintf("Error making request: %s", err), ) - return + return diags } defer response.Body.Close() bytes, err := io.ReadAll(response.Body) if err != nil { - resp.Diagnostics.AddError( + diags.AddError( "Error reading response body", fmt.Sprintf("Error reading response body: %s", err), ) - return + return diags } if !utf8.Valid(bytes) { - resp.Diagnostics.AddWarning( + diags.AddWarning( "Response body is not recognized as UTF-8", "Terraform may not properly handle the response_body if the contents are binary.", ) @@ -404,9 +413,9 @@ func (d *httpDataSource) Read(ctx context.Context, req datasource.ReadRequest, r } respHeadersState, diags := types.MapValueFrom(ctx, types.StringType, responseHeaders) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return + diags.Append(diags...) + if diags.HasError() { + return diags } model.ID = types.StringValue(requestURL) @@ -416,8 +425,7 @@ func (d *httpDataSource) Read(ctx context.Context, req datasource.ReadRequest, r model.ResponseBodyBase64 = types.StringValue(responseBodyBase64Std) model.StatusCode = types.Int64Value(int64(response.StatusCode)) - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) + return diags } type modelV0 struct { diff --git a/internal/provider/data_source_http_test.go b/internal/provider/data_source_http_test.go index 10dab105..a0cfef95 100644 --- a/internal/provider/data_source_http_test.go +++ b/internal/provider/data_source_http_test.go @@ -38,7 +38,7 @@ func TestDataSource_200(t *testing.T) { defer testServer.Close() resource.ParallelTest(t, resource.TestCase{ - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { Config: fmt.Sprintf(` @@ -71,7 +71,7 @@ func TestDataSource_200_SlashInPath(t *testing.T) { defer testServer.Close() resource.ParallelTest(t, resource.TestCase{ - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { Config: fmt.Sprintf(` @@ -98,7 +98,7 @@ func TestDataSource_404(t *testing.T) { defer testServer.Close() resource.ParallelTest(t, resource.TestCase{ - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { Config: fmt.Sprintf(` @@ -129,7 +129,7 @@ func TestDataSource_withAuthorizationRequestHeader_200(t *testing.T) { defer testServer.Close() resource.ParallelTest(t, resource.TestCase{ - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { Config: fmt.Sprintf(` @@ -159,7 +159,7 @@ func TestDataSource_withAuthorizationRequestHeader_403(t *testing.T) { defer testServer.Close() resource.ParallelTest(t, resource.TestCase{ - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { Config: fmt.Sprintf(` @@ -190,7 +190,7 @@ func TestDataSource_utf8_200(t *testing.T) { defer testServer.Close() resource.ParallelTest(t, resource.TestCase{ - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { Config: fmt.Sprintf(` @@ -218,7 +218,7 @@ func TestDataSource_utf16_200(t *testing.T) { defer testServer.Close() resource.ParallelTest(t, resource.TestCase{ - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { Config: fmt.Sprintf(` @@ -245,9 +245,9 @@ func TestDataSource_x509cert(t *testing.T) { defer testServer.Close() resource.ParallelTest(t, resource.TestCase{ - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - //test fails in TF 0.14.x due to https://github.com/hashicorp/terraform-provider-http/issues/58 + // test fails in TF 0.14.x due to https://github.com/hashicorp/terraform-provider-http/issues/58 tfversion.SkipBetween(tfversion.Version0_14_0, tfversion.Version0_15_0), }, Steps: []resource.TestStep{ @@ -299,7 +299,7 @@ func TestDataSource_UpgradeFromVersion2_2_0(t *testing.T) { ), }, { - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), Config: fmt.Sprintf(` data "http" "http_test" { url = "%s" @@ -307,7 +307,7 @@ func TestDataSource_UpgradeFromVersion2_2_0(t *testing.T) { PlanOnly: true, }, { - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), Config: fmt.Sprintf(` data "http" "http_test" { url = "%s" @@ -337,7 +337,7 @@ func TestDataSource_Provisioner(t *testing.T) { defer testServer.Close() resource.ParallelTest(t, resource.TestCase{ - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), ExternalProviders: map[string]resource.ExternalProvider{ "null": { VersionConstraint: "3.1.1", @@ -386,7 +386,7 @@ func TestDataSource_POST_200(t *testing.T) { defer testServer.Close() resource.ParallelTest(t, resource.TestCase{ - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { Config: fmt.Sprintf(` @@ -415,7 +415,7 @@ func TestDataSource_HEAD_204(t *testing.T) { defer testServer.Close() resource.ParallelTest(t, resource.TestCase{ - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { Config: fmt.Sprintf(` @@ -441,7 +441,7 @@ func TestDataSource_UnsupportedMethod(t *testing.T) { defer testServer.Close() resource.ParallelTest(t, resource.TestCase{ - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { Config: fmt.Sprintf(` @@ -462,7 +462,7 @@ func TestDataSource_WithCACertificate(t *testing.T) { defer testServer.Close() resource.ParallelTest(t, resource.TestCase{ - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { Config: fmt.Sprintf(` @@ -505,7 +505,7 @@ func TestDataSource_WithClientCert(t *testing.T) { defer testServer.Close() resource.UnitTest(t, resource.TestCase{ - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { // Note: %q is used to handle backspaces in the filepath on windows (\ becomes \\) @@ -541,7 +541,7 @@ func TestDataSource_WithCACertificateFalse(t *testing.T) { defer testServer.Close() resource.ParallelTest(t, resource.TestCase{ - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { Config: fmt.Sprintf(` @@ -563,7 +563,7 @@ func TestDataSource_InsecureTrue(t *testing.T) { defer testServer.Close() resource.ParallelTest(t, resource.TestCase{ - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { Config: fmt.Sprintf(` @@ -587,7 +587,7 @@ func TestDataSource_InsecureFalse(t *testing.T) { defer testServer.Close() resource.ParallelTest(t, resource.TestCase{ - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { Config: fmt.Sprintf(` @@ -616,7 +616,7 @@ func TestDataSource_InsecureUnconfigured(t *testing.T) { defer testServer.Close() resource.ParallelTest(t, resource.TestCase{ - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { Config: fmt.Sprintf(` @@ -642,7 +642,7 @@ func TestDataSource_UnsupportedInsecureCaCert(t *testing.T) { defer testServer.Close() resource.ParallelTest(t, resource.TestCase{ - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { Config: fmt.Sprintf(` @@ -678,7 +678,7 @@ func TestDataSource_HostRequestHeaderOverride_200(t *testing.T) { defer testServer.Close() resource.ParallelTest(t, resource.TestCase{ - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { Config: fmt.Sprintf(` @@ -726,7 +726,6 @@ func TestDataSource_HTTPViaProxyWithEnv(t *testing.T) { defer server.Close() serverURL, err := url.Parse(server.URL) - if err != nil { t.Fatalf("error parsing server URL: %s", err) } @@ -741,7 +740,7 @@ func TestDataSource_HTTPViaProxyWithEnv(t *testing.T) { t.Setenv("HTTPS_PROXY", proxy.URL) resource.Test(t, resource.TestCase{ - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { @@ -766,7 +765,7 @@ func TestDataSource_Timeout(t *testing.T) { defer svr.Close() resource.ParallelTest(t, resource.TestCase{ - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { Config: fmt.Sprintf(` @@ -784,7 +783,7 @@ func TestDataSource_Retry(t *testing.T) { uid := uuid.New() resource.ParallelTest(t, resource.TestCase{ - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { Config: fmt.Sprintf(` @@ -824,7 +823,7 @@ func TestDataSource_MinDelay(t *testing.T) { defer svr.Close() resource.ParallelTest(t, resource.TestCase{ - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { Config: fmt.Sprintf(` @@ -853,7 +852,7 @@ func TestDataSource_MaxDelay(t *testing.T) { defer svr.Close() resource.ParallelTest(t, resource.TestCase{ - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { Config: fmt.Sprintf(` @@ -879,7 +878,7 @@ func TestDataSource_MaxDelayAtLeastEqualToMinDelay(t *testing.T) { defer svr.Close() resource.ParallelTest(t, resource.TestCase{ - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { Config: fmt.Sprintf(` @@ -905,7 +904,6 @@ func TestDataSource_RequestBody(t *testing.T) { w.Header().Set("Content-Type", "text/plain") requestBody, err := io.ReadAll(r.Body) - if err != nil { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(`Request Body Read Error: ` + err.Error())) @@ -927,7 +925,7 @@ func TestDataSource_RequestBody(t *testing.T) { defer svr.Close() resource.UnitTest(t, resource.TestCase{ - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { Config: fmt.Sprintf(` @@ -970,7 +968,7 @@ func TestDataSource_ResponseBodyText(t *testing.T) { defer svr.Close() resource.ParallelTest(t, resource.TestCase{ - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { Config: fmt.Sprintf(` @@ -998,10 +996,10 @@ func TestDataSource_ResponseBodyBinary(t *testing.T) { defer svr.Close() resource.ParallelTest(t, resource.TestCase{ - ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - //test fails in TF 0.14.x due to quirk in behavior - //where a warning results in nothing being written to output. + // test fails in TF 0.14.x due to quirk in behavior + // where a warning results in nothing being written to output. tfversion.SkipBetween(tfversion.Version0_14_0, tfversion.Version0_15_0), }, Steps: []resource.TestStep{ diff --git a/internal/provider/ephemeral_http.go b/internal/provider/ephemeral_http.go new file mode 100644 index 00000000..072b1584 --- /dev/null +++ b/internal/provider/ephemeral_http.go @@ -0,0 +1,204 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ ephemeral.EphemeralResource = (*httpEphemeralResource)(nil) + +func NewHttpEphemeralResource() ephemeral.EphemeralResource { + return &httpEphemeralResource{} +} + +type httpEphemeralResource struct{} + +func (d *httpEphemeralResource) Metadata(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "http" +} + +func (d *httpEphemeralResource) Schema(ctx context.Context, req ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: ` +The ` + "`http`" + ` ephemeral resource makes an HTTP GET 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]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The URL used for the request.", + Computed: true, + }, + + "url": schema.StringAttribute{ + Description: "The URL for the request. Supported schemes are `http` and `https`.", + Required: true, + }, + + "method": schema.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": schema.MapAttribute{ + Description: "A map of request header field names and values.", + ElementType: types.StringType, + Optional: true, + }, + + "request_body": schema.StringAttribute{ + Description: "The request body as a string.", + Optional: true, + }, + + "request_timeout_ms": schema.Int64Attribute{ + Description: "The request timeout in milliseconds.", + Optional: true, + Validators: []validator.Int64{ + int64validator.AtLeast(1), + }, + }, + + "response_body": schema.StringAttribute{ + Description: "The response body returned as a string.", + Computed: true, + }, + + "body": schema.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": schema.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": schema.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": schema.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": schema.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": schema.BoolAttribute{ + Description: "Disables verification of the server's certificate chain and hostname. Defaults to `false`", + Optional: true, + }, + + "response_headers": schema.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": schema.Int64Attribute{ + Description: `The HTTP response status code.`, + Computed: true, + }, + }, + + Blocks: map[string]schema.Block{ + "retry": schema.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]schema.Attribute{ + "attempts": schema.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": schema.Int64Attribute{ + Description: "The minimum delay between retry requests in milliseconds.", + Optional: true, + Validators: []validator.Int64{ + int64validator.AtLeast(0), + }, + }, + "max_delay_ms": schema.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 (d *httpEphemeralResource) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + var model modelV0 + resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(doRequest(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Result.Set(ctx, model)...) +} diff --git a/internal/provider/ephemeral_http_test.go b/internal/provider/ephemeral_http_test.go new file mode 100644 index 00000000..8cbbd574 --- /dev/null +++ b/internal/provider/ephemeral_http_test.go @@ -0,0 +1,68 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestEphemeral_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") + _, err := w.Write([]byte("1.0.0")) + if err != nil { + t.Errorf("error writing body: %s", err) + } + })) + defer testServer.Close() + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: protoV6ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + ephemeral "http" "http_test" { + url = "%s" + } + provider "echo" { + data = ephemeral.http.http_test + } + resource "echo" "out" {}`, testServer.URL), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("echo.out", + tfjsonpath.New("data").AtMapKey("response_body"), + knownvalue.StringExact("1.0.0"), + ), + statecheck.ExpectKnownValue("echo.out", + tfjsonpath.New("data").AtMapKey("status_code"), + knownvalue.Int64Exact(200), + ), + statecheck.ExpectKnownValue("echo.out", + tfjsonpath.New("data").AtMapKey("response_headers").AtMapKey("Content-Type"), + knownvalue.StringExact("text/plain"), + ), + statecheck.ExpectKnownValue("echo.out", + tfjsonpath.New("data").AtMapKey("response_headers").AtMapKey("X-Single"), + knownvalue.StringExact("foobar"), + ), + statecheck.ExpectKnownValue("echo.out", + tfjsonpath.New("data").AtMapKey("response_headers").AtMapKey("X-Double"), + knownvalue.StringExact("1, 2"), + ), + }, + }, + }, + }) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index aa8a3f46..55be2d2e 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -7,6 +7,7 @@ import ( "context" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" ) @@ -15,7 +16,7 @@ func New() provider.Provider { return &httpProvider{} } -var _ provider.Provider = (*httpProvider)(nil) +var _ provider.ProviderWithEphemeralResources = (*httpProvider)(nil) type httpProvider struct{} @@ -38,3 +39,9 @@ func (p *httpProvider) DataSources(context.Context) []func() datasource.DataSour NewHttpDataSource, } } + +func (p *httpProvider) EphemeralResources(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + NewHttpEphemeralResource, + } +} diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index cfe168e0..7c79588d 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -5,12 +5,14 @@ package provider import ( "github.com/hashicorp/terraform-plugin-framework/providerserver" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/echoprovider" ) //nolint:unparam -func protoV5ProviderFactories() map[string]func() (tfprotov5.ProviderServer, error) { - return map[string]func() (tfprotov5.ProviderServer, error){ - "http": providerserver.NewProtocol5WithError(New()), +func protoV6ProviderFactories() map[string]func() (tfprotov6.ProviderServer, error) { + return map[string]func() (tfprotov6.ProviderServer, error){ + "http": providerserver.NewProtocol6WithError(New()), + "echo": echoprovider.NewProviderServer(), } }