From 8101646ba5709d31d592d32cdb4711ebadf9bcef Mon Sep 17 00:00:00 2001 From: TwinProduction Date: Thu, 1 Oct 2020 19:57:11 -0400 Subject: [PATCH] Work on #16: Support patterns --- core/condition.go | 29 +++++++++++++++++-- core/condition_test.go | 63 +++++++++++++++++++++++++++++++++++++++++ core/util.go | 5 ++-- pattern/pattern.go | 12 ++++++++ pattern/pattern_test.go | 37 ++++++++++++++++++++++++ 5 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 pattern/pattern.go create mode 100644 pattern/pattern_test.go diff --git a/core/condition.go b/core/condition.go index 8c50db53..8a676ef8 100644 --- a/core/condition.go +++ b/core/condition.go @@ -2,23 +2,25 @@ package core import ( "fmt" + "github.com/TwinProduction/gatus/pattern" "log" "strings" ) type Condition string +// evaluate the Condition with the Result of the health check func (c *Condition) evaluate(result *Result) bool { condition := string(*c) success := false var resolvedCondition string if strings.Contains(condition, "==") { parts := sanitizeAndResolve(strings.Split(condition, "=="), result) - success = parts[0] == parts[1] + success = isEqual(parts[0], parts[1]) resolvedCondition = fmt.Sprintf("%v == %v", parts[0], parts[1]) } else if strings.Contains(condition, "!=") { parts := sanitizeAndResolve(strings.Split(condition, "!="), result) - success = parts[0] != parts[1] + success = !isEqual(parts[0], parts[1]) resolvedCondition = fmt.Sprintf("%v != %v", parts[0], parts[1]) } else if strings.Contains(condition, "<=") { parts := sanitizeAndResolveNumerical(strings.Split(condition, "<="), result) @@ -49,3 +51,26 @@ func (c *Condition) evaluate(result *Result) bool { result.ConditionResults = append(result.ConditionResults, &ConditionResult{Condition: conditionToDisplay, Success: success}) return success } + +// isEqual compares two strings. +// +// It also supports the pattern function. That is to say, if one of the strings starts with PatternFunctionPrefix +// and ends with FunctionSuffix, it will be treated like a pattern. +func isEqual(first, second string) bool { + var isFirstPattern, isSecondPattern bool + if strings.HasPrefix(first, PatternFunctionPrefix) && strings.HasSuffix(first, FunctionSuffix) { + isFirstPattern = true + first = strings.TrimSuffix(strings.TrimPrefix(first, PatternFunctionPrefix), FunctionSuffix) + } + if strings.HasPrefix(second, PatternFunctionPrefix) && strings.HasSuffix(second, FunctionSuffix) { + isSecondPattern = true + second = strings.TrimSuffix(strings.TrimPrefix(second, PatternFunctionPrefix), FunctionSuffix) + } + if isFirstPattern && !isSecondPattern { + return pattern.Match(first, second) + } else if !isFirstPattern && isSecondPattern { + return pattern.Match(second, first) + } else { + return first == second + } +} diff --git a/core/condition_test.go b/core/condition_test.go index d72bd38b..7808cc4d 100644 --- a/core/condition_test.go +++ b/core/condition_test.go @@ -202,3 +202,66 @@ func TestCondition_evaluateWithBodyStringLength(t *testing.T) { t.Errorf("Condition '%s' should have been a success", condition) } } + +func TestCondition_evaluateWithBodyStringPattern(t *testing.T) { + condition := Condition("[BODY].name == pat(*ohn*)") + result := &Result{Body: []byte("{\"name\": \"john.doe\"}")} + condition.evaluate(result) + if !result.ConditionResults[0].Success { + t.Errorf("Condition '%s' should have been a success", condition) + } +} + +func TestCondition_evaluateWithBodyStringPatternFailure(t *testing.T) { + condition := Condition("[BODY].name == pat(bob*)") + result := &Result{Body: []byte("{\"name\": \"john.doe\"}")} + condition.evaluate(result) + if result.ConditionResults[0].Success { + t.Errorf("Condition '%s' should have been a failure", condition) + } +} + +func TestCondition_evaluateWithBodyPatternFailure(t *testing.T) { + condition := Condition("[BODY] == pat(*john*)") + result := &Result{Body: []byte("{\"name\": \"john.doe\"}")} + condition.evaluate(result) + if !result.ConditionResults[0].Success { + t.Errorf("Condition '%s' should have been a success", condition) + } +} + +func TestCondition_evaluateWithIPPattern(t *testing.T) { + condition := Condition("[IP] == pat(10.*)") + result := &Result{Ip: "10.0.0.0"} + condition.evaluate(result) + if !result.ConditionResults[0].Success { + t.Errorf("Condition '%s' should have been a success", condition) + } +} + +func TestCondition_evaluateWithIPPatternFailure(t *testing.T) { + condition := Condition("[IP] == pat(10.*)") + result := &Result{Ip: "255.255.255.255"} + condition.evaluate(result) + if result.ConditionResults[0].Success { + t.Errorf("Condition '%s' should have been a failure", condition) + } +} + +func TestCondition_evaluateWithStatusPattern(t *testing.T) { + condition := Condition("[STATUS] == pat(4*)") + result := &Result{HttpStatus: 404} + condition.evaluate(result) + if !result.ConditionResults[0].Success { + t.Errorf("Condition '%s' should have been a success", condition) + } +} + +func TestCondition_evaluateWithStatusPatternFailure(t *testing.T) { + condition := Condition("[STATUS] != pat(4*)") + result := &Result{HttpStatus: 404} + condition.evaluate(result) + if result.ConditionResults[0].Success { + t.Errorf("Condition '%s' should have been a failure", condition) + } +} diff --git a/core/util.go b/core/util.go index bc63dffb..c63c9be8 100644 --- a/core/util.go +++ b/core/util.go @@ -13,8 +13,9 @@ const ( ResponseTimePlaceHolder = "[RESPONSE_TIME]" BodyPlaceHolder = "[BODY]" - LengthFunctionPrefix = "len(" - FunctionSuffix = ")" + LengthFunctionPrefix = "len(" + PatternFunctionPrefix = "pat(" + FunctionSuffix = ")" InvalidConditionElementSuffix = "(INVALID)" ) diff --git a/pattern/pattern.go b/pattern/pattern.go new file mode 100644 index 00000000..44f02022 --- /dev/null +++ b/pattern/pattern.go @@ -0,0 +1,12 @@ +package pattern + +import "path/filepath" + +// Match checks whether a string matches a pattern +func Match(pattern, s string) bool { + if pattern == "*" { + return true + } + matched, _ := filepath.Match(pattern, s) + return matched +} diff --git a/pattern/pattern_test.go b/pattern/pattern_test.go new file mode 100644 index 00000000..6b56522e --- /dev/null +++ b/pattern/pattern_test.go @@ -0,0 +1,37 @@ +package pattern + +import "testing" + +func TestMatch(t *testing.T) { + testMatch(t, "*", "livingroom_123", true) + testMatch(t, "**", "livingroom_123", true) + testMatch(t, "living*", "livingroom_123", true) + testMatch(t, "*living*", "livingroom_123", true) + testMatch(t, "*123", "livingroom_123", true) + testMatch(t, "*_*", "livingroom_123", true) + testMatch(t, "living*_*3", "livingroom_123", true) + testMatch(t, "living*room_*3", "livingroom_123", true) + testMatch(t, "living*room_*3", "livingroom_123", true) + testMatch(t, "*vin*om*2*", "livingroom_123", true) + testMatch(t, "livingroom_123", "livingroom_123", true) + testMatch(t, "*livingroom_123*", "livingroom_123", true) + testMatch(t, "livingroom", "livingroom_123", false) + testMatch(t, "livingroom123", "livingroom_123", false) + testMatch(t, "what", "livingroom_123", false) + testMatch(t, "*what*", "livingroom_123", false) + testMatch(t, "*.*", "livingroom_123", false) + testMatch(t, "room*123", "livingroom_123", false) +} + +func testMatch(t *testing.T, pattern, key string, expectedToMatch bool) { + matched := Match(pattern, key) + if expectedToMatch { + if !matched { + t.Errorf("%s should've matched pattern '%s'", key, pattern) + } + } else { + if matched { + t.Errorf("%s shouldn't have matched pattern '%s'", key, pattern) + } + } +}