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:
keyhan
2026-05-23 19:00:09 +03:30
parent 736509708b
commit 695e05f948
55 changed files with 5575 additions and 600 deletions
@@ -8,8 +8,16 @@ import * as crypto from 'crypto';
import { Application } from './entities/application.entity';
import { CreateApplicationDto, UpdateApplicationDto } from './dto/application.dto';
import { ClustersService } from '../clusters/clusters.service';
import { UserRole, DatabaseType, CustomDomainStatus, AppRuntime } from '../common/enums';
import {
UserRole,
DatabaseType,
CustomDomainStatus,
AppRuntime,
ProductType,
isManagedProductType,
} from '../common/enums';
import { ensureAppUrlEnv } from './app-url.util';
import { normalizeCreateApplicationDto } from './managed-service.util';
@Injectable()
export class ApplicationsService {
@@ -23,6 +31,9 @@ export class ApplicationsService {
) {}
async create(userId: string, dto: CreateApplicationDto, userRole?: string): Promise<Application> {
dto = normalizeCreateApplicationDto(dto);
const productType = dto.productType ?? ProductType.APPLICATION;
// End users and technical staff cannot influence placement; only admins may manually assign.
const isAdmin = userRole === UserRole.ADMIN;
if (!isAdmin) {
@@ -63,9 +74,12 @@ export class ApplicationsService {
this.logger.log(`Generated DB credentials for app "${dto.name}" — user: ${dbUsername}`);
}
const customDomain = dto.customDomain?.toLowerCase().trim() || undefined;
const customDomain = isManagedProductType(productType)
? undefined
: dto.customDomain?.toLowerCase().trim() || undefined;
const defaultPort = [AppRuntime.WORDPRESS, AppRuntime.PHP, AppRuntime.LARAVEL].includes(dto.runtime)
const runtime = dto.runtime ?? AppRuntime.NODEJS;
const defaultPort = [AppRuntime.WORDPRESS, AppRuntime.PHP, AppRuntime.LARAVEL].includes(runtime)
? 80
: 3000;
@@ -74,26 +88,31 @@ export class ApplicationsService {
const app = this.appsRepository.create({
...dto,
productType,
runtime,
userId,
clusterId,
poolId,
dbUsername,
dbPassword,
replicas: isManagedProductType(productType) ? 0 : (dto.replicas ?? 1),
port: dto.port ?? defaultPort,
subdomain,
customDomain: customDomain || undefined,
customDomainStatus: customDomain ? CustomDomainStatus.PENDING_DNS : CustomDomainStatus.NONE,
envVars: ensureAppUrlEnv(
{
name: dto.name,
runtime: dto.runtime,
subdomain,
customDomain: customDomain || undefined,
customDomainStatus: customDomain ? CustomDomainStatus.PENDING_DNS : CustomDomainStatus.NONE,
envVars: dto.envVars ?? {},
},
platformDomain,
),
envVars: isManagedProductType(productType)
? (dto.envVars ?? {})
: ensureAppUrlEnv(
{
name: dto.name,
runtime,
subdomain,
customDomain: customDomain || undefined,
customDomainStatus: customDomain ? CustomDomainStatus.PENDING_DNS : CustomDomainStatus.NONE,
envVars: dto.envVars ?? {},
},
platformDomain,
),
});
const saved = await this.appsRepository.save(app);
if (allocationLogId) {
@@ -102,12 +121,32 @@ export class ApplicationsService {
return saved;
}
async findAllByUser(userId: string): Promise<Application[]> {
return this.appsRepository.find({
where: { userId },
relations: ['deployments'],
order: { createdAt: 'DESC' },
});
async findAllByUser(
userId: string,
options?: { productType?: 'application' | 'managed' },
): Promise<Application[]> {
const qb = this.appsRepository
.createQueryBuilder('app')
.leftJoinAndSelect('app.deployments', 'deployments')
.where('app.userId = :userId', { userId })
.orderBy('app.createdAt', 'DESC');
if (options?.productType === 'application') {
qb.andWhere(
'(app.productType = :applicationType OR app.productType IS NULL)',
{ applicationType: ProductType.APPLICATION },
);
} else if (options?.productType === 'managed') {
qb.andWhere('app.productType IN (:...managedTypes)', {
managedTypes: [
ProductType.MANAGED_DATABASE,
ProductType.MANAGED_REDIS,
ProductType.MANAGED_RABBITMQ,
],
});
}
return qb.getMany();
}
async findAll(search?: string): Promise<Application[]> {