feat: use Kubernetes revision-based rollback for instant app rollback
- Add revisionHistoryLimit: 10 and change-cause annotation to K8s deployments - Add getDeploymentRevisions() to list ReplicaSet revision history - Add rollbackDeploymentRevision() using K8s API (instant, no rebuild) - Refactor snapshot rollback: use K8s revision for app, keep file-based DB/wp-content restore - Add GET /snapshots/applications/:appId/revisions endpoint - Add POST /snapshots/applications/:appId/revisions/:rev/rollback endpoint - Add K8sRevision and K8sRevisionData types to frontend - Redesign UI with two tabs: K8s Revisions (instant) + File Snapshots (full backup) - K8s Revisions tab shows deployment history with one-click instant rollback - File Snapshots tab retains download, DB restore, and wp-content restore
This commit is contained in:
@@ -287,7 +287,8 @@ export class SnapshotsService {
|
||||
|
||||
/**
|
||||
* Rollback an application to a specific snapshot.
|
||||
* Restores: source code, wp-content (WordPress), and database.
|
||||
* Uses K8s revision rollback for the app deployment (instant, no rebuild).
|
||||
* File-based restore for DB dump and wp-content.
|
||||
*/
|
||||
async rollbackToSnapshot(snapshotId: string, userId: string): Promise<{ success: boolean; details: string[] }> {
|
||||
const snapshot = await this.findOne(snapshotId, userId);
|
||||
@@ -299,20 +300,45 @@ export class SnapshotsService {
|
||||
const app = await this.applicationsService.findOne(snapshot.applicationId, userId);
|
||||
const details: string[] = [];
|
||||
|
||||
// 1. Restore source code
|
||||
if (snapshot.appArchivePath && fs.existsSync(snapshot.appArchivePath)) {
|
||||
const uploadDir = this.configService.get<string>('platform.uploadDir') || './uploads';
|
||||
const appDir = path.join(uploadDir, app.userId, app.id);
|
||||
const destPath = path.join(appDir, 'source.zip');
|
||||
fs.mkdirSync(appDir, { recursive: true });
|
||||
fs.copyFileSync(snapshot.appArchivePath, destPath);
|
||||
// Update app codePath in DB
|
||||
await this.applicationsService.update(app.id, app.userId, { codePath: destPath } as any);
|
||||
details.push('✅ Source code restored');
|
||||
this.logger.log(`Rollback ${snapshotId}: restored source code`);
|
||||
// 1. Rollback K8s Deployment via revision (instant — no rebuild)
|
||||
// Find the revision that matches the snapshot's imageTag
|
||||
if (snapshot.imageTag) {
|
||||
try {
|
||||
const { revisions } = await this.kubernetesService.getDeploymentRevisions(app);
|
||||
const targetRevision = revisions.find((r) => r.image === snapshot.imageTag);
|
||||
|
||||
if (targetRevision && !targetRevision.isCurrent) {
|
||||
const result = await this.kubernetesService.rollbackDeploymentRevision(app, targetRevision.revision);
|
||||
if (result.success) {
|
||||
details.push(`✅ App rolled back to K8s revision ${targetRevision.revision} (instant)`);
|
||||
this.logger.log(`Rollback ${snapshotId}: K8s revision rollback to ${targetRevision.revision}`);
|
||||
} else {
|
||||
details.push(`⚠️ K8s revision rollback failed: ${result.message}`);
|
||||
}
|
||||
} else if (targetRevision?.isCurrent) {
|
||||
details.push('ℹ️ App is already running the snapshot\'s image — no deployment change needed');
|
||||
} else {
|
||||
// Revision not found — fall back to restoring source code and redeploying
|
||||
if (snapshot.appArchivePath && fs.existsSync(snapshot.appArchivePath)) {
|
||||
const uploadDir = this.configService.get<string>('platform.uploadDir') || './uploads';
|
||||
const appDir = path.join(uploadDir, app.userId, app.id);
|
||||
const destPath = path.join(appDir, 'source.zip');
|
||||
fs.mkdirSync(appDir, { recursive: true });
|
||||
fs.copyFileSync(snapshot.appArchivePath, destPath);
|
||||
await this.applicationsService.update(app.id, app.userId, { codePath: destPath } as any);
|
||||
details.push('✅ Source code restored (K8s revision expired — will need redeploy)');
|
||||
this.logger.log(`Rollback ${snapshotId}: source code restored (revision not found)`);
|
||||
} else {
|
||||
details.push('⚠️ K8s revision not found and no source archive available');
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.logger.warn(`Rollback ${snapshotId}: K8s revision rollback error — ${e.message}`);
|
||||
details.push(`⚠️ K8s rollback failed: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Restore wp-content (WordPress)
|
||||
// 2. Restore wp-content (WordPress) — file-based
|
||||
if (snapshot.wpContentArchivePath && fs.existsSync(snapshot.wpContentArchivePath)) {
|
||||
const archiveBuffer = fs.readFileSync(snapshot.wpContentArchivePath);
|
||||
const result = await this.kubernetesService.restoreWpContent(app, archiveBuffer);
|
||||
@@ -325,7 +351,7 @@ export class SnapshotsService {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Restore database
|
||||
// 3. Restore database — file-based
|
||||
if (snapshot.dbDumpPath && fs.existsSync(snapshot.dbDumpPath)) {
|
||||
const dumpBuffer = fs.readFileSync(snapshot.dbDumpPath);
|
||||
const result = await this.kubernetesService.restoreDatabaseDump(app, dumpBuffer);
|
||||
@@ -345,6 +371,27 @@ export class SnapshotsService {
|
||||
return { success: true, details };
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback using K8s revision directly (without a snapshot).
|
||||
* Fastest rollback — just switches the active ReplicaSet.
|
||||
*/
|
||||
async rollbackToRevision(
|
||||
applicationId: string,
|
||||
userId: string,
|
||||
targetRevision: number,
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
const app = await this.applicationsService.findOne(applicationId, userId);
|
||||
return this.kubernetesService.rollbackDeploymentRevision(app, targetRevision);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get K8s deployment revisions for an application.
|
||||
*/
|
||||
async getRevisions(applicationId: string, userId: string) {
|
||||
const app = await this.applicationsService.findOne(applicationId, userId);
|
||||
return this.kubernetesService.getDeploymentRevisions(app);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a specific snapshot.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user