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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user