fix: honor backup and cluster config updates

This commit is contained in:
2026-05-08 16:24:45 +00:00
parent 23a56a61e5
commit aa420d47bc
4 changed files with 1028 additions and 1176 deletions
+18 -18
View File
@@ -16,7 +16,7 @@
"buildDocs": "tsdoc"
},
"bin": {
"coreflow": "dist/cli.js"
"coreflow": "./cli.js"
},
"repository": {
"type": "git",
@@ -56,36 +56,36 @@
"homepage": "https://code.foss.global/serve.zone/coreflow#readme",
"devDependencies": {
"@git.zone/tsbuild": "^4.4.0",
"@git.zone/tsdocker": "^2.2.4",
"@git.zone/tsrun": "^2.0.2",
"@git.zone/tsdocker": "^2.2.5",
"@git.zone/tsrun": "^2.0.3",
"@git.zone/tstest": "^3.6.3",
"@git.zone/tswatch": "^3.3.2"
"@git.zone/tswatch": "^3.3.3"
},
"dependencies": {
"@api.global/typedrequest": "^3.3.0",
"@api.global/typedsocket": "^4.1.2",
"@apiclient.xyz/docker": "^5.1.4",
"@push.rocks/early": "^4.0.4",
"@push.rocks/lik": "^6.4.0",
"@push.rocks/lik": "^6.4.1",
"@push.rocks/projectinfo": "^5.1.0",
"@push.rocks/qenv": "^6.1.3",
"@push.rocks/smartcli": "^4.0.20",
"@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/qenv": "^6.1.4",
"@push.rocks/smartcli": "^4.0.21",
"@push.rocks/smartdelay": "^3.1.0",
"@push.rocks/smartlog": "^3.2.2",
"@push.rocks/smartnetwork": "4.7.0",
"@push.rocks/smartnetwork": "4.7.1",
"@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartrequest": "^5.0.1",
"@push.rocks/smartpromise": "^4.2.4",
"@push.rocks/smartrequest": "^5.0.3",
"@push.rocks/smartrx": "^3.0.10",
"@push.rocks/smartserve": "^2.0.3",
"@push.rocks/smartstate": "^2.3.0",
"@push.rocks/smartstream": "^3.4.0",
"@push.rocks/smartstring": "^4.1.0",
"@push.rocks/smartserve": "^2.0.4",
"@push.rocks/smartstate": "^2.3.1",
"@push.rocks/smartstream": "^3.4.2",
"@push.rocks/smartstring": "^4.1.1",
"@push.rocks/taskbuffer": "^8.0.2",
"@serve.zone/api": "^5.3.4",
"@serve.zone/interfaces": "^5.4.6",
"@tsclass/tsclass": "^9.5.0",
"@types/node": "25.6.0"
"@serve.zone/interfaces": "^5.5.0",
"@tsclass/tsclass": "^9.5.1",
"@types/node": "25.6.1"
},
"private": true,
"files": [
+889 -1155
View File
File diff suppressed because it is too large Load Diff
+120 -2
View File
@@ -19,6 +19,30 @@ type TBackupSnapshot = {
bucketName?: string;
};
type TBackupArchiveObject = {
path: string;
size: number;
sha256: string;
};
type TBackupArchiveManifest = {
version: 1;
backupId: string;
createdAt: number;
objects: TBackupArchiveObject[];
totalSize: number;
};
type TBackupReplicationResult = {
targetType: 's3' | 'smb';
targetPath: string;
manifestPath: string;
manifestSha256: string;
objectCount: number;
totalSize: number;
completedAt: number;
};
type TServiceVolumeConfig = {
name?: string;
source?: string;
@@ -56,14 +80,20 @@ export class CoreflowBackupManager {
backupId: string;
service: plugins.servezoneInterfaces.data.IService;
tags?: Record<string, string>;
replication?: {
enabled: boolean;
};
}) {
await this.provisionCorestoreBindingsIfAvailable(requestArg.service);
const snapshots: TBackupSnapshot[] = [];
snapshots.push(...await this.snapshotServiceVolumes(requestArg));
snapshots.push(...await this.snapshotServiceResources(requestArg));
const replication = requestArg.replication?.enabled
? await this.replicateBackupArchive(requestArg.backupId)
: undefined;
return { snapshots };
return { snapshots, replication };
}
public async executeServiceRestore(requestArg: {
@@ -72,8 +102,14 @@ export class CoreflowBackupManager {
snapshots: TBackupSnapshot[];
clear?: boolean;
resourceTypes?: TBackupSnapshotType[];
replication?: {
enabled: boolean;
};
}) {
await this.provisionCorestoreBindingsIfAvailable(requestArg.service);
if (requestArg.replication?.enabled) {
await this.ensureBackupArchiveAvailable(requestArg.backupId);
}
const selectedSnapshots = (requestArg.snapshots || []).filter((snapshotArg) => {
return !requestArg.resourceTypes?.length || requestArg.resourceTypes.includes(snapshotArg.type);
@@ -153,6 +189,88 @@ export class CoreflowBackupManager {
return responseText ? JSON.parse(responseText) as T : ({} as T);
}
private async fireCloudlyBackupRequest<T = any>(methodArg: string, payloadArg: Record<string, unknown>) {
const connector = this.coreflowRef.cloudlyConnector as any;
if (!connector.cloudlyApiClient || !connector.identity) {
throw new Error('Cloudly connection is required for backup replication');
}
const request = connector.cloudlyApiClient.typedsocketClient.createTypedRequest(methodArg);
return await request.fire({
identity: connector.identity,
...payloadArg,
}) as T;
}
private async getCorestoreArchiveManifest(backupIdArg: string) {
const response = await this.postCorestore<{ manifest: TBackupArchiveManifest }>('/archive/manifest', {
backupId: backupIdArg,
});
return response.manifest;
}
private async replicateBackupArchive(backupIdArg: string): Promise<TBackupReplicationResult> {
const manifest = await this.getCorestoreArchiveManifest(backupIdArg);
const prepareResult = await this.fireCloudlyBackupRequest<{ missingObjects: TBackupArchiveObject[] }>(
'prepareBackupReplication',
{
backupId: backupIdArg,
manifest,
},
);
for (const object of prepareResult.missingObjects || []) {
const readResult = await this.postCorestore<{
object: TBackupArchiveObject;
contentsBase64: string;
}>('/archive/object/read', { path: object.path });
if (readResult.object.sha256 !== object.sha256 || readResult.object.size !== object.size) {
throw new Error(`Corestore archive object changed during replication: ${object.path}`);
}
await this.fireCloudlyBackupRequest('uploadBackupArchiveObject', {
backupId: backupIdArg,
object,
contentsBase64: readResult.contentsBase64,
});
}
const completeResult = await this.fireCloudlyBackupRequest<{ replication: TBackupReplicationResult }>(
'completeBackupReplication',
{
backupId: backupIdArg,
manifest,
},
);
return completeResult.replication;
}
private async ensureBackupArchiveAvailable(backupIdArg: string) {
const manifestResult = await this.fireCloudlyBackupRequest<{ manifest: TBackupArchiveManifest }>(
'getBackupArchiveManifest',
{ backupId: backupIdArg },
);
const remoteManifest = manifestResult.manifest;
const localManifest = await this.getCorestoreArchiveManifest(backupIdArg);
const localObjectMap = new Map(localManifest.objects.map((objectArg) => [objectArg.path, objectArg]));
for (const remoteObject of remoteManifest.objects || []) {
const localObject = localObjectMap.get(remoteObject.path);
if (localObject?.sha256 === remoteObject.sha256 && localObject.size === remoteObject.size) {
continue;
}
const downloadResult = await this.fireCloudlyBackupRequest<{
object: TBackupArchiveObject;
contentsBase64: string;
}>('downloadBackupArchiveObject', {
backupId: backupIdArg,
object: remoteObject,
});
await this.postCorestore('/archive/object/write', {
...downloadResult.object,
contentsBase64: downloadResult.contentsBase64,
});
}
}
private getDockerSafeName(valueArg: string, maxLengthArg = 64) {
const safeName = valueArg
.replace(/[^a-zA-Z0-9_.-]+/g, '-')
@@ -163,7 +281,7 @@ export class CoreflowBackupManager {
}
private getServiceVolumeConfigs(serviceArg: plugins.servezoneInterfaces.data.IService) {
const serviceData = serviceArg.data as plugins.servezoneInterfaces.data.IService['data'] & {
const serviceData = serviceArg.data as Omit<plugins.servezoneInterfaces.data.IService['data'], 'volumes'> & {
volumes?: TServiceVolumeConfig[];
};
return (serviceData.volumes || []).filter((volumeArg) => {
+1 -1
View File
@@ -142,7 +142,7 @@ export class ClusterManager {
}
private getServiceVolumeConfigs(serviceArgFromCloudly: plugins.servezoneInterfaces.data.IService) {
const serviceData = serviceArgFromCloudly.data as plugins.servezoneInterfaces.data.IService['data'] & {
const serviceData = serviceArgFromCloudly.data as Omit<plugins.servezoneInterfaces.data.IService['data'], 'volumes'> & {
volumes?: TServiceVolumeConfig[];
};
return (serviceData.volumes || []).filter((volumeArg) => {