package traefik import ( "context" "fmt" "os/exec" "strings" ) // DefaultVersion is the traefik version tag for the orca-traefik image. const DefaultVersion = "v3.3.0" // DefaultImage is the full image reference for the orca-traefik container. // The tag is resolved at runtime from the orca version (or "latest" for // dev builds). The image is built from Dockerfile.traefik and published // per release (REQ-171). const DefaultImage = "git.cloudinit.dev/coreci/orca-traefik" // ContainerName is the podman container name for the traefik data plane. const ContainerName = "orca-traefik" // RemoteExecFunc runs a command on a remote host and returns combined // output. It is the same signature used by the v0.13 binary installer // and by the proxmox/linux bootstrap SSH sessions. type RemoteExecFunc func(cmd string) ([]byte, error) // ImageRef returns the full image:tag reference for the orca-traefik // container. If version is empty or "dev"/"0.1.0-dev", it falls back to // "latest" (dev builds don't have a published tag). func ImageRef(version string) string { tag := version if tag == "" || tag == "dev" || tag == "0.1.0-dev" || strings.HasSuffix(tag, "-dev") { return fmt.Sprintf("%s:latest", DefaultImage) } tag = strings.TrimPrefix(tag, "v") return fmt.Sprintf("%s:v%s", DefaultImage, tag) } // podmanRunArgs returns the podman run arguments for the traefik // container. The container uses --network host so traefik binds // 127.0.0.1:8080/8443 directly on the host (or LXC) loopback. nft // DNATs public :443/:80 to those loopback ports (R-017/R-024). // // Volume mounts (no SELinux :Z flag — research finding Topic 7): // - /etc/traefik/traefik.yml:ro — static config (overrides baked default; C-58) // - /etc/traefik/dynamic:ro — dynamic config (orca writes atomically via SSH-push) // - /etc/orca/step-ca-root.crt:ro — step-ca root CA (for future mTLS; v0.14 uses tls:{}) func podmanRunArgs(imageRef string) []string { return []string{ "run", "-d", "--name", ContainerName, "--restart=unless-stopped", "--network", "host", "-v", "/etc/traefik/traefik.yml:/etc/traefik/traefik.yml:ro", "-v", "/etc/traefik/dynamic:/etc/traefik/dynamic:ro", "-v", "/etc/orca/step-ca-root.crt:/etc/orca/step-ca-root.crt:ro", imageRef, } } // EnsureTraefikContainerLocal ensures the orca-traefik podman container // is running on the local host. It is idempotent: // 1. If the container is running → no-op. // 2. If the container exists but is stopped → start it. // 3. If the container does not exist → pull the image + run it. // // C-50: if podman is not installed, attempts apt-get install. If that // fails, returns an error with install instructions. // // C-57: if a legacy v0.13 systemd service exists (orca-traefik.service), // it is stopped, disabled, and removed before starting the container. func EnsureTraefikContainerLocal(ctx context.Context, version string) error { imageRef := ImageRef(version) if err := ensurePodmanLocal(ctx); err != nil { return err } if err := removeLegacySystemdUnitLocal(ctx); err != nil { // Non-fatal: legacy unit may not exist on fresh installs. _ = err } if err := ensureDirsLocal(); err != nil { return fmt.Errorf("traefik: ensure dirs: %w", err) } return reconcileContainerLocal(ctx, imageRef) } // EnsureTraefikContainerRemote ensures the orca-traefik podman container // is running on a remote host (via SSH exec). Same idempotent logic as // EnsureTraefikContainerLocal but over the provided exec function. func EnsureTraefikContainerRemote(ctx context.Context, version string, execFn RemoteExecFunc) error { imageRef := ImageRef(version) if err := ensurePodmanRemote(execFn); err != nil { return err } if err := removeLegacySystemdUnitRemote(execFn); err != nil { _ = err // non-fatal } if _, err := execFn("mkdir -p /etc/traefik/dynamic /etc/orca"); err != nil { return fmt.Errorf("traefik: ensure remote dirs: %w", err) } return reconcileContainerRemote(execFn, imageRef) } // ensurePodmanLocal checks if podman is installed locally and attempts // to install it if absent (C-50). func ensurePodmanLocal(ctx context.Context) error { if _, err := exec.LookPath("podman"); err == nil { return nil } // Attempt apt-get install (Ubuntu/Debian). cmd := exec.CommandContext(ctx, "bash", "-c", "apt-get update -qq && apt-get install -y -qq podman conmon crun fuse-overlayfs 2>&1") if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("podman not found and apt-get install failed: %w (output: %s).\nInstall podman manually: apt-get install podman conmon crun fuse-overlayfs", err, string(out)) } return nil } // ensurePodmanRemote checks if podman is installed on the remote host // and attempts to install it if absent (C-50). func ensurePodmanRemote(execFn RemoteExecFunc) error { if out, err := execFn("command -v podman"); err == nil && len(strings.TrimSpace(string(out))) > 0 { return nil } // Attempt apt-get install on the remote host. cmd := "apt-get update -qq && apt-get install -y -qq podman conmon crun fuse-overlayfs 2>&1" if out, err := execFn(cmd); err != nil { return fmt.Errorf("podman not found on remote and apt-get install failed: %w (output: %s)", err, string(out)) } return nil } // removeLegacySystemdUnitLocal stops, disables, and removes the legacy // v0.13 orca-traefik.service systemd unit + /usr/local/bin/traefik // binary (C-57). Idempotent — no-op if the unit doesn't exist. func removeLegacySystemdUnitLocal(ctx context.Context) error { // Check if the legacy unit exists. if _, err := exec.CommandContext(ctx, "systemctl", "is-active", "orca-traefik.service").CombinedOutput(); err == nil { // Unit is active or exists — stop + disable it. _ = exec.CommandContext(ctx, "systemctl", "stop", "orca-traefik.service").Run() _ = exec.CommandContext(ctx, "systemctl", "disable", "orca-traefik.service").Run() } // Remove the unit file and binary. _ = exec.CommandContext(ctx, "rm", "-f", "/etc/systemd/system/orca-traefik.service").Run() _ = exec.CommandContext(ctx, "rm", "-f", "/usr/local/bin/traefik").Run() _ = exec.CommandContext(ctx, "systemctl", "daemon-reload").Run() return nil } // removeLegacySystemdUnitRemote is the remote SSH variant (C-57). func removeLegacySystemdUnitRemote(execFn RemoteExecFunc) error { cmd := `systemctl is-active orca-traefik.service 2>/dev/null && systemctl stop orca-traefik.service 2>/dev/null; systemctl disable orca-traefik.service 2>/dev/null; rm -f /etc/systemd/system/orca-traefik.service /usr/local/bin/traefik; systemctl daemon-reload 2>/dev/null; true` _, _ = execFn(cmd) return nil } // ensureDirsLocal creates /etc/traefik/dynamic and /etc/orca locally. func ensureDirsLocal() error { if err := exec.Command("mkdir", "-p", "/etc/traefik/dynamic", "/etc/orca").Run(); err != nil { return fmt.Errorf("mkdir: %w", err) } return nil } // reconcileContainerLocal implements the idempotent pull+run logic // locally (C-50). func reconcileContainerLocal(ctx context.Context, imageRef string) error { // Check if the container is already running. out, err := exec.CommandContext(ctx, "podman", "inspect", "--format", "{{.State.Running}}", ContainerName).CombinedOutput() if err == nil { v := strings.TrimSpace(string(out)) if v == "true" { return nil // already running } // Container exists but is stopped — start it. if _, err := exec.CommandContext(ctx, "podman", "start", ContainerName).CombinedOutput(); err != nil { return fmt.Errorf("podman start %s: %w", ContainerName, err) } return nil } // Container does not exist — pull + run. if out, err := exec.CommandContext(ctx, "podman", "pull", imageRef).CombinedOutput(); err != nil { return fmt.Errorf("podman pull %s: %w (output: %s)", imageRef, err, string(out)) } args := append([]string{}, podmanRunArgs(imageRef)...) if out, err := exec.CommandContext(ctx, "podman", args...).CombinedOutput(); err != nil { return fmt.Errorf("podman run: %w (output: %s)", err, string(out)) } // Enable podman-restart.service for reboot persistence (research Topic 6). _ = exec.CommandContext(ctx, "systemctl", "enable", "--now", "podman-restart.service").Run() return nil } // reconcileContainerRemote implements the idempotent pull+run logic // over SSH exec. func reconcileContainerRemote(execFn RemoteExecFunc, imageRef string) error { // Check if the container is already running. out, err := execFn(fmt.Sprintf("podman inspect --format '{{.State.Running}}' %s 2>/dev/null", ContainerName)) if err == nil { v := strings.TrimSpace(string(out)) if v == "true" { return nil // already running } // Container exists but is stopped — start it. if _, err := execFn(fmt.Sprintf("podman start %s 2>/dev/null", ContainerName)); err != nil { return fmt.Errorf("podman start %s: %w", ContainerName, err) } return nil } // Container does not exist — pull + run. if out, err := execFn(fmt.Sprintf("podman pull %s", shellQuote(imageRef))); err != nil { return fmt.Errorf("podman pull %s: %w (output: %s)", imageRef, err, string(out)) } runArgs := strings.Join(podmanRunArgs(imageRef), " ") if out, err := execFn(fmt.Sprintf("podman %s", runArgs)); err != nil { return fmt.Errorf("podman run: %w (output: %s)", err, string(out)) } // Enable podman-restart.service for reboot persistence (research Topic 6). _, _ = execFn("systemctl enable --now podman-restart.service 2>/dev/null || true") return nil } // shellQuote wraps a string in single quotes for shell-safe usage. func shellQuote(s string) string { return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'" }