Files

118 lines
3.5 KiB
Go

package executor
import (
"context"
"errors"
"fmt"
"os/exec"
"strconv"
"strings"
"time"
)
// DiagnosticRequest is a structured, read-only diagnostic operation. It is deliberately
// not a shell command: every action maps to a fixed executable/argument set.
type DiagnosticRequest struct {
Action string `json:"action"`
Target string `json:"target,omitempty"`
Lines int `json:"lines,omitempty"`
}
type DiagnosticResult struct {
Action string `json:"action"`
Target string `json:"target,omitempty"`
ExitCode int `json:"exit_code"`
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
DurationMS int64 `json:"duration_ms"`
}
func Diagnostic(ctx context.Context, req DiagnosticRequest) (DiagnosticResult, error) {
args, err := diagnosticArgs(req)
if err != nil {
return DiagnosticResult{}, err
}
if req.Lines <= 0 {
req.Lines = 100
}
if req.Lines > 500 {
req.Lines = 500
}
// docker logs is the only operation where a caller-controlled numeric limit is useful.
if req.Action == "docker_logs" {
args = []string{"logs", "--tail", strconv.Itoa(req.Lines), req.Target}
}
start := time.Now()
c, cancel := context.WithTimeout(ctx, 15*time.Second)
defer cancel()
cmd := exec.CommandContext(c, args[0], args[1:]...)
out, runErr := cmd.CombinedOutput()
const maxOutput = 256 * 1024
if len(out) > maxOutput {
out = append(out[:maxOutput], []byte("\n[output truncated at 256 KiB]\n")...)
}
code := 0
if cmd.ProcessState != nil {
code = cmd.ProcessState.ExitCode()
}
result := DiagnosticResult{Action: req.Action, Target: req.Target, ExitCode: code, Stdout: string(out), DurationMS: time.Since(start).Milliseconds()}
if runErr != nil && code == -1 {
return result, fmt.Errorf("diagnostic failed: %w", runErr)
}
return result, nil
}
func diagnosticArgs(req DiagnosticRequest) ([]string, error) {
target := strings.TrimSpace(req.Target)
switch req.Action {
case "docker_ps":
if target != "" {
return nil, errors.New("docker_ps does not accept target")
}
return []string{"docker", "ps", "--no-trunc"}, nil
case "docker_logs":
if target == "" || strings.ContainsAny(target, " \t\r\n;|&<>$`") {
return nil, errors.New("invalid docker container target")
}
return []string{"docker", "logs", "--tail", "100", target}, nil
case "docker_inspect":
if target == "" || strings.ContainsAny(target, " \t\r\n;|&<>$`") {
return nil, errors.New("invalid docker target")
}
return []string{"docker", "inspect", target}, nil
case "systemctl_status":
if target == "" || strings.ContainsAny(target, " \t\r\n;|&<>$`") {
return nil, errors.New("invalid systemd unit")
}
return []string{"systemctl", "status", "--no-pager", "--full", target}, nil
case "journalctl":
if target == "" || strings.ContainsAny(target, " \t\r\n;|&<>$`") {
return nil, errors.New("invalid journal unit")
}
return []string{"journalctl", "--no-pager", "-u", target, "-n", "100"}, nil
case "ss_listen":
if target != "" {
return nil, errors.New("ss_listen does not accept target")
}
return []string{"ss", "-lntp"}, nil
case "df":
if target != "" {
return nil, errors.New("df does not accept target")
}
return []string{"df", "-h"}, nil
case "free":
if target != "" {
return nil, errors.New("free does not accept target")
}
return []string{"free", "-h"}, nil
case "uptime":
if target != "" {
return nil, errors.New("uptime does not accept target")
}
return []string{"uptime"}, nil
default:
return nil, fmt.Errorf("unsupported diagnostic action: %s", req.Action)
}
}