mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-08 09:53:25 +00:00
refactor(recovery): canonicalize item resource ids
This commit is contained in:
parent
f15a186a04
commit
5ef3fb59bf
16 changed files with 234 additions and 34 deletions
|
|
@ -728,6 +728,11 @@ operator-facing filter query for lifecycle-adjacent drill-down links. Any
|
|||
legacy `provider` alias support must remain compatibility-only input behind
|
||||
the shared API/router layer rather than becoming the route shape lifecycle
|
||||
surfaces copy back out to operators.
|
||||
That same lifecycle-adjacent recovery drill-down boundary must also stay on
|
||||
canonical `itemResourceId` filter and payload vocabulary. When lifecycle
|
||||
surfaces deep-link into shared recovery handlers or consume recovery payloads,
|
||||
they should treat legacy `subjectResourceId` only as an API-layer compatibility
|
||||
alias rather than reviving it as the route or runtime model they expose.
|
||||
|
||||
The updater/runtime surfaces must preserve the one-shot `updated_from`
|
||||
continuity handoff and the non-TLS continuity path for supported self-hosted
|
||||
|
|
|
|||
|
|
@ -200,6 +200,7 @@ Own canonical runtime payload shapes between backend and frontend.
|
|||
13. Keep recovery payload filters canonical across `/api/recovery/rollups`, `/api/recovery/points`, `/api/recovery/series`, and `/api/recovery/facets`: when `internal/api/recovery_handlers.go` adds a governed recovery filter or display field such as provider-neutral `itemType`, the same normalized transport must land across all four endpoints and the contract tests must pin both outbound payload shape and accepted query aliases in the same slice
|
||||
14. Keep recovery platform-query vocabulary canonical across that same `/api/recovery/*` surface: operator-facing transport must emit `platform` as the canonical query field, accepted legacy `provider` aliases must remain compatibility-only input, and `internal/api/contract_test.go` must pin that fallback behavior in the same slice as any handler change
|
||||
15. Keep recovery payload platform vocabulary canonical across that same `/api/recovery/*` surface: point payloads must expose `platform`, rollup payloads must expose `platforms`, and any compatibility `provider` / `providers` aliases must remain secondary fallback fields rather than replacing the shared response model
|
||||
16. Keep recovery linked-resource vocabulary canonical across that same `/api/recovery/*` surface: points and rollups must expose `itemResourceId` as the canonical linked-resource field, accepted legacy `subjectResourceId` aliases must remain compatibility-only input or secondary payload fields, and the shared proof surface must pin that normalization in the same slice as any handler change
|
||||
|
||||
## Current State
|
||||
|
||||
|
|
@ -1388,6 +1389,12 @@ operator-facing query field across `/api/recovery/rollups`, `/api/recovery/point
|
|||
mapping that boundary onto internal provider fields, but accepted legacy
|
||||
`provider` aliases must be compatibility-only input and must not replace the
|
||||
canonical transport query shape.
|
||||
That same recovery API boundary must also treat `itemResourceId` as the
|
||||
canonical linked-resource filter and payload field across those same
|
||||
`/api/recovery/*` endpoints. Accepted legacy `subjectResourceId` aliases may
|
||||
remain as compatibility-only input or secondary payload fields during the v6
|
||||
transition, but the shared transport contract and frontend decode path must
|
||||
normalize them back onto canonical `itemResourceId`.
|
||||
That same outbound recovery transport now also treats `platform` and
|
||||
`platforms` as the canonical response fields for point and rollup payloads.
|
||||
Compatibility `provider` and `providers` fields may remain during the v6
|
||||
|
|
|
|||
|
|
@ -200,6 +200,10 @@ That same rule applies to recovery detail helpers. Provider-specific helper
|
|||
names like `isPbsProvider` should become platform-specific helpers like
|
||||
`isPbsPlatform` once the runtime recovery model is already canonically
|
||||
platform-first.
|
||||
The same canonical boundary applies to linked-resource identifiers. Recovery
|
||||
API payloads, query filters, and normalized frontend runtime models should use
|
||||
`itemResourceId` as the canonical field while accepting or emitting
|
||||
`subjectResourceId` only as a compatibility alias during the transition.
|
||||
That same presenter boundary should also own canonical item-type derivation.
|
||||
Recovery surfaces must resolve rollup and point item types through one shared
|
||||
item-type helper instead of repeating `display.itemType` / `subjectType` /
|
||||
|
|
|
|||
62
frontend-modern/src/api/__tests__/recoveryTransport.test.ts
Normal file
62
frontend-modern/src/api/__tests__/recoveryTransport.test.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
normalizeRecoveryPointsResponse,
|
||||
normalizeRecoveryRollupsResponse,
|
||||
} from '@/utils/recoveryPlatformModel';
|
||||
|
||||
describe('recovery transport', () => {
|
||||
it('normalizes legacy subject resource ids onto canonical item resource ids', () => {
|
||||
expect(
|
||||
normalizeRecoveryPointsResponse({
|
||||
data: [
|
||||
{
|
||||
id: 'point-1',
|
||||
provider: 'truenas',
|
||||
kind: 'snapshot',
|
||||
mode: 'snapshot',
|
||||
outcome: 'success',
|
||||
subjectResourceId: 'res-1',
|
||||
},
|
||||
],
|
||||
meta: { page: 1, limit: 100, total: 1, totalPages: 1 },
|
||||
}),
|
||||
).toEqual({
|
||||
data: [
|
||||
{
|
||||
id: 'point-1',
|
||||
platform: 'truenas',
|
||||
kind: 'snapshot',
|
||||
mode: 'snapshot',
|
||||
outcome: 'success',
|
||||
itemResourceId: 'res-1',
|
||||
},
|
||||
],
|
||||
meta: { page: 1, limit: 100, total: 1, totalPages: 1 },
|
||||
});
|
||||
|
||||
expect(
|
||||
normalizeRecoveryRollupsResponse({
|
||||
data: [
|
||||
{
|
||||
rollupId: 'rollup-1',
|
||||
lastOutcome: 'success',
|
||||
providers: ['truenas'],
|
||||
subjectResourceId: 'res-1',
|
||||
},
|
||||
],
|
||||
meta: { page: 1, limit: 100, total: 1, totalPages: 1 },
|
||||
}),
|
||||
).toEqual({
|
||||
data: [
|
||||
{
|
||||
rollupId: 'rollup-1',
|
||||
lastOutcome: 'success',
|
||||
platforms: ['truenas'],
|
||||
itemResourceId: 'res-1',
|
||||
},
|
||||
],
|
||||
meta: { page: 1, limit: 100, total: 1, totalPages: 1 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -126,7 +126,7 @@ const Recovery: Component = () => {
|
|||
const label = getRecoveryRollupItemLabel(rollup, resourceIndex);
|
||||
const haystack = [
|
||||
rollup.rollupId,
|
||||
rollup.subjectResourceId || '',
|
||||
rollup.itemResourceId || '',
|
||||
label,
|
||||
rollupItemType,
|
||||
rollup.subjectRef?.namespace || '',
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ export const RecoveryPointDetails: Component<RecoveryPointDetailsProps> = (props
|
|||
|
||||
if (p.verified != null) pairs.push({ k: 'Verified', v: p.verified ? 'Verified' : 'Not Verified' });
|
||||
if (p.encrypted != null) pairs.push({ k: 'Encrypted', v: p.encrypted ? 'Encrypted' : 'Not Encrypted' });
|
||||
if (p.subjectResourceId) pairs.push({ k: 'Item Resource', v: p.subjectResourceId });
|
||||
if (p.itemResourceId) pairs.push({ k: 'Item Resource', v: p.itemResourceId });
|
||||
if (p.repositoryResourceId) pairs.push({ k: 'Target Resource', v: p.repositoryResourceId });
|
||||
if (p.subjectRef) pairs.push({ k: 'Item Ref', v: labelForRef(p.subjectRef) });
|
||||
if (p.repositoryRef) pairs.push({ k: 'Target Ref', v: labelForRef(p.repositoryRef) });
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ vi.mock('@solidjs/router', async () => {
|
|||
const rollupsPayload = [
|
||||
{
|
||||
rollupId: 'res:vm-123',
|
||||
subjectResourceId: 'vm-123',
|
||||
itemResourceId: 'vm-123',
|
||||
display: { subjectType: 'proxmox-vm', itemType: 'vm' },
|
||||
lastAttemptAt: '2026-02-14T10:00:00.000Z',
|
||||
lastSuccessAt: '2026-02-14T10:00:00.000Z',
|
||||
|
|
@ -260,7 +260,7 @@ describe('Recovery', () => {
|
|||
it('renders canonical rollup and history item labels when linked resources are unavailable', async () => {
|
||||
rollupsPayload.push({
|
||||
rollupId: 'res:vm-404',
|
||||
subjectResourceId: 'vm-404',
|
||||
itemResourceId: 'vm-404',
|
||||
display: { itemLabel: 'Archive VM' },
|
||||
lastAttemptAt: '2026-02-12T08:00:00.000Z',
|
||||
lastSuccessAt: '2026-02-12T08:00:00.000Z',
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export type RecoveryPointsQuery = {
|
|||
mode?: string | null;
|
||||
outcome?: string | null;
|
||||
itemType?: string | null;
|
||||
itemResourceId?: string | null;
|
||||
subjectResourceId?: string | null;
|
||||
|
||||
// Normalized filters (server-side)
|
||||
|
|
@ -58,7 +59,8 @@ const normalizeQuery = (query: RecoveryPointsQuery | undefined): RecoveryPointsQ
|
|||
mode: norm(q.mode) || null,
|
||||
outcome: norm(q.outcome) || null,
|
||||
itemType: norm(q.itemType) || null,
|
||||
subjectResourceId: norm(q.subjectResourceId) || null,
|
||||
itemResourceId: norm(q.itemResourceId) || norm(q.subjectResourceId) || null,
|
||||
subjectResourceId: null,
|
||||
|
||||
q: norm(q.q) || null,
|
||||
cluster: norm(q.cluster) || null,
|
||||
|
|
@ -98,7 +100,7 @@ const buildURL = (query: RecoveryPointsQuery | undefined): string => {
|
|||
if (q.mode) params.set('mode', q.mode);
|
||||
if (q.outcome) params.set('outcome', q.outcome);
|
||||
if (q.itemType) params.set('itemType', q.itemType);
|
||||
if (q.subjectResourceId) params.set('subjectResourceId', q.subjectResourceId);
|
||||
if (q.itemResourceId) params.set('itemResourceId', q.itemResourceId);
|
||||
|
||||
if (q.q) params.set('q', q.q);
|
||||
if (q.cluster) params.set('cluster', q.cluster);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export type RecoveryRollupsQuery = {
|
|||
mode?: string | null;
|
||||
outcome?: string | null;
|
||||
itemType?: string | null;
|
||||
itemResourceId?: string | null;
|
||||
subjectResourceId?: string | null;
|
||||
q?: string | null;
|
||||
cluster?: string | null;
|
||||
|
|
@ -36,7 +37,8 @@ const normalizeQuery = (query: RecoveryRollupsQuery | undefined): RecoveryRollup
|
|||
mode: norm(q.mode) || null,
|
||||
outcome: norm(q.outcome) || null,
|
||||
itemType: norm(q.itemType) || null,
|
||||
subjectResourceId: norm(q.subjectResourceId) || null,
|
||||
itemResourceId: norm(q.itemResourceId) || norm(q.subjectResourceId) || null,
|
||||
subjectResourceId: null,
|
||||
q: norm(q.q) || null,
|
||||
cluster: norm(q.cluster) || null,
|
||||
node: norm(q.node) || null,
|
||||
|
|
@ -72,7 +74,7 @@ const buildURL = (page: number, limit: number, query: RecoveryRollupsQuery | und
|
|||
if (q.mode) params.set('mode', q.mode);
|
||||
if (q.outcome) params.set('outcome', q.outcome);
|
||||
if (q.itemType) params.set('itemType', q.itemType);
|
||||
if (q.subjectResourceId) params.set('subjectResourceId', q.subjectResourceId);
|
||||
if (q.itemResourceId) params.set('itemResourceId', q.itemResourceId);
|
||||
if (q.q) params.set('q', q.q);
|
||||
if (q.cluster) params.set('cluster', q.cluster);
|
||||
if (q.node) params.set('node', q.node);
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export interface RecoveryPoint {
|
|||
encrypted?: boolean | null;
|
||||
immutable?: boolean | null;
|
||||
|
||||
subjectResourceId?: string;
|
||||
itemResourceId?: string;
|
||||
repositoryResourceId?: string;
|
||||
subjectRef?: RecoveryExternalRef | null;
|
||||
repositoryRef?: RecoveryExternalRef | null;
|
||||
|
|
@ -70,6 +70,7 @@ export interface RecoveryPoint {
|
|||
export interface RecoveryPointTransport extends RecoveryPoint {
|
||||
display?: RecoveryPointDisplayTransport | null;
|
||||
provider?: RecoveryPlatform;
|
||||
subjectResourceId?: string;
|
||||
}
|
||||
|
||||
export interface RecoveryResponseMeta {
|
||||
|
|
@ -91,7 +92,7 @@ export interface RecoveryPointsTransportResponse {
|
|||
|
||||
export interface ProtectionRollup {
|
||||
rollupId: string;
|
||||
subjectResourceId?: string;
|
||||
itemResourceId?: string;
|
||||
subjectRef?: RecoveryExternalRef | null;
|
||||
display?: RecoveryPointDisplay | null;
|
||||
|
||||
|
|
@ -105,6 +106,7 @@ export interface ProtectionRollup {
|
|||
export interface ProtectionRollupTransport extends ProtectionRollup {
|
||||
display?: RecoveryPointDisplayTransport | null;
|
||||
providers?: RecoveryPlatform[];
|
||||
subjectResourceId?: string;
|
||||
}
|
||||
|
||||
export interface RecoveryRollupsResponse {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ describe('recoveryPlatformModel', () => {
|
|||
kind: 'backup',
|
||||
mode: 'remote',
|
||||
outcome: 'success',
|
||||
subjectResourceId: 'vm-123',
|
||||
display: {
|
||||
subjectLabel: 'Archive VM',
|
||||
subjectType: 'proxmox-vm',
|
||||
|
|
@ -59,6 +60,7 @@ describe('recoveryPlatformModel', () => {
|
|||
kind: 'backup',
|
||||
mode: 'remote',
|
||||
outcome: 'success',
|
||||
itemResourceId: 'vm-123',
|
||||
display: {
|
||||
itemLabel: 'Archive VM',
|
||||
itemType: 'proxmox-vm',
|
||||
|
|
@ -69,6 +71,7 @@ describe('recoveryPlatformModel', () => {
|
|||
normalizeRecoveryRollup({
|
||||
rollupId: 'rollup-1',
|
||||
lastOutcome: 'success',
|
||||
subjectResourceId: 'vm-123',
|
||||
providers: ['proxmox-pbs', 'kubernetes'],
|
||||
display: {
|
||||
subjectLabel: 'Legacy Dataset',
|
||||
|
|
@ -78,6 +81,7 @@ describe('recoveryPlatformModel', () => {
|
|||
).toEqual({
|
||||
rollupId: 'rollup-1',
|
||||
lastOutcome: 'success',
|
||||
itemResourceId: 'vm-123',
|
||||
platforms: ['proxmox-pbs', 'kubernetes'],
|
||||
display: {
|
||||
itemLabel: 'Legacy Dataset',
|
||||
|
|
@ -96,19 +100,21 @@ describe('recoveryPlatformModel', () => {
|
|||
kind: 'snapshot',
|
||||
mode: 'snapshot',
|
||||
outcome: 'success',
|
||||
subjectResourceId: 'res-1',
|
||||
},
|
||||
],
|
||||
meta: { page: 1, limit: 100, total: 1, totalPages: 1 },
|
||||
}),
|
||||
).toEqual({
|
||||
data: [
|
||||
{
|
||||
id: 'point-1',
|
||||
platform: 'truenas',
|
||||
kind: 'snapshot',
|
||||
mode: 'snapshot',
|
||||
outcome: 'success',
|
||||
},
|
||||
{
|
||||
id: 'point-1',
|
||||
platform: 'truenas',
|
||||
kind: 'snapshot',
|
||||
mode: 'snapshot',
|
||||
outcome: 'success',
|
||||
itemResourceId: 'res-1',
|
||||
},
|
||||
],
|
||||
meta: { page: 1, limit: 100, total: 1, totalPages: 1 },
|
||||
});
|
||||
|
|
@ -119,6 +125,7 @@ describe('recoveryPlatformModel', () => {
|
|||
{
|
||||
rollupId: 'rollup-1',
|
||||
lastOutcome: 'warning',
|
||||
subjectResourceId: 'res-1',
|
||||
providers: ['truenas'],
|
||||
},
|
||||
],
|
||||
|
|
@ -126,11 +133,12 @@ describe('recoveryPlatformModel', () => {
|
|||
}),
|
||||
).toEqual({
|
||||
data: [
|
||||
{
|
||||
rollupId: 'rollup-1',
|
||||
lastOutcome: 'warning',
|
||||
platforms: ['truenas'],
|
||||
},
|
||||
{
|
||||
rollupId: 'rollup-1',
|
||||
lastOutcome: 'warning',
|
||||
itemResourceId: 'res-1',
|
||||
platforms: ['truenas'],
|
||||
},
|
||||
],
|
||||
meta: { page: 1, limit: 100, total: 1, totalPages: 1 },
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,11 +21,11 @@ describe('recoveryRecordPresentation', () => {
|
|||
|
||||
const rollup = {
|
||||
rollupId: 'rollup-1',
|
||||
subjectResourceId: 'res-1',
|
||||
itemResourceId: 'res-1',
|
||||
} as ProtectionRollup;
|
||||
const point = {
|
||||
id: 'point-1',
|
||||
subjectResourceId: 'res-1',
|
||||
itemResourceId: 'res-1',
|
||||
} as RecoveryPoint;
|
||||
|
||||
expect(getRecoveryRollupItemLabel(rollup, resources)).toBe('db-01');
|
||||
|
|
@ -69,12 +69,12 @@ describe('recoveryRecordPresentation', () => {
|
|||
|
||||
const rollup = {
|
||||
rollupId: 'res:res-3',
|
||||
subjectResourceId: 'res-3',
|
||||
itemResourceId: 'res-3',
|
||||
display: { itemLabel: 'Archive VM' },
|
||||
} as ProtectionRollup;
|
||||
const linkedPoint = {
|
||||
id: 'point-3',
|
||||
subjectResourceId: 'res-2',
|
||||
itemResourceId: 'res-2',
|
||||
display: { itemLabel: 'billing-api' },
|
||||
} as RecoveryPoint;
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,11 @@ interface RecoveryRollupPlatformsLike {
|
|||
providers?: string[] | null;
|
||||
}
|
||||
|
||||
interface RecoveryItemResourceLike {
|
||||
itemResourceId?: string | null;
|
||||
subjectResourceId?: string | null;
|
||||
}
|
||||
|
||||
const normalizeRecoveryDisplay = (
|
||||
display: RecoveryPointDisplay | RecoveryPointDisplayTransport | null | undefined,
|
||||
): RecoveryPointDisplay | null | undefined => {
|
||||
|
|
@ -48,6 +53,10 @@ const normalizeRecoveryDisplay = (
|
|||
};
|
||||
};
|
||||
|
||||
const getRecoveryItemResourceId = (
|
||||
value: RecoveryItemResourceLike | null | undefined,
|
||||
): string => toTrimmedString(value?.itemResourceId) || toTrimmedString(value?.subjectResourceId);
|
||||
|
||||
export const getRecoveryPointPlatform = (
|
||||
point: RecoveryPointPlatformLike | null | undefined,
|
||||
): string => toTrimmedString(point?.platform) || toTrimmedString(point?.provider);
|
||||
|
|
@ -73,12 +82,19 @@ const normalizeRecoveryMeta = (meta: RecoveryResponseMeta | null | undefined): R
|
|||
export const normalizeRecoveryPoint = (
|
||||
point: RecoveryPointTransport | RecoveryPoint,
|
||||
): RecoveryPoint => {
|
||||
const { provider: _provider, display, ...rest } = point as RecoveryPointTransport;
|
||||
const {
|
||||
provider: _provider,
|
||||
subjectResourceId: _subjectResourceId,
|
||||
display,
|
||||
...rest
|
||||
} = point as RecoveryPointTransport;
|
||||
const platform = getRecoveryPointPlatform(point);
|
||||
const itemResourceId = getRecoveryItemResourceId(point);
|
||||
const normalizedDisplay = normalizeRecoveryDisplay(display);
|
||||
return {
|
||||
...(rest as RecoveryPoint),
|
||||
...(platform ? { platform } : {}),
|
||||
...(itemResourceId ? { itemResourceId } : {}),
|
||||
...(display !== undefined ? { display: normalizedDisplay } : {}),
|
||||
};
|
||||
};
|
||||
|
|
@ -86,12 +102,19 @@ export const normalizeRecoveryPoint = (
|
|||
export const normalizeRecoveryRollup = (
|
||||
rollup: ProtectionRollupTransport | ProtectionRollup,
|
||||
): ProtectionRollup => {
|
||||
const { providers: _providers, display, ...rest } = rollup as ProtectionRollupTransport;
|
||||
const {
|
||||
providers: _providers,
|
||||
subjectResourceId: _subjectResourceId,
|
||||
display,
|
||||
...rest
|
||||
} = rollup as ProtectionRollupTransport;
|
||||
const platforms = getRecoveryRollupPlatforms(rollup);
|
||||
const itemResourceId = getRecoveryItemResourceId(rollup);
|
||||
const normalizedDisplay = normalizeRecoveryDisplay(display);
|
||||
return {
|
||||
...(rest as ProtectionRollup),
|
||||
...(platforms.length > 0 ? { platforms } : {}),
|
||||
...(itemResourceId ? { itemResourceId } : {}),
|
||||
...(display !== undefined ? { display: normalizedDisplay } : {}),
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export function getRecoveryRollupItemLabel(
|
|||
rollup: ProtectionRollup,
|
||||
resourcesById: Map<string, Resource>,
|
||||
): string {
|
||||
const itemResourceId = (rollup.subjectResourceId || '').trim();
|
||||
const itemResourceId = (rollup.itemResourceId || '').trim();
|
||||
const displayLabel = String(rollup.display?.itemLabel || '').trim();
|
||||
const linkedResourceLabel = getRecoveryLinkedResourceLabel(itemResourceId, resourcesById);
|
||||
if (linkedResourceLabel) return linkedResourceLabel;
|
||||
|
|
@ -47,7 +47,7 @@ export function getRecoveryPointItemLabel(
|
|||
point: RecoveryPoint,
|
||||
resourcesById: Map<string, Resource>,
|
||||
): string {
|
||||
const itemResourceId = (point.subjectResourceId || '').trim();
|
||||
const itemResourceId = (point.itemResourceId || '').trim();
|
||||
const displayLabel = String(point.display?.itemLabel || '').trim();
|
||||
const linkedResourceLabel = getRecoveryLinkedResourceLabel(itemResourceId, resourcesById);
|
||||
if (linkedResourceLabel) return linkedResourceLabel;
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ type recoveryPointPayload struct {
|
|||
Encrypted *bool `json:"encrypted,omitempty"`
|
||||
Immutable *bool `json:"immutable,omitempty"`
|
||||
|
||||
ItemResourceID string `json:"itemResourceId,omitempty"`
|
||||
SubjectResourceID string `json:"subjectResourceId,omitempty"`
|
||||
RepositoryResourceID string `json:"repositoryResourceId,omitempty"`
|
||||
SubjectRef *recovery.ExternalRef `json:"subjectRef,omitempty"`
|
||||
|
|
@ -73,6 +74,7 @@ type recoveryPointPayload struct {
|
|||
|
||||
type recoveryRollupPayload struct {
|
||||
RollupID string `json:"rollupId"`
|
||||
ItemResourceID string `json:"itemResourceId,omitempty"`
|
||||
SubjectResourceID string `json:"subjectResourceId,omitempty"`
|
||||
SubjectRef *recovery.ExternalRef `json:"subjectRef,omitempty"`
|
||||
Display *recovery.RecoveryPointDisplay `json:"display,omitempty"`
|
||||
|
|
@ -97,6 +99,7 @@ func buildRecoveryPointPayload(point recovery.RecoveryPoint) recoveryPointPayloa
|
|||
Verified: point.Verified,
|
||||
Encrypted: point.Encrypted,
|
||||
Immutable: point.Immutable,
|
||||
ItemResourceID: point.SubjectResourceID,
|
||||
SubjectResourceID: point.SubjectResourceID,
|
||||
RepositoryResourceID: point.RepositoryResourceID,
|
||||
SubjectRef: point.SubjectRef,
|
||||
|
|
@ -109,6 +112,7 @@ func buildRecoveryPointPayload(point recovery.RecoveryPoint) recoveryPointPayloa
|
|||
func buildRecoveryRollupPayload(rollup recovery.ProtectionRollup) recoveryRollupPayload {
|
||||
return recoveryRollupPayload{
|
||||
RollupID: rollup.RollupID,
|
||||
ItemResourceID: rollup.SubjectResourceID,
|
||||
SubjectResourceID: rollup.SubjectResourceID,
|
||||
SubjectRef: rollup.SubjectRef,
|
||||
Display: rollup.Display,
|
||||
|
|
@ -160,6 +164,13 @@ func parseRecoveryPlatformQuery(qs url.Values) recovery.Provider {
|
|||
)))
|
||||
}
|
||||
|
||||
func parseRecoveryItemResourceIDQuery(qs url.Values) string {
|
||||
return strings.TrimSpace(firstNonEmpty(
|
||||
qs.Get("itemResourceId"),
|
||||
qs.Get("subjectResourceId"),
|
||||
))
|
||||
}
|
||||
|
||||
func (h *RecoveryHandlers) HandleListPoints(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
|
|
@ -190,7 +201,7 @@ func (h *RecoveryHandlers) HandleListPoints(w http.ResponseWriter, r *http.Reque
|
|||
Mode: recovery.Mode(strings.TrimSpace(qs.Get("mode"))),
|
||||
Outcome: recovery.Outcome(strings.TrimSpace(qs.Get("outcome"))),
|
||||
ItemType: recovery.NormalizeRecoveryItemType(firstNonEmpty(qs.Get("itemType"), qs.Get("type"))),
|
||||
SubjectResourceID: strings.TrimSpace(qs.Get("subjectResourceId")),
|
||||
SubjectResourceID: parseRecoveryItemResourceIDQuery(qs),
|
||||
RollupID: strings.TrimSpace(qs.Get("rollupId")),
|
||||
From: from,
|
||||
To: to,
|
||||
|
|
@ -307,7 +318,7 @@ func (h *RecoveryHandlers) HandleListSeries(w http.ResponseWriter, r *http.Reque
|
|||
Mode: recovery.Mode(strings.TrimSpace(qs.Get("mode"))),
|
||||
Outcome: recovery.Outcome(strings.TrimSpace(qs.Get("outcome"))),
|
||||
ItemType: recovery.NormalizeRecoveryItemType(firstNonEmpty(qs.Get("itemType"), qs.Get("type"))),
|
||||
SubjectResourceID: strings.TrimSpace(qs.Get("subjectResourceId")),
|
||||
SubjectResourceID: parseRecoveryItemResourceIDQuery(qs),
|
||||
RollupID: strings.TrimSpace(qs.Get("rollupId")),
|
||||
From: from,
|
||||
To: to,
|
||||
|
|
@ -373,7 +384,7 @@ func (h *RecoveryHandlers) HandleListFacets(w http.ResponseWriter, r *http.Reque
|
|||
Mode: recovery.Mode(strings.TrimSpace(qs.Get("mode"))),
|
||||
Outcome: recovery.Outcome(strings.TrimSpace(qs.Get("outcome"))),
|
||||
ItemType: recovery.NormalizeRecoveryItemType(firstNonEmpty(qs.Get("itemType"), qs.Get("type"))),
|
||||
SubjectResourceID: strings.TrimSpace(qs.Get("subjectResourceId")),
|
||||
SubjectResourceID: parseRecoveryItemResourceIDQuery(qs),
|
||||
RollupID: strings.TrimSpace(qs.Get("rollupId")),
|
||||
From: from,
|
||||
To: to,
|
||||
|
|
@ -635,7 +646,7 @@ func (h *RecoveryHandlers) HandleListRollups(w http.ResponseWriter, r *http.Requ
|
|||
Mode: recovery.Mode(strings.TrimSpace(qs.Get("mode"))),
|
||||
Outcome: recovery.Outcome(strings.TrimSpace(qs.Get("outcome"))),
|
||||
ItemType: recovery.NormalizeRecoveryItemType(firstNonEmpty(qs.Get("itemType"), qs.Get("type"))),
|
||||
SubjectResourceID: strings.TrimSpace(qs.Get("subjectResourceId")),
|
||||
SubjectResourceID: parseRecoveryItemResourceIDQuery(qs),
|
||||
RollupID: strings.TrimSpace(qs.Get("rollupId")),
|
||||
From: from,
|
||||
To: to,
|
||||
|
|
|
|||
|
|
@ -52,6 +52,47 @@ func TestParseRecoveryPlatformQuery(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestParseRecoveryItemResourceIDQuery(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
qs url.Values
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "prefers canonical item resource query",
|
||||
qs: url.Values{
|
||||
"itemResourceId": []string{" vm-123 "},
|
||||
"subjectResourceId": []string{"legacy-vm"},
|
||||
},
|
||||
want: "vm-123",
|
||||
},
|
||||
{
|
||||
name: "falls back to legacy subject resource query",
|
||||
qs: url.Values{
|
||||
"subjectResourceId": []string{" vm-404 "},
|
||||
},
|
||||
want: "vm-404",
|
||||
},
|
||||
{
|
||||
name: "returns empty when neither is present",
|
||||
qs: url.Values{},
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := parseRecoveryItemResourceIDQuery(tc.qs); got != tc.want {
|
||||
t.Fatalf("parseRecoveryItemResourceIDQuery() = %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleListPointsAcceptsCanonicalPlatformQuery(t *testing.T) {
|
||||
prevMock := mock.IsMockEnabled()
|
||||
mock.SetEnabled(true)
|
||||
|
|
@ -90,6 +131,24 @@ func TestHandleListPointsAcceptsCanonicalPlatformQuery(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestBuildRecoveryPointPayloadExposesCanonicalItemResourceIDField(t *testing.T) {
|
||||
payload := buildRecoveryPointPayload(recovery.RecoveryPoint{
|
||||
ID: "point-1",
|
||||
Provider: recovery.Provider("truenas"),
|
||||
Kind: recovery.Kind("snapshot"),
|
||||
Mode: recovery.Mode("snapshot"),
|
||||
Outcome: recovery.Outcome("success"),
|
||||
SubjectResourceID: "vm-123",
|
||||
})
|
||||
|
||||
if payload.ItemResourceID != "vm-123" {
|
||||
t.Fatalf("payload.ItemResourceID = %q, want %q", payload.ItemResourceID, "vm-123")
|
||||
}
|
||||
if payload.SubjectResourceID != "vm-123" {
|
||||
t.Fatalf("payload.SubjectResourceID = %q, want %q", payload.SubjectResourceID, "vm-123")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleListRollupsExposeCanonicalPlatformsPayload(t *testing.T) {
|
||||
prevMock := mock.IsMockEnabled()
|
||||
mock.SetEnabled(true)
|
||||
|
|
@ -127,3 +186,18 @@ func TestHandleListRollupsExposeCanonicalPlatformsPayload(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRecoveryRollupPayloadExposesCanonicalItemResourceIDField(t *testing.T) {
|
||||
payload := buildRecoveryRollupPayload(recovery.ProtectionRollup{
|
||||
RollupID: "res:vm-123",
|
||||
SubjectResourceID: "vm-123",
|
||||
LastOutcome: recovery.Outcome("success"),
|
||||
})
|
||||
|
||||
if payload.ItemResourceID != "vm-123" {
|
||||
t.Fatalf("payload.ItemResourceID = %q, want %q", payload.ItemResourceID, "vm-123")
|
||||
}
|
||||
if payload.SubjectResourceID != "vm-123" {
|
||||
t.Fatalf("payload.SubjectResourceID = %q, want %q", payload.SubjectResourceID, "vm-123")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue