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:
@@ -0,0 +1,20 @@
|
||||
export function formatBytes(bytes?: number): string {
|
||||
if (!bytes) return '—';
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
export function parseCpuToMillicores(cpu: string): number {
|
||||
if (!cpu) return 0;
|
||||
if (cpu.endsWith('m')) return parseFloat(cpu);
|
||||
return parseFloat(cpu) * 1000;
|
||||
}
|
||||
|
||||
export function parseMemoryToMi(mem: string): number {
|
||||
if (!mem) return 0;
|
||||
if (mem.endsWith('Gi')) return parseFloat(mem) * 1024;
|
||||
if (mem.endsWith('Mi')) return parseFloat(mem);
|
||||
if (mem.endsWith('Ki')) return parseFloat(mem) / 1024;
|
||||
return parseFloat(mem);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import type {
|
||||
OptionalServiceResourceConfig,
|
||||
OptionalServiceResourcesMap,
|
||||
PricingCatalog,
|
||||
} from '@/types';
|
||||
|
||||
export type OptionalServiceKey = keyof OptionalServiceResourcesMap;
|
||||
|
||||
const FALLBACK_OPTIONAL_RESOURCES: Record<OptionalServiceKey, OptionalServiceResourceConfig> = {
|
||||
redis: {
|
||||
cpuRequest: '50m',
|
||||
cpuLimit: '200m',
|
||||
memoryRequest: '64Mi',
|
||||
memoryLimit: '256Mi',
|
||||
storageGi: 1,
|
||||
},
|
||||
rabbitmq: {
|
||||
cpuRequest: '100m',
|
||||
cpuLimit: '500m',
|
||||
memoryRequest: '256Mi',
|
||||
memoryLimit: '512Mi',
|
||||
storageGi: 2,
|
||||
},
|
||||
};
|
||||
|
||||
export function optionalDefaultsFromCatalog(
|
||||
catalog: PricingCatalog | undefined,
|
||||
service: OptionalServiceKey,
|
||||
): OptionalServiceResourceConfig {
|
||||
const profile = catalog?.optionalServices?.[service]?.profile;
|
||||
const fallback = FALLBACK_OPTIONAL_RESOURCES[service];
|
||||
if (!profile) return { ...fallback };
|
||||
return {
|
||||
cpuRequest: profile.cpuRequest || fallback.cpuRequest,
|
||||
cpuLimit: profile.cpuLimit || fallback.cpuLimit,
|
||||
memoryRequest: profile.memoryRequest || fallback.memoryRequest,
|
||||
memoryLimit: profile.memoryLimit || fallback.memoryLimit,
|
||||
storageGi: profile.storageGi ?? fallback.storageGi,
|
||||
};
|
||||
}
|
||||
|
||||
export function managedServiceTypeLabel(productType?: string): string {
|
||||
switch (productType) {
|
||||
case 'managed_database':
|
||||
return 'Managed Database';
|
||||
case 'managed_redis':
|
||||
return 'Managed Redis';
|
||||
case 'managed_rabbitmq':
|
||||
return 'Managed RabbitMQ';
|
||||
default:
|
||||
return 'Service';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { Application, ProductType } from '@/types';
|
||||
|
||||
const MANAGED_PRODUCT_TYPES: ProductType[] = [
|
||||
'managed_database',
|
||||
'managed_redis',
|
||||
'managed_rabbitmq',
|
||||
];
|
||||
|
||||
export function isManagedProduct(app: Pick<Application, 'productType'>): boolean {
|
||||
const t = app.productType;
|
||||
return !!t && MANAGED_PRODUCT_TYPES.includes(t);
|
||||
}
|
||||
|
||||
export function isApplicationProduct(app: Pick<Application, 'productType'>): boolean {
|
||||
return !isManagedProduct(app);
|
||||
}
|
||||
|
||||
export function filterApplications(apps: Application[]): Application[] {
|
||||
return apps.filter(isApplicationProduct);
|
||||
}
|
||||
|
||||
export function filterManagedServices(apps: Application[]): Application[] {
|
||||
return apps.filter(isManagedProduct);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/** Kubernetes-style Gi (1024³ bytes), aligned with wizard “GB” fields */
|
||||
export const ONE_GIB = 1024 ** 3;
|
||||
|
||||
export function minGiToFitFileBytes(bytes: number): number {
|
||||
return Math.max(1, Math.ceil(bytes / ONE_GIB));
|
||||
}
|
||||
Reference in New Issue
Block a user