1
0
Fork 0
mirror of https://github.com/TwiN/gatus.git synced 2024-12-15 17:51:09 +00:00

poc: custom scripts

This commit is contained in:
Jean-Christophe Saad-Dupuy 2023-10-18 16:31:28 +02:00
parent 619b69f480
commit 7081753947
3 changed files with 75 additions and 2 deletions

View file

@ -50,6 +50,10 @@ const (
// DomainExpirationPlaceholder is a placeholder for the duration before the domain expires, in milliseconds. // DomainExpirationPlaceholder is a placeholder for the duration before the domain expires, in milliseconds.
DomainExpirationPlaceholder = "[DOMAIN_EXPIRATION]" DomainExpirationPlaceholder = "[DOMAIN_EXPIRATION]"
ScriptExitCode = "[EXIT_CODE]"
ScriptStdOut = "[STDOUT]"
ScriptStdErr = "[STDERR]"
) )
// Functions // Functions
@ -261,6 +265,12 @@ func sanitizeAndResolve(elements []string, result *Result) ([]string, []string)
element = strconv.FormatInt(result.CertificateExpiration.Milliseconds(), 10) element = strconv.FormatInt(result.CertificateExpiration.Milliseconds(), 10)
case DomainExpirationPlaceholder: case DomainExpirationPlaceholder:
element = strconv.FormatInt(result.DomainExpiration.Milliseconds(), 10) element = strconv.FormatInt(result.DomainExpiration.Milliseconds(), 10)
case ScriptExitCode:
element = strconv.FormatInt(int64(result.ScriptExitCode), 10)
case ScriptStdErr:
element = string(result.ScriptStderr)
case ScriptStdOut:
element = string(result.ScriptStdout)
default: default:
// if contains the BodyPlaceholder, then evaluate json path // if contains the BodyPlaceholder, then evaluate json path
if strings.Contains(element, BodyPlaceholder) { if strings.Contains(element, BodyPlaceholder) {

View file

@ -10,6 +10,7 @@ import (
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"os/exec"
"strings" "strings"
"time" "time"
@ -45,7 +46,9 @@ const (
EndpointTypeHTTP EndpointType = "HTTP" EndpointTypeHTTP EndpointType = "HTTP"
EndpointTypeWS EndpointType = "WEBSOCKET" EndpointTypeWS EndpointType = "WEBSOCKET"
EndpointTypeSSH EndpointType = "SSH" EndpointTypeSSH EndpointType = "SSH"
EndpointTypeUNKNOWN EndpointType = "UNKNOWN"
EndpointTypeSCRIPT EndpointType = "SCRIPT"
EndpointTypeUNKNOWN EndpointType = "UNKNOWN"
) )
var ( var (
@ -76,6 +79,9 @@ var (
ErrEndpointWithoutSSHUsername = errors.New("you must specify a username for each endpoint with SSH") ErrEndpointWithoutSSHUsername = errors.New("you must specify a username for each endpoint with SSH")
// ErrEndpointWithoutSSHPassword is the error with which Gatus will panic if an endpoint with SSH monitoring is configured without a password. // ErrEndpointWithoutSSHPassword is the error with which Gatus will panic if an endpoint with SSH monitoring is configured without a password.
ErrEndpointWithoutSSHPassword = errors.New("you must specify a password for each endpoint with SSH") ErrEndpointWithoutSSHPassword = errors.New("you must specify a password for each endpoint with SSH")
// ErrEndpointScriptNoCommand is the error with which Gatus will panic if an endpoint is configured to be a script, with no command
ErrEndpointScriptNoCommand = errors.New("you must specify command for scripts endpoints")
) )
// Endpoint is the configuration of a monitored // Endpoint is the configuration of a monitored
@ -130,6 +136,13 @@ type Endpoint struct {
// SSH is the configuration of SSH monitoring. // SSH is the configuration of SSH monitoring.
SSH *SSH `yaml:"ssh,omitempty"` SSH *SSH `yaml:"ssh,omitempty"`
Script *Script `yaml:"script,omitempty"`
}
type Script struct {
Command string `yaml:"command,omitempty"`
Environment map[string]string `yaml:"environment,omitempty"`
} }
type SSH struct { type SSH struct {
@ -161,6 +174,9 @@ func (endpoint Endpoint) IsEnabled() bool {
// Type returns the endpoint type // Type returns the endpoint type
func (endpoint Endpoint) Type() EndpointType { func (endpoint Endpoint) Type() EndpointType {
switch { switch {
case endpoint.Script != nil:
return EndpointTypeSCRIPT
case endpoint.DNS != nil: case endpoint.DNS != nil:
return EndpointTypeDNS return EndpointTypeDNS
case strings.HasPrefix(endpoint.URL, "tcp://"): case strings.HasPrefix(endpoint.URL, "tcp://"):
@ -232,7 +248,14 @@ func (endpoint *Endpoint) ValidateAndSetDefaults() error {
if strings.ContainsAny(endpoint.Name, "\"\\") || strings.ContainsAny(endpoint.Group, "\"\\") { if strings.ContainsAny(endpoint.Name, "\"\\") || strings.ContainsAny(endpoint.Group, "\"\\") {
return ErrEndpointWithInvalidNameOrGroup return ErrEndpointWithInvalidNameOrGroup
} }
if len(endpoint.URL) == 0 { if endpoint.Type() == EndpointTypeSCRIPT {
if len(endpoint.Script.Command) == 0 {
return ErrEndpointScriptNoCommand
}
//TODO : handle default environement
}
if len(endpoint.URL) == 0 && endpoint.Type() != EndpointTypeSCRIPT {
return ErrEndpointWithNoURL return ErrEndpointWithNoURL
} }
if len(endpoint.Conditions) == 0 { if len(endpoint.Conditions) == 0 {
@ -349,6 +372,7 @@ func (endpoint *Endpoint) call(result *Result) {
request = endpoint.buildHTTPRequest() request = endpoint.buildHTTPRequest()
} }
startTime := time.Now() startTime := time.Now()
if endpointType == EndpointTypeDNS { if endpointType == EndpointTypeDNS {
endpoint.DNS.query(endpoint.URL, result) endpoint.DNS.query(endpoint.URL, result)
result.Duration = time.Since(startTime) result.Duration = time.Since(startTime)
@ -364,6 +388,38 @@ func (endpoint *Endpoint) call(result *Result) {
} }
result.Duration = time.Since(startTime) result.Duration = time.Since(startTime)
result.CertificateExpiration = time.Until(certificate.NotAfter) result.CertificateExpiration = time.Until(certificate.NotAfter)
} else if endpointType == EndpointTypeSCRIPT {
//log.Printf("[endpoint][monitor] Executing command=%s; endpoint=%s", endpoint.Script.Command, endpoint.Name)
// Split command
command := strings.Fields(endpoint.Script.Command)
cmd := exec.Command(command[0], command[1:]...)
// bind stderr / stdout
var outb, errb bytes.Buffer
cmd.Stdout = &outb
cmd.Stderr = &errb
// propagate custom environement
for key, value := range endpoint.Script.Environment {
log.Printf("[endpoint][monitor] endpoint=%s inject env %s %s", endpoint.Name, key, value)
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, value))
}
// Execute command
err := cmd.Run()
result.Duration = time.Since(startTime)
if err != nil {
result.AddError(err.Error())
}
// Fetch script exit code / stderr / stdout
result.ScriptExitCode = cmd.ProcessState.ExitCode()
result.ScriptStderr = errb.Bytes()
result.ScriptStdout = outb.Bytes()
// log.Printf("[endpoint][monitor] endpoint=%s stdout : '%s'", endpoint.Name, result.ScriptStdout)
} else if endpointType == EndpointTypeTCP { } else if endpointType == EndpointTypeTCP {
result.Connected = client.CanCreateTCPConnection(strings.TrimPrefix(endpoint.URL, "tcp://"), endpoint.ClientConfig) result.Connected = client.CanCreateTCPConnection(strings.TrimPrefix(endpoint.URL, "tcp://"), endpoint.ClientConfig)
result.Duration = time.Since(startTime) result.Duration = time.Since(startTime)
@ -478,3 +534,5 @@ func (endpoint *Endpoint) needsToRetrieveIP() bool {
} }
return false return false
} }
//

View file

@ -49,6 +49,11 @@ type Result struct {
// Note that this field is not persisted in the storage. // Note that this field is not persisted in the storage.
// It is used for health evaluation as well as debugging purposes. // It is used for health evaluation as well as debugging purposes.
Body []byte `json:"-"` Body []byte `json:"-"`
// for script
ScriptExitCode int `json:"exitCode"`
ScriptStdout []byte `json:"-"`
ScriptStderr []byte `json:"-"`
} }
// AddError adds an error to the result's list of errors. // AddError adds an error to the result's list of errors.