feat(P7): doctor ingress + docs + integration tests (REQ-177,178,179)
New 'orca doctor ingress' command: verifies podman orca-traefik
container running, nft DNAT+SNAT, /etc/traefik/dynamic exists,
step-ca root CA present.
UAT signoff script: replaced assertion 36 (systemd → podman
container), added assertions 40-46 (nft table, DNAT, SNAT, dynamic
dir, step-ca CA, traefik.yml, doctor ingress pass).
docs/ingress.md: R-024 podman traefik section — three topologies,
container config, nft ruleset, doctor ingress, Dockerfile.traefik.
TLS model updated (drop certResolver, tls:{} for v0.14, mTLS v0.15).
ARCHITECTURE.md: v0.14 deltas section — R-024, three topologies,
nft emitter changes, TLS model, migration 0009, new CLI.
Integration tests (tests/ingress_bootstrap_test.go): nft postrouting
+ DNATTarget, priority -10, traefik TLS model (tls:{} no
certResolver), image ref resolution, floating-IP LXC provisioning
commands (pct create with hwaddr/ip/gw/features), MAC generation.
---ci---
project: orca
phase: 7
milestone: v0.14
status: execute
---/ci---
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
// Package cli: doctor_ingress.go implements `orca doctor ingress`
|
||||
// (R-024, v0.14). The check verifies the podman traefik container is
|
||||
// running, nft DNAT+SNAT is applied, the dynamic config directory
|
||||
// exists, and the step-ca root CA is mounted.
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var doctorIngressCmd = &cobra.Command{
|
||||
Use: "ingress",
|
||||
Short: "Check the ingress stack (R-024: podman traefik + nft + CA)",
|
||||
Long: `Verify the orca ingress data plane is healthy:
|
||||
1. orca-traefik podman container is running
|
||||
2. nft DNAT + SNAT masquerade applied
|
||||
3. /etc/traefik/dynamic directory exists
|
||||
4. step-ca root CA mounted at /etc/orca/step-ca-root.crt
|
||||
|
||||
For remote peers, use --peer <name>.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
results := runIngressChecks(ctx)
|
||||
if jsonOutput {
|
||||
return printJSON(results)
|
||||
}
|
||||
allPass := true
|
||||
for _, r := range results {
|
||||
status := "✓"
|
||||
if r.Result != "PASS" {
|
||||
status = "✗"
|
||||
allPass = false
|
||||
}
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "%s %s: %s\n", status, r.Name, r.Message)
|
||||
}
|
||||
if !allPass {
|
||||
return fmt.Errorf("ingress checks failed")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// ingressCheckResult is one line of `orca doctor ingress` output.
|
||||
type ingressCheckResult struct {
|
||||
Name string `json:"name"`
|
||||
Result string `json:"result"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func runIngressChecks(ctx context.Context) []ingressCheckResult {
|
||||
t, err := nftTransportFromCtx()
|
||||
if err != nil {
|
||||
return []ingressCheckResult{{Name: "ingress:transport", Result: "FAIL", Message: err.Error()}}
|
||||
}
|
||||
peer := nftLeadPeer()
|
||||
var results []ingressCheckResult
|
||||
|
||||
// 1. Check podman orca-traefik container is running.
|
||||
out, err := t.Exec(ctx, peer, "podman inspect --format '{{.State.Running}}' orca-traefik 2>/dev/null")
|
||||
if err != nil {
|
||||
results = append(results, ingressCheckResult{Name: "ingress:container", Result: "FAIL", Message: fmt.Sprintf("podman inspect: %v", err)})
|
||||
} else {
|
||||
v := strings.TrimSpace(string(out))
|
||||
if v == "true" {
|
||||
results = append(results, ingressCheckResult{Name: "ingress:container", Result: "PASS", Message: "orca-traefik container running"})
|
||||
} else if v == "false" {
|
||||
results = append(results, ingressCheckResult{Name: "ingress:container", Result: "FAIL", Message: "orca-traefik container is stopped"})
|
||||
} else {
|
||||
results = append(results, ingressCheckResult{Name: "ingress:container", Result: "FAIL", Message: "orca-traefik container not found"})
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check nft DNAT + SNAT (reuse the nft table output).
|
||||
tableOut, tableErr := t.Exec(ctx, peer, "nft list table inet orca-ingress 2>/dev/null")
|
||||
if tableErr != nil {
|
||||
results = append(results, ingressCheckResult{Name: "ingress:nft", Result: "FAIL", Message: "nft table orca-ingress missing"})
|
||||
} else {
|
||||
tableStr := string(tableOut)
|
||||
hasDNAT := strings.Contains(tableStr, "dnat to")
|
||||
hasSNAT := strings.Contains(tableStr, "masquerade")
|
||||
if hasDNAT && hasSNAT {
|
||||
results = append(results, ingressCheckResult{Name: "ingress:nft", Result: "PASS", Message: "nft DNAT + SNAT masquerade present"})
|
||||
} else if hasDNAT {
|
||||
results = append(results, ingressCheckResult{Name: "ingress:nft", Result: "WARN", Message: "DNAT present but SNAT masquerade missing"})
|
||||
} else {
|
||||
results = append(results, ingressCheckResult{Name: "ingress:nft", Result: "FAIL", Message: "nft DNAT missing"})
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Check /etc/traefik/dynamic directory exists.
|
||||
if _, err := t.Exec(ctx, peer, "test -d /etc/traefik/dynamic"); err != nil {
|
||||
results = append(results, ingressCheckResult{Name: "ingress:dynamic-dir", Result: "FAIL", Message: "/etc/traefik/dynamic directory missing"})
|
||||
} else {
|
||||
results = append(results, ingressCheckResult{Name: "ingress:dynamic-dir", Result: "PASS", Message: "/etc/traefik/dynamic exists"})
|
||||
}
|
||||
|
||||
// 4. Check step-ca root CA is mounted/present.
|
||||
if _, err := t.Exec(ctx, peer, "test -f /etc/orca/step-ca-root.crt"); err != nil {
|
||||
results = append(results, ingressCheckResult{Name: "ingress:ca", Result: "WARN", Message: "/etc/orca/step-ca-root.crt missing (TLS not configured)"})
|
||||
} else {
|
||||
results = append(results, ingressCheckResult{Name: "ingress:ca", Result: "PASS", Message: "step-ca root CA present"})
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func init() {
|
||||
doctorCmd.AddCommand(doctorIngressCmd)
|
||||
}
|
||||
Reference in New Issue
Block a user