* feat(endpoint): Add ssh key support Fixes #1257 * test(config): Add tests for private key config --------- Co-authored-by: TwiN <twin@linux.com>
286 lines
12 KiB
Go
286 lines
12 KiB
Go
package endpoint
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/TwiN/gatus/v5/config/gontext"
|
|
"github.com/TwiN/gatus/v5/pattern"
|
|
)
|
|
|
|
const (
|
|
// maximumLengthBeforeTruncatingWhenComparedWithPattern is the maximum length an element being compared to a
|
|
// pattern can have.
|
|
//
|
|
// This is only used for aesthetic purposes; it does not influence whether the condition evaluation results in a
|
|
// success or a failure
|
|
maximumLengthBeforeTruncatingWhenComparedWithPattern = 25
|
|
)
|
|
|
|
// Condition is a condition that needs to be met in order for an Endpoint to be considered healthy.
|
|
type Condition string
|
|
|
|
// Validate checks if the Condition is valid
|
|
func (c Condition) Validate() error {
|
|
r := &Result{}
|
|
c.evaluate(r, false, nil)
|
|
if len(r.Errors) != 0 {
|
|
return errors.New(r.Errors[0])
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// evaluate the Condition with the Result and an optional context
|
|
func (c Condition) evaluate(result *Result, dontResolveFailedConditions bool, context *gontext.Gontext) bool {
|
|
condition := string(c)
|
|
success := false
|
|
conditionToDisplay := condition
|
|
if strings.Contains(condition, " == ") {
|
|
parameters, resolvedParameters := sanitizeAndResolveWithContext(strings.Split(condition, " == "), result, context)
|
|
success = isEqual(resolvedParameters[0], resolvedParameters[1])
|
|
if !success && !dontResolveFailedConditions {
|
|
conditionToDisplay = prettify(parameters, resolvedParameters, "==")
|
|
}
|
|
} else if strings.Contains(condition, " != ") {
|
|
parameters, resolvedParameters := sanitizeAndResolveWithContext(strings.Split(condition, " != "), result, context)
|
|
success = !isEqual(resolvedParameters[0], resolvedParameters[1])
|
|
if !success && !dontResolveFailedConditions {
|
|
conditionToDisplay = prettify(parameters, resolvedParameters, "!=")
|
|
}
|
|
} else if strings.Contains(condition, " <= ") {
|
|
parameters, resolvedParameters := sanitizeAndResolveNumericalWithContext(strings.Split(condition, " <= "), result, context)
|
|
success = resolvedParameters[0] <= resolvedParameters[1]
|
|
if !success && !dontResolveFailedConditions {
|
|
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, "<=")
|
|
}
|
|
} else if strings.Contains(condition, " >= ") {
|
|
parameters, resolvedParameters := sanitizeAndResolveNumericalWithContext(strings.Split(condition, " >= "), result, context)
|
|
success = resolvedParameters[0] >= resolvedParameters[1]
|
|
if !success && !dontResolveFailedConditions {
|
|
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, ">=")
|
|
}
|
|
} else if strings.Contains(condition, " > ") {
|
|
parameters, resolvedParameters := sanitizeAndResolveNumericalWithContext(strings.Split(condition, " > "), result, context)
|
|
success = resolvedParameters[0] > resolvedParameters[1]
|
|
if !success && !dontResolveFailedConditions {
|
|
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, ">")
|
|
}
|
|
} else if strings.Contains(condition, " < ") {
|
|
parameters, resolvedParameters := sanitizeAndResolveNumericalWithContext(strings.Split(condition, " < "), result, context)
|
|
success = resolvedParameters[0] < resolvedParameters[1]
|
|
if !success && !dontResolveFailedConditions {
|
|
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, "<")
|
|
}
|
|
} else {
|
|
result.AddError(fmt.Sprintf("invalid condition: %s", condition))
|
|
return false
|
|
}
|
|
if !success {
|
|
//logr.Debugf("[Condition.evaluate] Condition '%s' did not succeed because '%s' is false", condition, condition)
|
|
}
|
|
result.ConditionResults = append(result.ConditionResults, &ConditionResult{Condition: conditionToDisplay, Success: success})
|
|
return success
|
|
}
|
|
|
|
// hasBodyPlaceholder checks whether the condition has a BodyPlaceholder
|
|
// Used for determining whether the response body should be read or not
|
|
func (c Condition) hasBodyPlaceholder() bool {
|
|
return strings.Contains(string(c), BodyPlaceholder)
|
|
}
|
|
|
|
// hasDomainExpirationPlaceholder checks whether the condition has a DomainExpirationPlaceholder
|
|
// Used for determining whether a whois operation is necessary
|
|
func (c Condition) hasDomainExpirationPlaceholder() bool {
|
|
return strings.Contains(string(c), DomainExpirationPlaceholder)
|
|
}
|
|
|
|
// hasIPPlaceholder checks whether the condition has an IPPlaceholder
|
|
// Used for determining whether an IP lookup is necessary
|
|
func (c Condition) hasIPPlaceholder() bool {
|
|
return strings.Contains(string(c), IPPlaceholder)
|
|
}
|
|
|
|
// isEqual compares two strings.
|
|
//
|
|
// Supports the "pat" and the "any" functions.
|
|
// i.e. if one of the parameters starts with PatternFunctionPrefix and ends with FunctionSuffix, it will be treated like
|
|
// a pattern.
|
|
func isEqual(first, second string) bool {
|
|
firstHasFunctionSuffix := strings.HasSuffix(first, FunctionSuffix)
|
|
secondHasFunctionSuffix := strings.HasSuffix(second, FunctionSuffix)
|
|
if firstHasFunctionSuffix || secondHasFunctionSuffix {
|
|
var isFirstPattern, isSecondPattern bool
|
|
if strings.HasPrefix(first, PatternFunctionPrefix) && firstHasFunctionSuffix {
|
|
isFirstPattern = true
|
|
first = strings.TrimSuffix(strings.TrimPrefix(first, PatternFunctionPrefix), FunctionSuffix)
|
|
}
|
|
if strings.HasPrefix(second, PatternFunctionPrefix) && secondHasFunctionSuffix {
|
|
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)
|
|
}
|
|
var isFirstAny, isSecondAny bool
|
|
if strings.HasPrefix(first, AnyFunctionPrefix) && firstHasFunctionSuffix {
|
|
isFirstAny = true
|
|
first = strings.TrimSuffix(strings.TrimPrefix(first, AnyFunctionPrefix), FunctionSuffix)
|
|
}
|
|
if strings.HasPrefix(second, AnyFunctionPrefix) && secondHasFunctionSuffix {
|
|
isSecondAny = true
|
|
second = strings.TrimSuffix(strings.TrimPrefix(second, AnyFunctionPrefix), FunctionSuffix)
|
|
}
|
|
if isFirstAny && !isSecondAny {
|
|
options := strings.Split(first, ",")
|
|
for _, option := range options {
|
|
if strings.TrimSpace(option) == second {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
} else if !isFirstAny && isSecondAny {
|
|
options := strings.Split(second, ",")
|
|
for _, option := range options {
|
|
if strings.TrimSpace(option) == first {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
// test if inputs are integers
|
|
firstInt, err1 := strconv.ParseInt(first, 0, 64)
|
|
secondInt, err2 := strconv.ParseInt(second, 0, 64)
|
|
if err1 == nil && err2 == nil {
|
|
return firstInt == secondInt
|
|
}
|
|
|
|
return first == second
|
|
}
|
|
|
|
// sanitizeAndResolveWithContext sanitizes and resolves a list of elements with an optional context
|
|
func sanitizeAndResolveWithContext(elements []string, result *Result, context *gontext.Gontext) ([]string, []string) {
|
|
parameters := make([]string, len(elements))
|
|
resolvedParameters := make([]string, len(elements))
|
|
for i, element := range elements {
|
|
element = strings.TrimSpace(element)
|
|
parameters[i] = element
|
|
|
|
// Use the unified ResolvePlaceholder function
|
|
resolved, err := ResolvePlaceholder(element, result, context)
|
|
if err != nil {
|
|
// If there's an error, add it to the result
|
|
result.AddError(err.Error())
|
|
resolvedParameters[i] = element + " " + InvalidConditionElementSuffix
|
|
} else {
|
|
resolvedParameters[i] = resolved
|
|
}
|
|
}
|
|
return parameters, resolvedParameters
|
|
}
|
|
|
|
func sanitizeAndResolveNumericalWithContext(list []string, result *Result, context *gontext.Gontext) (parameters []string, resolvedNumericalParameters []int64) {
|
|
parameters, resolvedParameters := sanitizeAndResolveWithContext(list, result, context)
|
|
for _, element := range resolvedParameters {
|
|
if duration, err := time.ParseDuration(element); duration != 0 && err == nil {
|
|
// If the string is a duration, convert it to milliseconds
|
|
resolvedNumericalParameters = append(resolvedNumericalParameters, duration.Milliseconds())
|
|
} else if number, err := strconv.ParseInt(element, 0, 64); err != nil {
|
|
// It's not an int, so we'll check if it's a float
|
|
if f, err := strconv.ParseFloat(element, 64); err == nil {
|
|
// It's a float, but we'll convert it to an int. We're losing precision here, but it's better than
|
|
// just returning 0.
|
|
resolvedNumericalParameters = append(resolvedNumericalParameters, int64(f))
|
|
} else {
|
|
// Default to 0 if the string couldn't be converted to an integer or a float
|
|
resolvedNumericalParameters = append(resolvedNumericalParameters, 0)
|
|
}
|
|
} else {
|
|
resolvedNumericalParameters = append(resolvedNumericalParameters, number)
|
|
}
|
|
}
|
|
return parameters, resolvedNumericalParameters
|
|
}
|
|
|
|
func prettifyNumericalParameters(parameters []string, resolvedParameters []int64, operator string) string {
|
|
resolvedStrings := make([]string, 2)
|
|
for i := 0; i < 2; i++ {
|
|
// Check if the parameter is a certificate or domain expiration placeholder
|
|
if parameters[i] == CertificateExpirationPlaceholder || parameters[i] == DomainExpirationPlaceholder {
|
|
// Format as duration string (convert milliseconds back to duration)
|
|
duration := time.Duration(resolvedParameters[i]) * time.Millisecond
|
|
resolvedStrings[i] = formatDuration(duration)
|
|
} else if _, err := time.ParseDuration(parameters[i]); err == nil {
|
|
// If the original parameter was a duration string (like "48h"), format the resolved value
|
|
// as a duration string too so it matches and doesn't show in parentheses
|
|
duration := time.Duration(resolvedParameters[i]) * time.Millisecond
|
|
resolvedStrings[i] = formatDuration(duration)
|
|
} else {
|
|
// Format as integer
|
|
resolvedStrings[i] = strconv.Itoa(int(resolvedParameters[i]))
|
|
}
|
|
}
|
|
return prettify(parameters, resolvedStrings, operator)
|
|
}
|
|
|
|
// formatDuration formats a duration in a clean, human-readable way by removing unnecessary zero components.
|
|
// For example: 336h0m0s becomes 336h, 1h30m0s becomes 1h30m, but 1h0m15s stays as 1h0m15s.
|
|
// Truncates to whole seconds to avoid decimal values like 7353h5m54.67s.
|
|
func formatDuration(d time.Duration) string {
|
|
// Truncate to whole seconds to avoid decimal seconds
|
|
d = d.Truncate(time.Second)
|
|
s := d.String()
|
|
// Special case: if duration is zero, return "0s"
|
|
if s == "0s" {
|
|
return "0s"
|
|
}
|
|
// Remove trailing "0s" if present
|
|
if strings.HasSuffix(s, "0s") {
|
|
s = strings.TrimSuffix(s, "0s")
|
|
// Remove trailing "0m" if present after removing "0s"
|
|
s = strings.TrimSuffix(s, "0m")
|
|
}
|
|
return s
|
|
}
|
|
|
|
// prettify returns a string representation of a condition with its parameters resolved between parentheses
|
|
func prettify(parameters []string, resolvedParameters []string, operator string) string {
|
|
// Handle pattern function truncation first
|
|
if strings.HasPrefix(parameters[0], PatternFunctionPrefix) && strings.HasSuffix(parameters[0], FunctionSuffix) && len(resolvedParameters[1]) > maximumLengthBeforeTruncatingWhenComparedWithPattern {
|
|
resolvedParameters[1] = fmt.Sprintf("%.25s...(truncated)", resolvedParameters[1])
|
|
}
|
|
if strings.HasPrefix(parameters[1], PatternFunctionPrefix) && strings.HasSuffix(parameters[1], FunctionSuffix) && len(resolvedParameters[0]) > maximumLengthBeforeTruncatingWhenComparedWithPattern {
|
|
resolvedParameters[0] = fmt.Sprintf("%.25s...(truncated)", resolvedParameters[0])
|
|
}
|
|
// Determine the state of each parameter
|
|
leftChanged := parameters[0] != resolvedParameters[0]
|
|
rightChanged := parameters[1] != resolvedParameters[1]
|
|
leftInvalid := resolvedParameters[0] == parameters[0]+" "+InvalidConditionElementSuffix
|
|
rightInvalid := resolvedParameters[1] == parameters[1]+" "+InvalidConditionElementSuffix
|
|
// Build the output based on what was resolved
|
|
var left, right string
|
|
// Format left side
|
|
if leftChanged && !leftInvalid {
|
|
left = parameters[0] + " (" + resolvedParameters[0] + ")"
|
|
} else if leftInvalid {
|
|
left = resolvedParameters[0] // Already has (INVALID)
|
|
} else {
|
|
left = parameters[0] // Unchanged
|
|
}
|
|
// Format right side
|
|
if rightChanged && !rightInvalid {
|
|
right = parameters[1] + " (" + resolvedParameters[1] + ")"
|
|
} else if rightInvalid {
|
|
right = resolvedParameters[1] // Already has (INVALID)
|
|
} else {
|
|
right = parameters[1] // Unchanged
|
|
}
|
|
return left + " " + operator + " " + right
|
|
}
|