mirror of
https://github.com/TwiN/gatus.git
synced 2026-02-16 14:58:40 +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
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"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.
|
// 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 {
|
type Body struct {
|
||||||
Command string `json:"command"`
|
Command string `json:"command"`
|
||||||
}
|
}
|
||||||
@@ -309,26 +310,35 @@ func ExecuteSSHCommand(sshClient *ssh.Client, body string, config *Config) (bool
|
|||||||
var b Body
|
var b Body
|
||||||
body = parseLocalAddressPlaceholder(body, sshClient.Conn.LocalAddr())
|
body = parseLocalAddressPlaceholder(body, sshClient.Conn.LocalAddr())
|
||||||
if err := json.Unmarshal([]byte(body), &b); err != nil {
|
if err := json.Unmarshal([]byte(body), &b); err != nil {
|
||||||
return false, 0, err
|
return false, 0, nil, err
|
||||||
}
|
}
|
||||||
sess, err := sshClient.NewSession()
|
sess, err := sshClient.NewSession()
|
||||||
if err != nil {
|
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)
|
err = sess.Start(b.Command)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, 0, err
|
return false, 0, nil, err
|
||||||
}
|
}
|
||||||
defer sess.Close()
|
defer sess.Close()
|
||||||
err = sess.Wait()
|
err = sess.Wait()
|
||||||
|
|
||||||
|
// Get the output
|
||||||
|
output := stdout.Bytes()
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return true, 0, nil
|
return true, 0, output, nil
|
||||||
}
|
}
|
||||||
var exitErr *ssh.ExitError
|
var exitErr *ssh.ExitError
|
||||||
if ok := errors.As(err, &exitErr); !ok {
|
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
|
// 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())
|
result.AddError(err.Error())
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
return
|
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)
|
result.Duration = time.Since(startTime)
|
||||||
} else {
|
} else {
|
||||||
response, err = client.GetHTTPClient(e.ClientConfig).Do(request)
|
response, err = client.GetHTTPClient(e.ClientConfig).Do(request)
|
||||||
|
|||||||
@@ -834,6 +834,93 @@ func TestIntegrationEvaluateHealthForSSH(t *testing.T) {
|
|||||||
conditions: []Condition{Condition("[STATUS] == 1")},
|
conditions: []Condition{Condition("[STATUS] == 1")},
|
||||||
success: false,
|
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 {
|
for _, scenario := range scenarios {
|
||||||
|
|||||||
Reference in New Issue
Block a user