Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b7aac71f6 | |||
| ea42a17474 |
+41
-23
@@ -15,8 +15,8 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"git.cloudinit.dev/coreci/orca/internal/certpaths"
|
||||
"git.cloudinit.dev/coreci/orca/internal/linux"
|
||||
"git.cloudinit.dev/coreci/orca/internal/engine"
|
||||
"git.cloudinit.dev/coreci/orca/internal/linux"
|
||||
"git.cloudinit.dev/coreci/orca/internal/model"
|
||||
"git.cloudinit.dev/coreci/orca/internal/proxmox"
|
||||
"git.cloudinit.dev/coreci/orca/internal/security"
|
||||
@@ -46,20 +46,25 @@ func nodeRegistry() (*engine.NodeRegistry, func() error, error) {
|
||||
}
|
||||
|
||||
var (
|
||||
joinName string
|
||||
joinAddr string
|
||||
joinCAFinger string
|
||||
joinType string
|
||||
joinHost string
|
||||
joinSSHUser string
|
||||
joinSSHKey string
|
||||
joinSSHPort int
|
||||
joinHostKeyFP string
|
||||
joinName string
|
||||
joinAddr string
|
||||
joinCAFinger string
|
||||
joinType string
|
||||
joinHost string
|
||||
joinSSHUser string
|
||||
joinSSHKey string
|
||||
joinSSHPort int
|
||||
joinHostKeyFP string
|
||||
joinLXCTemplate string
|
||||
proxmoxUser string
|
||||
proxmoxRole string
|
||||
leaveID string
|
||||
nodeWatch bool
|
||||
proxmoxUser string
|
||||
proxmoxRole string
|
||||
ingressMode string
|
||||
floatingIP string
|
||||
gateway string
|
||||
macAddr string
|
||||
netPrefix int
|
||||
leaveID string
|
||||
nodeWatch bool
|
||||
)
|
||||
|
||||
var nodeCmd = &cobra.Command{
|
||||
@@ -178,6 +183,12 @@ func joinProxmox(cmd *cobra.Command) error {
|
||||
ctx, cancel := context.WithTimeout(cmd.Context(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Default ingress mode to "native" if not specified (R-024).
|
||||
effectiveIngressMode := ingressMode
|
||||
if effectiveIngressMode == "" {
|
||||
effectiveIngressMode = "native"
|
||||
}
|
||||
|
||||
result, err := proxmox.BootstrapProxmox(ctx, proxmox.Options{
|
||||
Host: joinHost,
|
||||
SSHUser: joinSSHUser,
|
||||
@@ -188,6 +199,7 @@ func joinProxmox(cmd *cobra.Command) error {
|
||||
HostKeyFingerprint: joinHostKeyFP,
|
||||
Logger: newLogger(),
|
||||
LXCTemplate: joinLXCTemplate,
|
||||
IngressMode: effectiveIngressMode,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("proxmox bootstrap: %w", err)
|
||||
@@ -204,14 +216,15 @@ func joinProxmox(cmd *cobra.Command) error {
|
||||
defer regCancel()
|
||||
|
||||
node := &model.Node{
|
||||
ID: uuid.NewString(),
|
||||
Name: result.NodeName,
|
||||
Address: result.NodeAddress,
|
||||
State: model.NodeStateReady,
|
||||
JoinedAt: time.Now().UTC(),
|
||||
LastSeen: time.Now().UTC(),
|
||||
Kind: string(model.NodeKindProxmox),
|
||||
OS: "pve",
|
||||
ID: uuid.NewString(),
|
||||
Name: result.NodeName,
|
||||
Address: result.NodeAddress,
|
||||
State: model.NodeStateReady,
|
||||
JoinedAt: time.Now().UTC(),
|
||||
LastSeen: time.Now().UTC(),
|
||||
Kind: string(model.NodeKindProxmox),
|
||||
OS: "pve",
|
||||
IngressMode: effectiveIngressMode,
|
||||
}
|
||||
if err := registry.Join(regCtx, node); err != nil {
|
||||
return fmt.Errorf("register proxmox node: %w", err)
|
||||
@@ -252,7 +265,7 @@ func joinLinux(cmd *cobra.Command) error {
|
||||
SSHPort: joinSSHPort,
|
||||
HostKeyFingerprint: joinHostKeyFP,
|
||||
Logger: newLogger(),
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("linux bootstrap: %w", err)
|
||||
}
|
||||
@@ -514,6 +527,11 @@ func init() {
|
||||
nodeJoinCmd.Flags().StringVar(&proxmoxRole, "proxmox-role", "OrcaOperator", "PVE custom role to create (config-overridable)")
|
||||
nodeJoinCmd.Flags().StringVar(&joinHostKeyFP, "host-key-fingerprint", "", "SSH host key SHA256:base64 fingerprint (pre-pin; supersedes TOFU for --type proxmox or --type linux)")
|
||||
nodeJoinCmd.Flags().StringVar(&joinLXCTemplate, "lxc-template", "ubuntu-24.04", "LXC template for Proxmox (default ubuntu-24.04; alternatives: alpine-3.20, debian-12)")
|
||||
nodeJoinCmd.Flags().StringVar(&ingressMode, "ingress-mode", "", "proxmox ingress mode: native (default, traefik in LXC) or floating-ip (ingress LXC owns floating IP)")
|
||||
nodeJoinCmd.Flags().StringVar(&floatingIP, "floating-ip", "", "floating public IP for the ingress LXC (required for --ingress-mode floating-ip)")
|
||||
nodeJoinCmd.Flags().StringVar(&gateway, "gateway", "", "gateway for the ingress LXC (required for --ingress-mode floating-ip)")
|
||||
nodeJoinCmd.Flags().StringVar(&macAddr, "mac", "", "MAC address for the ingress LXC net0 (required for --ingress-mode floating-ip in --json mode; auto-generated in interactive mode)")
|
||||
nodeJoinCmd.Flags().IntVar(&netPrefix, "net-prefix", 24, "network prefix (CIDR) for the ingress LXC IP (default 24; valid 8-32)")
|
||||
nodeLeaveCmd.Flags().StringVar(&leaveID, "id", "", "node id")
|
||||
nodeListCmd.Flags().BoolVar(&nodeWatch, "watch", false, "stream nodes until Ctrl-C (table refresh or --json per-event)")
|
||||
|
||||
|
||||
@@ -9,11 +9,14 @@ package ingress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.cloudinit.dev/coreci/orca/internal/certpaths"
|
||||
"git.cloudinit.dev/coreci/orca/internal/emitter"
|
||||
@@ -117,3 +120,121 @@ func BootstrapLocalIngress(ctx context.Context, version string) error {
|
||||
|
||||
// nftConfigPath mirrors the emitter constant.
|
||||
const nftConfigPath = "/etc/nftables.d/orca.nft"
|
||||
|
||||
// RemoteExecFunc runs a command on a remote host and returns combined
|
||||
// output. Same signature as traefik.RemoteExecFunc.
|
||||
type RemoteExecFunc func(cmd string) ([]byte, error)
|
||||
|
||||
// BootstrapRemoteIngress ensures the complete ingress stack is running
|
||||
// on a remote host (linux worker). It is called from
|
||||
// `orca node join --type linux` after the user setup.
|
||||
//
|
||||
// Steps (each non-fatal — logs a warning and continues):
|
||||
// 1. mkdir -p /etc/traefik/dynamic /etc/orca (remote)
|
||||
// 2. Push step-ca root CA to remote /etc/orca/step-ca-root.crt
|
||||
// (C-60: certpaths.CACertPath)
|
||||
// 3. Render + write static config to remote /etc/traefik/traefik.yml
|
||||
// (C-58: preserves traefik-on-public-ip opt-out)
|
||||
// 4. Render + write nft ruleset to remote /etc/nftables.d/orca.nft
|
||||
// 5. Pre-create nft table (C-55: avoids first-apply flush-table error)
|
||||
// 6. Apply: nft -f (remote)
|
||||
// 7. Ensure podman traefik container running (remote)
|
||||
func BootstrapRemoteIngress(ctx context.Context, version string, execFn RemoteExecFunc) error {
|
||||
var errs []error
|
||||
log := slog.Default()
|
||||
|
||||
// Step 1: ensure directories.
|
||||
if _, err := execFn("mkdir -p /etc/traefik/dynamic /etc/orca"); err != nil {
|
||||
log.Warn("ingress: remote mkdir failed", "err", err)
|
||||
errs = append(errs, fmt.Errorf("remote mkdir: %w", err))
|
||||
}
|
||||
|
||||
// Step 2: push step-ca root CA (C-60: CACertPath, not CAPath).
|
||||
if caData, err := os.ReadFile(certpaths.CACertPath()); err == nil {
|
||||
if err := remoteWriteFile(execFn, "/etc/orca/step-ca-root.crt", caData, "0644"); err != nil {
|
||||
log.Warn("ingress: remote step-ca CA write failed", "err", err)
|
||||
errs = append(errs, fmt.Errorf("remote write step-ca-root.crt: %w", err))
|
||||
}
|
||||
} else {
|
||||
// Write a placeholder so the podman volume mount doesn't fail.
|
||||
_ = remoteWriteFile(execFn, "/etc/orca/step-ca-root.crt", []byte{}, "0644")
|
||||
log.Warn("ingress: step-ca root CA not found locally, wrote remote placeholder")
|
||||
}
|
||||
|
||||
// Step 3: render + write static config (C-58).
|
||||
staticFiles, err := emitter.TraefikEmitter{}.RenderTraefikStaticConfig(emitter.TraefikStaticOpts{})
|
||||
if err != nil {
|
||||
log.Warn("ingress: render traefik static config failed", "err", err)
|
||||
errs = append(errs, fmt.Errorf("render traefik static: %w", err))
|
||||
} else {
|
||||
for _, f := range staticFiles {
|
||||
_, _ = execFn(fmt.Sprintf("mkdir -p %s", filepath.Dir(f.Path)))
|
||||
if err := remoteWriteFile(execFn, f.Path, []byte(f.Content), f.Mode); err != nil {
|
||||
log.Warn("ingress: remote write traefik static config failed", "path", f.Path, "err", err)
|
||||
errs = append(errs, fmt.Errorf("remote write %s: %w", f.Path, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: render + write nft ruleset.
|
||||
nftFiles, err := emitter.NftEmitter{}.RenderNftConfig(emitter.NftClusterConfig{})
|
||||
if err != nil {
|
||||
log.Warn("ingress: render nft config failed", "err", err)
|
||||
errs = append(errs, fmt.Errorf("render nft: %w", err))
|
||||
} else {
|
||||
for _, f := range nftFiles {
|
||||
_, _ = execFn(fmt.Sprintf("mkdir -p %s", filepath.Dir(f.Path)))
|
||||
if err := remoteWriteFile(execFn, f.Path, []byte(f.Content), f.Mode); err != nil {
|
||||
log.Warn("ingress: remote write nft config failed", "path", f.Path, "err", err)
|
||||
errs = append(errs, fmt.Errorf("remote write %s: %w", f.Path, err))
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: pre-create nft table (C-55).
|
||||
_, _ = execFn("nft add table inet orca-ingress 2>/dev/null || true")
|
||||
|
||||
// Step 6: apply nft ruleset.
|
||||
if out, err := execFn("nft -f /etc/nftables.d/orca.nft 2>&1"); err != nil {
|
||||
log.Warn("ingress: remote nft apply failed", "err", err, "output", string(out))
|
||||
errs = append(errs, fmt.Errorf("remote nft -f: %w (output: %s)", err, string(out)))
|
||||
}
|
||||
}
|
||||
|
||||
// Step 7: ensure podman traefik container (C-50).
|
||||
traefikExecFn := traefik.RemoteExecFunc(execFn)
|
||||
if err := traefik.EnsureTraefikContainerRemote(ctx, version, traefikExecFn); err != nil {
|
||||
log.Warn("ingress: remote ensure traefik container failed", "err", err)
|
||||
errs = append(errs, fmt.Errorf("remote ensure traefik container: %w", err))
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("remote ingress bootstrap: %d errors (first: %w)", len(errs), errs[0])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// remoteWriteFile writes content to a remote path via a heredoc
|
||||
// (same pattern as sshpush.idempotency.writeFile). The heredoc
|
||||
// delimiter is a random hex string verified absent from the content
|
||||
// (F9 injection guard).
|
||||
func remoteWriteFile(execFn RemoteExecFunc, path string, content []byte, mode string) error {
|
||||
// Generate a random delimiter unlikely to be in the content.
|
||||
delim := "EOF_"
|
||||
for {
|
||||
b := make([]byte, 8)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return fmt.Errorf("rand: %w", err)
|
||||
}
|
||||
delim = "EOF_" + hex.EncodeToString(b)
|
||||
if !strings.Contains(string(content), delim) {
|
||||
break
|
||||
}
|
||||
}
|
||||
dir := filepath.Dir(path)
|
||||
cmd := fmt.Sprintf("mkdir -p %s && cat > %s <<'%s'\n%s\n%s\nchmod %s %s",
|
||||
dir, path, delim, string(content), delim, mode, path)
|
||||
if out, err := execFn(cmd); err != nil {
|
||||
return fmt.Errorf("remote write %s: %w (output: %s)", path, err, string(out))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -32,9 +32,9 @@ import (
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"git.cloudinit.dev/coreci/orca/internal/certpaths"
|
||||
"git.cloudinit.dev/coreci/orca/internal/ingress"
|
||||
"git.cloudinit.dev/coreci/orca/internal/proxmox"
|
||||
"git.cloudinit.dev/coreci/orca/internal/security"
|
||||
"git.cloudinit.dev/coreci/orca/internal/traefik"
|
||||
)
|
||||
|
||||
// DefaultSSHUser is the default SSH username for the initial connection.
|
||||
@@ -157,8 +157,10 @@ func BootstrapLinux(ctx context.Context, opts Options) (*Result, error) {
|
||||
}
|
||||
opts.Logger.Info("linux bootstrap: user created", "user", opts.OrcaUser)
|
||||
|
||||
// Step 4d: Ensure orca-traefik podman container on the remote host
|
||||
// (REQ-172, R-024). Replaces v0.13 binary+systemd install.
|
||||
// Step 4d: Ensure complete ingress stack on the remote host (R-024).
|
||||
// Renders+applies nft DNAT/SNAT, pushes step-ca root CA, renders+
|
||||
// writes traefik static config, ensures podman container running.
|
||||
// All non-fatal (offline host tolerance).
|
||||
sshExecFn := func(cmd string) ([]byte, error) {
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
@@ -167,10 +169,10 @@ func BootstrapLinux(ctx context.Context, opts Options) (*Result, error) {
|
||||
defer session.Close()
|
||||
return session.CombinedOutput(cmd)
|
||||
}
|
||||
ctx, cancelContainer := context.WithTimeout(ctx, 120*time.Second)
|
||||
defer cancelContainer()
|
||||
if err := traefik.EnsureTraefikContainerRemote(ctx, "", sshExecFn); err != nil {
|
||||
opts.Logger.Warn("linux bootstrap: traefik container ensure failed", "err", err)
|
||||
ctx, cancelIngress := context.WithTimeout(ctx, 120*time.Second)
|
||||
defer cancelIngress()
|
||||
if err := ingress.BootstrapRemoteIngress(ctx, "", ingress.RemoteExecFunc(sshExecFn)); err != nil {
|
||||
opts.Logger.Warn("linux bootstrap: ingress bootstrap failed", "err", err)
|
||||
}
|
||||
|
||||
// Step 5: Create the drift-events directory.
|
||||
|
||||
@@ -39,4 +39,9 @@ type Node struct {
|
||||
// OS is the auto-detected OS identifier from /etc/os-release ID=
|
||||
// (ubuntu|debian|alpine|pve|linux). Empty for pre-0006 rows.
|
||||
OS string `json:"os,omitempty"`
|
||||
// IngressMode is the ingress configuration for the node (R-024,
|
||||
// v0.14). Values: "" (legacy/default for linux/localhost),
|
||||
// "native" (proxmox native — traefik in LXC), "floating-ip"
|
||||
// (proxmox floating-IP — ingress LXC owns the floating IP).
|
||||
IngressMode string `json:"ingress_mode,omitempty"`
|
||||
}
|
||||
|
||||
+133
-11
@@ -37,6 +37,7 @@ import (
|
||||
"golang.org/x/crypto/ssh/knownhosts"
|
||||
|
||||
"git.cloudinit.dev/coreci/orca/internal/certpaths"
|
||||
"git.cloudinit.dev/coreci/orca/internal/emitter"
|
||||
"git.cloudinit.dev/coreci/orca/internal/security"
|
||||
"git.cloudinit.dev/coreci/orca/internal/traefik"
|
||||
)
|
||||
@@ -86,6 +87,13 @@ type Options struct {
|
||||
// LXCTemplate is the LXC template to download during bootstrap
|
||||
// (default "ubuntu-24.04"; alternatives: "alpine-3.20", "debian-12").
|
||||
LXCTemplate string
|
||||
// IngressMode is the proxmox ingress mode (R-024, v0.14).
|
||||
// "native" (default): traefik runs in an unprivileged LXC with
|
||||
// nesting=1,keyctl=1,fuse=1 on the PVE host. nft on the PVE host
|
||||
// DNATs to the LXC bridge IP.
|
||||
// "floating-ip": a separate ingress LXC owns the floating IP;
|
||||
// nft runs inside that LXC. See ProvisionIngressLXC (P6).
|
||||
IngressMode string
|
||||
}
|
||||
|
||||
// Result is the outcome of a successful bootstrap.
|
||||
@@ -247,23 +255,24 @@ func BootstrapProxmox(ctx context.Context, opts Options) (*Result, error) {
|
||||
return nil, fmt.Errorf("validate sudoers: %w", err)
|
||||
}
|
||||
|
||||
// Step 9a: Ensure orca-traefik podman container on the Proxmox host
|
||||
// (REQ-172, R-024). Replaces v0.13 binary+systemd install.
|
||||
// In native mode (default for v0.14 P5), the container runs inside
|
||||
// an LXC with nesting. For now, this installs on the PVE host OS.
|
||||
// Idempotent: no-op if container already running.
|
||||
if err := traefik.EnsureTraefikContainerRemote(ctx, "", runRemote); err != nil {
|
||||
log.Warn("proxmox.traefik_container_failed", "err", err)
|
||||
}
|
||||
|
||||
// Step 9b: Download default LXC template (REQ-167, Phase C).
|
||||
// Default: ubuntu-24.04. Configurable via --lxc-template.
|
||||
// Step 9a: Proxmox native ingress mode (R-024, REQ-175).
|
||||
// Create an unprivileged LXC with nesting=1,keyctl=1,fuse=1 (research
|
||||
// Topic 3), install podman inside it, and run the orca-traefik
|
||||
// container. nft on the PVE host DNATs to the LXC bridge IP.
|
||||
// Default mode is "native"; floating-ip mode is handled separately
|
||||
// (P6 — ProvisionIngressLXC).
|
||||
template := opts.LXCTemplate
|
||||
if template == "" {
|
||||
template = "ubuntu-24.04"
|
||||
}
|
||||
_, _ = runRemote(fmt.Sprintf("pveam download local %s 2>/dev/null || true", shellQuote(template)))
|
||||
|
||||
if opts.IngressMode != "floating-ip" {
|
||||
if err := provisionNativeIngressLXC(ctx, runRemote, template, log); err != nil {
|
||||
log.Warn("proxmox.native_ingress_lxc_failed", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("proxmox.bootstrap_ok",
|
||||
slog.String("event", "proxmox.bootstrap_ok"),
|
||||
slog.String("host", opts.Host),
|
||||
@@ -278,6 +287,119 @@ func BootstrapProxmox(ctx context.Context, opts Options) (*Result, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// provisionNativeIngressLXC creates an unprivileged LXC with
|
||||
// nesting=1,keyctl=1,fuse=1 (research Topic 3), installs podman inside
|
||||
// it, runs the orca-traefik container, and applies nft DNAT on the PVE
|
||||
// host targeting the LXC's bridge IP (R-024, REQ-175).
|
||||
//
|
||||
// The LXC is named "orca-traefik" and uses a deterministic VMID derived
|
||||
// from the host. It is idempotent: if the LXC already exists, it is
|
||||
// not re-created (C-53: apt-get install is skipped if podman present).
|
||||
func provisionNativeIngressLXC(ctx context.Context, runRemote func(string) ([]byte, error), template string, log *slog.Logger) error {
|
||||
// Deterministic VMID for the native ingress LXC.
|
||||
// Use a fixed VMID in the 200-299 range (Proxmox convention for CTs).
|
||||
const vmid = "200"
|
||||
const lxcName = "orca-traefik"
|
||||
|
||||
// Check if the LXC already exists.
|
||||
existOut, _ := runRemote(fmt.Sprintf("pct status %s 2>/dev/null || echo absent", vmid))
|
||||
existStr := strings.TrimSpace(string(existOut))
|
||||
if existStr == "absent" {
|
||||
// Create the LXC (research Topic 3: nesting=1,keyctl=1,fuse=1).
|
||||
log.Info("proxmox.creating_native_ingress_lxc", "vmid", vmid, "name", lxcName)
|
||||
createCmd := fmt.Sprintf(
|
||||
"pct create %s local:vztmpl/%s --hostname %s --unprivileged 1 --features nesting=1,keyctl=1,fuse=1 --onboot 1 --memory 2048 --swap 0 --rootfs local:8 2>&1",
|
||||
vmid, shellQuote(template), lxcName,
|
||||
)
|
||||
if out, err := runRemote(createCmd); err != nil {
|
||||
return fmt.Errorf("pct create native ingress LXC: %w (output: %s)", err, string(out))
|
||||
}
|
||||
if out, err := runRemote(fmt.Sprintf("pct start %s", vmid)); err != nil {
|
||||
return fmt.Errorf("pct start native ingress LXC: %w (output: %s)", err, string(out))
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for LXC network (retry for up to 60s).
|
||||
lxcIP := ""
|
||||
for i := 0; i < 12; i++ {
|
||||
ipOut, _ := runRemote(fmt.Sprintf("pct exec %s -- hostname -I 2>/dev/null", vmid))
|
||||
ipStr := strings.TrimSpace(string(ipOut))
|
||||
if ipStr != "" {
|
||||
fields := strings.Fields(ipStr)
|
||||
if len(fields) > 0 {
|
||||
lxcIP = fields[0]
|
||||
break
|
||||
}
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
if lxcIP == "" {
|
||||
return fmt.Errorf("native ingress LXC: could not discover IP after 60s")
|
||||
}
|
||||
log.Info("proxmox.native_ingress_lxc_ip", "vmid", vmid, "ip", lxcIP)
|
||||
|
||||
// Install podman inside the LXC (C-53: idempotent — check first).
|
||||
_, _ = runRemote(fmt.Sprintf(
|
||||
"pct exec %s -- bash -c 'command -v podman >/dev/null 2>&1 || (apt-get update -qq && apt-get install -y -qq podman conmon crun fuse-overlayfs nftables 2>&1)' 2>&1",
|
||||
vmid,
|
||||
))
|
||||
|
||||
// Enable podman-restart.service inside the LXC (research Topic 6).
|
||||
_, _ = runRemote(fmt.Sprintf("pct exec %s -- systemctl enable --now podman-restart.service 2>/dev/null", vmid))
|
||||
|
||||
// Push step-ca root CA into the LXC (placeholder if absent locally).
|
||||
caPath := certpaths.CACertPath()
|
||||
caData, caErr := os.ReadFile(caPath)
|
||||
if caErr != nil {
|
||||
caData = []byte{}
|
||||
}
|
||||
// Write CA via pct exec heredoc.
|
||||
caDelim := "EOF_CA"
|
||||
_, _ = runRemote(fmt.Sprintf(
|
||||
"pct exec %s -- bash -c 'mkdir -p /etc/orca && cat > /etc/orca/step-ca-root.crt <<%s\\n%s\\n%s'",
|
||||
vmid, caDelim, string(caData), caDelim,
|
||||
))
|
||||
|
||||
// Render + write traefik static config inside the LXC.
|
||||
staticFiles, err := emitter.TraefikEmitter{}.RenderTraefikStaticConfig(emitter.TraefikStaticOpts{})
|
||||
if err == nil {
|
||||
for _, f := range staticFiles {
|
||||
delim := "EOF_TF"
|
||||
_, _ = runRemote(fmt.Sprintf(
|
||||
"pct exec %s -- bash -c 'mkdir -p /etc/traefik/dynamic && cat > %s <<%s\\n%s\\n%s'",
|
||||
vmid, f.Path, delim, f.Content, delim,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure podman orca-traefik container inside the LXC.
|
||||
traefikExecFn := func(cmd string) ([]byte, error) {
|
||||
return runRemote(fmt.Sprintf("pct exec %s -- bash -c %s 2>&1", vmid, shellQuote(cmd)))
|
||||
}
|
||||
if err := traefik.EnsureTraefikContainerRemote(ctx, "", traefikExecFn); err != nil {
|
||||
log.Warn("proxmox.native_ingress_lxc_traefik_failed", "err", err)
|
||||
}
|
||||
|
||||
// Render + apply nft on the PVE host with DNATTarget = LXC IP.
|
||||
nftFiles, err := emitter.NftEmitter{}.RenderNftConfig(emitter.NftClusterConfig{
|
||||
DNATTarget: lxcIP,
|
||||
})
|
||||
if err == nil {
|
||||
for _, f := range nftFiles {
|
||||
nftDelim := "EOF_NF"
|
||||
_, _ = runRemote(fmt.Sprintf("mkdir -p /etc/nftables.d && cat > %s <<%s\\n%s\\n%s",
|
||||
f.Path, nftDelim, f.Content, nftDelim))
|
||||
}
|
||||
_, _ = runRemote("nft add table inet orca-ingress 2>/dev/null || true")
|
||||
if out, err := runRemote("nft -f /etc/nftables.d/orca.nft 2>&1"); err != nil {
|
||||
log.Warn("proxmox.native_ingress_nft_apply_failed", "err", err, "output", string(out))
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("proxmox.native_ingress_lxc_ok", "vmid", vmid, "ip", lxcIP)
|
||||
return nil
|
||||
}
|
||||
|
||||
// sshDialer is the dialer used by BootstrapProxmox. It's a package-level
|
||||
// variable so tests can override it with a fake SSH server.
|
||||
var sshDialer sshDialerType = defaultSSHDialer{}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
-- REQ-175 / R-024: add ingress_mode column to nodes.
|
||||
-- Values: '' (legacy/default), 'native' (proxmox native — traefik
|
||||
-- in LXC), 'floating-ip' (proxmox floating-IP — ingress LXC owns
|
||||
-- the floating IP). Defaults to empty string for backward
|
||||
-- compatibility with pre-v0.14 nodes.
|
||||
ALTER TABLE nodes ADD COLUMN ingress_mode TEXT NOT NULL DEFAULT '';
|
||||
Reference in New Issue
Block a user