fix: honor backup and cluster config updates
This commit is contained in:
+18
-18
@@ -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": [
|
||||
|
||||
Generated
+889
-1155
File diff suppressed because it is too large
Load Diff
@@ -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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user