mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-21 10:43:36 +00:00
Fix platform identity source canonicalization
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user