fix!: Enforce mandatory space around condition operator (#382)

BREAKING CHANGE: The comparator in each condition must now be wrapped by a space (e.g. [STATUS] == 200) or the condition will not be valid.
This commit is contained in:
TwiN 2022-12-06 01:37:05 -05:00 committed by GitHub
parent 741109f25d
commit 2346a6ee4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 85 additions and 40 deletions

View File

@ -1,6 +1,7 @@
package core package core
import ( import (
"errors"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
@ -86,8 +87,17 @@ const (
// Condition is a condition that needs to be met in order for an Endpoint to be considered healthy. // Condition is a condition that needs to be met in order for an Endpoint to be considered healthy.
type Condition string type Condition string
// Validate checks if the Condition is valid
func (c Condition) Validate() error {
r := &Result{}
c.evaluate(r, false)
if len(r.Errors) != 0 {
return errors.New(r.Errors[0])
}
return nil
}
// evaluate the Condition with the Result of the health check // evaluate the Condition with the Result of the health check
// TODO: Add a mandatory space between each operators (e.g. " == " instead of "==") (BREAKING CHANGE)
func (c Condition) evaluate(result *Result, dontResolveFailedConditions bool) bool { func (c Condition) evaluate(result *Result, dontResolveFailedConditions bool) bool {
condition := string(c) condition := string(c)
success := false success := false
@ -129,7 +139,7 @@ func (c Condition) evaluate(result *Result, dontResolveFailedConditions bool) bo
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, "<") conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, "<")
} }
} else { } else {
result.AddError(fmt.Sprintf("invalid condition '%s' has been provided", condition)) result.AddError(fmt.Sprintf("invalid condition: %s", condition))
return false return false
} }
if !success { if !success {

View File

@ -1,11 +1,48 @@
package core package core
import ( import (
"errors"
"fmt"
"strconv" "strconv"
"testing" "testing"
"time" "time"
) )
func TestCondition_Validate(t *testing.T) {
scenarios := []struct {
condition Condition
expectedErr error
}{
{condition: "[STATUS] == 200", expectedErr: nil},
{condition: "[STATUS] != 200", expectedErr: nil},
{condition: "[STATUS] <= 200", expectedErr: nil},
{condition: "[STATUS] >= 200", expectedErr: nil},
{condition: "[STATUS] < 200", expectedErr: nil},
{condition: "[STATUS] > 200", expectedErr: nil},
{condition: "[STATUS] == any(200, 201, 202, 203)", expectedErr: nil},
{condition: "[STATUS] == [BODY].status", expectedErr: nil},
{condition: "[BODY].test == wat", expectedErr: nil},
{condition: "[BODY].test == wat", expectedErr: nil},
{condition: "[BODY].test.wat == wat", expectedErr: nil},
{condition: "[BODY].users[0].id == 1", expectedErr: nil},
{condition: "len([BODY].users) == 100", expectedErr: nil},
{condition: "has([BODY].users[0].name) == 100", expectedErr: nil},
{condition: "raw == raw", expectedErr: nil},
{condition: "[STATUS] ? 201", expectedErr: errors.New("invalid condition: [STATUS] ? 201")},
{condition: "[STATUS]==201", expectedErr: errors.New("invalid condition: [STATUS]==201")},
{condition: "[STATUS] = = 201", expectedErr: errors.New("invalid condition: [STATUS] = = 201")},
// FIXME: Should return an error, but doesn't because jsonpath isn't evaluated due to body being empty in Condition.Validate()
//{condition: "len([BODY].users == 100", expectedErr: nil},
}
for _, scenario := range scenarios {
t.Run(string(scenario.condition), func(t *testing.T) {
if err := scenario.condition.Validate(); fmt.Sprint(err) != fmt.Sprint(scenario.expectedErr) {
t.Errorf("expected err %v, got %v", scenario.expectedErr, err)
}
})
}
}
func TestCondition_evaluate(t *testing.T) { func TestCondition_evaluate(t *testing.T) {
scenarios := []struct { scenarios := []struct {
Name string Name string

View File

@ -5,6 +5,7 @@ import (
"crypto/x509" "crypto/x509"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"io" "io"
"net" "net"
"net/http" "net/http"
@ -60,6 +61,9 @@ var (
// ErrUnknownEndpointType is the error with which Gatus will panic if an endpoint has an unknown type // ErrUnknownEndpointType is the error with which Gatus will panic if an endpoint has an unknown type
ErrUnknownEndpointType = errors.New("unknown endpoint type") ErrUnknownEndpointType = errors.New("unknown endpoint type")
// ErrInvalidConditionFormat is the error with which Gatus will panic if a condition has an invalid format
ErrInvalidConditionFormat = errors.New("invalid condition format: does not match '<VALUE> <COMPARATOR> <VALUE>'")
// ErrInvalidEndpointIntervalForDomainExpirationPlaceholder is the error with which Gatus will panic if an endpoint // ErrInvalidEndpointIntervalForDomainExpirationPlaceholder is the error with which Gatus will panic if an endpoint
// has both an interval smaller than 5 minutes and a condition with DomainExpirationPlaceholder. // has both an interval smaller than 5 minutes and a condition with DomainExpirationPlaceholder.
// This is because the free whois service we are using should not be abused, especially considering the fact that // This is because the free whois service we are using should not be abused, especially considering the fact that
@ -202,11 +206,12 @@ func (endpoint *Endpoint) ValidateAndSetDefaults() error {
if len(endpoint.Conditions) == 0 { if len(endpoint.Conditions) == 0 {
return ErrEndpointWithNoCondition return ErrEndpointWithNoCondition
} }
if endpoint.Interval < 5*time.Minute { for _, c := range endpoint.Conditions {
for _, condition := range endpoint.Conditions { if endpoint.Interval < 5*time.Minute && c.hasDomainExpirationPlaceholder() {
if condition.hasDomainExpirationPlaceholder() {
return ErrInvalidEndpointIntervalForDomainExpirationPlaceholder return ErrInvalidEndpointIntervalForDomainExpirationPlaceholder
} }
if err := c.Validate(); err != nil {
return fmt.Errorf("%v: %w", ErrInvalidConditionFormat, err)
} }
} }
if endpoint.DNS != nil { if endpoint.DNS != nil {

View File

@ -346,7 +346,9 @@ func TestEndpoint_ValidateAndSetDefaults(t *testing.T) {
Conditions: []Condition{Condition("[STATUS] == 200")}, Conditions: []Condition{Condition("[STATUS] == 200")},
Alerts: []*alert.Alert{{Type: alert.TypePagerDuty}}, Alerts: []*alert.Alert{{Type: alert.TypePagerDuty}},
} }
endpoint.ValidateAndSetDefaults() if err := endpoint.ValidateAndSetDefaults(); err != nil {
t.Errorf("Expected no error, got %v", err)
}
if endpoint.ClientConfig == nil { if endpoint.ClientConfig == nil {
t.Error("client configuration should've been set to the default configuration") t.Error("client configuration should've been set to the default configuration")
} else { } else {
@ -383,6 +385,17 @@ func TestEndpoint_ValidateAndSetDefaults(t *testing.T) {
} }
} }
func TestEndpoint_ValidateAndSetDefaultsWithInvalidCondition(t *testing.T) {
endpoint := Endpoint{
Name: "invalid-condition",
URL: "https://twin.sh/health",
Conditions: []Condition{"[STATUS] invalid 200"},
}
if err := endpoint.ValidateAndSetDefaults(); err == nil {
t.Error("endpoint validation should've returned an error, but didn't")
}
}
func TestEndpoint_ValidateAndSetDefaultsWithClientConfig(t *testing.T) { func TestEndpoint_ValidateAndSetDefaultsWithClientConfig(t *testing.T) {
endpoint := Endpoint{ endpoint := Endpoint{
Name: "website-health", Name: "website-health",
@ -605,26 +618,6 @@ func TestIntegrationEvaluateHealth(t *testing.T) {
} }
} }
func TestIntegrationEvaluateHealthWithInvalidCondition(t *testing.T) {
condition := Condition("[STATUS] invalid 200")
endpoint := Endpoint{
Name: "invalid-condition",
URL: "https://twin.sh/health",
Conditions: []Condition{condition},
}
if err := endpoint.ValidateAndSetDefaults(); err != nil {
// XXX: Should this really not return an error? After all, the condition is not valid and conditions are part of the endpoint...
t.Error("endpoint validation should've been successful, but wasn't")
}
result := endpoint.EvaluateHealth()
if result.Success {
t.Error("Because one of the conditions was invalid, result.Success should have been false")
}
if len(result.Errors) == 0 {
t.Error("There should've been an error")
}
}
func TestIntegrationEvaluateHealthWithErrorAndHideURL(t *testing.T) { func TestIntegrationEvaluateHealthWithErrorAndHideURL(t *testing.T) {
endpoint := Endpoint{ endpoint := Endpoint{
Name: "invalid-url", Name: "invalid-url",