Skip to content

Commit

Permalink
Merge pull request #18 from tetratelabs/body-jsonpath
Browse files Browse the repository at this point in the history
Add JSON path evaluation
  • Loading branch information
chirauki authored Feb 21, 2024
2 parents e46aad7 + 0bc525e commit 2f47cfe
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 27 deletions.
24 changes: 24 additions & 0 deletions docs/resources/http_health.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,28 @@ resource "checkmate_http_health" "example_insecure_tls" {
consecutive_successes = 2
insecure_tls = true
}
resource "checkmate_http_health" "example_json_path" {
url = "https://httpbin.org/headers"
request_timeout = 1000
method = "GET"
interval = 1
status_code = 200
consecutive_successes = 2
jsonpath = "{ .Host }"
json_value = "httpbin.org"
}
resource "checkmate_http_health" "example_json_path_regex" {
url = "https://httpbin.org/headers"
request_timeout = 1000
method = "GET"
interval = 1
status_code = 200
consecutive_successes = 2
jsonpath = "{ .User-Agent }"
json_value = "curl/.*"
}
```

<!-- schema generated by tfplugindocs -->
Expand All @@ -82,6 +104,8 @@ resource "checkmate_http_health" "example_insecure_tls" {
- `headers` (Map of String) HTTP Request Headers
- `insecure_tls` (Boolean) Wether or not to completely skip the TLS CA verification. Default false.
- `interval` (Number) Interval in milliseconds between attemps. Default 200
- `json_value` (String) Optional regular expression to apply to the result of the JSONPath expression. If the expression matches, the check will pass.
- `jsonpath` (String) Optional JSONPath expression (same syntax as kubectl jsonpath output) to apply to the result body. If the expression matches, the check will pass.
- `keepers` (Map of String) Arbitrary map of string values that when changed will cause the healthcheck to run again.
- `method` (String) HTTP Method, defaults to GET
- `request_body` (String) Optional request body to send on each attempt.
Expand Down
22 changes: 22 additions & 0 deletions examples/resources/checkmate_http_health/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,25 @@ resource "checkmate_http_health" "example_insecure_tls" {
consecutive_successes = 2
insecure_tls = true
}

resource "checkmate_http_health" "example_json_path" {
url = "https://httpbin.org/headers"
request_timeout = 1000
method = "GET"
interval = 1
status_code = 200
consecutive_successes = 2
jsonpath = "{ .Host }"
json_value = "httpbin.org"
}

resource "checkmate_http_health" "example_json_path_regex" {
url = "https://httpbin.org/headers"
request_timeout = 1000
method = "GET"
interval = 1
status_code = 200
consecutive_successes = 2
jsonpath = "{ .User-Agent }"
json_value = "curl/.*"
}
9 changes: 5 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ go 1.21

require (
github.com/google/uuid v1.5.0
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/terraform-plugin-docs v0.16.0
github.com/hashicorp/terraform-plugin-framework v1.4.2
github.com/hashicorp/terraform-plugin-framework-validators v0.12.0
github.com/hashicorp/terraform-plugin-go v0.19.1
github.com/hashicorp/terraform-plugin-log v0.9.0
github.com/hashicorp/terraform-plugin-sdk/v2 v2.30.0
k8s.io/client-go v0.29.2
)

require (
Expand All @@ -30,7 +32,6 @@ require (
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect
github.com/hashicorp/go-hclog v1.5.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-plugin v1.5.2 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
Expand Down Expand Up @@ -61,11 +62,11 @@ require (
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/zclconf/go-cty v1.14.1 // indirect
golang.org/x/crypto v0.15.0 // indirect
golang.org/x/crypto v0.16.0 // indirect
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
golang.org/x/mod v0.13.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
Expand Down
17 changes: 10 additions & 7 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,9 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
Expand All @@ -206,8 +207,8 @@ golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
Expand All @@ -222,8 +223,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand All @@ -244,8 +245,8 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
Expand Down Expand Up @@ -289,3 +290,5 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/client-go v0.29.2 h1:FEg85el1TeZp+/vYJM7hkDlSTFZ+c5nnK44DJ4FyoRg=
k8s.io/client-go v0.29.2/go.mod h1:knlvFZE58VpqbQpJNbCbctTVXcd35mMyAAwBdpt4jrA=
36 changes: 36 additions & 0 deletions pkg/healthcheck/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@
package healthcheck

import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"time"
Expand All @@ -31,6 +34,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/tetratelabs/terraform-provider-checkmate/pkg/helpers"
"k8s.io/client-go/util/jsonpath"
)

type HttpHealthArgs struct {
Expand All @@ -48,6 +52,8 @@ type HttpHealthArgs struct {
ResultBody string
CABundle string
InsecureTLS bool
JSONPath string
JSONValue string
}

func HealthCheck(ctx context.Context, data *HttpHealthArgs, diag *diag.Diagnostics) error {
Expand All @@ -60,6 +66,11 @@ func HealthCheck(ctx context.Context, data *HttpHealthArgs, diag *diag.Diagnosti
return fmt.Errorf("parse url %q: %w", data.URL, err)
}

if (data.JSONPath != "" && data.JSONValue == "") || (data.JSONPath == "" && data.JSONValue != "") {
diagAddError(diag, "Client Error", "Both JSONPath and JSONValue must be specified")
return errors.New("both JSONPath and JSONValue must be specified")
}

var checkCode func(int) (bool, error)
// check the pattern once
checkStatusCode(data.StatusCode, 0, diag)
Expand Down Expand Up @@ -145,6 +156,31 @@ func HealthCheck(ctx context.Context, data *HttpHealthArgs, diag *diag.Diagnosti
} else {
tflog.Trace(ctx, fmt.Sprintf("FAILURE CODE %d", httpResponse.StatusCode))
}

// Check JSONPath
if data.JSONPath != "" && data.JSONValue != "" {
j := jsonpath.New("parser")
err = j.Parse(data.JSONPath)
if err != nil {
tflog.Warn(ctx, fmt.Sprintf("ERROR PARSING JSONPATH EXPRESSION %v", err))
return false
}
var respJSON interface{}
err = json.Unmarshal([]byte(data.ResultBody), &respJSON)
if err != nil {
tflog.Warn(ctx, fmt.Sprintf("ERROR UNMARSHALLING JSON %v", err))
return false
}
buf := new(bytes.Buffer)
err = j.Execute(buf, respJSON)
if err != nil {
tflog.Warn(ctx, fmt.Sprintf("ERROR EXECUTING JSONPATH %v", err))
return false
}
re := regexp.MustCompile(data.JSONValue)
return re.MatchString(buf.String())
}

return success
})

Expand Down
12 changes: 12 additions & 0 deletions pkg/provider/resource_http_health.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,14 @@ func (*HttpHealthResource) Schema(ctx context.Context, req resource.SchemaReques
Optional: true,
MarkdownDescription: "Wether or not to completely skip the TLS CA verification. Default false.",
},
"jsonpath": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Optional JSONPath expression (same syntax as kubectl jsonpath output) to apply to the result body. If the expression matches, the check will pass.",
},
"json_value": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Optional regular expression to apply to the result of the JSONPath expression. If the expression matches, the check will pass.",
},
"keepers": schema.MapAttribute{
ElementType: types.StringType,
MarkdownDescription: "Arbitrary map of string values that when changed will cause the healthcheck to run again.",
Expand All @@ -149,6 +157,8 @@ type HttpHealthResourceModel struct {
CABundle types.String `tfsdk:"ca_bundle"`
InsecureTLS types.Bool `tfsdk:"insecure_tls"`
Keepers types.Map `tfsdk:"keepers"`
JSONPath types.String `tfsdk:"jsonpath"`
JSONValue types.String `tfsdk:"json_value"`
}

func (r *HttpHealthResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
Expand Down Expand Up @@ -192,6 +202,8 @@ func (r *HttpHealthResource) HealthCheck(ctx context.Context, data *HttpHealthRe
RequestBody: data.RequestBody.ValueString(),
CABundle: data.CABundle.ValueString(),
InsecureTLS: data.InsecureTLS.ValueBool(),
JSONPath: data.JSONPath.ValueString(),
JSONValue: data.JSONValue.ValueString(),
}

err := healthcheck.HealthCheck(ctx, &args, diag)
Expand Down
52 changes: 36 additions & 16 deletions pkg/provider/resource_http_health_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,10 @@ import (
)

func TestAccHttpHealthResource(t *testing.T) {
// testUrl := "http://example.com"
timeout := 6000 // 6s
httpBin, envExists := os.LookupEnv("HTTPBIN")
if !envExists {
httpBin = "https://httpbin.org"
httpBin = "https://httpbin.platform.tetrate.com"
}
url200 := httpBin + "/status/200"
urlPost := httpBin + "/post"
Expand Down Expand Up @@ -58,20 +57,18 @@ func TestAccHttpHealthResource(t *testing.T) {
resource.TestCheckResourceAttrWith("checkmate_http_health.test_post", "result_body", checkResponse("hello")),
),
},
// ImportState testing
// {
// ResourceName: "checkmate_http_health.test",
// ImportState: true,
// ImportStateVerify: true,
// },
// Update and Read testing
// {
// Config: testAccHttpHealthResourceConfig(testUrl2),
// Check: resource.ComposeAggregateTestCheckFunc(
// resource.TestCheckResourceAttr("checkmate_http_health.test", "url", testUrl2),
// ),
// },
// Delete testing automatically occurs in TestCase
{
Config: testJSONPath("test_jp", "https://httpbin.platform.tetrate.com/headers", "{ .headers.Host }", "httpbin.platform.tetrate.com"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("checkmate_http_health.test_jp", "passed", "true"),
),
},
{
Config: testJSONPath("test_jp_re", urlHeaders, "{ .headers.User-Agent }", "Go-(http|https)-client.*"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("checkmate_http_health.test_jp_re", "passed", "true"),
),
},
},
})
}
Expand Down Expand Up @@ -122,6 +119,7 @@ resource "checkmate_http_health" %[1]q {
}
`, name, url, timeout)
}

func testAccHttpHealthResourceConfigWithBody(name string, url string, body string, timeout int) string {
return fmt.Sprintf(`
resource "checkmate_http_health" %[1]q {
Expand All @@ -138,6 +136,21 @@ resource "checkmate_http_health" %[1]q {

}

func testJSONPath(name string, url, jsonpath, json_value string) string {
return fmt.Sprintf(`
resource "checkmate_http_health" %[1]q {
url = %[2]q
consecutive_successes = 1
method = "GET"
timeout = 1000 * 10
interval = 1000 * 2
jsonpath = %[3]q
json_value = %[4]q
}
`, name, url, jsonpath, json_value)

}

func checkHeader(key string, value string) func(string) error {
return func(responseBody string) error {
var parsed map[string]map[string]string
Expand Down Expand Up @@ -169,3 +182,10 @@ func checkResponse(value string) func(string) error {
return fmt.Errorf("Bad response from httpbin")
}
}

func checkPassed(value string) error {
if value == "true" {
return nil
}
return fmt.Errorf("test did not pass")
}

0 comments on commit 2f47cfe

Please sign in to comment.