020aa01623
P11: orca job lint <spec.md> — schema/CEL/body/migration/best-practice checks; --explain, --format json; exit 0/1 by errors found. P12: orca job verify <spec.md> — dry-run txn (render + stage + verify without apply); reports planned allocs/files/units; no side effects; --namespace, --json. ---ci--- project: orca phase: 11 milestone: v0.11 status: execute ---/ci---
311 lines
7.6 KiB
Go
311 lines
7.6 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func writeMDSpec(t *testing.T, content string) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
p := filepath.Join(dir, "spec.md")
|
|
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
|
|
t.Fatalf("write spec: %v", err)
|
|
}
|
|
return p
|
|
}
|
|
|
|
func writeHCLSpec(t *testing.T, content string) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
p := filepath.Join(dir, "spec.hcl")
|
|
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
|
|
t.Fatalf("write spec: %v", err)
|
|
}
|
|
return p
|
|
}
|
|
|
|
const validJobMD = "---\n" +
|
|
"kind: Job\n" +
|
|
"name: my-job\n" +
|
|
"runtime:\n" +
|
|
" one_of: process\n" +
|
|
" command: /bin/true\n" +
|
|
"---\n" +
|
|
"# My Job\n\nRuns /bin/true.\n"
|
|
|
|
const validServiceMD = "---\n" +
|
|
"kind: Service\n" +
|
|
"name: web\n" +
|
|
"count: 3\n" +
|
|
"runtime:\n" +
|
|
" one_of: process\n" +
|
|
" command: /bin/http\n" +
|
|
"ports:\n" +
|
|
" - name: http\n" +
|
|
" port: 8080\n" +
|
|
"restart:\n" +
|
|
" mode: service\n" +
|
|
"update:\n" +
|
|
" strategy: rolling\n" +
|
|
" max_surge: 1\n" +
|
|
"health:\n" +
|
|
" check_type: http\n" +
|
|
" interval: 5s\n" +
|
|
"---\n" +
|
|
"# Web service\n\nServes HTTP.\n"
|
|
|
|
const invalidKindMD = "---\n" +
|
|
"kind: CronJob\n" +
|
|
"name: bad\n" +
|
|
"---\nbody\n"
|
|
|
|
const serviceMissingHealthMD = "---\n" +
|
|
"kind: Service\n" +
|
|
"name: web\n" +
|
|
"count: 2\n" +
|
|
"runtime:\n" +
|
|
" one_of: process\n" +
|
|
" command: /bin/http\n" +
|
|
"ports:\n" +
|
|
" - name: http\n" +
|
|
" port: 8080\n" +
|
|
"restart:\n" +
|
|
" mode: service\n" +
|
|
"update:\n" +
|
|
" strategy: rolling\n" +
|
|
"---\n" +
|
|
"# web\n\nbody\n"
|
|
|
|
const serviceMissingPortsMD = "---\n" +
|
|
"kind: Service\n" +
|
|
"name: web\n" +
|
|
"runtime:\n" +
|
|
" one_of: process\n" +
|
|
" command: /bin/http\n" +
|
|
"restart:\n" +
|
|
" mode: service\n" +
|
|
"update:\n" +
|
|
" strategy: rolling\n" +
|
|
"health:\n" +
|
|
" check_type: http\n" +
|
|
"---\nbody\n"
|
|
|
|
const emptyBodyMD = "---\n" +
|
|
"kind: Job\n" +
|
|
"name: emptybody\n" +
|
|
"runtime:\n" +
|
|
" command: /bin/true\n" +
|
|
"---\n"
|
|
|
|
const badCELMD = "---\n" +
|
|
"kind: Job\n" +
|
|
"name: badcel\n" +
|
|
"runtime:\n" +
|
|
" command: /bin/true\n" +
|
|
"constraints:\n" +
|
|
" - 'node.role == \"web\"'\n" +
|
|
" - 'region == (\"us\"'\n" +
|
|
"---\nbody\n"
|
|
|
|
func TestJobLintCmdRegistered(t *testing.T) {
|
|
for _, c := range jobCmd.Commands() {
|
|
if c.Name() == "lint" {
|
|
return
|
|
}
|
|
}
|
|
t.Fatal("job lint command not registered on jobCmd")
|
|
}
|
|
|
|
func TestJobLintValidSpec(t *testing.T) {
|
|
resetRootFlags(t)
|
|
spec := writeMDSpec(t, validJobMD)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"job", "lint", spec})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("job lint valid: %v", err)
|
|
}
|
|
out := buf.String()
|
|
if !strings.Contains(out, "0 error(s)") {
|
|
t.Errorf("expected 0 errors, got: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestJobLintValidServiceSpec(t *testing.T) {
|
|
resetRootFlags(t)
|
|
spec := writeMDSpec(t, validServiceMD)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"job", "lint", spec})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("job lint valid service: %v", err)
|
|
}
|
|
out := buf.String()
|
|
if !strings.Contains(out, "0 error(s)") {
|
|
t.Errorf("expected 0 errors, got: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestJobLintInvalidKind(t *testing.T) {
|
|
resetRootFlags(t)
|
|
spec := writeMDSpec(t, invalidKindMD)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"job", "lint", spec})
|
|
if err := rootCmd.Execute(); err == nil {
|
|
t.Fatal("expected error for invalid kind, got nil")
|
|
}
|
|
}
|
|
|
|
func TestJobLintInvalidKindReportsError(t *testing.T) {
|
|
resetRootFlags(t)
|
|
spec := writeMDSpec(t, invalidKindMD)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"job", "lint", spec})
|
|
_ = rootCmd.Execute()
|
|
out := buf.String()
|
|
if !strings.Contains(strings.ToLower(out), "cronjob") && !strings.Contains(strings.ToLower(out), "kind") {
|
|
t.Errorf("expected error about kind, got: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestJobLintMissingRequiredField(t *testing.T) {
|
|
resetRootFlags(t)
|
|
spec := writeMDSpec(t, serviceMissingPortsMD)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"job", "lint", spec})
|
|
if err := rootCmd.Execute(); err == nil {
|
|
t.Fatal("expected error for missing ports, got nil")
|
|
}
|
|
out := buf.String()
|
|
if !strings.Contains(strings.ToLower(out), "port") {
|
|
t.Errorf("expected error mentioning ports, got: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestJobLintDeprecatedHCLWarning(t *testing.T) {
|
|
resetRootFlags(t)
|
|
spec := writeHCLSpec(t, trueJobSpec)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"job", "lint", spec})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("job lint hcl: %v", err)
|
|
}
|
|
out := buf.String()
|
|
if !strings.Contains(out, "migration") && !strings.Contains(strings.ToLower(out), "legacy") && !strings.Contains(strings.ToLower(out), "hcl") {
|
|
t.Errorf("expected migration/legacy warning, got: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestJobLintExplain(t *testing.T) {
|
|
resetRootFlags(t)
|
|
spec := writeMDSpec(t, serviceMissingHealthMD)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"job", "lint", spec, "--explain"})
|
|
_ = rootCmd.Execute()
|
|
out := buf.String()
|
|
if !strings.Contains(out, "->") {
|
|
t.Errorf("expected rationale lines with '->', got: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestJobLintFormatJSON(t *testing.T) {
|
|
resetRootFlags(t)
|
|
spec := writeMDSpec(t, serviceMissingHealthMD)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"job", "lint", spec, "--format", "json"})
|
|
_ = rootCmd.Execute()
|
|
out := buf.String()
|
|
var findings []lintFinding
|
|
if err := json.Unmarshal(bytes.TrimSpace([]byte(out)), &findings); err != nil {
|
|
t.Fatalf("unmarshal json findings: %v\n%s", err, out)
|
|
}
|
|
if len(findings) == 0 {
|
|
t.Fatalf("expected at least one finding")
|
|
}
|
|
foundHealth := false
|
|
for _, f := range findings {
|
|
if strings.Contains(strings.ToLower(f.Message), "health") {
|
|
foundHealth = true
|
|
}
|
|
}
|
|
if !foundHealth {
|
|
t.Errorf("expected a health-related finding, got: %+v", findings)
|
|
}
|
|
}
|
|
|
|
func TestJobLintMissingHealthCheckWarning(t *testing.T) {
|
|
resetRootFlags(t)
|
|
spec := writeMDSpec(t, serviceMissingHealthMD)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"job", "lint", spec})
|
|
_ = rootCmd.Execute()
|
|
out := buf.String()
|
|
if !strings.Contains(strings.ToLower(out), "health") {
|
|
t.Errorf("expected health-related warning, got: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestJobLintEmptyBodyWarning(t *testing.T) {
|
|
resetRootFlags(t)
|
|
spec := writeMDSpec(t, emptyBodyMD)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"job", "lint", spec})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("job lint empty body (warnings only): %v", err)
|
|
}
|
|
out := buf.String()
|
|
if !strings.Contains(strings.ToLower(out), "body") {
|
|
t.Errorf("expected body warning, got: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestJobLintBadCEL(t *testing.T) {
|
|
resetRootFlags(t)
|
|
spec := writeMDSpec(t, badCELMD)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"job", "lint", spec})
|
|
if err := rootCmd.Execute(); err == nil {
|
|
t.Fatal("expected error for unbalanced CEL, got nil")
|
|
}
|
|
out := buf.String()
|
|
if !strings.Contains(strings.ToLower(out), "cel") && !strings.Contains(strings.ToLower(out), "parenthes") {
|
|
t.Errorf("expected CEL/parentheses error, got: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestJobLintMissingFile(t *testing.T) {
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"job", "lint", "/nonexistent/spec.md"})
|
|
if err := rootCmd.Execute(); err == nil {
|
|
t.Fatal("expected error for missing file, got nil")
|
|
}
|
|
}
|