278 lines
11 KiB
TypeScript
278 lines
11 KiB
TypeScript
import type {
|
|
PreflightPreview,
|
|
EnrollmentStorageRootSelection,
|
|
ManagedStorageStatus,
|
|
TrustedAppdataRoot,
|
|
TrustedAppdataRootReadinessStatus,
|
|
} from '@/types/appops';
|
|
|
|
export const LEGACY_APPDATA_ROOT: TrustedAppdataRoot = '/srv/example/appdata';
|
|
export const MANAGED_APPDATA_ROOT: TrustedAppdataRoot = '/var/lib/docker/volumes/itworx-managed-appdata/_data';
|
|
export const MANAGED_STORAGE_INITIALIZE_ACTION = 'controller.managed-storage.initialize';
|
|
export const MANAGED_STORAGE_TARGET = 'tower/deployment-controller';
|
|
|
|
export interface EnrollmentStorageView {
|
|
configured: boolean;
|
|
trustStatus: 'ready' | 'blocked' | 'needs_setup' | 'unknown';
|
|
currentRoot: TrustedAppdataRoot | null;
|
|
backupAcknowledged: boolean;
|
|
rootReadiness: Record<TrustedAppdataRoot, TrustedAppdataRootReadinessStatus>;
|
|
managedStatus: ManagedStorageStatus;
|
|
managedBlocker: string | null;
|
|
managedCapabilityValid: boolean;
|
|
}
|
|
|
|
const isRecord = (value: unknown): value is Record<string, unknown> => (
|
|
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
);
|
|
|
|
const isRoot = (value: unknown): value is TrustedAppdataRoot => (
|
|
value === LEGACY_APPDATA_ROOT || value === MANAGED_APPDATA_ROOT
|
|
);
|
|
|
|
const isRootReadiness = (value: unknown): value is TrustedAppdataRootReadinessStatus => (
|
|
value === 'ready' || value === 'needs_initialization' || value === 'unavailable'
|
|
);
|
|
|
|
const isManagedStatus = (value: unknown): value is ManagedStorageStatus => (
|
|
value === 'not_selected' || value === 'blocked' || value === 'ready' || value === 'initialization_required' || value === 'unavailable'
|
|
);
|
|
|
|
export const buildEnrollmentStorageView = (trust: unknown, capabilities: unknown): EnrollmentStorageView => {
|
|
const safeTrust = isRecord(trust) ? trust : {};
|
|
const safeCapabilities = isRecord(capabilities) ? capabilities : {};
|
|
const trustedRoot = isRoot(safeTrust.trustedAppdataRoot) ? safeTrust.trustedAppdataRoot : null;
|
|
const managed = isRecord(safeCapabilities.managedStorage) ? safeCapabilities.managedStorage : null;
|
|
const reportedRoots = safeCapabilities.availableTrustedAppdataRoots;
|
|
let rootReadiness: Record<TrustedAppdataRoot, TrustedAppdataRootReadinessStatus> = {
|
|
[LEGACY_APPDATA_ROOT]: 'unavailable',
|
|
[MANAGED_APPDATA_ROOT]: 'unavailable',
|
|
};
|
|
let rootsValid = false;
|
|
|
|
if (Array.isArray(reportedRoots) && reportedRoots.length === 2) {
|
|
const entries = new Map<TrustedAppdataRoot, TrustedAppdataRootReadinessStatus>();
|
|
const valid = reportedRoots.every((entry) => {
|
|
if (!isRecord(entry) || !isRoot(entry.root) || !isRootReadiness(entry.status) || entries.has(entry.root)) return false;
|
|
entries.set(entry.root, entry.status);
|
|
return true;
|
|
});
|
|
if (valid && entries.has(LEGACY_APPDATA_ROOT) && entries.has(MANAGED_APPDATA_ROOT)) {
|
|
rootsValid = true;
|
|
rootReadiness = {
|
|
[LEGACY_APPDATA_ROOT]: entries.get(LEGACY_APPDATA_ROOT)!,
|
|
[MANAGED_APPDATA_ROOT]: entries.get(MANAGED_APPDATA_ROOT)!,
|
|
};
|
|
}
|
|
}
|
|
|
|
const backupAcknowledged = safeTrust.managedAppdataBackupAcknowledged;
|
|
const trustSelectionConsistent = typeof backupAcknowledged === 'boolean' && (
|
|
trustedRoot === MANAGED_APPDATA_ROOT
|
|
? backupAcknowledged && managed?.root === MANAGED_APPDATA_ROOT && managed.backupAcknowledged === backupAcknowledged
|
|
: trustedRoot === LEGACY_APPDATA_ROOT && !backupAcknowledged
|
|
);
|
|
const managedStatusConsistent = Boolean(
|
|
managed && (
|
|
(managed.status !== 'ready' || rootReadiness[MANAGED_APPDATA_ROOT] === 'ready') &&
|
|
(managed.status !== 'initialization_required' || rootReadiness[MANAGED_APPDATA_ROOT] === 'needs_initialization') &&
|
|
(trustedRoot !== MANAGED_APPDATA_ROOT || managed.status !== 'not_selected') &&
|
|
(trustedRoot !== MANAGED_APPDATA_ROOT || managed.backupAcknowledged === backupAcknowledged)
|
|
)
|
|
);
|
|
const managedCapabilityValid = Boolean(
|
|
rootsValid &&
|
|
trustSelectionConsistent &&
|
|
managed &&
|
|
managed.root === MANAGED_APPDATA_ROOT &&
|
|
managed.backupScope === 'dockerRootDir' &&
|
|
typeof managed.backupAcknowledged === 'boolean' &&
|
|
isManagedStatus(managed.status) &&
|
|
managedStatusConsistent,
|
|
);
|
|
const rawTrustStatus = safeTrust.status === 'ready' || safeTrust.status === 'blocked' || safeTrust.status === 'needs_setup'
|
|
? safeTrust.status
|
|
: 'unknown';
|
|
const configured = safeTrust.configured === true && trustedRoot !== null && trustSelectionConsistent;
|
|
const trustStatus = rawTrustStatus === 'needs_setup' || rawTrustStatus === 'blocked'
|
|
? rawTrustStatus
|
|
: configured && rawTrustStatus === 'ready' ? 'ready' : 'unknown';
|
|
|
|
return {
|
|
configured,
|
|
trustStatus,
|
|
currentRoot: configured ? trustedRoot : null,
|
|
backupAcknowledged: backupAcknowledged === true,
|
|
rootReadiness,
|
|
managedStatus: managedCapabilityValid ? managed!.status as ManagedStorageStatus : 'unavailable',
|
|
managedBlocker: managedCapabilityValid && typeof managed!.blockerCode === 'string' && managed!.blockerCode.length <= 100
|
|
? managed!.blockerCode || null
|
|
: null,
|
|
managedCapabilityValid,
|
|
};
|
|
};
|
|
|
|
const isBoundedStringArray = (value: unknown): value is string[] => (
|
|
Array.isArray(value) && value.length <= 100 && value.every((item) => typeof item === 'string' && item.length <= 500)
|
|
);
|
|
|
|
export const normalizeManagedStoragePreview = (value: unknown): PreflightPreview | null => {
|
|
if (!isRecord(value)) return null;
|
|
const rollbackAvailable = value.rollbackAvailable;
|
|
if (
|
|
typeof value.action !== 'string' || value.action.length > 120 ||
|
|
typeof value.target !== 'string' || value.target.length > 200 ||
|
|
(value.risk !== 'low' && value.risk !== 'medium' && value.risk !== 'high' && value.risk !== 'critical') ||
|
|
(value.policyDecision !== 'allowed' && value.policyDecision !== 'approval_required' && value.policyDecision !== 'blocked') ||
|
|
typeof value.auditPreview !== 'string' || value.auditPreview.length > 2000 ||
|
|
!isBoundedStringArray(value.affectedApps) ||
|
|
!isBoundedStringArray(value.dependencies) ||
|
|
!isBoundedStringArray(value.preChecks) ||
|
|
!isBoundedStringArray(value.postChecks) ||
|
|
(rollbackAvailable !== undefined && typeof rollbackAvailable !== 'boolean')
|
|
) return null;
|
|
|
|
const normalizedRollbackAvailable: boolean | undefined = typeof rollbackAvailable === 'boolean'
|
|
? rollbackAvailable
|
|
: undefined;
|
|
|
|
return {
|
|
action: value.action,
|
|
target: value.target,
|
|
risk: value.risk,
|
|
policyDecision: value.policyDecision,
|
|
auditPreview: value.auditPreview,
|
|
affectedApps: value.affectedApps,
|
|
dependencies: value.dependencies,
|
|
preChecks: value.preChecks,
|
|
postChecks: value.postChecks,
|
|
...(normalizedRollbackAvailable === undefined ? {} : { rollbackAvailable: normalizedRollbackAvailable }),
|
|
};
|
|
};
|
|
|
|
export const buildStorageRootSelection = (
|
|
root: TrustedAppdataRoot,
|
|
backupAcknowledged: boolean,
|
|
): EnrollmentStorageRootSelection | null => {
|
|
if (root === MANAGED_APPDATA_ROOT && !backupAcknowledged) return null;
|
|
return {
|
|
trusted_appdata_root: root,
|
|
managed_appdata_backup_acknowledged: root === MANAGED_APPDATA_ROOT,
|
|
};
|
|
};
|
|
|
|
export const canInitializeManagedStorage = (
|
|
view: EnrollmentStorageView,
|
|
preview: unknown,
|
|
): boolean => (
|
|
view.configured &&
|
|
view.currentRoot === MANAGED_APPDATA_ROOT &&
|
|
view.backupAcknowledged &&
|
|
view.managedCapabilityValid &&
|
|
view.managedStatus === 'initialization_required' &&
|
|
isRecord(preview) &&
|
|
preview.action === MANAGED_STORAGE_INITIALIZE_ACTION &&
|
|
preview.target === MANAGED_STORAGE_TARGET &&
|
|
preview.risk === 'high' &&
|
|
preview.policyDecision === 'allowed'
|
|
);
|
|
|
|
export interface ManagedStorageInitializationGate {
|
|
canManage: boolean;
|
|
view: EnrollmentStorageView | null | undefined;
|
|
preview: unknown;
|
|
dirty: boolean;
|
|
loadError: string;
|
|
conflict: string;
|
|
refreshing: boolean;
|
|
saving: boolean;
|
|
initializing: boolean;
|
|
}
|
|
|
|
export const canRequestManagedStorageInitialization = (gate: ManagedStorageInitializationGate): boolean => (
|
|
gate.canManage &&
|
|
Boolean(gate.view && canInitializeManagedStorage(gate.view, gate.preview)) &&
|
|
!gate.dirty &&
|
|
!gate.loadError &&
|
|
!gate.conflict &&
|
|
!gate.refreshing &&
|
|
!gate.saving &&
|
|
!gate.initializing
|
|
);
|
|
|
|
export interface StorageRootSaveGate {
|
|
canManage: boolean;
|
|
configured: boolean;
|
|
payloadAvailable: boolean;
|
|
dirty: boolean;
|
|
loadError: string;
|
|
refreshing: boolean;
|
|
saving: boolean;
|
|
initializing: boolean;
|
|
}
|
|
|
|
export const canSaveStorageRootSelection = (gate: StorageRootSaveGate): boolean => (
|
|
gate.canManage &&
|
|
gate.configured &&
|
|
gate.payloadAvailable &&
|
|
gate.dirty &&
|
|
!gate.loadError &&
|
|
!gate.refreshing &&
|
|
!gate.saving &&
|
|
!gate.initializing
|
|
);
|
|
|
|
export interface ManagedStorageActionOutcome {
|
|
tone: 'status' | 'alert';
|
|
message: string;
|
|
}
|
|
|
|
const UNCONFIRMED_INITIALIZATION_MESSAGE = 'Het initialisatieverzoek is niet bevestigd met geldige referenties.';
|
|
const isBoundedIdentifier = (value: unknown): value is string => (
|
|
typeof value === 'string' && value.trim().length > 0 && value.length <= 200
|
|
);
|
|
|
|
export const normalizeManagedStorageActionOutcome = (value: unknown): ManagedStorageActionOutcome => {
|
|
if (!isRecord(value) || typeof value.message !== 'string' || value.message.trim().length === 0 || value.message.length > 500) {
|
|
return { tone: 'alert', message: UNCONFIRMED_INITIALIZATION_MESSAGE };
|
|
}
|
|
|
|
if (value.success === true && isBoundedIdentifier(value.jobId) && isBoundedIdentifier(value.auditId)) {
|
|
return {
|
|
tone: 'status',
|
|
message: `Initialisatieverzoek geregistreerd. Job ${value.jobId.trim()}; audit ${value.auditId.trim()}. De opslag wordt pas als gereed getoond nadat de controller dat bevestigt.`,
|
|
};
|
|
}
|
|
|
|
if (value.success === false) {
|
|
return { tone: 'alert', message: value.message.trim() };
|
|
}
|
|
|
|
return { tone: 'alert', message: UNCONFIRMED_INITIALIZATION_MESSAGE };
|
|
};
|
|
|
|
export const rootReadinessLabel = (status: TrustedAppdataRootReadinessStatus): string => {
|
|
if (status === 'ready') return 'Controller confirms ready';
|
|
if (status === 'needs_initialization') return 'Controller reports initialization needed';
|
|
return 'Not currently confirmed';
|
|
};
|
|
|
|
export const managedStorageStatusLabel = (status: ManagedStorageStatus): string => {
|
|
switch (status) {
|
|
case 'not_selected': return 'Not selected for future targets';
|
|
case 'blocked': return 'Blocked by a required safety condition';
|
|
case 'ready': return 'Controller reports ready';
|
|
case 'initialization_required': return 'Controller requires one-time initialization';
|
|
default: return 'Unavailable or not confirmed';
|
|
}
|
|
};
|
|
|
|
export const managedStorageBlockerLabel = (blocker: string | null): string | null => {
|
|
switch (blocker) {
|
|
case 'managed_appdata_backup_acknowledgement_required': return 'Confirm the Docker-root backup responsibility before continuing.';
|
|
case 'managed_appdata_initialization_required': return 'The controller reports that the managed storage volume needs initialization.';
|
|
case 'managed_appdata_volume_unavailable': return 'The controller cannot currently verify the managed storage volume.';
|
|
default: return blocker ? 'The controller reports an additional storage blocker.' : null;
|
|
}
|
|
};
|