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:
parent
2226bdacf8
commit
689f229c72
2 changed files with 172 additions and 97 deletions
65
internal/monitoring/enrich_container_metadata_test.go
Normal file
65
internal/monitoring/enrich_container_metadata_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -2358,25 +2358,28 @@ func (m *Monitor) enrichContainerMetadata(ctx context.Context, client PVEClientI
|
||||||
|
|
||||||
ensureContainerRootDiskEntry(container)
|
ensureContainerRootDiskEntry(container)
|
||||||
|
|
||||||
if client == nil || container.Status != "running" {
|
if client == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
statusCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
isRunning := container.Status == "running"
|
||||||
status, err := client.GetContainerStatus(statusCtx, nodeName, container.VMID)
|
|
||||||
cancel()
|
var status *proxmox.Container
|
||||||
if err != nil {
|
if isRunning {
|
||||||
log.Debug().
|
statusCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
Err(err).
|
statusResp, err := client.GetContainerStatus(statusCtx, nodeName, container.VMID)
|
||||||
Str("instance", instanceName).
|
cancel()
|
||||||
Str("node", nodeName).
|
if err != nil {
|
||||||
Str("container", container.Name).
|
log.Debug().
|
||||||
Int("vmid", container.VMID).
|
Err(err).
|
||||||
Msg("Container status metadata unavailable")
|
Str("instance", instanceName).
|
||||||
return
|
Str("node", nodeName).
|
||||||
}
|
Str("container", container.Name).
|
||||||
if status == nil {
|
Int("vmid", container.VMID).
|
||||||
return
|
Msg("Container status metadata unavailable")
|
||||||
|
} else {
|
||||||
|
status = statusResp
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootDeviceHint := ""
|
rootDeviceHint := ""
|
||||||
|
|
@ -2396,55 +2399,60 @@ func (m *Monitor) enrichContainerMetadata(ctx context.Context, client PVEClientI
|
||||||
addressOrder = append(addressOrder, addr)
|
addressOrder = append(addressOrder, addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, addr := range sanitizeGuestAddressStrings(status.IP) {
|
if status != nil {
|
||||||
addAddress(addr)
|
for _, addr := range sanitizeGuestAddressStrings(status.IP) {
|
||||||
}
|
addAddress(addr)
|
||||||
for _, addr := range sanitizeGuestAddressStrings(status.IP6) {
|
}
|
||||||
addAddress(addr)
|
for _, addr := range sanitizeGuestAddressStrings(status.IP6) {
|
||||||
}
|
addAddress(addr)
|
||||||
for _, addr := range parseContainerRawIPs(status.IPv4) {
|
}
|
||||||
addAddress(addr)
|
for _, addr := range parseContainerRawIPs(status.IPv4) {
|
||||||
}
|
addAddress(addr)
|
||||||
for _, addr := range parseContainerRawIPs(status.IPv6) {
|
}
|
||||||
addAddress(addr)
|
for _, addr := range parseContainerRawIPs(status.IPv6) {
|
||||||
|
addAddress(addr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
networkIfaces := make([]models.GuestNetworkInterface, 0, len(status.Network))
|
networkIfaces := make([]models.GuestNetworkInterface, 0, 4)
|
||||||
for rawName, cfg := range status.Network {
|
if status != nil {
|
||||||
if cfg == (proxmox.ContainerNetworkConfig{}) {
|
networkIfaces = make([]models.GuestNetworkInterface, 0, len(status.Network))
|
||||||
continue
|
for rawName, cfg := range status.Network {
|
||||||
}
|
if cfg == (proxmox.ContainerNetworkConfig{}) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
iface := models.GuestNetworkInterface{}
|
iface := models.GuestNetworkInterface{}
|
||||||
name := strings.TrimSpace(cfg.Name)
|
name := strings.TrimSpace(cfg.Name)
|
||||||
if name == "" {
|
if name == "" {
|
||||||
name = strings.TrimSpace(rawName)
|
name = strings.TrimSpace(rawName)
|
||||||
}
|
}
|
||||||
if name != "" {
|
if name != "" {
|
||||||
iface.Name = name
|
iface.Name = name
|
||||||
}
|
}
|
||||||
if mac := strings.TrimSpace(cfg.HWAddr); mac != "" {
|
if mac := strings.TrimSpace(cfg.HWAddr); mac != "" {
|
||||||
iface.MAC = mac
|
iface.MAC = mac
|
||||||
}
|
}
|
||||||
|
|
||||||
addrCandidates := make([]string, 0, 4)
|
addrCandidates := make([]string, 0, 4)
|
||||||
addrCandidates = append(addrCandidates, collectIPsFromInterface(cfg.IP)...)
|
addrCandidates = append(addrCandidates, collectIPsFromInterface(cfg.IP)...)
|
||||||
addrCandidates = append(addrCandidates, collectIPsFromInterface(cfg.IP6)...)
|
addrCandidates = append(addrCandidates, collectIPsFromInterface(cfg.IP6)...)
|
||||||
addrCandidates = append(addrCandidates, collectIPsFromInterface(cfg.IPv4)...)
|
addrCandidates = append(addrCandidates, collectIPsFromInterface(cfg.IPv4)...)
|
||||||
addrCandidates = append(addrCandidates, collectIPsFromInterface(cfg.IPv6)...)
|
addrCandidates = append(addrCandidates, collectIPsFromInterface(cfg.IPv6)...)
|
||||||
|
|
||||||
if len(addrCandidates) > 0 {
|
if len(addrCandidates) > 0 {
|
||||||
deduped := dedupeStringsPreserveOrder(addrCandidates)
|
deduped := dedupeStringsPreserveOrder(addrCandidates)
|
||||||
if len(deduped) > 0 {
|
if len(deduped) > 0 {
|
||||||
iface.Addresses = deduped
|
iface.Addresses = deduped
|
||||||
for _, addr := range deduped {
|
for _, addr := range deduped {
|
||||||
addAddress(addr)
|
addAddress(addr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if iface.Name != "" || iface.MAC != "" || len(iface.Addresses) > 0 {
|
if iface.Name != "" || iface.MAC != "" || len(iface.Addresses) > 0 {
|
||||||
networkIfaces = append(networkIfaces, iface)
|
networkIfaces = append(networkIfaces, iface)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2510,38 +2518,26 @@ func (m *Monitor) enrichContainerMetadata(ctx context.Context, client PVEClientI
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(addressOrder) == 0 {
|
if len(addressOrder) == 0 {
|
||||||
interfacesCtx, cancelInterfaces := context.WithTimeout(ctx, 5*time.Second)
|
if isRunning {
|
||||||
ifaceDetails, ifaceErr := client.GetContainerInterfaces(interfacesCtx, nodeName, container.VMID)
|
interfacesCtx, cancelInterfaces := context.WithTimeout(ctx, 5*time.Second)
|
||||||
cancelInterfaces()
|
ifaceDetails, ifaceErr := client.GetContainerInterfaces(interfacesCtx, nodeName, container.VMID)
|
||||||
if ifaceErr != nil {
|
cancelInterfaces()
|
||||||
log.Debug().
|
if ifaceErr != nil {
|
||||||
Err(ifaceErr).
|
log.Debug().
|
||||||
Str("instance", instanceName).
|
Err(ifaceErr).
|
||||||
Str("node", nodeName).
|
Str("instance", instanceName).
|
||||||
Str("container", container.Name).
|
Str("node", nodeName).
|
||||||
Int("vmid", container.VMID).
|
Str("container", container.Name).
|
||||||
Msg("Container interface metadata unavailable")
|
Int("vmid", container.VMID).
|
||||||
} else if len(ifaceDetails) > 0 {
|
Msg("Container interface metadata unavailable")
|
||||||
for _, detail := range ifaceDetails {
|
} else if len(ifaceDetails) > 0 {
|
||||||
parsed := containerNetworkDetails{}
|
for _, detail := range ifaceDetails {
|
||||||
parsed.Name = strings.TrimSpace(detail.Name)
|
parsed := containerNetworkDetails{}
|
||||||
parsed.MAC = strings.ToUpper(strings.TrimSpace(detail.HWAddr))
|
parsed.Name = strings.TrimSpace(detail.Name)
|
||||||
|
parsed.MAC = strings.ToUpper(strings.TrimSpace(detail.HWAddr))
|
||||||
|
|
||||||
for _, addr := range detail.IPAddresses {
|
for _, addr := range detail.IPAddresses {
|
||||||
stripped := strings.TrimSpace(addr.Address)
|
stripped := strings.TrimSpace(addr.Address)
|
||||||
if stripped == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if slash := strings.Index(stripped, "/"); slash > 0 {
|
|
||||||
stripped = stripped[:slash]
|
|
||||||
}
|
|
||||||
parsed.Addresses = append(parsed.Addresses, sanitizeGuestAddressStrings(stripped)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(parsed.Addresses) == 0 && strings.TrimSpace(detail.Inet) != "" {
|
|
||||||
parts := strings.Fields(detail.Inet)
|
|
||||||
for _, part := range parts {
|
|
||||||
stripped := strings.TrimSpace(part)
|
|
||||||
if stripped == "" {
|
if stripped == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -2550,18 +2546,32 @@ func (m *Monitor) enrichContainerMetadata(ctx context.Context, client PVEClientI
|
||||||
}
|
}
|
||||||
parsed.Addresses = append(parsed.Addresses, sanitizeGuestAddressStrings(stripped)...)
|
parsed.Addresses = append(parsed.Addresses, sanitizeGuestAddressStrings(stripped)...)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
parsed.Addresses = dedupeStringsPreserveOrder(parsed.Addresses)
|
if len(parsed.Addresses) == 0 && strings.TrimSpace(detail.Inet) != "" {
|
||||||
|
parts := strings.Fields(detail.Inet)
|
||||||
if len(parsed.Addresses) > 0 {
|
for _, part := range parts {
|
||||||
for _, addr := range parsed.Addresses {
|
stripped := strings.TrimSpace(part)
|
||||||
addAddress(addr)
|
if stripped == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if slash := strings.Index(stripped, "/"); slash > 0 {
|
||||||
|
stripped = stripped[:slash]
|
||||||
|
}
|
||||||
|
parsed.Addresses = append(parsed.Addresses, sanitizeGuestAddressStrings(stripped)...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if parsed.Name != "" || parsed.MAC != "" || len(parsed.Addresses) > 0 {
|
parsed.Addresses = dedupeStringsPreserveOrder(parsed.Addresses)
|
||||||
mergeContainerNetworkInterface(&networkIfaces, parsed)
|
|
||||||
|
if len(parsed.Addresses) > 0 {
|
||||||
|
for _, addr := range parsed.Addresses {
|
||||||
|
addAddress(addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsed.Name != "" || parsed.MAC != "" || len(parsed.Addresses) > 0 {
|
||||||
|
mergeContainerNetworkInterface(&networkIfaces, parsed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue