Files
spinifex/tests/e2e/harness/spxcli.go
T
2026-06-12 12:38:31 +10:00

164 lines
4.8 KiB
Go

//go:build e2e
package harness
import (
"bytes"
"os"
"os/exec"
"regexp"
"strings"
"testing"
)
// AccountInfo is the parsed result of `spx admin account create`. Fields
// match the lines emitted by cmd/spinifex/cmd/admin.go:runAccountCreate.
type AccountInfo struct {
AccountID string
AccountName string
AccessKeyID string
SecretAccessKey string
Profile string
}
// SpxBin returns the spx binary path (SPX_BIN env var or "spx" on PATH).
func SpxBin() string {
if v := os.Getenv("SPX_BIN"); v != "" {
return v
}
return "spx"
}
// spxChildEnv returns a sanitized environment for `spx` child processes.
// Harness SPINIFEX_* vars would override cluster.toml fields via viper
// AutomaticEnv, so we strip them except for the small set spx explicitly
// binds via BindEnv.
func spxChildEnv() []string {
keep := map[string]struct{}{
"SPINIFEX_CONFIG_PATH": {},
"SPINIFEX_ACCESS_KEY": {},
"SPINIFEX_SECRET_KEY": {},
"SPINIFEX_HOST": {},
"SPINIFEX_BASE_DIR": {},
"SPINIFEX_NATS_HOST": {},
"SPINIFEX_NATS_TOKEN": {},
"SPINIFEX_NATS_SUBJECT": {},
}
out := os.Environ()[:0]
for _, kv := range os.Environ() {
eq := strings.IndexByte(kv, '=')
if eq < 0 {
out = append(out, kv)
continue
}
k := kv[:eq]
if strings.HasPrefix(k, "SPINIFEX_") {
if _, ok := keep[k]; !ok {
continue
}
}
out = append(out, kv)
}
return out
}
// SpxRun runs `spx <args...>` and returns combined stdout+stderr. If wantErr
// is false the test fails on non-zero exit; if true the call returns the
// output and the test continues (caller asserts on output content).
func SpxRun(t *testing.T, wantErr bool, args ...string) string {
t.Helper()
var buf bytes.Buffer
cmd := exec.Command(SpxBin(), args...)
cmd.Env = spxChildEnv()
cmd.Stdout = &buf
cmd.Stderr = &buf
err := cmd.Run()
out := buf.String()
if err != nil && !wantErr {
t.Fatalf("spx %s failed: %v\noutput:\n%s", strings.Join(args, " "), err, out)
}
if err == nil && wantErr {
t.Fatalf("spx %s expected non-zero exit, got success\noutput:\n%s", strings.Join(args, " "), out)
}
return out
}
// SpxGetNodes runs `spx get nodes`.
func SpxGetNodes(t *testing.T) string {
t.Helper()
return SpxRun(t, false, "get", "nodes")
}
// SpxGetVMs runs `spx get vms`.
func SpxGetVMs(t *testing.T) string {
t.Helper()
return SpxRun(t, false, "get", "vms")
}
// SpxRunBestEffort runs `spx <args...>` and returns combined output, ignoring
// the exit code. CLI NATS dial can race cluster join and return an error even
// when the data path is healthy.
func SpxRunBestEffort(t *testing.T, args ...string) string {
t.Helper()
var buf bytes.Buffer
cmd := exec.Command(SpxBin(), args...)
cmd.Env = spxChildEnv()
cmd.Stdout = &buf
cmd.Stderr = &buf
_ = cmd.Run()
return buf.String()
}
// SpxTopNodes runs `spx top nodes`.
func SpxTopNodes(t *testing.T) string {
t.Helper()
return SpxRun(t, false, "top", "nodes")
}
// admin account create output is a fixed two-column key/value list; pin each
// field to a line anchor so a layout change (added/reordered keys) surfaces
// loudly instead of silently matching the wrong value.
var (
spxAcctRE = regexp.MustCompile(`(?m)^\s+Account ID:\s+(\S+)`)
spxNameRE = regexp.MustCompile(`(?m)^\s+Account Name:\s+(.+?)\s*$`)
spxKeyIDRE = regexp.MustCompile(`(?m)^\s+Access Key ID:\s+(\S+)`)
spxSecretRE = regexp.MustCompile(`(?m)^\s+Secret Access Key:\s+(\S+)`)
spxProfRE = regexp.MustCompile(`(?m)^\s+AWS Profile:\s+(\S+)`)
)
// SpxAdminAccountCreate creates a tenant account via the spx CLI and parses
// the printed credentials block. email is forwarded as --email when non-empty;
// `spx admin account create` ignores unknown flags today, but the CLI may
// adopt --email in future and any caller that wants to gate on it (e.g. IAM
// e-mail uniqueness checks) should pass it through unchanged.
func SpxAdminAccountCreate(t *testing.T, name, email string) AccountInfo {
t.Helper()
args := []string{"admin", "account", "create", "--name", name}
if email != "" {
args = append(args, "--email", email)
}
out := SpxRun(t, false, args...)
info := AccountInfo{
AccountID: firstSubmatch(spxAcctRE, out),
AccountName: firstSubmatch(spxNameRE, out),
AccessKeyID: firstSubmatch(spxKeyIDRE, out),
SecretAccessKey: firstSubmatch(spxSecretRE, out),
Profile: firstSubmatch(spxProfRE, out),
}
if info.AccountID == "" || info.AccessKeyID == "" || info.SecretAccessKey == "" {
// Dump raw output to the artifact dir if available — otherwise the
// formatted log block lives only in the JUnit XML.
t.Fatalf("spx admin account create: failed to parse credentials block\n--- raw output ---\n%s\n--- end ---", out)
}
return info
}
func firstSubmatch(re *regexp.Regexp, s string) string {
m := re.FindStringSubmatch(s)
if len(m) < 2 {
return ""
}
return strings.TrimSpace(m[1])
}