From cdec581d000172140e32c6265f5f923880bdfc1e Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 20 Aug 2026 11:49:12 +0100 Subject: [PATCH] Give mock mode Docker-in-LXC coverage The Proxmox page's nested-container row cue, the drawer's nested Docker card, and the proxmox-pve platform scoping of nested workloads had no local reproduction at all: mock mode never generated a Docker host with the proxmox-lxc-docker: identity prefix, so the surface could only be seen against a live estate with node agents, exec-scoped tokens, and the server-side inventory opt-in. That is how it shipped invisible. The fixture now marks the first two running LXC guests as probed Docker hosts and appends a nested Docker host for each, following the production proxmoxGuestDockerAgentID convention of proxmox-lxc-docker:::. One guest nests a single container and one nests several, so the row cue renders both its singular and plural counts. The nested hosts mirror the shape of the real pct exec inventory: guest-derived sizing and usage, one rootfs disk, and no native engine inventory, host I/O rates, temperature, machine ID, or Swarm membership. --- internal/mock/generator.go | 98 +++++++++++++++++++++++++++++++++ internal/mock/generator_test.go | 93 +++++++++++++++++++++++++++++-- 2 files changed, 187 insertions(+), 4 deletions(-) diff --git a/internal/mock/generator.go b/internal/mock/generator.go index faaf9a367..405936295 100644 --- a/internal/mock/generator.go +++ b/internal/mock/generator.go @@ -641,6 +641,7 @@ func buildFixtureState(config MockConfig) models.StateSnapshot { ensureFreshFilesystemFixture(&data) ensureConfigOnlyMountFixture(&data) + ensureMockDockerInLXCFixture(&data, config) // Calculate stats data.Stats.StartTime = time.Now() @@ -2690,6 +2691,103 @@ func generateDockerHosts(config MockConfig) []models.DockerHost { return hosts } +// mockProxmoxLXCDockerHostSourcePrefix mirrors the production Docker host +// identity prefix from monitoring.proxmoxGuestDockerAgentID (and the platform +// scope match in unifiedresources): hosts whose source ID carries it are +// treated as Docker runtimes discovered inside a Proxmox LXC guest. +const mockProxmoxLXCDockerHostSourcePrefix = "proxmox-lxc-docker:" + +// ensureMockDockerInLXCFixture adds Proxmox-side Docker-in-LXC discovery to +// the fixture estate: the first running LXC guests are marked HasDocker (as +// the node's pct exec socket probe would) and each gains a nested Docker host +// whose identity follows the production proxmoxGuestDockerAgentID convention, +// "proxmox-lxc-docker:" + the guest's instance:node:vmid coordinates. That +// prefix is what grants the nested workloads the proxmox-pve platform scope +// and lets the Proxmox page match them back to their parent guest for the +// row cue and drawer card, so without this fixture the whole surface has no +// local reproduction. +func ensureMockDockerInLXCFixture(data *models.StateSnapshot, config MockConfig) { + if data == nil || config.DockerHostCount <= 0 { + return + } + + // One single-container guest and one busier guest, so the row cue + // renders both its singular and plural counts. + nestedCounts := []int{1, 4} + now := time.Now() + nested := 0 + for i := range data.Containers { + if nested >= len(nestedCounts) { + break + } + ct := &data.Containers[i] + if ct.Status != "running" || ct.IsOCI { + continue + } + + ct.HasDocker = true + ct.DockerCheckedAt = now + + sourceID := fmt.Sprintf("%s%s:%s:%d", mockProxmoxLXCDockerHostSourcePrefix, ct.Instance, ct.Node, ct.VMID) + containers := generateDockerContainers(ct.Name, i, config, false) + if len(containers) > nestedCounts[nested] { + containers = containers[:nestedCounts[nested]] + } + + version := dockerVersions[rand.Intn(len(dockerVersions))] + kernelVersion := "6.8.12-4-pve" + for _, node := range data.Nodes { + if node.Name == ct.Node && node.KernelVersion != "" { + kernelVersion = node.KernelVersion + break + } + } + + // Host info mirrors what parseProxmoxGuestDockerInventory plus + // enrichGuestDockerReportFromContainer report for a real guest: the + // guest's own sizing and usage, one rootfs disk, no machine ID, no + // agent version, no network interfaces. + host := models.DockerHost{ + ID: sourceID, + AgentID: sourceID, + Hostname: ct.Name, + DisplayName: ct.Name, + OS: "linux", + KernelVersion: kernelVersion, + Architecture: "x86_64", + Runtime: "docker", + RuntimeVersion: version, + DockerVersion: version, + CPUs: ct.CPUs, + TotalMemoryBytes: ct.Memory.Total, + UptimeSeconds: ct.Uptime, + CPUUsage: clampFloat(ct.CPU*100, 0, 100), + Memory: models.Memory{ + Total: ct.Memory.Total, + Used: ct.Memory.Used, + Free: ct.Memory.Free, + Usage: ct.Memory.Usage, + }, + Disks: []models.Disk{{ + Total: ct.Disk.Total, + Used: ct.Disk.Used, + Free: ct.Disk.Free, + Usage: ct.Disk.Usage, + Mountpoint: "/", + Type: "rootfs", + }}, + Status: "online", + LastSeen: now, + IntervalSeconds: 30, + Containers: containers, + } + + data.DockerHosts = append(data.DockerHosts, host) + data.ConnectionHealth[dockerConnectionPrefix+host.ID] = true + nested++ + } +} + func ensureMockDockerNativeInventory(host *models.DockerHost, hostIndex int, now time.Time) { if host == nil { return diff --git a/internal/mock/generator_test.go b/internal/mock/generator_test.go index 10be0d4d0..7b6b51a8f 100644 --- a/internal/mock/generator_test.go +++ b/internal/mock/generator_test.go @@ -18,14 +18,18 @@ func TestBuildFixtureStateIncludesDockerHosts(t *testing.T) { data := buildFixtureState(cfg) - if len(data.DockerHosts) != cfg.DockerHostCount { - t.Fatalf("expected %d docker hosts, got %d", cfg.DockerHostCount, len(data.DockerHosts)) - } - + // Nested Docker-in-LXC hosts are appended on top of the configured + // agent-reported hosts and deliberately carry no native engine + // inventory, so the native-host expectations exclude them. + nativeHosts := 0 for _, host := range data.DockerHosts { if host.ID == "" { t.Fatalf("docker host missing id: %+v", host) } + if strings.HasPrefix(host.ID, mockProxmoxLXCDockerHostSourcePrefix) { + continue + } + nativeHosts++ if len(host.Containers) == 0 { t.Fatalf("docker host %s has no containers", host.Hostname) } @@ -42,6 +46,9 @@ func TestBuildFixtureStateIncludesDockerHosts(t *testing.T) { t.Fatalf("docker host %s has no engine storage usage inventory: %+v", host.Hostname, host.StorageUsage) } } + if nativeHosts != cfg.DockerHostCount { + t.Fatalf("expected %d native docker hosts, got %d", cfg.DockerHostCount, nativeHosts) + } } func TestComputeGuestCountsHandlesZeroBaselines(t *testing.T) { @@ -300,6 +307,11 @@ func TestBuildFixtureStatePopulatesDockerHostIORates(t *testing.T) { } for _, host := range data.DockerHosts { + if strings.HasPrefix(host.ID, mockProxmoxLXCDockerHostSourcePrefix) { + // Docker-in-LXC hosts mirror the production pct exec inventory, + // which reports no host-level I/O rates or temperature. + continue + } if host.Status == "offline" { if host.NetInRate != 0 || host.NetOutRate != 0 || host.DiskReadRate != 0 || host.DiskWriteRate != 0 { t.Fatalf("offline docker host %s should have zero I/O rates", host.ID) @@ -893,3 +905,76 @@ func TestEnsureConfigOnlyMountFixtureSeedsUnknownUsageMount(t *testing.T) { t.Fatalf("config-only mount must keep its mp key and device, got %+v", archive) } } + +func TestBuildFixtureStateIncludesDockerInLXCFixture(t *testing.T) { + cfg := DefaultConfig + cfg.StoppedPercent = 0 // keep every LXC running so the fixture always binds + + data := buildFixtureState(cfg) + + var nested []models.DockerHost + for _, host := range data.DockerHosts { + if strings.HasPrefix(host.ID, mockProxmoxLXCDockerHostSourcePrefix) { + nested = append(nested, host) + } + } + if len(nested) != 2 { + t.Fatalf("expected 2 nested Docker-in-LXC hosts, got %d", len(nested)) + } + + guestsBySourceID := make(map[string]models.Container, len(data.Containers)) + for _, ct := range data.Containers { + key := fmt.Sprintf("%s%s:%s:%d", mockProxmoxLXCDockerHostSourcePrefix, ct.Instance, ct.Node, ct.VMID) + guestsBySourceID[key] = ct + } + + containerCounts := make(map[int]bool) + for _, host := range nested { + guest, ok := guestsBySourceID[host.ID] + if !ok { + t.Fatalf("nested docker host %s has no matching LXC guest", host.ID) + } + if guest.Status != "running" { + t.Fatalf("nested docker host %s bound to non-running guest %s", host.ID, guest.ID) + } + if !guest.HasDocker || guest.DockerCheckedAt.IsZero() { + t.Fatalf("parent LXC %s not marked as probed Docker host", guest.ID) + } + if host.AgentID != host.ID { + t.Fatalf("nested docker host agent id %q must equal its source id %q", host.AgentID, host.ID) + } + if host.Hostname != guest.Name { + t.Fatalf("nested docker host hostname %q should be the guest name %q", host.Hostname, guest.Name) + } + if len(host.Containers) == 0 { + t.Fatalf("nested docker host %s has no containers", host.ID) + } + containerCounts[len(host.Containers)] = true + if len(host.Images) != 0 || len(host.Volumes) != 0 || len(host.Networks) != 0 || host.StorageUsage != nil { + t.Fatalf("nested docker host %s must not fabricate native engine inventory (production pct exec inventory has none)", host.ID) + } + if host.Swarm != nil { + t.Fatalf("nested docker host %s must not join the mock Swarm", host.ID) + } + if !data.ConnectionHealth[dockerConnectionPrefix+host.ID] { + t.Fatalf("nested docker host %s missing connection health entry", host.ID) + } + } + if !containerCounts[1] { + t.Fatalf("expected one single-container nested host so the row cue renders its singular form, got counts %v", containerCounts) + } + + // Disabling Docker in the mock estate disables the nested fixture too. + cfg.DockerHostCount = 0 + dataNoDocker := buildFixtureState(cfg) + for _, host := range dataNoDocker.DockerHosts { + if strings.HasPrefix(host.ID, mockProxmoxLXCDockerHostSourcePrefix) { + t.Fatalf("nested docker host %s generated despite DockerHostCount=0", host.ID) + } + } + for _, ct := range dataNoDocker.Containers { + if ct.HasDocker { + t.Fatalf("LXC %s marked HasDocker despite DockerHostCount=0", ct.ID) + } + } +}