fix: Allow OCI detection for stopped containers

- Refactored enrichContainerMetadata to not return early when container is stopped
- Status API calls are still skipped for stopped containers (as expected)
- Config fetch now runs regardless of status, enabling OCI detection
- Added test for OCI detection on stopped containers

Discovered: Proxmox 9.1 requires VM.Config.Options permission to read
OCI container configs (not just VM.Audit). Document this in setup guides.
This commit is contained in:
rcourtman 2025-12-12 19:12:06 +00:00
parent 2226bdacf8
commit 689f229c72
2 changed files with 172 additions and 97 deletions

View file

@ -0,0 +1,65 @@
package monitoring
import (
"context"
"testing"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rcourtman/pulse-go-rewrite/pkg/proxmox"
)
type stubPVEClientContainerMetadata struct {
stubPVEClient
config map[string]interface{}
configCalls int
statusCalls int
}
func (s *stubPVEClientContainerMetadata) GetContainerConfig(ctx context.Context, node string, vmid int) (map[string]interface{}, error) {
s.configCalls++
return s.config, nil
}
func (s *stubPVEClientContainerMetadata) GetContainerStatus(ctx context.Context, node string, vmid int) (*proxmox.Container, error) {
s.statusCalls++
return nil, nil
}
func TestEnrichContainerMetadata_DetectsOCIForStoppedContainer(t *testing.T) {
t.Parallel()
monitor := &Monitor{}
client := &stubPVEClientContainerMetadata{
config: map[string]interface{}{
"entrypoint": "/bin/sh",
"ostype": "unmanaged",
"cmode": "console",
"lxc": "lxc.signal.halt: SIGTERM",
},
}
container := &models.Container{
VMID: 300,
Name: "oci-alpine",
Status: "stopped",
Type: "lxc",
}
monitor.enrichContainerMetadata(context.Background(), client, "delly", "delly", container)
if client.configCalls != 1 {
t.Fatalf("expected 1 config call, got %d", client.configCalls)
}
if client.statusCalls != 0 {
t.Fatalf("expected 0 status calls for stopped container, got %d", client.statusCalls)
}
if !container.IsOCI {
t.Fatalf("expected container.IsOCI true, got false")
}
if container.Type != "oci" {
t.Fatalf("expected container.Type oci, got %q", container.Type)
}
}

View file

@ -2358,12 +2358,16 @@ func (m *Monitor) enrichContainerMetadata(ctx context.Context, client PVEClientI
ensureContainerRootDiskEntry(container) ensureContainerRootDiskEntry(container)
if client == nil || container.Status != "running" { if client == nil {
return return
} }
isRunning := container.Status == "running"
var status *proxmox.Container
if isRunning {
statusCtx, cancel := context.WithTimeout(ctx, 5*time.Second) statusCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
status, err := client.GetContainerStatus(statusCtx, nodeName, container.VMID) statusResp, err := client.GetContainerStatus(statusCtx, nodeName, container.VMID)
cancel() cancel()
if err != nil { if err != nil {
log.Debug(). log.Debug().
@ -2373,10 +2377,9 @@ func (m *Monitor) enrichContainerMetadata(ctx context.Context, client PVEClientI
Str("container", container.Name). Str("container", container.Name).
Int("vmid", container.VMID). Int("vmid", container.VMID).
Msg("Container status metadata unavailable") Msg("Container status metadata unavailable")
return } else {
status = statusResp
} }
if status == nil {
return
} }
rootDeviceHint := "" rootDeviceHint := ""
@ -2396,6 +2399,7 @@ func (m *Monitor) enrichContainerMetadata(ctx context.Context, client PVEClientI
addressOrder = append(addressOrder, addr) addressOrder = append(addressOrder, addr)
} }
if status != nil {
for _, addr := range sanitizeGuestAddressStrings(status.IP) { for _, addr := range sanitizeGuestAddressStrings(status.IP) {
addAddress(addr) addAddress(addr)
} }
@ -2408,8 +2412,11 @@ func (m *Monitor) enrichContainerMetadata(ctx context.Context, client PVEClientI
for _, addr := range parseContainerRawIPs(status.IPv6) { for _, addr := range parseContainerRawIPs(status.IPv6) {
addAddress(addr) addAddress(addr)
} }
}
networkIfaces := make([]models.GuestNetworkInterface, 0, len(status.Network)) networkIfaces := make([]models.GuestNetworkInterface, 0, 4)
if status != nil {
networkIfaces = make([]models.GuestNetworkInterface, 0, len(status.Network))
for rawName, cfg := range status.Network { for rawName, cfg := range status.Network {
if cfg == (proxmox.ContainerNetworkConfig{}) { if cfg == (proxmox.ContainerNetworkConfig{}) {
continue continue
@ -2447,6 +2454,7 @@ func (m *Monitor) enrichContainerMetadata(ctx context.Context, client PVEClientI
networkIfaces = append(networkIfaces, iface) networkIfaces = append(networkIfaces, iface)
} }
} }
}
configCtx, cancelConfig := context.WithTimeout(ctx, 5*time.Second) configCtx, cancelConfig := context.WithTimeout(ctx, 5*time.Second)
configData, configErr := client.GetContainerConfig(configCtx, nodeName, container.VMID) configData, configErr := client.GetContainerConfig(configCtx, nodeName, container.VMID)
@ -2510,6 +2518,7 @@ func (m *Monitor) enrichContainerMetadata(ctx context.Context, client PVEClientI
} }
if len(addressOrder) == 0 { if len(addressOrder) == 0 {
if isRunning {
interfacesCtx, cancelInterfaces := context.WithTimeout(ctx, 5*time.Second) interfacesCtx, cancelInterfaces := context.WithTimeout(ctx, 5*time.Second)
ifaceDetails, ifaceErr := client.GetContainerInterfaces(interfacesCtx, nodeName, container.VMID) ifaceDetails, ifaceErr := client.GetContainerInterfaces(interfacesCtx, nodeName, container.VMID)
cancelInterfaces() cancelInterfaces()
@ -2566,6 +2575,7 @@ func (m *Monitor) enrichContainerMetadata(ctx context.Context, client PVEClientI
} }
} }
} }
}
if len(networkIfaces) > 1 { if len(networkIfaces) > 1 {
sort.SliceStable(networkIfaces, func(i, j int) bool { sort.SliceStable(networkIfaces, func(i, j int) bool {