Add managed databases and services with billing-aligned upgrades.
Introduce product types for managed PostgreSQL, Redis, and RabbitMQ with a dedicated dashboard, Helm-only deploy pipeline, external access, snapshots with progress, and prorated resource or storage upgrades matching application billing rules. PVCs use an expandable StorageClass with automatic migration when legacy disks cannot resize in place. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -37,7 +37,17 @@ import {
|
||||
import { UpdatePricingCatalogDto } from './dto/pricing-catalog.dto';
|
||||
import { RolesGuard } from '../common/guards/roles.guard';
|
||||
import { Roles } from '../common/decorators/roles.decorator';
|
||||
import { UserRole, BillingCycle, AppLifecycleStatus, InvoiceReason, InvoiceStatus, PaymentMethod } from '../common/enums';
|
||||
import {
|
||||
UserRole,
|
||||
BillingCycle,
|
||||
AppLifecycleStatus,
|
||||
InvoiceReason,
|
||||
InvoiceStatus,
|
||||
PaymentMethod,
|
||||
ProductType,
|
||||
DatabaseType,
|
||||
} from '../common/enums';
|
||||
import { Application } from '../applications/entities/application.entity';
|
||||
|
||||
@ApiTags('Billing')
|
||||
@ApiBearerAuth()
|
||||
@@ -722,33 +732,15 @@ export class BillingController {
|
||||
paidInvoice = paid.invoice;
|
||||
}
|
||||
|
||||
// Apply the resource changes
|
||||
const updatedApp = await this.applicationsService.update(app.id, app.userId, {
|
||||
cpuRequest: dto.cpuRequest || app.cpuRequest,
|
||||
cpuLimit: dto.cpuLimit || app.cpuLimit,
|
||||
memoryRequest: dto.memoryRequest || app.memoryRequest,
|
||||
memoryLimit: dto.memoryLimit || app.memoryLimit,
|
||||
replicas: dto.replicas ?? app.replicas,
|
||||
dbStorageSize: dto.dbStorageSize || app.dbStorageSize,
|
||||
appStorageSize: dto.appStorageSize || app.appStorageSize,
|
||||
});
|
||||
const updatedApp = await this.applicationsService.update(
|
||||
app.id,
|
||||
app.userId,
|
||||
this.buildUpgradeEntityPatch(app, dto),
|
||||
);
|
||||
|
||||
// Update Kubernetes resources
|
||||
try {
|
||||
await this.kubernetesService.updateResources(updatedApp, {
|
||||
cpuRequest: dto.cpuRequest,
|
||||
cpuLimit: dto.cpuLimit,
|
||||
memoryRequest: dto.memoryRequest,
|
||||
memoryLimit: dto.memoryLimit,
|
||||
replicas: dto.replicas,
|
||||
});
|
||||
|
||||
// Resize app storage PVC if changed
|
||||
if (dto.appStorageSize && dto.appStorageSize !== app.appStorageSize) {
|
||||
await this.kubernetesService.resizeAppStoragePvc(updatedApp, dto.appStorageSize);
|
||||
}
|
||||
await this.applyUpgradeToKubernetes(updatedApp, dto, app);
|
||||
} catch (e: any) {
|
||||
// Log error but don't fail - DB is updated, K8s will sync on next deploy
|
||||
console.warn(`K8s resource update failed for ${app.name}: ${e.message}`);
|
||||
}
|
||||
|
||||
@@ -803,29 +795,15 @@ export class BillingController {
|
||||
|
||||
if (action === 'upgrade') {
|
||||
const app = await this.applicationsService.findOne(invoice.applicationId);
|
||||
const resources = invoice.metadata?.resources || {};
|
||||
const updatedApp = await this.applicationsService.update(app.id, app.userId, {
|
||||
cpuRequest: resources.cpuRequest || app.cpuRequest,
|
||||
cpuLimit: resources.cpuLimit || app.cpuLimit,
|
||||
memoryRequest: resources.memoryRequest || app.memoryRequest,
|
||||
memoryLimit: resources.memoryLimit || app.memoryLimit,
|
||||
replicas: resources.replicas ?? app.replicas,
|
||||
dbStorageSize: resources.dbStorageSize || app.dbStorageSize,
|
||||
appStorageSize: resources.appStorageSize || app.appStorageSize,
|
||||
});
|
||||
const resources = (invoice.metadata?.resources || {}) as UpgradeResourcesDto;
|
||||
const updatedApp = await this.applicationsService.update(
|
||||
app.id,
|
||||
app.userId,
|
||||
this.buildUpgradeEntityPatch(app, resources),
|
||||
);
|
||||
|
||||
try {
|
||||
await this.kubernetesService.updateResources(updatedApp, {
|
||||
cpuRequest: resources.cpuRequest,
|
||||
cpuLimit: resources.cpuLimit,
|
||||
memoryRequest: resources.memoryRequest,
|
||||
memoryLimit: resources.memoryLimit,
|
||||
replicas: resources.replicas,
|
||||
});
|
||||
|
||||
if (resources.appStorageSize && resources.appStorageSize !== app.appStorageSize) {
|
||||
await this.kubernetesService.resizeAppStoragePvc(updatedApp, resources.appStorageSize);
|
||||
}
|
||||
await this.applyUpgradeToKubernetes(updatedApp, resources, app);
|
||||
} catch (e: any) {
|
||||
console.warn(`K8s resource update failed for ${app.name}: ${e.message}`);
|
||||
}
|
||||
@@ -849,6 +827,136 @@ export class BillingController {
|
||||
return null;
|
||||
}
|
||||
|
||||
private buildUpgradeEntityPatch(app: Application, dto: UpgradeResourcesDto): Partial<Application> {
|
||||
const pt = app.productType ?? ProductType.APPLICATION;
|
||||
|
||||
if (pt === ProductType.MANAGED_REDIS && dto.redisResources) {
|
||||
return {
|
||||
optionalServiceResources: {
|
||||
...app.optionalServiceResources,
|
||||
redis: {
|
||||
...app.optionalServiceResources?.redis,
|
||||
...dto.redisResources,
|
||||
storageGi:
|
||||
dto.redisResources.storageGi ?? app.optionalServiceResources?.redis?.storageGi ?? 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (pt === ProductType.MANAGED_RABBITMQ && dto.rabbitmqResources) {
|
||||
return {
|
||||
optionalServiceResources: {
|
||||
...app.optionalServiceResources,
|
||||
rabbitmq: {
|
||||
...app.optionalServiceResources?.rabbitmq,
|
||||
...dto.rabbitmqResources,
|
||||
storageGi:
|
||||
dto.rabbitmqResources.storageGi ??
|
||||
app.optionalServiceResources?.rabbitmq?.storageGi ??
|
||||
2,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
cpuRequest: dto.cpuRequest || app.cpuRequest,
|
||||
cpuLimit: dto.cpuLimit || app.cpuLimit,
|
||||
memoryRequest: dto.memoryRequest || app.memoryRequest,
|
||||
memoryLimit: dto.memoryLimit || app.memoryLimit,
|
||||
replicas: dto.replicas ?? app.replicas,
|
||||
dbStorageSize: dto.dbStorageSize || app.dbStorageSize,
|
||||
appStorageSize: dto.appStorageSize || app.appStorageSize,
|
||||
};
|
||||
}
|
||||
|
||||
private async applyUpgradeToKubernetes(
|
||||
app: Application,
|
||||
dto: UpgradeResourcesDto,
|
||||
previous: Application,
|
||||
): Promise<void> {
|
||||
const pt = app.productType ?? ProductType.APPLICATION;
|
||||
|
||||
if (pt === ProductType.MANAGED_DATABASE) {
|
||||
await this.kubernetesService.updateResources(
|
||||
app,
|
||||
{
|
||||
cpuRequest: dto.cpuRequest,
|
||||
cpuLimit: dto.cpuLimit,
|
||||
memoryRequest: dto.memoryRequest,
|
||||
memoryLimit: dto.memoryLimit,
|
||||
},
|
||||
'database',
|
||||
);
|
||||
if (dto.dbStorageSize && dto.dbStorageSize !== previous.dbStorageSize) {
|
||||
const resize = await this.kubernetesService.resizeDatabasePvc(app, dto.dbStorageSize);
|
||||
if (!resize.success) {
|
||||
throw new BadRequestException(resize.message);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (pt === ProductType.MANAGED_REDIS) {
|
||||
const res = app.optionalServiceResources?.redis;
|
||||
if (res) {
|
||||
await this.kubernetesService.updateResources(
|
||||
app,
|
||||
{
|
||||
cpuRequest: res.cpuRequest,
|
||||
cpuLimit: res.cpuLimit,
|
||||
memoryRequest: res.memoryRequest,
|
||||
memoryLimit: res.memoryLimit,
|
||||
},
|
||||
'redis',
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (pt === ProductType.MANAGED_RABBITMQ) {
|
||||
const res = app.optionalServiceResources?.rabbitmq;
|
||||
if (res) {
|
||||
await this.kubernetesService.updateResources(
|
||||
app,
|
||||
{
|
||||
cpuRequest: res.cpuRequest,
|
||||
cpuLimit: res.cpuLimit,
|
||||
memoryRequest: res.memoryRequest,
|
||||
memoryLimit: res.memoryLimit,
|
||||
},
|
||||
'rabbitmq',
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await this.kubernetesService.updateResources(app, {
|
||||
cpuRequest: dto.cpuRequest,
|
||||
cpuLimit: dto.cpuLimit,
|
||||
memoryRequest: dto.memoryRequest,
|
||||
memoryLimit: dto.memoryLimit,
|
||||
replicas: dto.replicas,
|
||||
});
|
||||
|
||||
if (dto.appStorageSize && dto.appStorageSize !== previous.appStorageSize) {
|
||||
await this.kubernetesService.resizeAppStoragePvc(app, dto.appStorageSize);
|
||||
}
|
||||
|
||||
if (
|
||||
dto.dbStorageSize &&
|
||||
dto.dbStorageSize !== previous.dbStorageSize &&
|
||||
previous.databaseType &&
|
||||
previous.databaseType !== DatabaseType.NONE
|
||||
) {
|
||||
const resize = await this.kubernetesService.resizeDatabasePvc(app, dto.dbStorageSize);
|
||||
if (!resize.success) {
|
||||
throw new BadRequestException(resize.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async getAppWithAccess(user: any, applicationId: string) {
|
||||
const isAdminOrSales = user.role === UserRole.ADMIN || user.role === UserRole.SALES;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user