diff --git a/.dockerignore b/.dockerignore index c1f70b21..c7c11b97 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,5 @@ example +Dockerfile .github .idea +.git diff --git a/Dockerfile b/Dockerfile index d926f7c7..83bb78a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ # Build the go application into a binary FROM golang:alpine as builder -WORKDIR /app -ADD . ./ -RUN CGO_ENABLED=0 GOOS=linux go build -mod vendor -a -installsuffix cgo -o gatus . RUN apk --update add ca-certificates +WORKDIR /app +COPY . ./ +RUN CGO_ENABLED=0 GOOS=linux go build -mod vendor -a -installsuffix cgo -o gatus . # Run Tests inside docker image if you don't have a configured go environment #RUN apk update && apk add --virtual build-dependencies build-base gcc @@ -17,4 +17,4 @@ COPY --from=builder /app/static static/ COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt ENV PORT=8080 EXPOSE ${PORT} -ENTRYPOINT ["/gatus"] \ No newline at end of file +ENTRYPOINT ["/gatus"] diff --git a/README.md b/README.md index 8c82b70e..da1aec01 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,7 @@ Here are some examples of conditions you can use: | `len([BODY].data) < 5` | Array at JSONPath `$.data` has less than 5 elements | `{"data":[{"id":1}]}` | | | `len([BODY].name) == 8` | String at JSONPath `$.name` has a length of 8 | `{"name":"john.doe"}` | `{"name":"bob"}` | | `[BODY].name == pat(john*)` | String at JSONPath `$.name` matches pattern `john*` | `{"name":"john.doe"}` | `{"name":"bob"}` | +| `[CERTIFICATE_EXPIRATION] > 48h` | Certificate expiration is more than 48h away | 49h, 50h, 123h | 1h, 24h, ... | #### Placeholders @@ -171,7 +172,7 @@ Here are some examples of conditions you can use: | `[IP]` | Resolves into the IP of the target host | 192.168.0.232 | `[BODY]` | Resolves into the response body. Supports JSONPath. | `{"name":"john.doe"}` | `[CONNECTED]` | Resolves into whether a connection could be established | `true` -| `[CERTIFICATE_EXPIRATION]` | Resolves into the duration before certificate expiration, in ms | 4461677039, 0 (if not using HTTPS) +| `[CERTIFICATE_EXPIRATION]` | Resolves into the duration before certificate expiration | `24h`, `48h`, 0 (if not using HTTPS) #### Functions diff --git a/alerting/provider/slack/slack.go b/alerting/provider/slack/slack.go index a7d1f153..2c613a58 100644 --- a/alerting/provider/slack/slack.go +++ b/alerting/provider/slack/slack.go @@ -2,6 +2,7 @@ package slack import ( "fmt" + "github.com/TwinProduction/gatus/alerting/provider/custom" "github.com/TwinProduction/gatus/core" ) @@ -18,8 +19,7 @@ func (provider *AlertProvider) IsValid() bool { // ToCustomAlertProvider converts the provider into a custom.AlertProvider func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, alert *core.Alert, result *core.Result, resolved bool) *custom.AlertProvider { - var message string - var color string + var message, color, results string if resolved { message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", service.Name, alert.SuccessThreshold) color = "#36A64F" @@ -27,7 +27,6 @@ func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, aler message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", service.Name, alert.FailureThreshold) color = "#DD0000" } - var results string for _, conditionResult := range result.ConditionResults { var prefix string if conditionResult.Success { diff --git a/alerting/provider/slack/slack_test.go b/alerting/provider/slack/slack_test.go index 9b853d5b..4f1c8d2d 100644 --- a/alerting/provider/slack/slack_test.go +++ b/alerting/provider/slack/slack_test.go @@ -1,9 +1,10 @@ package slack import ( - "github.com/TwinProduction/gatus/core" "strings" "testing" + + "github.com/TwinProduction/gatus/core" ) func TestAlertProvider_IsValid(t *testing.T) { diff --git a/core/condition.go b/core/condition.go index 792b788a..220b9307 100644 --- a/core/condition.go +++ b/core/condition.go @@ -2,11 +2,13 @@ package core import ( "fmt" - "github.com/TwinProduction/gatus/jsonpath" - "github.com/TwinProduction/gatus/pattern" "log" "strconv" "strings" + "time" + + "github.com/TwinProduction/gatus/jsonpath" + "github.com/TwinProduction/gatus/pattern" ) const ( @@ -15,30 +17,30 @@ const ( // Values that could replace the placeholder: 200, 404, 500, ... StatusPlaceholder = "[STATUS]" - // IPPlaceHolder is a placeholder for an IP. + // IPPlaceholder is a placeholder for an IP. // // Values that could replace the placeholder: 127.0.0.1, 10.0.0.1, ... - IPPlaceHolder = "[IP]" + IPPlaceholder = "[IP]" // DNSRCodePlaceHolder is a place holder for DNS_RCODE // // Values that could be NOERROR, FORMERR, SERVFAIL, NXDOMAIN, NOTIMP and REFUSED DNSRCodePlaceHolder = "[DNS_RCODE]" - // ResponseTimePlaceHolder is a placeholder for the request response time, in milliseconds. + // ResponseTimePlaceholder is a placeholder for the request response time, in milliseconds. // // Values that could replace the placeholder: 1, 500, 1000, ... - ResponseTimePlaceHolder = "[RESPONSE_TIME]" + ResponseTimePlaceholder = "[RESPONSE_TIME]" - // BodyPlaceHolder is a placeholder for the body of the response + // BodyPlaceholder is a placeholder for the body of the response // // Values that could replace the placeholder: {}, {"data":{"name":"john"}}, ... - BodyPlaceHolder = "[BODY]" + BodyPlaceholder = "[BODY]" - // ConnectedPlaceHolder is a placeholder for whether a connection was successfully established. + // ConnectedPlaceholder is a placeholder for whether a connection was successfully established. // // Values that could replace the placeholder: true, false - ConnectedPlaceHolder = "[CONNECTED]" + ConnectedPlaceholder = "[CONNECTED]" // CertificateExpirationPlaceholder is a placeholder for the duration before certificate expiration, in milliseconds. // @@ -144,27 +146,27 @@ func sanitizeAndResolve(list []string, result *Result) []string { switch strings.ToUpper(element) { case StatusPlaceholder: element = strconv.Itoa(result.HTTPStatus) - case IPPlaceHolder: + case IPPlaceholder: element = result.IP - case ResponseTimePlaceHolder: + case ResponseTimePlaceholder: element = strconv.Itoa(int(result.Duration.Milliseconds())) + case BodyPlaceholder: + element = body case DNSRCodePlaceHolder: element = result.DNSRCode - case BodyPlaceHolder: - element = body - case ConnectedPlaceHolder: + case ConnectedPlaceholder: element = strconv.FormatBool(result.Connected) case CertificateExpirationPlaceholder: - element = strconv.FormatInt(int64(result.CertificateExpiration.Milliseconds()), 10) + element = strconv.FormatInt(result.CertificateExpiration.Milliseconds(), 10) default: - // if contains the BodyPlaceHolder, then evaluate json path - if strings.Contains(element, BodyPlaceHolder) { + // if contains the BodyPlaceholder, then evaluate json path + if strings.Contains(element, BodyPlaceholder) { wantLength := false if strings.HasPrefix(element, LengthFunctionPrefix) && strings.HasSuffix(element, FunctionSuffix) { wantLength = true element = strings.TrimSuffix(strings.TrimPrefix(element, LengthFunctionPrefix), FunctionSuffix) } - resolvedElement, resolvedElementLength, err := jsonpath.Eval(strings.Replace(element, fmt.Sprintf("%s.", BodyPlaceHolder), "", 1), result.Body) + resolvedElement, resolvedElementLength, err := jsonpath.Eval(strings.Replace(element, fmt.Sprintf("%s.", BodyPlaceholder), "", 1), result.Body) if err != nil { if err.Error() != "unexpected end of JSON input" { result.Errors = append(result.Errors, err.Error()) @@ -192,7 +194,9 @@ func sanitizeAndResolveNumerical(list []string, result *Result) []int64 { var sanitizedNumbers []int64 sanitizedList := sanitizeAndResolve(list, result) for _, element := range sanitizedList { - if number, err := strconv.ParseInt(element, 10, 64); err != nil { + if duration, err := time.ParseDuration(element); duration != 0 && err == nil { + sanitizedNumbers = append(sanitizedNumbers, duration.Milliseconds()) + } else if number, err := strconv.ParseInt(element, 10, 64); err != nil { // Default to 0 if the string couldn't be converted to an integer sanitizedNumbers = append(sanitizedNumbers, 0) } else { diff --git a/core/condition_test.go b/core/condition_test.go index d4e929df..3aa3fc6d 100644 --- a/core/condition_test.go +++ b/core/condition_test.go @@ -60,7 +60,27 @@ func TestCondition_evaluateWithResponseTimeUsingLessThan(t *testing.T) { } } +func TestCondition_evaluateWithResponseTimeUsingLessThanDuration(t *testing.T) { + condition := Condition("[RESPONSE_TIME] < 1s") + result := &Result{Duration: time.Millisecond * 50} + condition.evaluate(result) + if !result.ConditionResults[0].Success { + t.Errorf("Condition '%s' should have been a success", condition) + } +} + +func TestCondition_evaluateWithResponseTimeUsingLessThanInvalid(t *testing.T) { + condition := Condition("[RESPONSE_TIME] < potato") + result := &Result{Duration: time.Millisecond * 50} + condition.evaluate(result) + if result.ConditionResults[0].Success { + t.Errorf("Condition '%s' should have failed because the condition has an invalid numerical value that should've automatically resolved to 0", condition) + } +} + func TestCondition_evaluateWithResponseTimeUsingGreaterThan(t *testing.T) { + // Not exactly sure why you'd want to have a condition that checks if the response time is too fast, + // but hey, who am I to judge? condition := Condition("[RESPONSE_TIME] > 500") result := &Result{Duration: time.Millisecond * 750} condition.evaluate(result) @@ -69,6 +89,15 @@ func TestCondition_evaluateWithResponseTimeUsingGreaterThan(t *testing.T) { } } +func TestCondition_evaluateWithResponseTimeUsingGreaterThanDuration(t *testing.T) { + condition := Condition("[RESPONSE_TIME] > 1s") + result := &Result{Duration: time.Second * 2} + condition.evaluate(result) + if !result.ConditionResults[0].Success { + t.Errorf("Condition '%s' should have been a success", condition) + } +} + func TestCondition_evaluateWithResponseTimeUsingGreaterThanOrEqualTo(t *testing.T) { condition := Condition("[RESPONSE_TIME] >= 500") result := &Result{Duration: time.Millisecond * 500} @@ -320,7 +349,7 @@ func TestCondition_evaluateWithUnsetCertificateExpiration(t *testing.T) { } } -func TestCondition_evaluateWithCertificateExpirationGreaterThan(t *testing.T) { +func TestCondition_evaluateWithCertificateExpirationGreaterThanNumerical(t *testing.T) { acceptable := (time.Hour * 24 * 28).Milliseconds() condition := Condition("[CERTIFICATE_EXPIRATION] > " + strconv.FormatInt(acceptable, 10)) result := &Result{CertificateExpiration: time.Hour * 24 * 60} @@ -330,7 +359,7 @@ func TestCondition_evaluateWithCertificateExpirationGreaterThan(t *testing.T) { } } -func TestCondition_evaluateWithCertificateExpirationGreaterThanFailure(t *testing.T) { +func TestCondition_evaluateWithCertificateExpirationGreaterThanNumericalFailure(t *testing.T) { acceptable := (time.Hour * 24 * 28).Milliseconds() condition := Condition("[CERTIFICATE_EXPIRATION] > " + strconv.FormatInt(acceptable, 10)) result := &Result{CertificateExpiration: time.Hour * 24 * 14} @@ -339,3 +368,21 @@ func TestCondition_evaluateWithCertificateExpirationGreaterThanFailure(t *testin t.Errorf("Condition '%s' should have been a failure", condition) } } + +func TestCondition_evaluateWithCertificateExpirationGreaterThanDuration(t *testing.T) { + condition := Condition("[CERTIFICATE_EXPIRATION] > 12h") + result := &Result{CertificateExpiration: 24 * time.Hour} + condition.evaluate(result) + if !result.ConditionResults[0].Success { + t.Errorf("Condition '%s' should have been a success", condition) + } +} + +func TestCondition_evaluateWithCertificateExpirationGreaterThanDurationFailure(t *testing.T) { + condition := Condition("[CERTIFICATE_EXPIRATION] > 48h") + result := &Result{CertificateExpiration: 24 * time.Hour} + condition.evaluate(result) + if result.ConditionResults[0].Success { + t.Errorf("Condition '%s' should have been a failure", condition) + } +} diff --git a/core/service.go b/core/service.go index fcaf5519..3ed67ce0 100644 --- a/core/service.go +++ b/core/service.go @@ -184,7 +184,7 @@ func (service *Service) call(result *Result) { result.Errors = append(result.Errors, err.Error()) return } - if response.TLS != nil { + if response.TLS != nil && len(response.TLS.PeerCertificates) > 0 { certificate := response.TLS.PeerCertificates[0] result.CertificateExpiration = certificate.NotAfter.Sub(time.Now()) }