feat(SSH): Add support for SSH endpoint (#473)
* feat(SSH): Add support for SSH endpoint This commit adds support for SSH endpoint monitoring. Users can now configure an endpoint to be monitored using an SSH command by prefixing the endpoint's URL with ssh:\\. The configuration options for an SSH endpoint include the username, password, and command to be executed on the remote server. In addition, two placeholders are supported for SSH endpoints: [CONNECTED] and [STATUS]. This commit also updates the README to include instructions on how to configure SSH endpoints and the placeholders that can be used in their conditions. The README has been updated to include the new SSH-related options in the endpoints[] configuration object. Here's a summary of the changes made in this commit: Added support for SSH endpoint monitoring Updated the documentation to include instructions on how to configure SSH endpoints and the placeholders that can be used in their conditions
This commit is contained in:
@ -17,6 +17,7 @@ import (
|
||||
"github.com/TwiN/gatus/v5/client"
|
||||
"github.com/TwiN/gatus/v5/core/ui"
|
||||
"github.com/TwiN/gatus/v5/util"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type EndpointType string
|
||||
@ -43,6 +44,7 @@ const (
|
||||
EndpointTypeTLS EndpointType = "TLS"
|
||||
EndpointTypeHTTP EndpointType = "HTTP"
|
||||
EndpointTypeWS EndpointType = "WEBSOCKET"
|
||||
EndpointTypeSSH EndpointType = "SSH"
|
||||
EndpointTypeUNKNOWN EndpointType = "UNKNOWN"
|
||||
)
|
||||
|
||||
@ -70,6 +72,10 @@ var (
|
||||
// This is because the free whois service we are using should not be abused, especially considering the fact that
|
||||
// the data takes a while to be updated.
|
||||
ErrInvalidEndpointIntervalForDomainExpirationPlaceholder = errors.New("the minimum interval for an endpoint with a condition using the " + DomainExpirationPlaceholder + " placeholder is 300s (5m)")
|
||||
// ErrEndpointWithoutSSHUsername is the error with which Gatus will panic if an endpoint with SSH monitoring is configured without a user.
|
||||
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 = errors.New("you must specify a password for each endpoint with SSH")
|
||||
)
|
||||
|
||||
// Endpoint is the configuration of a monitored
|
||||
@ -121,6 +127,27 @@ type Endpoint struct {
|
||||
|
||||
// NumberOfSuccessesInARow is the number of successful evaluations in a row
|
||||
NumberOfSuccessesInARow int `yaml:"-"`
|
||||
|
||||
// SSH is the configuration of SSH monitoring.
|
||||
SSH *SSH `yaml:"ssh,omitempty"`
|
||||
}
|
||||
|
||||
type SSH struct {
|
||||
// Username is the username to use when connecting to the SSH server.
|
||||
Username string `yaml:"username,omitempty"`
|
||||
// Password is the password to use when connecting to the SSH server.
|
||||
Password string `yaml:"password,omitempty"`
|
||||
}
|
||||
|
||||
// Validate validates the endpoint
|
||||
func (s *SSH) ValidateAndSetDefaults() error {
|
||||
if s.Username == "" {
|
||||
return ErrEndpointWithoutSSHUsername
|
||||
}
|
||||
if s.Password == "" {
|
||||
return ErrEndpointWithoutSSHPassword
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsEnabled returns whether the endpoint is enabled or not
|
||||
@ -152,6 +179,8 @@ func (endpoint Endpoint) Type() EndpointType {
|
||||
return EndpointTypeHTTP
|
||||
case strings.HasPrefix(endpoint.URL, "ws://") || strings.HasPrefix(endpoint.URL, "wss://"):
|
||||
return EndpointTypeWS
|
||||
case strings.HasPrefix(endpoint.URL, "ssh://"):
|
||||
return EndpointTypeSSH
|
||||
default:
|
||||
return EndpointTypeUNKNOWN
|
||||
}
|
||||
@ -228,6 +257,9 @@ func (endpoint *Endpoint) ValidateAndSetDefaults() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if endpoint.SSH != nil {
|
||||
return endpoint.SSH.ValidateAndSetDefaults()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -350,6 +382,19 @@ func (endpoint *Endpoint) call(result *Result) {
|
||||
result.AddError(err.Error())
|
||||
return
|
||||
}
|
||||
} else if endpointType == EndpointTypeSSH {
|
||||
var cli *ssh.Client
|
||||
result.Connected, cli, err = client.CanCreateSSHConnection(strings.TrimPrefix(endpoint.URL, "ssh://"), endpoint.SSH.Username, endpoint.SSH.Password, endpoint.ClientConfig)
|
||||
if err != nil {
|
||||
result.AddError(err.Error())
|
||||
return
|
||||
}
|
||||
result.Success, result.HTTPStatus, err = client.ExecuteSSHCommand(cli, endpoint.Body, endpoint.ClientConfig)
|
||||
if err != nil {
|
||||
result.AddError(err.Error())
|
||||
return
|
||||
}
|
||||
result.Duration = time.Since(startTime)
|
||||
} else {
|
||||
response, err = client.GetHTTPClient(endpoint.ClientConfig).Do(request)
|
||||
result.Duration = time.Since(startTime)
|
||||
|
@ -256,6 +256,7 @@ func TestEndpoint_Type(t *testing.T) {
|
||||
type args struct {
|
||||
URL string
|
||||
DNS *DNS
|
||||
SSH *SSH
|
||||
}
|
||||
tests := []struct {
|
||||
args args
|
||||
@ -325,6 +326,16 @@ func TestEndpoint_Type(t *testing.T) {
|
||||
},
|
||||
want: EndpointTypeWS,
|
||||
},
|
||||
{
|
||||
args: args{
|
||||
URL: "ssh://example.com:22",
|
||||
SSH: &SSH{
|
||||
Username: "root",
|
||||
Password: "password",
|
||||
},
|
||||
},
|
||||
want: EndpointTypeSSH,
|
||||
},
|
||||
{
|
||||
args: args{
|
||||
URL: "invalid://example.org",
|
||||
@ -454,6 +465,52 @@ func TestEndpoint_ValidateAndSetDefaultsWithDNS(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEndpoint_ValidateAndSetDefaultsWithSSH(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
username string
|
||||
password string
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "fail when has no user",
|
||||
username: "",
|
||||
password: "password",
|
||||
expectedErr: ErrEndpointWithoutSSHUsername,
|
||||
},
|
||||
{
|
||||
name: "fail when has no password",
|
||||
username: "username",
|
||||
password: "",
|
||||
expectedErr: ErrEndpointWithoutSSHPassword,
|
||||
},
|
||||
{
|
||||
name: "success when all fields are set",
|
||||
username: "username",
|
||||
password: "password",
|
||||
expectedErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
endpoint := &Endpoint{
|
||||
Name: "ssh-test",
|
||||
URL: "https://example.com",
|
||||
SSH: &SSH{
|
||||
Username: test.username,
|
||||
Password: test.password,
|
||||
},
|
||||
Conditions: []Condition{Condition("[STATUS] == 0")},
|
||||
}
|
||||
err := endpoint.ValidateAndSetDefaults()
|
||||
if err != test.expectedErr {
|
||||
t.Errorf("expected error %v, got %v", test.expectedErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEndpoint_ValidateAndSetDefaultsWithSimpleErrors(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
endpoint *Endpoint
|
||||
@ -680,6 +737,55 @@ func TestIntegrationEvaluateHealthForDNS(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationEvaluateHealthForSSH(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
endpoint Endpoint
|
||||
conditions []Condition
|
||||
success bool
|
||||
}{
|
||||
{
|
||||
name: "ssh-success",
|
||||
endpoint: Endpoint{
|
||||
Name: "ssh-success",
|
||||
URL: "ssh://localhost",
|
||||
SSH: &SSH{
|
||||
Username: "test",
|
||||
Password: "test",
|
||||
},
|
||||
Body: "{ \"command\": \"uptime\" }",
|
||||
},
|
||||
conditions: []Condition{Condition("[STATUS] == 0")},
|
||||
success: true,
|
||||
},
|
||||
{
|
||||
name: "ssh-failure",
|
||||
endpoint: Endpoint{
|
||||
Name: "ssh-failure",
|
||||
URL: "ssh://localhost",
|
||||
SSH: &SSH{
|
||||
Username: "test",
|
||||
Password: "test",
|
||||
},
|
||||
Body: "{ \"command\": \"uptime\" }",
|
||||
},
|
||||
conditions: []Condition{Condition("[STATUS] == 1")},
|
||||
success: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
test.endpoint.ValidateAndSetDefaults()
|
||||
test.endpoint.Conditions = test.conditions
|
||||
result := test.endpoint.EvaluateHealth()
|
||||
if result.Success != test.success {
|
||||
t.Errorf("Expected success to be %v, but was %v", test.success, result.Success)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationEvaluateHealthForICMP(t *testing.T) {
|
||||
endpoint := Endpoint{
|
||||
Name: "icmp-test",
|
||||
|
Reference in New Issue
Block a user