Fix platform identity source canonicalization

This commit is contained in:
rcourtman
2026-05-08 12:51:39 +01:00
parent a6ea15bfa9
commit 2baa05c4e4
5 changed files with 492 additions and 42 deletions
@@ -194,6 +194,11 @@ cross-source deduplication.
no-parity posture, but those labels must derive from canonical incident or
storage-risk summaries already present on the resource rather than local
platform heuristics or generic status-only wording.
Frontend canonicalization must treat top-level source facets as source
evidence when `platformData.sources` is absent, but it must not invent
provider facets from generic agent telemetry: flat agent disk payloads stay
under the agent source and do not become Proxmox facets without explicit
Proxmox source, node, or workload-shape evidence.
Resource detail mappers now reuse the shared
`frontend-modern/src/utils/textPresentation.ts` title-case helper for sensor
@@ -272,6 +277,11 @@ AI-only summary payloads, or page-local heuristics.
must merge into the existing canonical resource snapshot instead of
downgrading richer REST-only infrastructure details such as disk I/O, source
metadata, or platform summary fields after first hydrate. For default
Source lists and their source-specific facets are the exception: a current
snapshot with canonical source evidence replaces stale source lists and
removes provider facets that no longer have matching source evidence, so
rows do not keep displaying a previous platform identity after websocket
refreshes. For default
`initialHydration: 'immediate'` consumers, that same path must not paint the
thinner websocket transport before the first canonical REST snapshot exists;
only explicit `prefer-ws` consumers may render directly from the realtime
@@ -92,6 +92,54 @@ describe('resourceBadgePresentation', () => {
).toEqual(['Docker / Podman']);
});
it('shows Unraid as the system identity for agent resources that also report Docker', () => {
expect(
getInfrastructureSystemIdentityBadges(
makeResource({
type: 'agent',
platformType: 'docker',
sourceType: 'hybrid',
platformData: {
sources: ['docker', 'agent'],
docker: {
runtime: 'docker',
},
agent: {
platform: 'linux',
hostProfile: 'unraid',
osName: 'Unraid',
osVersion: '7.2.2',
},
},
}),
).map((badge) => badge.label),
).toEqual(['Unraid']);
});
it('does not let stale platform sources override explicit agent host profiles', () => {
expect(
getInfrastructureSystemIdentityBadges(
makeResource({
type: 'agent',
platformType: 'proxmox-pve',
sourceType: 'hybrid',
platformData: {
sources: ['proxmox', 'docker', 'agent'],
docker: {
runtime: 'docker',
},
agent: {
platform: 'linux',
hostProfile: 'unraid',
osName: 'Unraid',
osVersion: '7.2.2',
},
},
}),
).map((badge) => badge.label),
).toEqual(['Unraid']);
});
it('uses governed host identity tokens for legacy agent profile reports', () => {
expect(
getInfrastructureSystemIdentityBadges(
@@ -151,6 +199,28 @@ describe('resourceBadgePresentation', () => {
expect(getInfrastructureSystemIdentitySortLabel(resource)).toBe('PVE');
});
it('keeps top-level platform facets ahead of collection method identity', () => {
const resource = makeResource({
platformType: 'agent',
sourceType: 'agent',
proxmox: {
nodeName: 'pi',
instance: 'pi',
},
agent: {
hostname: 'pi',
platform: 'debian',
osName: 'Debian GNU/Linux',
osVersion: '12',
},
} as Partial<Resource>);
expect(getInfrastructureSystemIdentityBadges(resource).map((badge) => badge.label)).toEqual([
'PVE',
]);
expect(getInfrastructureSystemIdentitySortLabel(resource)).toBe('PVE');
});
it('uses probe protocol identity for agentless network endpoints', () => {
const resource = makeResource({
type: 'network-endpoint',
@@ -304,6 +304,161 @@ describe('resourceStateAdapters nodeFromResource', () => {
expect((resource.platformData as Record<string, unknown>).sources).toEqual(['availability']);
});
it('canonicalizes top-level Proxmox and agent facets as one hybrid PVE resource', () => {
const [resource] = mergeCanonicalResourceSnapshot(
[
{
id: 'agent-pi',
type: 'agent',
name: 'pi',
displayName: 'pi',
platformId: 'pi',
platformType: 'agent',
sourceType: 'agent',
status: 'online',
lastSeen: Date.now(),
proxmox: {
nodeName: 'pi',
instance: 'pi',
},
agent: {
hostname: 'pi',
platform: 'debian',
osName: 'Debian GNU/Linux',
osVersion: '12',
},
} as Resource,
],
[],
);
expect(resource.platformType).toBe('proxmox-pve');
expect(resource.sourceType).toBe('hybrid');
expect(resource.proxmox).toMatchObject({ nodeName: 'pi', instance: 'pi' });
expect(resource.agent).toMatchObject({ hostname: 'pi', osName: 'Debian GNU/Linux' });
expect((resource.platformData as Record<string, unknown>).sources).toEqual([
'proxmox',
'agent',
]);
expect((resource.platformData as Record<string, unknown>).proxmox).toMatchObject({
nodeName: 'pi',
});
expect((resource.platformData as Record<string, unknown>).agent).toMatchObject({
hostname: 'pi',
});
});
it('replaces stale platform source facets with the current snapshot sources', () => {
const existing = {
id: 'agent-tower',
type: 'agent',
name: 'Tower',
displayName: 'Tower',
platformId: 'tower',
platformType: 'proxmox-pve',
sourceType: 'hybrid',
status: 'degraded',
lastSeen: Date.now(),
proxmox: {
nodeName: 'Tower',
},
agent: {
hostname: 'Tower',
hostProfile: 'unraid',
osName: 'Unraid',
},
platformData: {
sources: ['proxmox', 'docker', 'agent'],
proxmox: {
nodeName: 'Tower',
},
docker: {
runtime: 'docker',
},
agent: {
hostname: 'Tower',
hostProfile: 'unraid',
osName: 'Unraid',
},
},
} as Resource;
const [resource] = mergeCanonicalResourceSnapshot(
[
{
id: 'agent-tower',
type: 'agent',
name: 'Tower',
displayName: 'Tower',
platformId: 'tower',
platformType: 'agent',
sourceType: 'hybrid',
status: 'degraded',
lastSeen: Date.now(),
agent: {
hostname: 'Tower',
hostProfile: 'unraid',
osName: 'Unraid',
},
docker: {
runtime: 'docker',
},
} as Resource,
],
[existing],
);
expect(resource.platformType).toBe('docker');
expect(resource.sourceType).toBe('hybrid');
expect(resource.proxmox).toBeUndefined();
expect(resource.agent).toMatchObject({ hostProfile: 'unraid', osName: 'Unraid' });
expect((resource.platformData as Record<string, unknown>).sources).toEqual(['docker', 'agent']);
expect((resource.platformData as Record<string, unknown>).proxmox).toBeUndefined();
});
it('does not synthesize a Proxmox facet from flat agent disk telemetry', () => {
const [resource] = mergeCanonicalResourceSnapshot(
[
{
id: 'agent-tower',
type: 'agent',
name: 'Tower',
displayName: 'Tower',
platformId: 'tower',
platformType: 'agent',
sourceType: 'hybrid',
status: 'degraded',
lastSeen: Date.now(),
agent: {
hostname: 'Tower',
hostProfile: 'unraid',
osName: 'Unraid',
},
docker: {
runtime: 'docker',
},
platformData: {
osName: 'Unraid',
osVersion: '7.2.2',
platform: 'linux',
disks: [
{
device: 'rootfs',
mountpoint: '/',
filesystem: 'rootfs',
},
],
},
} as Resource,
],
[],
);
expect(resource.platformType).toBe('docker');
expect((resource.platformData as Record<string, unknown>).sources).toEqual(['docker', 'agent']);
expect((resource.platformData as Record<string, unknown>).proxmox).toBeUndefined();
});
it('preserves richer existing resource details when realtime updates are thinner', () => {
const existing: Resource = {
id: 'node-1',
@@ -64,6 +64,41 @@ const hostOsLabelPatterns: Array<{ pattern: RegExp; label: string }> = [
const trimString = (value: unknown): string => (typeof value === 'string' ? value.trim() : '');
const asRecord = (value: unknown): Record<string, unknown> | undefined =>
value && typeof value === 'object' ? (value as Record<string, unknown>) : undefined;
const getResourceRecord = (resource: Resource): Record<string, unknown> =>
resource as unknown as Record<string, unknown>;
const getFacetRecord = (
resource: Resource,
platformData: Record<string, unknown> | undefined,
key: string,
): Record<string, unknown> | undefined =>
asRecord(getResourceRecord(resource)[key]) || asRecord(platformData?.[key]);
const deriveSourceKeysFromFacets = (
resource: Resource,
platformData: Record<string, unknown> | undefined,
): string[] => {
const sources: string[] = [];
for (const [key, source] of [
['proxmox', 'proxmox'],
['pbs', 'pbs'],
['pmg', 'pmg'],
['vmware', 'vmware'],
['kubernetes', 'kubernetes'],
['docker', 'docker'],
['availability', 'availability'],
['agent', 'agent'],
] as const) {
if (getFacetRecord(resource, platformData, key)) {
sources.push(source);
}
}
return sources;
};
const titleFromParts = (...parts: Array<string | undefined>): string | undefined => {
const title = parts
.map((part) => (part || '').trim())
@@ -220,6 +255,23 @@ const getKnownHostIdentitySource = (...values: string[]): KnownSourcePlatform |
return null;
};
const getAgentRecord = (resource: Resource): Record<string, unknown> | undefined =>
getPlatformAgentRecord(resource) ?? asRecord(getResourceRecord(resource).agent);
const hasExplicitAgentHostProfile = (resource: Resource): boolean => {
const agent = getAgentRecord(resource);
const hostProfile = trimString(agent?.hostProfile);
return Boolean(hostProfile && getAgentHostProfileManifestEntry(hostProfile));
};
const hasPrimarySystemFacet = (
resource: Resource,
platformData: Record<string, unknown> | undefined,
): boolean =>
['proxmox', 'pbs', 'pmg', 'vmware', 'kubernetes'].some((key) =>
Boolean(getFacetRecord(resource, platformData, key)),
);
const getHostOsLabel = (...values: string[]): string | null => {
for (const value of values) {
if (!value) continue;
@@ -230,7 +282,7 @@ const getHostOsLabel = (...values: string[]): string | null => {
};
const getAgentSystemIdentityBadge = (resource: Resource): ResourceBadge | null => {
const agent = getPlatformAgentRecord(resource);
const agent = getAgentRecord(resource);
if (!agent) return null;
const hostProfile = trimString(agent.hostProfile);
@@ -277,15 +329,16 @@ const getAgentSystemIdentityBadge = (resource: Resource): ResourceBadge | null =
const getAvailabilitySystemIdentityBadge = (
resource: Resource,
platformData: Record<string, unknown> | undefined,
rawSources: string[],
): ResourceBadge | null => {
const availability = platformData?.availability as
const availability = (asRecord(platformData?.availability) ||
asRecord(getResourceRecord(resource).availability)) as
| { address?: string; protocol?: string; port?: number }
| undefined;
const sources = (platformData?.sources as string[] | undefined) ?? [];
const isAvailabilityEndpoint =
resource.type === 'network-endpoint' ||
Boolean(availability) ||
sources.some((source) => source.trim().toLowerCase() === 'availability');
rawSources.some((source) => source.trim().toLowerCase() === 'availability');
if (!isAvailabilityEndpoint) return null;
@@ -316,18 +369,34 @@ export function getInfrastructureSystemIdentityBadges(resource: Resource): Resou
const platformData = getPlatformDataRecord(resource) as
| (Record<string, unknown> & { sources?: string[] })
| undefined;
const sources = normalizeUnifiedSourceKeys(platformData?.sources);
const availabilityIdentityBadge = getAvailabilitySystemIdentityBadge(resource, platformData);
const rawSources = [
...(Array.isArray(platformData?.sources) ? platformData.sources : []),
...deriveSourceKeysFromFacets(resource, platformData),
];
const sources = normalizeUnifiedSourceKeys(rawSources);
const availabilityIdentityBadge = getAvailabilitySystemIdentityBadge(
resource,
platformData,
rawSources,
);
if (availabilityIdentityBadge) {
return [availabilityIdentityBadge];
}
const agentIdentityBadge = getAgentSystemIdentityBadge(resource);
if (
agentIdentityBadge &&
hasExplicitAgentHostProfile(resource) &&
!hasPrimarySystemFacet(resource, platformData)
) {
return [agentIdentityBadge];
}
const systemSource = firstSystemSource(sources, resource.platformType);
if (systemSource) {
return buildUnifiedSourceBadges([systemSource]);
}
const agentIdentityBadge = getAgentSystemIdentityBadge(resource);
if (agentIdentityBadge) {
return [agentIdentityBadge];
}
@@ -345,7 +414,7 @@ export function getInfrastructureSystemIdentityBadges(resource: Resource): Resou
return [platformBadge];
}
return getInfrastructurePlatformBadges(platformData?.sources);
return getInfrastructurePlatformBadges(rawSources);
}
export function getInfrastructureSystemIdentitySortLabel(resource: Resource): string {
@@ -31,6 +31,7 @@ import {
getPreferredResourceHostname,
} from '@/utils/resourceIdentity';
import {
normalizeSourcePlatformKey,
resolvePlatformTypeFromSources,
resolveSourceTypeFromSources,
} from '@/utils/sourcePlatforms';
@@ -77,6 +78,28 @@ const mergeStringArrays = (incoming?: string[], existing?: string[]): string[] |
return merged.length > 0 ? Array.from(new Set(merged)) : undefined;
};
const readStringArray = (value: unknown): string[] | undefined => {
if (!Array.isArray(value)) return undefined;
const normalized = value
.map((entry) => asString(entry))
.filter((entry): entry is string => Boolean(entry));
return normalized.length > 0 ? Array.from(new Set(normalized)) : undefined;
};
const normalizeSourceToken = (value: string): string =>
normalizeSourcePlatformKey(value) || value.trim().toLowerCase();
const sourceListHas = (sources: string[] | undefined, ...candidates: string[]): boolean => {
if (!sources || sources.length === 0) return false;
const sourceSet = new Set(sources.map((source) => normalizeSourceToken(source)));
return candidates.some((candidate) => sourceSet.has(normalizeSourceToken(candidate)));
};
const shouldKeepSourceFacet = (
authoritativeSources: string[] | undefined,
...sourceCandidates: string[]
): boolean => !authoritativeSources || sourceListHas(authoritativeSources, ...sourceCandidates);
const mergeRecord = <T extends JsonRecord>(incoming?: T, existing?: T): T | undefined => {
if (!incoming) return existing;
if (!existing) return incoming;
@@ -92,22 +115,32 @@ const mergePlatformData = (
if (!incoming) return existingValue;
if (!existing) return incomingValue;
const incomingSources = readStringArray(incoming.sources);
const existingSources = readStringArray(existing.sources);
const sources = incomingSources ?? existingSources;
const merged: JsonRecord = { ...existing, ...incoming };
for (const key of [
'agent',
'docker',
'proxmox',
'pbs',
'pmg',
'kubernetes',
'vmware',
'storage',
'availability',
'physicalDisk',
'ceph',
'metrics',
'discoveryTarget',
]) {
for (const [key, ...sourceCandidates] of [
['agent', 'agent'],
['docker', 'docker'],
['proxmox', 'proxmox-pve'],
['pbs', 'proxmox-pbs'],
['pmg', 'proxmox-pmg'],
['kubernetes', 'kubernetes'],
['vmware', 'vmware-vsphere'],
['availability', 'availability'],
] as const) {
if (!shouldKeepSourceFacet(incomingSources, ...sourceCandidates)) {
delete merged[key];
continue;
}
const nested = mergeRecord(asRecord(incoming[key]), asRecord(existing[key]));
if (nested) {
merged[key] = nested;
}
}
for (const key of ['storage', 'physicalDisk', 'ceph', 'metrics', 'discoveryTarget']) {
const nested = mergeRecord(asRecord(incoming[key]), asRecord(existing[key]));
if (nested) {
merged[key] = nested;
@@ -122,10 +155,6 @@ const mergePlatformData = (
merged.sourceStatus = sourceStatus;
}
const sources = mergeStringArrays(
Array.isArray(incoming.sources) ? (incoming.sources as string[]) : undefined,
Array.isArray(existing.sources) ? (existing.sources as string[]) : undefined,
);
if (sources) {
merged.sources = sources;
}
@@ -133,7 +162,35 @@ const mergePlatformData = (
return merged;
};
const deriveLegacySourceList = (resource: Resource): string[] | undefined => {
const getResourceRecord = (resource: Resource): JsonRecord => resource as unknown as JsonRecord;
const getFacetRecord = (
resource: Resource,
platformData: Resource['platformData'] | undefined,
key: string,
): JsonRecord | undefined => {
const resourceRecord = getResourceRecord(resource);
const platformRecord = asRecord(platformData);
return asRecord(resourceRecord[key]) || asRecord(platformRecord?.[key]);
};
const deriveLegacySourceList = (
resource: Resource,
platformData: Resource['platformData'] | undefined = resource.platformData,
): string[] | undefined => {
const sources: string[] = [];
if (getFacetRecord(resource, platformData, 'proxmox')) sources.push('proxmox');
if (getFacetRecord(resource, platformData, 'pbs')) sources.push('pbs');
if (getFacetRecord(resource, platformData, 'pmg')) sources.push('pmg');
if (getFacetRecord(resource, platformData, 'vmware')) sources.push('vmware');
if (getFacetRecord(resource, platformData, 'kubernetes')) sources.push('kubernetes');
if (getFacetRecord(resource, platformData, 'docker')) sources.push('docker');
if (getFacetRecord(resource, platformData, 'availability')) sources.push('availability');
if (getFacetRecord(resource, platformData, 'agent')) sources.push('agent');
if (sources.length > 0) {
return Array.from(new Set(sources));
}
if (
resource.type === 'network-endpoint' ||
resource.platformType === 'availability' ||
@@ -163,22 +220,81 @@ const deriveLegacySourceList = (resource: Resource): string[] | undefined => {
}
};
const hasLegacyProxmoxShape = (
resource: Resource,
platformData: JsonRecord,
sources?: string[],
): boolean =>
resource.platformType === 'proxmox-pve' ||
sourceListHas(sources, 'proxmox-pve') ||
[
'instance',
'node',
'clusterName',
'vmid',
'cpus',
'template',
'swapUsed',
'swapTotal',
'balloon',
].some((key) => platformData[key] !== undefined);
const canonicalizeLegacyPlatformData = (resource: Resource): Resource['platformData'] => {
const platformData = asRecord(resource.platformData);
if (!platformData) {
return resource.platformData;
const normalizedSources = deriveLegacySourceList(resource);
if (!normalizedSources || normalizedSources.length === 0) {
return resource.platformData;
}
const normalized: JsonRecord = { sources: normalizedSources };
const resourceRecord = getResourceRecord(resource);
for (const [key, value] of [
['agent', resourceRecord.agent],
['docker', resourceRecord.docker],
['proxmox', resourceRecord.proxmox],
['pbs', resourceRecord.pbs],
['pmg', resourceRecord.pmg],
['kubernetes', resourceRecord.kubernetes],
['vmware', resourceRecord.vmware],
['storage', resourceRecord.storage],
['availability', resourceRecord.availability],
['physicalDisk', resourceRecord.physicalDisk],
] as const) {
if (value !== undefined) {
normalized[key] = value;
}
}
return normalized;
}
const normalized: JsonRecord = { ...platformData };
const resourceRecord = getResourceRecord(resource);
for (const key of [
'agent',
'docker',
'proxmox',
'pbs',
'pmg',
'kubernetes',
'vmware',
'storage',
'availability',
'physicalDisk',
] as const) {
if (!asRecord(normalized[key]) && asRecord(resourceRecord[key])) {
normalized[key] = resourceRecord[key];
}
}
const normalizedSources =
Array.isArray(platformData.sources) && platformData.sources.length > 0
? (platformData.sources as string[])
: deriveLegacySourceList(resource);
: deriveLegacySourceList(resource, platformData);
if (normalizedSources && normalizedSources.length > 0) {
normalized.sources = normalizedSources;
}
if (!asRecord(platformData.agent)) {
if (!asRecord(normalized.agent)) {
const agentPayload: JsonRecord = {};
for (const [legacyKey, nextKey] of [
['agentId', 'agentId'],
@@ -204,7 +320,7 @@ const canonicalizeLegacyPlatformData = (resource: Resource): Resource['platformD
}
}
if (!asRecord(platformData.docker)) {
if (!asRecord(normalized.docker)) {
const dockerPayload: JsonRecord = {};
for (const [legacyKey, nextKey] of [
['agentId', 'agentId'],
@@ -237,7 +353,10 @@ const canonicalizeLegacyPlatformData = (resource: Resource): Resource['platformD
}
}
if (!asRecord(platformData.proxmox)) {
if (
!asRecord(normalized.proxmox) &&
hasLegacyProxmoxShape(resource, platformData, normalizedSources)
) {
const proxmoxPayload: JsonRecord = {};
for (const [legacyKey, nextKey] of [
['instance', 'instance'],
@@ -260,7 +379,7 @@ const canonicalizeLegacyPlatformData = (resource: Resource): Resource['platformD
}
}
if (!asRecord(platformData.pbs)) {
if (!asRecord(normalized.pbs)) {
const pbsPayload: JsonRecord = {};
if (platformData.host !== undefined) pbsPayload.hostname = platformData.host;
if (platformData.version !== undefined) pbsPayload.version = platformData.version;
@@ -275,7 +394,7 @@ const canonicalizeLegacyPlatformData = (resource: Resource): Resource['platformD
}
}
if (!asRecord(platformData.pmg)) {
if (!asRecord(normalized.pmg)) {
const pmgPayload: JsonRecord = {};
if (platformData.host !== undefined) pmgPayload.hostname = platformData.host;
if (platformData.version !== undefined) pmgPayload.version = platformData.version;
@@ -299,7 +418,7 @@ const canonicalizeLegacyPlatformData = (resource: Resource): Resource['platformD
}
}
if (!asRecord(platformData.kubernetes)) {
if (!asRecord(normalized.kubernetes)) {
const kubernetesPayload: JsonRecord = {};
for (const [legacyKey, nextKey] of [
['agentId', 'agentId'],
@@ -329,7 +448,7 @@ const getCanonicalSourceList = (
const platformRecord = asRecord(platformData);
return Array.isArray(platformRecord?.sources) && platformRecord.sources.length > 0
? (platformRecord.sources as string[])
: deriveLegacySourceList({ ...resource, platformData });
: deriveLegacySourceList(resource, platformData);
};
const hasAvailabilityFacet = (
@@ -393,11 +512,22 @@ const mergeCanonicalIdentity = (
};
};
const mergeCanonicalSourceFacet = <T extends JsonRecord>(
incomingFacet: T | undefined,
existingFacet: T | undefined,
incomingSources: string[] | undefined,
...sourceCandidates: string[]
): T | undefined =>
shouldKeepSourceFacet(incomingSources, ...sourceCandidates)
? mergeRecord(incomingFacet, existingFacet)
: incomingFacet;
export const mergeCanonicalResource = (incoming: Resource, existing?: Resource): Resource => {
if (!existing) {
return incoming;
}
const existingCanonical = canonicalizeRealtimeResource(existing);
const incomingSources = getCanonicalSourceList(incoming, incoming.platformData);
return {
...existingCanonical,
...incoming,
@@ -413,26 +543,42 @@ export const mergeCanonicalResource = (incoming: Resource, existing?: Resource):
recentChanges: incoming.recentChanges ?? existingCanonical.recentChanges,
facetCounts: incoming.facetCounts ?? existingCanonical.facetCounts,
diskIO: incoming.diskIO ?? existingCanonical.diskIO,
agent: mergeRecord(
agent: mergeCanonicalSourceFacet(
incoming.agent as JsonRecord | undefined,
existingCanonical.agent as JsonRecord | undefined,
incomingSources,
'agent',
) as Resource['agent'],
proxmox: mergeRecord(
proxmox: mergeCanonicalSourceFacet(
incoming.proxmox as JsonRecord | undefined,
existingCanonical.proxmox as JsonRecord | undefined,
incomingSources,
'proxmox-pve',
) as Resource['proxmox'],
pbs: mergeRecord(
pbs: mergeCanonicalSourceFacet(
incoming.pbs as JsonRecord | undefined,
existingCanonical.pbs as JsonRecord | undefined,
incomingSources,
'proxmox-pbs',
) as Resource['pbs'],
kubernetes: mergeRecord(
kubernetes: mergeCanonicalSourceFacet(
incoming.kubernetes as JsonRecord | undefined,
existingCanonical.kubernetes as JsonRecord | undefined,
incomingSources,
'kubernetes',
) as Resource['kubernetes'],
vmware: mergeRecord(
vmware: mergeCanonicalSourceFacet(
incoming.vmware as JsonRecord | undefined,
existingCanonical.vmware as JsonRecord | undefined,
incomingSources,
'vmware-vsphere',
) as Resource['vmware'],
availability: mergeCanonicalSourceFacet(
incoming.availability as JsonRecord | undefined,
existingCanonical.availability as JsonRecord | undefined,
incomingSources,
'availability',
) as Resource['availability'],
storage: mergeRecord(
incoming.storage as JsonRecord | undefined,
existingCanonical.storage as JsonRecord | undefined,