ecdba833d9
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---
116 lines
4.3 KiB
Go
116 lines
4.3 KiB
Go
// 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)
|
|
}
|