fix: honor backup and cluster config updates
This commit is contained in:
+18
-18
@@ -16,7 +16,7 @@
|
|||||||
"buildDocs": "tsdoc"
|
"buildDocs": "tsdoc"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"coreflow": "dist/cli.js"
|
"coreflow": "./cli.js"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -56,36 +56,36 @@
|
|||||||
"homepage": "https://code.foss.global/serve.zone/coreflow#readme",
|
"homepage": "https://code.foss.global/serve.zone/coreflow#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^4.4.0",
|
"@git.zone/tsbuild": "^4.4.0",
|
||||||
"@git.zone/tsdocker": "^2.2.4",
|
"@git.zone/tsdocker": "^2.2.5",
|
||||||
"@git.zone/tsrun": "^2.0.2",
|
"@git.zone/tsrun": "^2.0.3",
|
||||||
"@git.zone/tstest": "^3.6.3",
|
"@git.zone/tstest": "^3.6.3",
|
||||||
"@git.zone/tswatch": "^3.3.2"
|
"@git.zone/tswatch": "^3.3.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@api.global/typedrequest": "^3.3.0",
|
"@api.global/typedrequest": "^3.3.0",
|
||||||
"@api.global/typedsocket": "^4.1.2",
|
"@api.global/typedsocket": "^4.1.2",
|
||||||
"@apiclient.xyz/docker": "^5.1.4",
|
"@apiclient.xyz/docker": "^5.1.4",
|
||||||
"@push.rocks/early": "^4.0.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/projectinfo": "^5.1.0",
|
||||||
"@push.rocks/qenv": "^6.1.3",
|
"@push.rocks/qenv": "^6.1.4",
|
||||||
"@push.rocks/smartcli": "^4.0.20",
|
"@push.rocks/smartcli": "^4.0.21",
|
||||||
"@push.rocks/smartdelay": "^3.0.5",
|
"@push.rocks/smartdelay": "^3.1.0",
|
||||||
"@push.rocks/smartlog": "^3.2.2",
|
"@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/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartpromise": "^4.2.3",
|
"@push.rocks/smartpromise": "^4.2.4",
|
||||||
"@push.rocks/smartrequest": "^5.0.1",
|
"@push.rocks/smartrequest": "^5.0.3",
|
||||||
"@push.rocks/smartrx": "^3.0.10",
|
"@push.rocks/smartrx": "^3.0.10",
|
||||||
"@push.rocks/smartserve": "^2.0.3",
|
"@push.rocks/smartserve": "^2.0.4",
|
||||||
"@push.rocks/smartstate": "^2.3.0",
|
"@push.rocks/smartstate": "^2.3.1",
|
||||||
"@push.rocks/smartstream": "^3.4.0",
|
"@push.rocks/smartstream": "^3.4.2",
|
||||||
"@push.rocks/smartstring": "^4.1.0",
|
"@push.rocks/smartstring": "^4.1.1",
|
||||||
"@push.rocks/taskbuffer": "^8.0.2",
|
"@push.rocks/taskbuffer": "^8.0.2",
|
||||||
"@serve.zone/api": "^5.3.4",
|
"@serve.zone/api": "^5.3.4",
|
||||||
"@serve.zone/interfaces": "^5.4.6",
|
"@serve.zone/interfaces": "^5.5.0",
|
||||||
"@tsclass/tsclass": "^9.5.0",
|
"@tsclass/tsclass": "^9.5.1",
|
||||||
"@types/node": "25.6.0"
|
"@types/node": "25.6.1"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
Generated
+889
-1155
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,30 @@ type TBackupSnapshot = {
|
|||||||
bucketName?: string;
|
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 = {
|
type TServiceVolumeConfig = {
|
||||||
name?: string;
|
name?: string;
|
||||||
source?: string;
|
source?: string;
|
||||||
@@ -56,14 +80,20 @@ export class CoreflowBackupManager {
|
|||||||
backupId: string;
|
backupId: string;
|
||||||
service: plugins.servezoneInterfaces.data.IService;
|
service: plugins.servezoneInterfaces.data.IService;
|
||||||
tags?: Record<string, string>;
|
tags?: Record<string, string>;
|
||||||
|
replication?: {
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
}) {
|
}) {
|
||||||
await this.provisionCorestoreBindingsIfAvailable(requestArg.service);
|
await this.provisionCorestoreBindingsIfAvailable(requestArg.service);
|
||||||
|
|
||||||
const snapshots: TBackupSnapshot[] = [];
|
const snapshots: TBackupSnapshot[] = [];
|
||||||
snapshots.push(...await this.snapshotServiceVolumes(requestArg));
|
snapshots.push(...await this.snapshotServiceVolumes(requestArg));
|
||||||
snapshots.push(...await this.snapshotServiceResources(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: {
|
public async executeServiceRestore(requestArg: {
|
||||||
@@ -72,8 +102,14 @@ export class CoreflowBackupManager {
|
|||||||
snapshots: TBackupSnapshot[];
|
snapshots: TBackupSnapshot[];
|
||||||
clear?: boolean;
|
clear?: boolean;
|
||||||
resourceTypes?: TBackupSnapshotType[];
|
resourceTypes?: TBackupSnapshotType[];
|
||||||
|
replication?: {
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
}) {
|
}) {
|
||||||
await this.provisionCorestoreBindingsIfAvailable(requestArg.service);
|
await this.provisionCorestoreBindingsIfAvailable(requestArg.service);
|
||||||
|
if (requestArg.replication?.enabled) {
|
||||||
|
await this.ensureBackupArchiveAvailable(requestArg.backupId);
|
||||||
|
}
|
||||||
|
|
||||||
const selectedSnapshots = (requestArg.snapshots || []).filter((snapshotArg) => {
|
const selectedSnapshots = (requestArg.snapshots || []).filter((snapshotArg) => {
|
||||||
return !requestArg.resourceTypes?.length || requestArg.resourceTypes.includes(snapshotArg.type);
|
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);
|
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) {
|
private getDockerSafeName(valueArg: string, maxLengthArg = 64) {
|
||||||
const safeName = valueArg
|
const safeName = valueArg
|
||||||
.replace(/[^a-zA-Z0-9_.-]+/g, '-')
|
.replace(/[^a-zA-Z0-9_.-]+/g, '-')
|
||||||
@@ -163,7 +281,7 @@ export class CoreflowBackupManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getServiceVolumeConfigs(serviceArg: plugins.servezoneInterfaces.data.IService) {
|
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[];
|
volumes?: TServiceVolumeConfig[];
|
||||||
};
|
};
|
||||||
return (serviceData.volumes || []).filter((volumeArg) => {
|
return (serviceData.volumes || []).filter((volumeArg) => {
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export class ClusterManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getServiceVolumeConfigs(serviceArgFromCloudly: plugins.servezoneInterfaces.data.IService) {
|
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[];
|
volumes?: TServiceVolumeConfig[];
|
||||||
};
|
};
|
||||||
return (serviceData.volumes || []).filter((volumeArg) => {
|
return (serviceData.volumes || []).filter((volumeArg) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user