From b0450fc54902f4975d1312dea0e6b83a61c681a0 Mon Sep 17 00:00:00 2001 From: Marc Cirauqui Date: Fri, 31 May 2024 11:11:08 +0200 Subject: [PATCH 1/2] Add persistent_response_regex to tcp echo check --- pkg/provider/resource_tcp_echo.go | 84 +++++++++++++++++++++----- pkg/provider/resource_tcp_echo_test.go | 33 ++++++++++ 2 files changed, 103 insertions(+), 14 deletions(-) diff --git a/pkg/provider/resource_tcp_echo.go b/pkg/provider/resource_tcp_echo.go index 76dc6c6..2aacf3f 100644 --- a/pkg/provider/resource_tcp_echo.go +++ b/pkg/provider/resource_tcp_echo.go @@ -15,9 +15,11 @@ package provider import ( + "bytes" "context" "fmt" "net" + "regexp" "strconv" "strings" "time" @@ -73,6 +75,18 @@ func (*TCPEchoResource) Schema(ctx context.Context, req resource.SchemaRequest, Computed: true, Default: stringdefault.StaticString(""), }, + "persistent_response_regex": schema.StringAttribute{ + MarkdownDescription: `A regex pattern that the response need to match in every attempt to be considered successful. +If not provided, the response is not checked. + +If using multiple attempts, this regex will be evaulated against the response text. For every susequent attempt, the regex +will be evaluated against the response text and compared against the first obtained value. The check will be deemed successful +if the regex matches the response text in every attempt. A single response not matching such value will cause the check to fail.`, + Required: false, + Optional: true, + Computed: true, + Default: stringdefault.StaticString(""), + }, "expect_write_failure": schema.BoolAttribute{ MarkdownDescription: "Wether or not the check is expected to fail after successfully connecting to the target. If true, the check will be considered successful if it fails. Defaults to false.", Required: false, @@ -132,20 +146,21 @@ func (*TCPEchoResource) Schema(ctx context.Context, req resource.SchemaRequest, } type TCPEchoResourceModel struct { - Id types.String `tfsdk:"id"` - Host types.String `tfsdk:"host"` - Port types.Int64 `tfsdk:"port"` - Message types.String `tfsdk:"message"` - ExpectedMessage types.String `tfsdk:"expected_message"` - ExpectWriteFailure types.Bool `tfsdk:"expect_write_failure"` - ConnectionTimeout types.Int64 `tfsdk:"connection_timeout"` - SingleAttemptTimeout types.Int64 `tfsdk:"single_attempt_timeout"` - Timeout types.Int64 `tfsdk:"timeout"` - Interval types.Int64 `tfsdk:"interval"` - ConsecutiveSuccesses types.Int64 `tfsdk:"consecutive_successes"` - IgnoreFailure types.Bool `tfsdk:"create_anyway_on_check_failure"` - Passed types.Bool `tfsdk:"passed"` - Keepers types.Map `tfsdk:"keepers"` + Id types.String `tfsdk:"id"` + Host types.String `tfsdk:"host"` + Port types.Int64 `tfsdk:"port"` + Message types.String `tfsdk:"message"` + ExpectedMessage types.String `tfsdk:"expected_message"` + PersistentResponseRegex types.String `tfsdk:"persistent_response_regex"` + ExpectWriteFailure types.Bool `tfsdk:"expect_write_failure"` + ConnectionTimeout types.Int64 `tfsdk:"connection_timeout"` + SingleAttemptTimeout types.Int64 `tfsdk:"single_attempt_timeout"` + Timeout types.Int64 `tfsdk:"timeout"` + Interval types.Int64 `tfsdk:"interval"` + ConsecutiveSuccesses types.Int64 `tfsdk:"consecutive_successes"` + IgnoreFailure types.Bool `tfsdk:"create_anyway_on_check_failure"` + Passed types.Bool `tfsdk:"passed"` + Keepers types.Map `tfsdk:"keepers"` } // ImportState implements resource.ResourceWithImportState @@ -190,6 +205,19 @@ func (r *TCPEchoResource) TCPEcho(ctx context.Context, data *TCPEchoResourceMode ConsecutiveSuccesses: int(data.ConsecutiveSuccesses.ValueInt64()), } + firstAttemptRegexValue := "" + regexValueStoredAttempt := 0 + var persistentResponseRegex *regexp.Regexp + var err error + if data.PersistentResponseRegex.ValueString() != "" { + persistentResponseRegex, err = regexp.Compile(data.PersistentResponseRegex.ValueString()) + if err != nil { + tflog.Error(ctx, fmt.Sprintf("could not compile regex %q: %v", data.PersistentResponseRegex.ValueString(), err.Error())) + diag.AddError("Invalid regex", fmt.Sprintf("Could not compile regex %q: %v", data.PersistentResponseRegex.ValueString(), err.Error())) + return + } + } + result := window.Do(func(attempt int, success int) bool { exepctFailure := data.ExpectWriteFailure.ValueBool() destStr := data.Host.ValueString() + ":" + strconv.Itoa(int(data.Port.ValueInt64())) @@ -231,8 +259,36 @@ func (r *TCPEchoResource) TCPEcho(ctx context.Context, data *TCPEchoResourceMode return false } + // remove null char from response + reply = bytes.Trim(reply, "\x00") + + if persistentResponseRegex != nil { + limits := persistentResponseRegex.FindStringIndex(string(reply)) + if limits == nil { + tflog.Warn(ctx, fmt.Sprintf("Got response %q, which does not match regex %q", string(reply), data.PersistentResponseRegex.ValueString())) + diag.AddWarning("Check failed", fmt.Sprintf("Got response %q, which does not match regex %q", string(reply), data.PersistentResponseRegex.ValueString())) + return false + } + result := string(reply)[limits[0]:limits[1]] + // result := persistentResponseRegex.FindString(string(reply)) + tflog.Info(ctx, fmt.Sprintf("Result: %s", result)) + + // Avoid comparison on first attempt + if regexValueStoredAttempt == 0 { + firstAttemptRegexValue = result + regexValueStoredAttempt = attempt + return true + } + if firstAttemptRegexValue != result { + tflog.Warn(ctx, fmt.Sprintf("Got response %q, which does not match previous attempt %q", result, firstAttemptRegexValue)) + diag.AddWarning("Check failed", fmt.Sprintf("Got response %q, which does not match previous attempt %q", result, firstAttemptRegexValue)) + return false + } + } + if !strings.Contains(string(reply), data.ExpectedMessage.ValueString()) { tflog.Warn(ctx, fmt.Sprintf("Got response %q, which does not include expected message %q", string(reply), data.ExpectedMessage.ValueString())) + diag.AddWarning("Check failed", fmt.Sprintf("Got response %q, which does not include expected message %q", string(reply), data.ExpectedMessage.ValueString())) return false } diff --git a/pkg/provider/resource_tcp_echo_test.go b/pkg/provider/resource_tcp_echo_test.go index 609d0de..95f9bd0 100644 --- a/pkg/provider/resource_tcp_echo_test.go +++ b/pkg/provider/resource_tcp_echo_test.go @@ -38,6 +38,24 @@ func TestAccTCPEchoResource(t *testing.T) { resource.TestCheckResourceAttr("checkmate_tcp_echo.test_failure", "passed", "false"), ), }, + { + Config: testAccTCPEchoResourceConfig("test_failure", "foo.bar", 1234, "foobar", "foobar", true), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("checkmate_tcp_echo.test_failure", "passed", "false"), + ), + }, + { + Config: testTCPEchoResourceRegex("test_regex_ok", `\(.*\)`, false), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("checkmate_tcp_echo.test_regex_ok", "passed", "true"), + ), + }, + { + Config: testTCPEchoResourceRegex("test_regex_not_match_pass_anyway", "test", true), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("checkmate_tcp_echo.test_regex_not_match_pass_anyway", "passed", "false"), + ), + }, }, }) } @@ -54,3 +72,18 @@ resource "checkmate_tcp_echo" %q { }`, name, host, port, message, expected_message, ignore_failure) } + +func testTCPEchoResourceRegex(name, regex string, ignore_failure bool) string { + return fmt.Sprintf(` +resource "checkmate_tcp_echo" %q { + host = "tcpecho.platform.tetrate.com" + port = 15080 + message = "foobar (123)" + timeout = 1000 * 10 + expected_message = "foobar (123)" + persistent_response_regex = %q + create_anyway_on_check_failure = %t + consecutive_successes = 2 +}`, name, regex, ignore_failure) + +} From 7da13e4b1ea3ff419893d9bfb613bb14bc916fb6 Mon Sep 17 00:00:00 2001 From: Marc Cirauqui Date: Fri, 31 May 2024 11:22:12 +0200 Subject: [PATCH 2/2] check --- docs/resources/tcp_echo.md | 6 ++++++ pkg/provider/resource_tcp_echo.go | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/resources/tcp_echo.md b/docs/resources/tcp_echo.md index a28eca4..24913c6 100644 --- a/docs/resources/tcp_echo.md +++ b/docs/resources/tcp_echo.md @@ -81,6 +81,12 @@ resource "checkmate_tcp_echo" "example" { - `expected_message` (String) The message expected to be included in the echo response - `interval` (Number) Interval in milliseconds between attemps. Default 200 - `keepers` (Map of String) Arbitrary map of string values that when changed will cause the check to run again. +- `persistent_response_regex` (String) A regex pattern that the response need to match in every attempt to be considered successful. + If not provided, the response is not checked. + + If using multiple attempts, this regex will be evaulated against the response text. For every susequent attempt, the regex + will be evaluated against the response text and compared against the first obtained value. The check will be deemed successful + if the regex matches the response text in every attempt. A single response not matching such value will cause the check to fail. - `single_attempt_timeout` (Number) Timeout for an individual attempt. If exceeded, the attempt will be considered failure and potentially retried. Default 5000ms - `timeout` (Number) Overall timeout in milliseconds for the check before giving up, default 10000 diff --git a/pkg/provider/resource_tcp_echo.go b/pkg/provider/resource_tcp_echo.go index 2aacf3f..05eda12 100644 --- a/pkg/provider/resource_tcp_echo.go +++ b/pkg/provider/resource_tcp_echo.go @@ -77,11 +77,11 @@ func (*TCPEchoResource) Schema(ctx context.Context, req resource.SchemaRequest, }, "persistent_response_regex": schema.StringAttribute{ MarkdownDescription: `A regex pattern that the response need to match in every attempt to be considered successful. -If not provided, the response is not checked. + If not provided, the response is not checked. -If using multiple attempts, this regex will be evaulated against the response text. For every susequent attempt, the regex -will be evaluated against the response text and compared against the first obtained value. The check will be deemed successful -if the regex matches the response text in every attempt. A single response not matching such value will cause the check to fail.`, + If using multiple attempts, this regex will be evaulated against the response text. For every susequent attempt, the regex + will be evaluated against the response text and compared against the first obtained value. The check will be deemed successful + if the regex matches the response text in every attempt. A single response not matching such value will cause the check to fail.`, Required: false, Optional: true, Computed: true,