1
0
mirror of https://github.com/TwiN/gatus.git synced 2026-02-04 11:46:46 +00:00

feat(client): Add ssh private-key support (#1390)

* feat(endpoint): Add ssh key support

Fixes #1257

* test(config): Add tests for private key config

---------

Co-authored-by: TwiN <twin@linux.com>
This commit is contained in:
Mufeed Ali
2025-11-20 03:06:36 +05:30
committed by GitHub
parent 5d626f2934
commit 6f9db4107c
7 changed files with 94 additions and 35 deletions

View File

@@ -244,9 +244,7 @@ func formatDuration(d time.Duration) string {
if strings.HasSuffix(s, "0s") {
s = strings.TrimSuffix(s, "0s")
// Remove trailing "0m" if present after removing "0s"
if strings.HasSuffix(s, "0m") {
s = strings.TrimSuffix(s, "0m")
}
s = strings.TrimSuffix(s, "0m")
}
return s
}

View File

@@ -503,8 +503,8 @@ func (e *Endpoint) call(result *Result) {
}
result.Duration = time.Since(startTime)
} else if endpointType == TypeSSH {
// If there's no username/password specified, attempt to validate just the SSH banner
if e.SSHConfig == nil || (len(e.SSHConfig.Username) == 0 && len(e.SSHConfig.Password) == 0) {
// If there's no username, password or private key specified, attempt to validate just the SSH banner
if e.SSHConfig == nil || (len(e.SSHConfig.Username) == 0 && len(e.SSHConfig.Password) == 0 && len(e.SSHConfig.PrivateKey) == 0) {
result.Connected, result.HTTPStatus, err = client.CheckSSHBanner(strings.TrimPrefix(e.URL, "ssh://"), e.ClientConfig)
if err != nil {
result.AddError(err.Error())
@@ -515,7 +515,7 @@ func (e *Endpoint) call(result *Result) {
return
}
var cli *ssh.Client
result.Connected, cli, err = client.CanCreateSSHConnection(strings.TrimPrefix(e.URL, "ssh://"), e.SSHConfig.Username, e.SSHConfig.Password, e.ClientConfig)
result.Connected, cli, err = client.CanCreateSSHConnection(strings.TrimPrefix(e.URL, "ssh://"), e.SSHConfig.Username, e.SSHConfig.Password, e.SSHConfig.PrivateKey, e.ClientConfig)
if err != nil {
result.AddError(err.Error())
return

View File

@@ -511,26 +511,40 @@ func TestEndpoint_ValidateAndSetDefaultsWithSSH(t *testing.T) {
name string
username string
password string
privateKey string
expectedErr error
}{
{
name: "fail when has no user",
name: "fail when has no user but has password",
username: "",
password: "password",
expectedErr: ssh.ErrEndpointWithoutSSHUsername,
},
{
name: "fail when has no password",
username: "username",
password: "",
expectedErr: ssh.ErrEndpointWithoutSSHPassword,
name: "fail when has no user but has private key",
username: "",
privateKey: "-----BEGIN",
expectedErr: ssh.ErrEndpointWithoutSSHUsername,
},
{
name: "success when all fields are set",
name: "fail when has no password or private key",
username: "username",
password: "",
privateKey: "",
expectedErr: ssh.ErrEndpointWithoutSSHAuth,
},
{
name: "success when username and password are set",
username: "username",
password: "password",
expectedErr: nil,
},
{
name: "success when username and private key are set",
username: "username",
privateKey: "-----BEGIN",
expectedErr: nil,
},
}
for _, scenario := range scenarios {
@@ -539,8 +553,9 @@ func TestEndpoint_ValidateAndSetDefaultsWithSSH(t *testing.T) {
Name: "ssh-test",
URL: "https://example.com",
SSHConfig: &ssh.Config{
Username: scenario.username,
Password: scenario.password,
Username: scenario.username,
Password: scenario.password,
PrivateKey: scenario.privateKey,
},
Conditions: []Condition{Condition("[STATUS] == 0")},
}
@@ -1605,7 +1620,7 @@ func TestEndpoint_HideUIFeatures(t *testing.T) {
}
}
if tt.checkConditions {
hasConditions := result.ConditionResults != nil && len(result.ConditionResults) > 0
hasConditions := len(result.ConditionResults) > 0
if hasConditions != tt.expectConditions {
t.Errorf("Expected conditions=%v, got conditions=%v (actual: %v)", tt.expectConditions, hasConditions, result.ConditionResults)
}

View File

@@ -8,26 +8,29 @@ var (
// 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 SSH endpoint")
// 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 SSH endpoint")
// ErrEndpointWithoutSSHAuth is the error with which Gatus will panic if an endpoint with SSH monitoring is configured without a password or private key.
ErrEndpointWithoutSSHAuth = errors.New("you must specify a password or private-key for each SSH endpoint")
)
type Config struct {
Username string `yaml:"username,omitempty"`
Password string `yaml:"password,omitempty"`
Username string `yaml:"username,omitempty"`
Password string `yaml:"password,omitempty"`
PrivateKey string `yaml:"private-key,omitempty"`
}
// Validate the SSH configuration
func (cfg *Config) Validate() error {
// If there's no username and password, this endpoint can still check the SSH banner, so the endpoint is still valid
if len(cfg.Username) == 0 && len(cfg.Password) == 0 {
// If there's no username, password, or private key, this endpoint can still check the SSH banner, so the endpoint is still valid
if len(cfg.Username) == 0 && len(cfg.Password) == 0 && len(cfg.PrivateKey) == 0 {
return nil
}
// If any authentication method is provided (password or private key), a username is required
if len(cfg.Username) == 0 {
return ErrEndpointWithoutSSHUsername
}
if len(cfg.Password) == 0 {
return ErrEndpointWithoutSSHPassword
// If a username is provided, require at least a password or a private key
if len(cfg.Password) == 0 && len(cfg.PrivateKey) == 0 {
return ErrEndpointWithoutSSHAuth
}
return nil
}

View File

@@ -5,7 +5,7 @@ import (
"testing"
)
func TestSSH_validate(t *testing.T) {
func TestSSH_validatePasswordCfg(t *testing.T) {
cfg := &Config{}
if err := cfg.Validate(); err != nil {
t.Error("didn't expect an error")
@@ -13,11 +13,26 @@ func TestSSH_validate(t *testing.T) {
cfg.Username = "username"
if err := cfg.Validate(); err == nil {
t.Error("expected an error")
} else if !errors.Is(err, ErrEndpointWithoutSSHPassword) {
t.Errorf("expected error to be '%v', got '%v'", ErrEndpointWithoutSSHPassword, err)
} else if !errors.Is(err, ErrEndpointWithoutSSHAuth) {
t.Errorf("expected error to be '%v', got '%v'", ErrEndpointWithoutSSHAuth, err)
}
cfg.Password = "password"
if err := cfg.Validate(); err != nil {
t.Errorf("expected no error, got '%v'", err)
}
}
func TestSSH_validatePrivateKeyCfg(t *testing.T) {
t.Run("fail when username missing but private key provided", func(t *testing.T) {
cfg := &Config{PrivateKey: "-----BEGIN"}
if err := cfg.Validate(); !errors.Is(err, ErrEndpointWithoutSSHUsername) {
t.Fatalf("expected ErrEndpointWithoutSSHUsername, got %v", err)
}
})
t.Run("success when username with private key", func(t *testing.T) {
cfg := &Config{Username: "user", PrivateKey: "-----BEGIN"}
if err := cfg.Validate(); err != nil {
t.Fatalf("expected no error, got %v", err)
}
})
}