Files
orca/tests/ingress_bootstrap_test.go
T
Jon Chery ecdba833d9 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---
2026-08-10 20:24:13 +00:00

164 lines
5.0 KiB
Go

package tests
import (
"log/slog"
"strings"
"testing"
"git.cloudinit.dev/coreci/orca/internal/emitter"
"git.cloudinit.dev/coreci/orca/internal/jobspec"
"git.cloudinit.dev/coreci/orca/internal/proxmox"
"git.cloudinit.dev/coreci/orca/internal/traefik"
)
// TestNftEmitter_PostroutingAndDNATTarget (REQ-173) verifies the nft
// emitter renders the postrouting masquerade chain and supports
// DNATTarget substitution.
func TestNftEmitter_PostroutingAndDNATTarget(t *testing.T) {
files, err := emitter.NftEmitter{}.RenderNftConfig(emitter.NftClusterConfig{
DNATTarget: "10.99.0.10",
EnableSNAT: true,
})
if err != nil {
t.Fatalf("RenderNftConfig: %v", err)
}
c := files[0].Content
if !strings.Contains(c, "chain postrouting") {
t.Errorf("missing postrouting chain:\n%s", c)
}
if !strings.Contains(c, "masquerade") {
t.Errorf("missing masquerade rule:\n%s", c)
}
if !strings.Contains(c, "dnat to 10.99.0.10:8443") {
t.Errorf("missing custom DNAT target:\n%s", c)
}
}
// TestNftEmitter_PriorityMinus10 (research Topic 2) verifies the input
// and forward chains use priority -10 for pve-firewall coexistence.
func TestNftEmitter_PriorityMinus10(t *testing.T) {
files, _ := emitter.NftEmitter{}.RenderNftConfig(emitter.NftClusterConfig{})
c := files[0].Content
if !strings.Contains(c, "hook input priority -10;") {
t.Errorf("input chain should use priority -10:\n%s", c)
}
if !strings.Contains(c, "hook forward priority -10;") {
t.Errorf("forward chain should use priority -10:\n%s", c)
}
}
// TestTraefikEmitter_TLSModel (REQ-172) verifies the dynamic config
// emits tls: {} and does NOT contain certResolver (dropped in v0.14).
func TestTraefikEmitter_TLSModel(t *testing.T) {
spec := &jobspec.WorkloadSpec{
Name: "test-svc",
Kind: "Service",
Ports: []jobspec.PortSpec{{Name: "http"}},
}
node := &emitter.Node{
Hostname: "test-node",
}
files, err := emitter.TraefikEmitter{}.Render(spec, node)
if err != nil {
t.Fatalf("Render: %v", err)
}
c := files[0].Content
if !strings.Contains(c, "tls: {}") {
t.Errorf("missing tls: {} (v0.14 model):\n%s", c)
}
if strings.Contains(c, "certResolver: orca") {
t.Errorf("certResolver: orca should be removed (v0.14):\n%s", c)
}
}
// TestTraefikImageRef verifies the image reference resolution for the
// orca-traefik podman container.
func TestTraefikImageRef(t *testing.T) {
ref := traefik.ImageRef("v0.13.7")
want := "git.cloudinit.dev/coreci/orca-traefik:v0.13.7"
if ref != want {
t.Errorf("ImageRef(v0.13.7) = %q, want %q", ref, want)
}
// Dev build falls back to latest.
ref = traefik.ImageRef("dev")
if ref != "git.cloudinit.dev/coreci/orca-traefik:latest" {
t.Errorf("ImageRef(dev) = %q, want latest", ref)
}
}
// TestProxmox_FloatingIP_LXC_ProvisioningCommands (REQ-176) verifies
// the ProvisionIngressLXC function sends the correct pct create
// command with the right net0 parameters.
func TestProxmox_FloatingIP_LXC_ProvisioningCommands(t *testing.T) {
var cmds []string
execFn := func(cmd string) ([]byte, error) {
cmds = append(cmds, cmd)
// Simulate: pct status returns "absent" on first call, then OK.
if strings.Contains(cmd, "pct status 201") {
return []byte("absent\n"), nil
}
if strings.Contains(cmd, "pct create") {
return []byte(""), nil
}
if strings.Contains(cmd, "pct start 201") {
return []byte(""), nil
}
if strings.Contains(cmd, "hostname -I") {
return []byte("203.0.113.10\n"), nil
}
return []byte(""), nil
}
err := proxmox.ProvisionIngressLXC(nil, execFn, proxmox.FloatingIPOptions{
FloatingIP: "203.0.113.10",
Gateway: "203.0.113.1",
MAC: "02:01:02:03:04:05",
NetPrefix: 24,
LXCTemplate: "ubuntu-24.04",
}, slog.Default())
if err != nil {
t.Fatalf("ProvisionIngressLXC: %v", err)
}
// Verify pct create has the right net0 params.
foundCreate := false
for _, c := range cmds {
if strings.Contains(c, "pct create") {
foundCreate = true
if !strings.Contains(c, "hostname ingress") {
t.Errorf("pct create missing hostname ingress: %s", c)
}
if !strings.Contains(c, "hwaddr=02:01:02:03:04:05") {
t.Errorf("pct create missing hwaddr: %s", c)
}
if !strings.Contains(c, "ip=203.0.113.10/24") {
t.Errorf("pct create missing ip: %s", c)
}
if !strings.Contains(c, "gw=203.0.113.1") {
t.Errorf("pct create missing gw: %s", c)
}
if !strings.Contains(c, "nesting=1,keyctl=1,fuse=1") {
t.Errorf("pct create missing features (research Topic 3): %s", c)
}
}
}
if !foundCreate {
t.Errorf("pct create command not sent\ncommands: %v", cmds)
}
}
// TestProxmox_GenerateRandomMAC (D-261) verifies MAC generation produces
// a valid locally-administered MAC.
func TestProxmox_GenerateRandomMAC(t *testing.T) {
mac, err := proxmox.GenerateRandomMAC()
if err != nil {
t.Fatalf("GenerateRandomMAC: %v", err)
}
if !strings.HasPrefix(mac, "02:") {
t.Errorf("MAC should start with 02: (locally administered): %s", mac)
}
// Verify it's 6 octets.
parts := strings.Split(mac, ":")
if len(parts) != 6 {
t.Errorf("MAC should have 6 octets: %s", mac)
}
}