feat: dynamic database storage size with PVC expansion

- Add dbStorageSize column to Application entity (default: 1Gi)
- Add dbStorageSize to CreateApplicationDto, frontend types
- Use dynamic storage size in K8s deployDatabase instead of hardcoded 5Gi
- Deploy page: storage size selector with +/- buttons (min 1GB, max 100GB)
- Auto-suggest storage based on DB dump file size (3x dump size, min 1GB)
- Show DB storage in Review step
- App detail: Database Storage section with expand button
- GET /applications/:id/db-storage — read current PVC size from K8s
- PATCH /applications/:id/db-storage — expand PVC (only increase, no shrink)
- PVC resize uses JSON patch on K8s API
This commit is contained in:
keyhan
2026-04-07 01:28:01 +03:30
parent e775b57421
commit ac489c88d8
8 changed files with 289 additions and 3 deletions
+69 -1
View File
@@ -26,6 +26,7 @@ interface ManifestContext {
dbUsername: string;
dbPassword: string;
dbVersion: string;
dbStorageSize: string;
}
@Injectable()
@@ -99,6 +100,7 @@ export class KubernetesService implements OnModuleInit {
dbUsername: app.dbUsername || 'appuser',
dbPassword: app.dbPassword || this.generatePassword(),
dbVersion: app.dbVersion || '',
dbStorageSize: app.dbStorageSize || '1Gi',
};
const manifests: Record<string, any> = {};
@@ -396,7 +398,7 @@ export class KubernetesService implements OnModuleInit {
await this.createDbSecret(coreApi, ctx.namespace, ctx.appName, ctx.dbPassword, ctx.dbUsername);
// Create PVC for DB
await this.createPVC(coreApi, ctx.namespace, dbName, '5Gi');
await this.createPVC(coreApi, ctx.namespace, dbName, ctx.dbStorageSize);
// Deploy database
const isPostgres = ctx.databaseType === DatabaseType.POSTGRESQL;
@@ -1025,6 +1027,72 @@ export class KubernetesService implements OnModuleInit {
return { success: true, logs };
}
/**
* Resize (expand) the database PVC for an application.
* K8s only supports PVC expansion, not shrinking.
*/
async resizeDatabasePvc(app: Application, newSize: string): Promise<{ success: boolean; message: string }> {
const { coreApi } = await this.getK8sClient(app.clusterId);
const namespace = `user-${app.userId.split('-')[0]}`;
const pvcName = `${app.name}-db`;
try {
// Read current PVC to check current size
const currentPvc = await coreApi.readNamespacedPersistentVolumeClaim(pvcName, namespace);
const currentSize = currentPvc.body.spec?.resources?.requests?.storage || '1Gi';
const currentGi = parseInt(currentSize.replace('Gi', ''), 10) || 1;
const newGi = parseInt(newSize.replace('Gi', ''), 10) || 1;
if (newGi <= currentGi) {
return { success: false, message: `New size (${newSize}) must be larger than current size (${currentSize})` };
}
// Patch PVC to expand
const patch = [
{
op: 'replace',
path: '/spec/resources/requests/storage',
value: newSize,
},
];
await coreApi.patchNamespacedPersistentVolumeClaim(
pvcName,
namespace,
patch,
undefined,
undefined,
undefined,
undefined,
undefined,
{ headers: { 'Content-Type': 'application/json-patch+json' } },
);
this.logger.log(`Resized PVC ${pvcName} from ${currentSize} to ${newSize}`);
return { success: true, message: `Database storage expanded from ${currentSize} to ${newSize}` };
} catch (e: any) {
this.logger.error(`Failed to resize PVC ${pvcName}: ${e.message}`);
return { success: false, message: e.body?.message || e.message || 'Failed to resize database storage' };
}
}
/**
* Get current PVC size for an application's database.
*/
async getDatabasePvcSize(app: Application): Promise<string> {
try {
const { coreApi } = await this.getK8sClient(app.clusterId);
const namespace = `user-${app.userId.split('-')[0]}`;
const pvcName = `${app.name}-db`;
const pvc = await coreApi.readNamespacedPersistentVolumeClaim(pvcName, namespace);
return pvc.body.spec?.resources?.requests?.storage || app.dbStorageSize || '1Gi';
} catch {
return app.dbStorageSize || '1Gi';
}
}
private generatePassword(length = 24): string {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%';
let password = '';