diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ee93140..2fd20549 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 3.0.0 (Unreleased) + +BREAKING CHANGES: + +* data-source/http: There is no longer a check that the status code is 200 following a request. `status_code` attribute has been added and should be used in +[precondition and postcondition](https://www.terraform.io/language/expressions/custom-conditions) checks instead ([114](https://github.com/hashicorp/terraform-provider-http/pull/114)). +* data-source/http: `body` has been removed ([#137](https://github.com/hashicorp/terraform-provider-http/pull/137)). + ## 2.2.0 (June 02, 2022) ENHANCEMENTS: diff --git a/docs/data-sources/http.md b/docs/data-sources/http.md index c2819df1..0fa557d7 100644 --- a/docs/data-sources/http.md +++ b/docs/data-sources/http.md @@ -1,5 +1,4 @@ --- -# generated by https://github.com/hashicorp/terraform-plugin-docs page_title: "http Data Source - terraform-provider-http" subcategory: "" description: |- @@ -45,6 +44,54 @@ data "http" "example" { } ``` +## Usage with Postcondition + +[Precondition and Postcondition](https://www.terraform.io/language/expressions/custom-conditions) +checks are available with Terraform v1.2.0 and later. + +```terraform +data "http" "example" { + url = "https://checkpoint-api.hashicorp.com/v1/check/terraform" + + # Optional request headers + request_headers = { + Accept = "application/json" + } + + lifecycle { + postcondition { + condition = contains([201, 204], self.status_code) + error_message = "Status code invalid" + } + } +} +``` + +## Usage with Precondition + +[Precondition and Postcondition](https://www.terraform.io/language/expressions/custom-conditions) +checks are available with Terraform v1.2.0 and later. + +```terraform +data "http" "example" { + url = "https://checkpoint-api.hashicorp.com/v1/check/terraform" + + # Optional request headers + request_headers = { + Accept = "application/json" + } +} + +resource "random_uuid" "example" { + lifecycle { + precondition { + condition = contains([201, 204], data.http.example.status_code) + error_message = "Status code invalid" + } + } +} +``` + ## Schema @@ -58,9 +105,7 @@ data "http" "example" { ### Read-Only -- `body` (String, Deprecated) The response body returned as a string. **NOTE**: This is deprecated, use `response_body` instead. - `id` (String) The ID of this resource. - `response_body` (String) The response body returned as a string. -- `response_headers` (Map of String) A map of response header field names and values. Duplicate headers are concatenated with according to [RFC2616](https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2). - - +- `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. diff --git a/examples/data-sources/http/postcondition.tf b/examples/data-sources/http/postcondition.tf new file mode 100644 index 00000000..00f13dd4 --- /dev/null +++ b/examples/data-sources/http/postcondition.tf @@ -0,0 +1,15 @@ +data "http" "example" { + url = "https://checkpoint-api.hashicorp.com/v1/check/terraform" + + # Optional request headers + request_headers = { + Accept = "application/json" + } + + lifecycle { + postcondition { + condition = contains([201, 204], self.status_code) + error_message = "Status code invalid" + } + } +} diff --git a/examples/data-sources/http/precondition.tf b/examples/data-sources/http/precondition.tf new file mode 100644 index 00000000..7c5e5040 --- /dev/null +++ b/examples/data-sources/http/precondition.tf @@ -0,0 +1,17 @@ +data "http" "example" { + url = "https://checkpoint-api.hashicorp.com/v1/check/terraform" + + # Optional request headers + request_headers = { + Accept = "application/json" + } +} + +resource "random_uuid" "example" { + lifecycle { + precondition { + condition = contains([201, 204], data.http.example.status_code) + error_message = "Status code invalid" + } + } +} diff --git a/go.mod b/go.mod index bde5090e..2ae48d14 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/terraform-providers/terraform-provider-http go 1.17 require ( - github.com/hashicorp/terraform-plugin-docs v0.9.0 + github.com/hashicorp/terraform-plugin-docs v0.10.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.17.0 ) diff --git a/go.sum b/go.sum index d47cdd4c..62a88538 100644 --- a/go.sum +++ b/go.sum @@ -148,8 +148,8 @@ github.com/hashicorp/terraform-exec v0.16.1/go.mod h1:aj0lVshy8l+MHhFNoijNHtqTJQ github.com/hashicorp/terraform-json v0.13.0/go.mod h1:y5OdLBCT+rxbwnpxZs9kGL7R9ExU76+cpdY8zHwoazk= github.com/hashicorp/terraform-json v0.14.0 h1:sh9iZ1Y8IFJLx+xQiKHGud6/TSUCM0N8e17dKDpqV7s= github.com/hashicorp/terraform-json v0.14.0/go.mod h1:5A9HIWPkk4e5aeeXIBbkcOvaZbIYnAIkEyqP2pNSckM= -github.com/hashicorp/terraform-plugin-docs v0.9.0 h1:CEu7NToNWRR2os6DfT/Du2s+8qzXHyIcZQ10oiMdbJs= -github.com/hashicorp/terraform-plugin-docs v0.9.0/go.mod h1:47ZcsxMUJxAjGzHf+dZ9q78oYf4PeJxO1N+i5XDtXBc= +github.com/hashicorp/terraform-plugin-docs v0.10.0 h1:LwoFJ3RoKElDFhBRomsDkaEn59OUNXa92M21VCuL7Vk= +github.com/hashicorp/terraform-plugin-docs v0.10.0/go.mod h1:47ZcsxMUJxAjGzHf+dZ9q78oYf4PeJxO1N+i5XDtXBc= github.com/hashicorp/terraform-plugin-go v0.9.1 h1:vXdHaQ6aqL+OF076nMSBV+JKPdmXlzG5mzVDD04WyPs= github.com/hashicorp/terraform-plugin-go v0.9.1/go.mod h1:ItjVSlQs70otlzcCwlPcU8FRXLdO973oYFRZwAOxy8M= github.com/hashicorp/terraform-plugin-log v0.4.0 h1:F3eVnm8r2EfQCe2k9blPIiF/r2TT01SHijXnS7bujvc= diff --git a/internal/provider/data_source.go b/internal/provider/data_source.go index 75fc8508..ef6dc539 100644 --- a/internal/provider/data_source.go +++ b/internal/provider/data_source.go @@ -46,14 +46,6 @@ your control should be treated as untrustworthy.`, }, }, - "body": { - Description: "The response body returned as a string. " + - "**NOTE**: This is deprecated, use `response_body` instead.", - Type: schema.TypeString, - Computed: true, - Deprecated: "Use response_body instead", - }, - "response_body": { Description: "The response body returned as a string.", Type: schema.TypeString, @@ -62,13 +54,19 @@ your control should be treated as untrustworthy.`, "response_headers": { Description: `A map of response header field names and values.` + - ` Duplicate headers are concatenated with according to [RFC2616](https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2).`, + ` Duplicate headers are concatenated according to [RFC2616](https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2).`, Type: schema.TypeMap, Computed: true, Elem: &schema.Schema{ Type: schema.TypeString, }, }, + + "status_code": { + Description: `The HTTP response status code.`, + Type: schema.TypeInt, + Computed: true, + }, }, } } @@ -95,10 +93,6 @@ func dataSourceRead(ctx context.Context, d *schema.ResourceData, meta interface{ defer resp.Body.Close() - if resp.StatusCode != 200 { - return append(diags, diag.Errorf("HTTP request error. Response code: %d", resp.StatusCode)...) - } - contentType := resp.Header.Get("Content-Type") if !isContentTypeText(contentType) { diags = append(diags, diag.Diagnostic{ @@ -120,10 +114,6 @@ func dataSourceRead(ctx context.Context, d *schema.ResourceData, meta interface{ responseHeaders[k] = strings.Join(v, ", ") } - if err = d.Set("body", string(bytes)); err != nil { - return append(diags, diag.Errorf("Error setting HTTP response body: %s", err)...) - } - if err = d.Set("response_body", string(bytes)); err != nil { return append(diags, diag.Errorf("Error setting HTTP response body: %s", err)...) } @@ -132,6 +122,10 @@ func dataSourceRead(ctx context.Context, d *schema.ResourceData, meta interface{ return append(diags, diag.Errorf("Error setting HTTP response headers: %s", err)...) } + if err = d.Set("status_code", resp.StatusCode); err != nil { + return append(diags, diag.Errorf("Error setting HTTP status code: %s", err)...) + } + // set ID as something more stable than time d.SetId(url) diff --git a/internal/provider/data_source_test.go b/internal/provider/data_source_test.go index 8c032a67..f2bf5e56 100644 --- a/internal/provider/data_source_test.go +++ b/internal/provider/data_source_test.go @@ -4,13 +4,12 @@ import ( "fmt" "net/http" "net/http/httptest" - "regexp" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) -func TestDataSource_http200(t *testing.T) { +func TestDataSource_200(t *testing.T) { testHttpMock := setUpMockHttpServer() defer testHttpMock.server.Close() @@ -19,19 +18,23 @@ func TestDataSource_http200(t *testing.T) { ProviderFactories: testProviders(), Steps: []resource.TestStep{ { - Config: fmt.Sprintf(testDataSourceConfigBasic, testHttpMock.server.URL, 200), + Config: fmt.Sprintf(` +data "http" "http_test" { + url = "%s/200" +}`, testHttpMock.server.URL), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("data.http.http_test", "body", "1.0.0"), resource.TestCheckResourceAttr("data.http.http_test", "response_body", "1.0.0"), + resource.TestCheckResourceAttr("data.http.http_test", "response_headers.Content-Type", "text/plain"), resource.TestCheckResourceAttr("data.http.http_test", "response_headers.X-Single", "foobar"), resource.TestCheckResourceAttr("data.http.http_test", "response_headers.X-Double", "1, 2"), + resource.TestCheckResourceAttr("data.http.http_test", "status_code", "200"), ), }, }, }) } -func TestDataSource_http404(t *testing.T) { +func TestDataSource_404(t *testing.T) { testHttpMock := setUpMockHttpServer() defer testHttpMock.server.Close() @@ -40,14 +43,20 @@ func TestDataSource_http404(t *testing.T) { ProviderFactories: testProviders(), Steps: []resource.TestStep{ { - Config: fmt.Sprintf(testDataSourceConfigBasic, testHttpMock.server.URL, 404), - ExpectError: regexp.MustCompile("HTTP request error. Response code: 404"), + Config: fmt.Sprintf(` +data "http" "http_test" { + url = "%s/404" +}`, testHttpMock.server.URL), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.http.http_test", "response_body", ""), + resource.TestCheckResourceAttr("data.http.http_test", "status_code", "404"), + ), }, }, }) } -func TestDataSource_withHeaders200(t *testing.T) { +func TestDataSource_withAuthorizationRequestHeader_200(t *testing.T) { testHttpMock := setUpMockHttpServer() defer testHttpMock.server.Close() @@ -56,17 +65,51 @@ func TestDataSource_withHeaders200(t *testing.T) { ProviderFactories: testProviders(), Steps: []resource.TestStep{ { - Config: fmt.Sprintf(testDataSourceConfigWithHeaders, testHttpMock.server.URL, 200), + Config: fmt.Sprintf(` +data "http" "http_test" { + url = "%s/restricted" + + request_headers = { + "Authorization" = "Zm9vOmJhcg==" + } +}`, testHttpMock.server.URL), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("data.http.http_test", "body", "1.0.0"), resource.TestCheckResourceAttr("data.http.http_test", "response_body", "1.0.0"), + resource.TestCheckResourceAttr("data.http.http_test", "status_code", "200"), + ), + }, + }, + }) +} + +func TestDataSource_withAuthorizationRequestHeader_403(t *testing.T) { + testHttpMock := setUpMockHttpServer() + + defer testHttpMock.server.Close() + + resource.UnitTest(t, resource.TestCase{ + ProviderFactories: testProviders(), + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` +data "http" "http_test" { + url = "%s/restricted" + + request_headers = { + "Authorization" = "unauthorized" + } +} +`, testHttpMock.server.URL), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.http.http_test", "response_body", ""), + resource.TestCheckResourceAttr("data.http.http_test", "status_code", "403"), ), }, }, }) } -func TestDataSource_utf8(t *testing.T) { +func TestDataSource_utf8_200(t *testing.T) { testHttpMock := setUpMockHttpServer() defer testHttpMock.server.Close() @@ -75,17 +118,22 @@ func TestDataSource_utf8(t *testing.T) { ProviderFactories: testProviders(), Steps: []resource.TestStep{ { - Config: fmt.Sprintf(testDataSourceConfigUTF8, testHttpMock.server.URL, 200), + Config: fmt.Sprintf(` +data "http" "http_test" { + url = "%s/utf-8/200" +} +`, testHttpMock.server.URL), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("data.http.http_test", "body", "1.0.0"), resource.TestCheckResourceAttr("data.http.http_test", "response_body", "1.0.0"), + resource.TestCheckResourceAttr("data.http.http_test", "response_headers.Content-Type", "text/plain; charset=UTF-8"), + resource.TestCheckResourceAttr("data.http.http_test", "status_code", "200"), ), }, }, }) } -func TestDataSource_utf16(t *testing.T) { +func TestDataSource_utf16_200(t *testing.T) { testHttpMock := setUpMockHttpServer() defer testHttpMock.server.Close() @@ -94,9 +142,13 @@ func TestDataSource_utf16(t *testing.T) { ProviderFactories: testProviders(), Steps: []resource.TestStep{ { - Config: fmt.Sprintf(testDataSourceConfigUTF16, testHttpMock.server.URL, 200), + Config: fmt.Sprintf(` +data "http" "http_test" { + url = "%s/utf-16/200" +} +`, testHttpMock.server.URL), // This should now be a warning, but unsure how to test for it... - //ExpectWarning: regexp.MustCompile("Content-Type is not a text type. Got: application/json; charset=UTF-16"), + // ExpectWarning: regexp.MustCompile("Content-Type is not a text type. Got: application/json; charset=UTF-16"), }, }, }) @@ -109,7 +161,7 @@ func TestDataSource_utf16(t *testing.T) { // // const testDataSourceConfig_x509cert = ` // data "http" "http_test" { -// url = "%s/x509/cert.pem" +// url = "%s/x509-ca-cert/200" // } // output "body" { @@ -149,34 +201,6 @@ func TestDataSource_utf16(t *testing.T) { // }) // } -const testDataSourceConfigBasic = ` -data "http" "http_test" { - url = "%s/meta_%d.txt" -} -` - -const testDataSourceConfigWithHeaders = ` -data "http" "http_test" { - url = "%s/restricted/meta_%d.txt" - - request_headers = { - "Authorization" = "Zm9vOmJhcg==" - } -} -` - -const testDataSourceConfigUTF8 = ` -data "http" "http_test" { - url = "%s/utf-8/meta_%d.txt" -} -` - -const testDataSourceConfigUTF16 = ` -data "http" "http_test" { - url = "%s/utf-16/meta_%d.txt" -} -` - type TestHttpMock struct { server *httptest.Server } @@ -184,36 +208,35 @@ type TestHttpMock struct { func setUpMockHttpServer() *TestHttpMock { Server := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/plain") w.Header().Add("X-Single", "foobar") w.Header().Add("X-Double", "1") w.Header().Add("X-Double", "2") - if r.URL.Path == "/meta_200.txt" { + + switch r.URL.Path { + case "/200": w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("1.0.0")) - } else if r.URL.Path == "/restricted/meta_200.txt" { + case "/restricted": if r.Header.Get("Authorization") == "Zm9vOmJhcg==" { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("1.0.0")) } else { w.WriteHeader(http.StatusForbidden) } - } else if r.URL.Path == "/utf-8/meta_200.txt" { + case "/utf-8/200": w.Header().Set("Content-Type", "text/plain; charset=UTF-8") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("1.0.0")) - } else if r.URL.Path == "/utf-16/meta_200.txt" { + case "/utf-16/200": w.Header().Set("Content-Type", "application/json; charset=UTF-16") w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte("\"1.0.0\"")) - } else if r.URL.Path == "/x509/cert.pem" { + _, _ = w.Write([]byte("1.0.0")) + case "/x509-ca-cert/200": w.Header().Set("Content-Type", "application/x-x509-ca-cert") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("pem")) - } else if r.URL.Path == "/meta_404.txt" { - w.WriteHeader(http.StatusNotFound) - } else { + default: w.WriteHeader(http.StatusNotFound) } }), diff --git a/templates/data-sources/http.md.tmpl b/templates/data-sources/http.md.tmpl new file mode 100644 index 00000000..418b1048 --- /dev/null +++ b/templates/data-sources/http.md.tmpl @@ -0,0 +1,30 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +## Example Usage + +{{ tffile "examples/data-sources/http/data-source.tf" }} + +## Usage with Postcondition + +[Precondition and Postcondition](https://www.terraform.io/language/expressions/custom-conditions) +checks are available with Terraform v1.2.0 and later. + +{{ tffile "examples/data-sources/http/postcondition.tf" }} + +## Usage with Precondition + +[Precondition and Postcondition](https://www.terraform.io/language/expressions/custom-conditions) +checks are available with Terraform v1.2.0 and later. + +{{ tffile "examples/data-sources/http/precondition.tf" }} + +{{ .SchemaMarkdown | trimspace }}