1
0
mirror of https://github.com/TwiN/gatus.git synced 2026-02-06 00:57:57 +00:00

feat(ssh): Add BODY placeholder support for SSH endpoints

- Modify ExecuteSSHCommand to capture stdout output
- Update SSH endpoint handling to use needsToReadBody() mechanism
- Add comprehensive test cases for SSH BODY functionality
- Support basic body content, pattern matching, JSONPath, and functions
- Maintain backward compatibility with existing SSH endpoints
This commit is contained in:
YanSH
2025-09-23 09:34:33 +08:00
parent 5a06a74cc3
commit 25d1e1044e
3 changed files with 110 additions and 8 deletions

View File

@@ -1,6 +1,7 @@
package client
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
@@ -301,7 +302,7 @@ func CheckSSHBanner(address string, cfg *Config) (bool, int, error) {
}
// ExecuteSSHCommand executes a command to an address using the SSH protocol.
func ExecuteSSHCommand(sshClient *ssh.Client, body string, config *Config) (bool, int, error) {
func ExecuteSSHCommand(sshClient *ssh.Client, body string, config *Config) (bool, int, []byte, error) {
type Body struct {
Command string `json:"command"`
}
@@ -309,26 +310,35 @@ func ExecuteSSHCommand(sshClient *ssh.Client, body string, config *Config) (bool
var b Body
body = parseLocalAddressPlaceholder(body, sshClient.Conn.LocalAddr())
if err := json.Unmarshal([]byte(body), &b); err != nil {
return false, 0, err
return false, 0, nil, err
}
sess, err := sshClient.NewSession()
if err != nil {
return false, 0, err
return false, 0, nil, err
}
// Capture stdout
var stdout bytes.Buffer
sess.Stdout = &stdout
err = sess.Start(b.Command)
if err != nil {
return false, 0, err
return false, 0, nil, err
}
defer sess.Close()
err = sess.Wait()
// Get the output
output := stdout.Bytes()
if err == nil {
return true, 0, nil
return true, 0, output, nil
}
var exitErr *ssh.ExitError
if ok := errors.As(err, &exitErr); !ok {
return false, 0, err
return false, 0, nil, err
}
return true, exitErr.ExitStatus(), nil
return true, exitErr.ExitStatus(), output, nil
}
// Ping checks if an address can be pinged and returns the round-trip time if the address can be pinged

View File

@@ -514,11 +514,16 @@ func (e *Endpoint) call(result *Result) {
result.AddError(err.Error())
return
}
result.Success, result.HTTPStatus, err = client.ExecuteSSHCommand(cli, e.getParsedBody(), e.ClientConfig)
var output []byte
result.Success, result.HTTPStatus, output, err = client.ExecuteSSHCommand(cli, e.getParsedBody(), e.ClientConfig)
if err != nil {
result.AddError(err.Error())
return
}
// Only store the output in result.Body if there's a condition that uses the BodyPlaceholder
if e.needsToReadBody() {
result.Body = output
}
result.Duration = time.Since(startTime)
} else {
response, err = client.GetHTTPClient(e.ClientConfig).Do(request)

View File

@@ -834,6 +834,93 @@ func TestIntegrationEvaluateHealthForSSH(t *testing.T) {
conditions: []Condition{Condition("[STATUS] == 1")},
success: false,
},
{
name: "ssh-body-basic",
endpoint: Endpoint{
Name: "ssh-body-basic",
URL: "ssh://localhost",
SSHConfig: &ssh.Config{
Username: "scenario",
Password: "scenario",
},
Body: "{ \"command\": \"echo 'test-output'\" }",
},
conditions: []Condition{
Condition("[STATUS] == 0"),
Condition("[BODY] == test-output"),
},
success: true,
},
{
name: "ssh-body-pattern",
endpoint: Endpoint{
Name: "ssh-body-pattern",
URL: "ssh://localhost",
SSHConfig: &ssh.Config{
Username: "scenario",
Password: "scenario",
},
Body: "{ \"command\": \"echo 'test-pattern-match'\" }",
},
conditions: []Condition{
Condition("[STATUS] == 0"),
Condition("[BODY] == pat(*pattern*)"),
},
success: true,
},
{
name: "ssh-body-json-path",
endpoint: Endpoint{
Name: "ssh-body-json",
URL: "ssh://localhost",
SSHConfig: &ssh.Config{
Username: "scenario",
Password: "scenario",
},
Body: "{ \"command\": \"echo '{\\\"status\\\": \\\"healthy\\\", \\\"memory\\\": {\\\"used\\\": 512}}'\" }",
},
conditions: []Condition{
Condition("[STATUS] == 0"),
Condition("[BODY].status == healthy"),
Condition("[BODY].memory.used > 500"),
},
success: true,
},
{
name: "ssh-body-functions",
endpoint: Endpoint{
Name: "ssh-body-functions",
URL: "ssh://localhost",
SSHConfig: &ssh.Config{
Username: "scenario",
Password: "scenario",
},
Body: "{ \"command\": \"echo -n '12345'\" }",
},
conditions: []Condition{
Condition("[STATUS] == 0"),
Condition("len([BODY]) == 5"),
Condition("has([BODY]) == true"),
},
success: true,
},
{
name: "ssh-command-fail-with-body",
endpoint: Endpoint{
Name: "ssh-command-fail-body",
URL: "ssh://localhost",
SSHConfig: &ssh.Config{
Username: "scenario",
Password: "scenario",
},
Body: "{ \"command\": \"echo 'error message'; exit 1\" }",
},
conditions: []Condition{
Condition("[STATUS] == 1"),
Condition("[BODY] == pat(*error*)"),
},
success: true,
},
}
for _, scenario := range scenarios {