From 4eb77151715d401e6a8d86aa974c074781d815ef Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 12 Jul 2026 19:50:09 +0100 Subject: [PATCH] fix(frontend): key drawer guest web-interface URLs by the canonical workload id The infrastructure resource drawer saved guest metadata (web interface URLs) under the unified resource hash or the discovery resource id, a keyspace no workloads table reads: the tables and the workloads drawer resolve metadata by the canonical id (instance:node:vmid for PVE guests, resource id otherwise), which is also the key v5 upgrades carry over in guest_metadata.json. A URL saved from that drawer was stranded where only the same drawer could read it back. Route the drawer's guest metadata id through getCanonicalWorkloadIdForResource, and canonicalize the resource type inside that helper so lxc/oci-container/qemu spellings build the same node-scoped id the workloads surfaces use. Refs #1556 --- .../ResourceDetailDrawer.discovery.test.ts | 4 +++- ...urceDetailDiscoveryModel.branchcov.test.ts | 24 ++++++++++++++++--- .../resourceDetailDiscoveryModel.ts | 17 ++++++++----- frontend-modern/src/utils/workloads.ts | 3 ++- 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.discovery.test.ts b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.discovery.test.ts index 67fd28281..b081674c3 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.discovery.test.ts +++ b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.discovery.test.ts @@ -216,13 +216,15 @@ describe('toDiscoveryConfig', () => { }, }; + // Guest metadata keys off the canonical workload id (the resource id for + // non-PVE guests), not the vSphere managed-object id. expect(toDiscoveryConfig(resource)).toEqual({ resourceType: 'vm', agentId: 'guest-agent-01', resourceId: 'vm-201', hostname: 'warehouse-api-01.internal', metadataKind: 'guest', - metadataId: 'vm-201', + metadataId: 'vm-270aaf3d37cc7501', targetLabel: 'guest', }); }); diff --git a/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDiscoveryModel.branchcov.test.ts b/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDiscoveryModel.branchcov.test.ts index fcba43f58..1e9818c2a 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDiscoveryModel.branchcov.test.ts +++ b/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDiscoveryModel.branchcov.test.ts @@ -164,17 +164,35 @@ describe('toDiscoveryConfig — explicit discoveryTarget branch', () => { }), }); // No explicit hostname => getPreferredHostLabel(resource) wins. + // Guest metadata keys off the canonical workload id (resource id here, + // no PVE identity on the fixture), not the discovery resource id. expect(toDiscoveryConfig(resource)).toEqual({ resourceType: 'vm', agentId: 'vm-agent', resourceId: 'vm-rid', hostname: 'vm-name', metadataKind: 'guest', - metadataId: 'vm-rid', + metadataId: 'res-1', targetLabel: 'guest', }); }); + it('keys explicit vm guest metadata by the canonical instance:node:vmid id', () => { + const resource = baseResource({ + type: 'vm', + name: 'vm-name', + proxmox: { instance: 'pve-main', nodeName: 'node1', vmid: 105 }, + discoveryTarget: discoveryTarget({ + resourceType: 'vm', + agentId: 'vm-agent', + resourceId: 'vm-rid', + }), + }); + const result = toDiscoveryConfig(resource); + expect(result?.metadataKind).toBe('guest'); + expect(result?.metadataId).toBe('pve-main:node1:105'); + }); + it('maps an explicit system-container target to a guest config', () => { const resource = baseResource({ type: 'system-container', @@ -187,7 +205,7 @@ describe('toDiscoveryConfig — explicit discoveryTarget branch', () => { const result = toDiscoveryConfig(resource); expect(result?.resourceType).toBe('system-container'); expect(result?.metadataKind).toBe('guest'); - expect(result?.metadataId).toBe('sc-rid'); + expect(result?.metadataId).toBe('res-1'); expect(result?.targetLabel).toBe('guest'); }); @@ -218,7 +236,7 @@ describe('toDiscoveryConfig — explicit discoveryTarget branch', () => { const result = toDiscoveryConfig(resource); expect(result?.resourceType).toBe('app-container'); expect(result?.metadataKind).toBe('guest'); - expect(result?.metadataId).toBe('app-rid'); + expect(result?.metadataId).toBe('res-1'); expect(result?.targetLabel).toBe('container'); }); diff --git a/frontend-modern/src/components/Infrastructure/resourceDetailDiscoveryModel.ts b/frontend-modern/src/components/Infrastructure/resourceDetailDiscoveryModel.ts index fefd96dfa..7c613fb11 100644 --- a/frontend-modern/src/components/Infrastructure/resourceDetailDiscoveryModel.ts +++ b/frontend-modern/src/components/Infrastructure/resourceDetailDiscoveryModel.ts @@ -14,6 +14,7 @@ import { getPreferredInfrastructureDisplayName, getPreferredResourceHostname, } from '@/utils/resourceIdentity'; +import { getCanonicalWorkloadIdForResource } from '@/utils/workloads'; export type DiscoveryConfig = { resourceType: DiscoveryResourceType; @@ -102,7 +103,6 @@ const getDockerContainerMetadataId = ( const getMetadataTarget = ( resource: Resource, resourceType: DiscoveryResourceType, - fallbackMetadataId: string, platformData: PlatformData | undefined, ): Pick => { if (resourceType === 'app-container') { @@ -115,9 +115,14 @@ const getMetadataTarget = ( } } + // Guest metadata is keyed by the canonical workload id shared with the + // workloads surfaces (`instance:node:vmid` for PVE guests, resource id + // otherwise — also the key v5 upgrades carry over). Saving under any other + // id (unified resource hash, bare vmid, discovery resource id) strands the + // URL where no table reads it. return { metadataKind: 'guest', - metadataId: fallbackMetadataId, + metadataId: getCanonicalWorkloadIdForResource(resource), }; }; @@ -159,7 +164,7 @@ export const toDiscoveryConfig = (resource: Resource): DiscoveryConfig | null => const isHostDiscovery = isAgentDiscoveryResourceType(resourceType); const metadataTarget = isHostDiscovery ? { metadataKind: 'agent' as const, metadataId: explicitDiscoveryAgentId } - : getMetadataTarget(resource, resourceType, explicitDiscoveryResourceId, platformData); + : getMetadataTarget(resource, resourceType, platformData); const targetLabel = isHostDiscovery ? 'agent' : resourceType === 'app-container' @@ -267,7 +272,7 @@ export const toDiscoveryConfig = (resource: Resource): DiscoveryConfig | null => resourceId: vmidResourceId || resource.id, hostname, metadataKind: 'guest', - metadataId: resource.id, + metadataId: getCanonicalWorkloadIdForResource(resource), targetLabel: 'guest', }; case 'system-container': @@ -281,7 +286,7 @@ export const toDiscoveryConfig = (resource: Resource): DiscoveryConfig | null => resourceId: vmidResourceId || resource.id, hostname, metadataKind: 'guest', - metadataId: resource.id, + metadataId: getCanonicalWorkloadIdForResource(resource), targetLabel: 'guest', }; case 'app-container': @@ -293,7 +298,7 @@ export const toDiscoveryConfig = (resource: Resource): DiscoveryConfig | null => agentId: workloadAgentId, resourceId: asString(dockerPlatformData?.containerId) || resource.id, hostname, - ...getMetadataTarget(resource, 'app-container', resource.id, platformData), + ...getMetadataTarget(resource, 'app-container', platformData), targetLabel: 'container', }; case 'pod': diff --git a/frontend-modern/src/utils/workloads.ts b/frontend-modern/src/utils/workloads.ts index 8bb3e700b..3b3aeb604 100644 --- a/frontend-modern/src/utils/workloads.ts +++ b/frontend-modern/src/utils/workloads.ts @@ -209,7 +209,8 @@ export const buildCanonicalNodeScopedWorkloadId = ({ export const getCanonicalWorkloadIdForResource = ( resource: Pick, ): string => { - if (resource.type === 'vm' || resource.type === 'system-container') { + const workloadType = resolveWorkloadTypeFromString(resource.type); + if (workloadType === 'vm' || workloadType === 'system-container') { const canonicalId = buildCanonicalNodeScopedWorkloadId({ instance: resource.proxmox?.instance || resource.clusterId, node: resource.proxmox?.node || resource.proxmox?.nodeName,