Skip to content

Commit

Permalink
Add HTTP status code patterns (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
masterfuzz authored Apr 21, 2023
1 parent 1c62673 commit 36a2e6d
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 18 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ jobs:

test:
runs-on: ubuntu-latest
services:
httpbin:
image: kennethreitz/httpbin
ports: [ "80:80" ]
steps:
- uses: actions/checkout@v3

Expand All @@ -43,4 +47,6 @@ jobs:

- name: Build
run: make test
env:
HTTPBIN: "http://localhost"

2 changes: 1 addition & 1 deletion docs/resources/http_health.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ resource "checkmate_http_health" "example_insecure_tls" {
- `method` (String) HTTP Method, defaults to GET
- `request_body` (String) Optional request body to send on each attempt.
- `request_timeout` (Number) Timeout for an individual request. If exceeded, the attempt will be considered failure and potentially retried. Default 1000
- `status_code` (String) Status Code to expect. Default 200
- `status_code` (String) Status Code to expect. Can be a comma seperated list of ranges like '100-200,500'. Default 200
- `timeout` (Number) Overall timeout in milliseconds for the check before giving up. Default 5000

### Read-Only
Expand Down
64 changes: 53 additions & 11 deletions internal/provider/resource_http_health.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func (*HttpHealthResource) Schema(ctx context.Context, req resource.SchemaReques
PlanModifiers: []planmodifier.Int64{modifiers.DefaultInt64(200)},
},
"status_code": schema.StringAttribute{
MarkdownDescription: "Status Code to expect. Default 200",
MarkdownDescription: "Status Code to expect. Can be a comma seperated list of ranges like '100-200,500'. Default 200",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{modifiers.DefaultString("200")},
Expand Down Expand Up @@ -190,15 +190,12 @@ func (r *HttpHealthResource) HealthCheck(ctx context.Context, data *HttpHealthRe
}

var checkCode func(int) bool
if data.StatusCode.IsNull() {
checkCode = func(c int) bool { return c < 400 }
} else {
v, err := strconv.Atoi(data.StatusCode.ValueString())
if err != nil {
diag.AddError("Error", fmt.Sprintf("Unable to parse status code pattern %s", err))
}
checkCode = func(c int) bool { return c == v }
// check the pattern once
checkStatusCode(data.StatusCode.ValueString(), 0, diag)
if diag.HasError() {
return
}
checkCode = func(c int) bool { return checkStatusCode(data.StatusCode.ValueString(), c, diag) }

// normalize headers
headers := make(map[string][]string)
Expand Down Expand Up @@ -260,19 +257,25 @@ func (r *HttpHealthResource) HealthCheck(ctx context.Context, data *HttpHealthRe
Body: io.NopCloser(strings.NewReader(data.RequestBody.ValueString())),
})
if err != nil {
diag.AddWarning("Error connecting to healthcheck endpoint", fmt.Sprintf("%s", err))
tflog.Trace(ctx, fmt.Sprintf("CONNECTION FAILURE %v", err))
diag.AddWarning("Error connecting to healthcheck endpoint", fmt.Sprintf("%v", err))
return false
}

success := checkCode(httpResponse.StatusCode)
if success {
tflog.Trace(ctx, fmt.Sprintf("SUCCESS CODE %d", httpResponse.StatusCode))
body, err := io.ReadAll(httpResponse.Body)
if err != nil {
tflog.Trace(ctx, fmt.Sprintf("ERROR READING BODY %v", err))
diag.AddWarning("Error reading response body", fmt.Sprintf("%s", err))
data.ResultBody = types.StringValue("")
} else {
tflog.Trace(ctx, fmt.Sprintf("READ %d BYTES", len(body)))
data.ResultBody = types.StringValue(string(body))
}
} else {
tflog.Trace(ctx, fmt.Sprintf("FAILURE CODE %d", httpResponse.StatusCode))
}
return success
})
Expand All @@ -283,7 +286,7 @@ func (r *HttpHealthResource) HealthCheck(ctx context.Context, data *HttpHealthRe
case helpers.TimeoutExceeded:
diag.AddWarning("Timeout exceeded", fmt.Sprintf("Timeout of %d milliseconds exceeded", data.Timeout.ValueInt64()))
if !data.IgnoreFailure.ValueBool() {
diag.AddError("Check failed", "The check did not pass and create_anyway_on_check_failure is false")
diag.AddError("Check failed", "The check did not pass within the timeout and create_anyway_on_check_failure is false")
return
}
}
Expand Down Expand Up @@ -324,3 +327,42 @@ func (r *HttpHealthResource) Delete(ctx context.Context, req resource.DeleteRequ
func (r *HttpHealthResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}

func checkStatusCode(pattern string, code int, diag *diag.Diagnostics) bool {
ranges := strings.Split(pattern, ",")
for _, r := range ranges {
bounds := strings.Split(r, "-")
if len(bounds) == 2 {
left, err := strconv.Atoi(bounds[0])
if err != nil {
diag.AddError("Bad status code pattern", fmt.Sprintf("Can't convert %s to integer. %s", bounds[0], err))
return false
}
right, err := strconv.Atoi(bounds[1])
if err != nil {
diag.AddError("Bad status code pattern", fmt.Sprintf("Can't convert %s to integer. %s", bounds[1], err))
return false
}
if left > right {
diag.AddError("Bad status code pattern", fmt.Sprintf("Left bound %d is greater than right bound %d", left, right))
return false
}
if left <= code && right >= code {
return true
}
} else if len(bounds) == 1 {
val, err := strconv.Atoi(bounds[0])
if err != nil {
diag.AddError("Bad status code pattern", fmt.Sprintf("Can't convert %s to integer. %s", bounds[0], err))
return false
}
if val == code {
return true
}
} else {
diag.AddError("Bad status code pattern", "Too many dashes in range pattern")
return false
}
}
return false
}
54 changes: 48 additions & 6 deletions internal/provider/resource_http_health_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,35 +17,43 @@ package provider
import (
"encoding/json"
"fmt"
"os"
"testing"

"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

func TestAccHttpHealthResource(t *testing.T) {
// testUrl := "http://example.com"
httpBinTimeout := 60000 // 60s
testUrl := "https://httpbin.org/status/200"
timeout := 6000 // 6s
httpBin, envExists := os.LookupEnv("HTTPBIN")
if !envExists {
httpBin = "https://httpbin.org"
}
url200 := httpBin + "/status/200"
urlPost := httpBin + "/post"
urlHeaders := httpBin + "/headers"

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Create and Read testing
{
Config: testAccHttpHealthResourceConfig("test", testUrl, httpBinTimeout),
Config: testAccHttpHealthResourceConfig("test", url200, timeout),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("checkmate_http_health.test", "url", testUrl),
resource.TestCheckResourceAttr("checkmate_http_health.test", "url", url200),
),
},
{
Config: testAccHttpHealthResourceConfig("test_headers", "https://httpbin.org/headers", httpBinTimeout),
Config: testAccHttpHealthResourceConfig("test_headers", urlHeaders, timeout),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrWith("checkmate_http_health.test_headers", "result_body", checkHeader("Hello", "world")),
),
},
{
Config: testAccHttpHealthResourceConfigWithBody("test_post", "https://httpbin.org/post", "hello", httpBinTimeout),
Config: testAccHttpHealthResourceConfigWithBody("test_post", urlPost, "hello", timeout),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrWith("checkmate_http_health.test_post", "result_body", checkResponse("hello")),
),
Expand All @@ -68,6 +76,40 @@ func TestAccHttpHealthResource(t *testing.T) {
})
}

func TestStatusCodePattern(t *testing.T) {
tests := []struct {
pattern string
code int
want bool
wantErr bool
}{
{"200", 200, true, false},
{"200-204,300-305", 204, true, false},
{"200-204,300-305", 299, false, false},
{"foo", 200, false, true},
{"200-204,204-300", 200, true, false},
{"200-204-300", 200, false, true},
{"200,,", 0, false, true},
{"--200", 0, false, true},
}
for _, tt := range tests {
diag := &diag.Diagnostics{}
got := checkStatusCode(tt.pattern, tt.code, diag)
if got != tt.want {
t.Errorf("checkStatusCode(%q, %d) got %v, want %v", tt.pattern, tt.code, got, tt.want)
}
if tt.wantErr {
if !diag.HasError() {
t.Errorf("checkStatusCode(%q, %d) expected an error, but got none", tt.pattern, tt.code)
}
} else {
if diag.HasError() {
t.Errorf("checkStatusCode(%q, %d) got unexpected errors: %v", tt.pattern, tt.code, diag)
}
}
}
}

func testAccHttpHealthResourceConfig(name string, url string, timeout int) string {
return fmt.Sprintf(`
resource "checkmate_http_health" %[1]q {
Expand Down
1 change: 1 addition & 0 deletions internal/provider/resource_tcp_echo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ resource "checkmate_tcp_echo" %q {
host = %q
port = %d
message = %q
timeout = 1000
expected_message = %q
create_anyway_on_check_failure = %t
}`, name, host, port, message, expected_message, ignore_failure)
Expand Down

0 comments on commit 36a2e6d

Please sign in to comment.