feat(deploy): configurable CPU/RAM/disk for app databases

Let users size the database workload from the deploy wizard the same way
Redis/RabbitMQ are configured: a resource card (CPU request/limit, memory
request/limit, storage) on the Resources step, stored in
optionalServiceResources.database.

- entity/DTO: add `database` to optionalServiceResources
- k8s: resolveDatabaseResources() applies user-selected resources on both
  the Helm and K8s-API deploy paths (was hardcoded 100m/256Mi→500m/512Mi)
- billing: bill database CPU/RAM as a separate line on top of the app's
  resources; merge it through the upgrade path too
- wizard: db resource card on the Resources step, disk moved into the card,
  cost preview + review summary include the database resources

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
keyhan
2026-06-16 01:20:33 +03:30
parent e53fc8e2ff
commit 901a20eb01
11 changed files with 198 additions and 74 deletions
@@ -18,6 +18,12 @@ import { AppRuntime, DatabaseType, ProductType } from '../../common/enums';
import { OptionalServiceResourcesDto } from '../../billing/dto/optional-service-resources.dto'; import { OptionalServiceResourcesDto } from '../../billing/dto/optional-service-resources.dto';
export class OptionalServiceResourcesMapDto { export class OptionalServiceResourcesMapDto {
@ApiPropertyOptional({ type: OptionalServiceResourcesDto })
@IsOptional()
@ValidateNested()
@Type(() => OptionalServiceResourcesDto)
database?: OptionalServiceResourcesDto;
@ApiPropertyOptional({ type: OptionalServiceResourcesDto }) @ApiPropertyOptional({ type: OptionalServiceResourcesDto })
@IsOptional() @IsOptional()
@ValidateNested() @ValidateNested()
@@ -82,9 +82,16 @@ export class Application {
@Column({ type: 'jsonb', nullable: true }) @Column({ type: 'jsonb', nullable: true })
logPaths: string[]; // Custom log paths to collect (e.g., ['/app/logs/*.log']) logPaths: string[]; // Custom log paths to collect (e.g., ['/app/logs/*.log'])
/** User-selected CPU/RAM/storage per optional service (redis, rabbitmq). */ /** User-selected CPU/RAM/storage per optional service (database, redis, rabbitmq). */
@Column({ type: 'jsonb', nullable: true }) @Column({ type: 'jsonb', nullable: true })
optionalServiceResources?: { optionalServiceResources?: {
database?: {
cpuRequest?: string;
cpuLimit: string;
memoryRequest?: string;
memoryLimit: string;
storageGi: number;
};
redis?: { redis?: {
cpuRequest?: string; cpuRequest?: string;
cpuLimit: string; cpuLimit: string;
+15 -1
View File
@@ -637,6 +637,16 @@ export class BillingService {
replicas: newResources.replicas ?? base.replicas, replicas: newResources.replicas ?? base.replicas,
...(newResources.dbStorageSize && { dbStorageSize: newResources.dbStorageSize }), ...(newResources.dbStorageSize && { dbStorageSize: newResources.dbStorageSize }),
...(newResources.appStorageSize && { appStorageSize: newResources.appStorageSize }), ...(newResources.appStorageSize && { appStorageSize: newResources.appStorageSize }),
...(newResources.databaseResources && {
databaseResources: {
...base.databaseResources,
...newResources.databaseResources,
storageGi:
newResources.databaseResources.storageGi ??
base.databaseResources?.storageGi ??
1,
},
}),
...(newResources.redisResources && { ...(newResources.redisResources && {
redisResources: { redisResources: {
...base.redisResources, ...base.redisResources,
@@ -734,7 +744,9 @@ export class BillingService {
? (app as Application).optionalServiceResources ? (app as Application).optionalServiceResources
: undefined; : undefined;
const dtoExtras = const dtoExtras =
'redisResources' in app ? (app as CalculateCostDto) : undefined; 'redisResources' in app || 'databaseResources' in app
? (app as CalculateCostDto)
: undefined;
const productType = const productType =
'productType' in app 'productType' in app
? ((app as Application).productType ?? ProductType.APPLICATION) ? ((app as Application).productType ?? ProductType.APPLICATION)
@@ -753,6 +765,7 @@ export class BillingService {
enableRabbitmq: !!app.enableRabbitmq, enableRabbitmq: !!app.enableRabbitmq,
enableElasticsearch: managed ? false : !!app.enableElasticsearch, enableElasticsearch: managed ? false : !!app.enableElasticsearch,
enableCustomDomain: managed ? false : enableCustomDomain, enableCustomDomain: managed ? false : enableCustomDomain,
databaseResources: optionalRes?.database ?? dtoExtras?.databaseResources,
redisResources: optionalRes?.redis ?? dtoExtras?.redisResources, redisResources: optionalRes?.redis ?? dtoExtras?.redisResources,
rabbitmqResources: optionalRes?.rabbitmq ?? dtoExtras?.rabbitmqResources, rabbitmqResources: optionalRes?.rabbitmq ?? dtoExtras?.rabbitmqResources,
}; };
@@ -919,6 +932,7 @@ export class BillingService {
enableRabbitmq: config.enableRabbitmq, enableRabbitmq: config.enableRabbitmq,
enableElasticsearch: config.enableElasticsearch, enableElasticsearch: config.enableElasticsearch,
enableCustomDomain: config.enableCustomDomain, enableCustomDomain: config.enableCustomDomain,
databaseResources: config.databaseResources,
redisResources: config.redisResources, redisResources: config.redisResources,
rabbitmqResources: config.rabbitmqResources, rabbitmqResources: config.rabbitmqResources,
}; };
+12
View File
@@ -144,6 +144,12 @@ export class CalculateCostDto {
@IsBoolean() @IsBoolean()
enableElasticsearch?: boolean; enableElasticsearch?: boolean;
@ApiPropertyOptional({ type: OptionalServiceResourcesDto, description: 'Per-database CPU/RAM/storage limits' })
@IsOptional()
@ValidateNested()
@Type(() => OptionalServiceResourcesDto)
databaseResources?: OptionalServiceResourcesDto;
@ApiPropertyOptional({ type: OptionalServiceResourcesDto }) @ApiPropertyOptional({ type: OptionalServiceResourcesDto })
@IsOptional() @IsOptional()
@ValidateNested() @ValidateNested()
@@ -213,6 +219,12 @@ export class UpgradeResourcesDto {
@IsString() @IsString()
appStorageSize?: string; appStorageSize?: string;
@ApiPropertyOptional({ type: OptionalServiceResourcesDto, description: 'Database resource limits' })
@IsOptional()
@ValidateNested()
@Type(() => OptionalServiceResourcesDto)
databaseResources?: OptionalServiceResourcesDto;
@ApiPropertyOptional({ type: OptionalServiceResourcesDto, description: 'Managed Redis resource limits' }) @ApiPropertyOptional({ type: OptionalServiceResourcesDto, description: 'Managed Redis resource limits' })
@IsOptional() @IsOptional()
@ValidateNested() @ValidateNested()
@@ -654,8 +654,14 @@ export class PricingCatalogService implements OnModuleInit {
} }
const replicas = dto.replicas ?? 1; const replicas = dto.replicas ?? 1;
const hasDatabase = dto.databaseType !== DatabaseType.NONE && dto.databaseType !== 'none'; const hasDatabase = dto.databaseType !== DatabaseType.NONE && dto.databaseType !== 'none';
const cpuQty = this.parseCpuToCores(dto.cpuLimit) * replicas; let cpuQty = this.parseCpuToCores(dto.cpuLimit) * replicas;
const memoryQty = this.parseMemoryToGb(dto.memoryLimit) * replicas; let memoryQty = this.parseMemoryToGb(dto.memoryLimit) * replicas;
// The database runs as its own single-replica workload; bill its CPU/RAM on
// top of the app's, using the user-selected per-database resources.
if (hasDatabase && dto.databaseResources) {
cpuQty += this.parseCpuToCores(dto.databaseResources.cpuLimit);
memoryQty += this.parseMemoryToGb(dto.databaseResources.memoryLimit);
}
const storageQty = const storageQty =
(dto.dbStorageSize (dto.dbStorageSize
? parseFloat(String(dto.dbStorageSize).replace(/Gi$/i, '')) || 0 ? parseFloat(String(dto.dbStorageSize).replace(/Gi$/i, '')) || 0
+35 -14
View File
@@ -38,6 +38,10 @@ interface ManifestContext {
dbPassword: string; dbPassword: string;
dbVersion: string; dbVersion: string;
dbStorageSize: string; dbStorageSize: string;
dbCpuRequest: string;
dbCpuLimit: string;
dbMemoryRequest: string;
dbMemoryLimit: string;
appStorageSize: string; appStorageSize: string;
enableRedis: boolean; enableRedis: boolean;
redisVersion: string; redisVersion: string;
@@ -126,6 +130,21 @@ export class KubernetesService implements OnModuleInit {
/** /**
* Build Helm values object from an Application entity and image URI. * Build Helm values object from an Application entity and image URI.
*/ */
/**
* Resolve the database workload's CPU/RAM. Prefers the user-selected
* per-database resources (optionalServiceResources.database); falls back to
* the app's main resources for managed databases / legacy apps.
*/
private resolveDatabaseResources(app: Application) {
const res = app.optionalServiceResources?.database;
return {
cpuRequest: res?.cpuRequest || app.cpuRequest || '100m',
cpuLimit: res?.cpuLimit || app.cpuLimit || '500m',
memoryRequest: res?.memoryRequest || app.memoryRequest || '256Mi',
memoryLimit: res?.memoryLimit || app.memoryLimit || '512Mi',
};
}
private buildRedisHelmBlock(app: Application) { private buildRedisHelmBlock(app: Application) {
const res = app.optionalServiceResources?.redis; const res = app.optionalServiceResources?.redis;
const storageGi = res?.storageGi ?? 1; const storageGi = res?.storageGi ?? 1;
@@ -254,12 +273,7 @@ export class KubernetesService implements OnModuleInit {
username: app.dbUsername || 'appuser', username: app.dbUsername || 'appuser',
password: app.dbPassword || this.generatePassword(), password: app.dbPassword || this.generatePassword(),
storageSize: app.dbStorageSize || '1Gi', storageSize: app.dbStorageSize || '1Gi',
resources: { resources: this.resolveDatabaseResources(app),
cpuRequest: app.cpuRequest || '100m',
cpuLimit: app.cpuLimit || '500m',
memoryRequest: app.memoryRequest || '256Mi',
memoryLimit: app.memoryLimit || '512Mi',
},
}, },
redis: { enabled: false, storageSize: '1Gi', resources: {} }, redis: { enabled: false, storageSize: '1Gi', resources: {} },
rabbitmq: { enabled: false, storageSize: '2Gi', resources: {} }, rabbitmq: { enabled: false, storageSize: '2Gi', resources: {} },
@@ -342,12 +356,7 @@ export class KubernetesService implements OnModuleInit {
username: app.dbUsername || 'appuser', username: app.dbUsername || 'appuser',
password: app.dbPassword || this.generatePassword(), password: app.dbPassword || this.generatePassword(),
storageSize: app.dbStorageSize || '1Gi', storageSize: app.dbStorageSize || '1Gi',
resources: { resources: this.resolveDatabaseResources(app),
cpuRequest: app.cpuRequest || '100m',
cpuLimit: app.cpuLimit || '500m',
memoryRequest: app.memoryRequest || '256Mi',
memoryLimit: app.memoryLimit || '512Mi',
},
}, },
wordpress: { wordpress: {
enabled: isWordPress, enabled: isWordPress,
@@ -481,6 +490,10 @@ export class KubernetesService implements OnModuleInit {
dbPassword: app.dbPassword || '', dbPassword: app.dbPassword || '',
dbVersion: app.dbVersion || '', dbVersion: app.dbVersion || '',
dbStorageSize: app.dbStorageSize || '1Gi', dbStorageSize: app.dbStorageSize || '1Gi',
dbCpuRequest: this.resolveDatabaseResources(app).cpuRequest,
dbCpuLimit: this.resolveDatabaseResources(app).cpuLimit,
dbMemoryRequest: this.resolveDatabaseResources(app).memoryRequest,
dbMemoryLimit: this.resolveDatabaseResources(app).memoryLimit,
appStorageSize: app.appStorageSize || '2Gi', appStorageSize: app.appStorageSize || '2Gi',
enableRedis: app.enableRedis || false, enableRedis: app.enableRedis || false,
redisVersion: app.redisVersion || '7.2', redisVersion: app.redisVersion || '7.2',
@@ -559,6 +572,10 @@ export class KubernetesService implements OnModuleInit {
dbPassword: app.dbPassword || this.generatePassword(), dbPassword: app.dbPassword || this.generatePassword(),
dbVersion: app.dbVersion || '', dbVersion: app.dbVersion || '',
dbStorageSize: app.dbStorageSize || '1Gi', dbStorageSize: app.dbStorageSize || '1Gi',
dbCpuRequest: this.resolveDatabaseResources(app).cpuRequest,
dbCpuLimit: this.resolveDatabaseResources(app).cpuLimit,
dbMemoryRequest: this.resolveDatabaseResources(app).memoryRequest,
dbMemoryLimit: this.resolveDatabaseResources(app).memoryLimit,
appStorageSize: app.appStorageSize || '2Gi', appStorageSize: app.appStorageSize || '2Gi',
enableRedis: false, enableRedis: false,
redisVersion: app.redisVersion || '7.2', redisVersion: app.redisVersion || '7.2',
@@ -626,6 +643,10 @@ export class KubernetesService implements OnModuleInit {
dbPassword: app.dbPassword || this.generatePassword(), dbPassword: app.dbPassword || this.generatePassword(),
dbVersion: app.dbVersion || '', dbVersion: app.dbVersion || '',
dbStorageSize: app.dbStorageSize || '1Gi', dbStorageSize: app.dbStorageSize || '1Gi',
dbCpuRequest: this.resolveDatabaseResources(app).cpuRequest,
dbCpuLimit: this.resolveDatabaseResources(app).cpuLimit,
dbMemoryRequest: this.resolveDatabaseResources(app).memoryRequest,
dbMemoryLimit: this.resolveDatabaseResources(app).memoryLimit,
appStorageSize: app.appStorageSize || '2Gi', appStorageSize: app.appStorageSize || '2Gi',
enableRedis: app.enableRedis || false, enableRedis: app.enableRedis || false,
redisVersion: app.redisVersion || '7.2', redisVersion: app.redisVersion || '7.2',
@@ -1808,8 +1829,8 @@ export class KubernetesService implements OnModuleInit {
env: envVars, env: envVars,
volumeMounts: [{ name: 'db-storage', mountPath: dataPath }], volumeMounts: [{ name: 'db-storage', mountPath: dataPath }],
resources: { resources: {
requests: { cpu: '100m', memory: '256Mi' }, requests: { cpu: ctx.dbCpuRequest, memory: ctx.dbMemoryRequest },
limits: { cpu: '500m', memory: '512Mi' }, limits: { cpu: ctx.dbCpuLimit, memory: ctx.dbMemoryLimit },
}, },
readinessProbe, readinessProbe,
livenessProbe, livenessProbe,
+101 -55
View File
@@ -36,9 +36,16 @@ function sanitizePayloadForWordPressRuntime(payload: CreateApplicationDto): Crea
}; };
} }
type OptionalServiceKey = 'redis' | 'rabbitmq'; type OptionalServiceKey = 'database' | 'redis' | 'rabbitmq';
const FALLBACK_OPTIONAL_RESOURCES: Record<OptionalServiceKey, OptionalServiceResourceConfig> = { const FALLBACK_OPTIONAL_RESOURCES: Record<OptionalServiceKey, OptionalServiceResourceConfig> = {
database: {
cpuRequest: '100m',
cpuLimit: '500m',
memoryRequest: '256Mi',
memoryLimit: '512Mi',
storageGi: 1,
},
redis: { redis: {
cpuRequest: '50m', cpuRequest: '50m',
cpuLimit: '200m', cpuLimit: '200m',
@@ -313,6 +320,22 @@ export default function DeployPage() {
return Math.max(1, minGiToFitFileBytes(dbDumpFile.size)); return Math.max(1, minGiToFitFileBytes(dbDumpFile.size));
}, [form.databaseType, dbDumpFile]); }, [form.databaseType, dbDumpFile]);
/** Keep the database workload's storageGi in lockstep with the dbStorageSize disk field. */
const dbSyncedOptionalResources = (): OptionalServiceResourcesMap | undefined => {
const osr = form.optionalServiceResources;
if (!osr) return osr;
if (form.databaseType === 'none') {
// Drop any leftover database entry when no database is selected.
const { database: _drop, ...rest } = osr;
return rest;
}
if (!osr.database) return osr;
return {
...osr,
database: { ...osr.database, storageGi: parseInt(form.dbStorageSize || '1', 10) || 1 },
};
};
const { data: clusters = [] } = useQuery<ClusterPublic[]>({ const { data: clusters = [] } = useQuery<ClusterPublic[]>({
queryKey: ['clusters-public'], queryKey: ['clusters-public'],
queryFn: () => api.get('/clusters/public').then((r) => r.data), queryFn: () => api.get('/clusters/public').then((r) => r.data),
@@ -373,6 +396,8 @@ export default function DeployPage() {
enableRedis: form.runtime === 'wordpress' ? false : form.enableRedis, enableRedis: form.runtime === 'wordpress' ? false : form.enableRedis,
enableRabbitmq: form.runtime === 'wordpress' ? false : form.enableRabbitmq, enableRabbitmq: form.runtime === 'wordpress' ? false : form.enableRabbitmq,
enableElasticsearch: form.runtime === 'wordpress' ? false : form.enableElasticsearch, enableElasticsearch: form.runtime === 'wordpress' ? false : form.enableElasticsearch,
databaseResources:
form.databaseType !== 'none' ? form.optionalServiceResources?.database : undefined,
redisResources: redisResources:
form.runtime !== 'wordpress' && form.enableRedis ? form.optionalServiceResources?.redis : undefined, form.runtime !== 'wordpress' && form.enableRedis ? form.optionalServiceResources?.redis : undefined,
rabbitmqResources: rabbitmqResources:
@@ -410,6 +435,7 @@ export default function DeployPage() {
setDeployStage('creating'); setDeployStage('creating');
const payload = sanitizePayloadForWordPressRuntime({ const payload = sanitizePayloadForWordPressRuntime({
...form, ...form,
optionalServiceResources: dbSyncedOptionalResources(),
...(form.databaseType !== 'none' && form.dbStorageSize ...(form.databaseType !== 'none' && form.dbStorageSize
? { dbStorageSize: `${parseInt(form.dbStorageSize, 10) || 1}Gi` } ? { dbStorageSize: `${parseInt(form.dbStorageSize, 10) || 1}Gi` }
: {}), : {}),
@@ -492,6 +518,7 @@ export default function DeployPage() {
setDeployStage('creating'); setDeployStage('creating');
const payload = sanitizePayloadForWordPressRuntime({ const payload = sanitizePayloadForWordPressRuntime({
...form, ...form,
optionalServiceResources: dbSyncedOptionalResources(),
...(form.databaseType !== 'none' && form.dbStorageSize ...(form.databaseType !== 'none' && form.dbStorageSize
? { dbStorageSize: `${parseInt(form.dbStorageSize, 10) || 1}Gi` } ? { dbStorageSize: `${parseInt(form.dbStorageSize, 10) || 1}Gi` }
: {}), : {}),
@@ -645,7 +672,7 @@ export default function DeployPage() {
const handleSubmit = () => { const handleSubmit = () => {
if (!validateRestoreStorageOrShowModal()) return; if (!validateRestoreStorageOrShowModal()) return;
const payload = { ...form }; const payload = { ...form, optionalServiceResources: dbSyncedOptionalResources() };
// Format dbStorageSize with Gi suffix // Format dbStorageSize with Gi suffix
if (payload.databaseType !== 'none' && payload.dbStorageSize) { if (payload.databaseType !== 'none' && payload.dbStorageSize) {
payload.dbStorageSize = `${parseInt(payload.dbStorageSize, 10) || 1}Gi`; payload.dbStorageSize = `${parseInt(payload.dbStorageSize, 10) || 1}Gi`;
@@ -1477,7 +1504,20 @@ export default function DeployPage() {
else if (opt.value === 'mysql') dbVersion = '8.0'; else if (opt.value === 'mysql') dbVersion = '8.0';
else if (opt.value === 'mariadb') dbVersion = '11.4'; else if (opt.value === 'mariadb') dbVersion = '11.4';
else if (opt.value === 'mongodb') dbVersion = '7.0'; else if (opt.value === 'mongodb') dbVersion = '7.0';
setForm({ ...form, databaseType: opt.value as any, dbVersion }); const hasDb = opt.value !== 'none';
setForm({
...form,
databaseType: opt.value as any,
dbVersion,
optionalServiceResources: hasDb
? {
...form.optionalServiceResources,
database:
form.optionalServiceResources?.database ??
optionalDefaultsFromCatalog(pricingCatalog, 'database'),
}
: form.optionalServiceResources,
});
}} }}
className={`p-4 rounded-xl border-2 text-center transition-colors ${ className={`p-4 rounded-xl border-2 text-center transition-colors ${
form.databaseType === opt.value ? 'border-primary-500 bg-primary-50' : 'border-gray-200 hover:border-gray-300' form.databaseType === opt.value ? 'border-primary-500 bg-primary-50' : 'border-gray-200 hover:border-gray-300'
@@ -1665,60 +1705,14 @@ export default function DeployPage() {
</div> </div>
</div> </div>
{/* Database Storage Size */} {/* CPU, RAM and disk for the database are configured on the Resources step. */}
<div className="pt-2"> <p className="text-xs text-gray-400 pt-1">
<label className="block text-xs text-gray-500 mb-2">{dw.databaseStorageSize}</label> {dw.databaseResourcesHint}
<div className="flex items-center gap-3"> {dbDumpFile
<div className="flex items-center border border-gray-300 rounded-lg overflow-hidden"> ? ` (${dw.databaseStorageSize}${minDbGiFromRestoreDump} GB)`
<button : ''}
type="button"
onClick={() => {
const current = parseInt(form.dbStorageSize || '1', 10);
const min = minDbGiFromRestoreDump;
if (current > min) setForm({ ...form, dbStorageSize: String(current - 1) });
}}
disabled={parseInt(form.dbStorageSize || '1', 10) <= minDbGiFromRestoreDump}
className="px-3 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 font-bold disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
</button>
<input
type="number"
min={minDbGiFromRestoreDump}
max={100}
value={form.dbStorageSize || '1'}
onChange={(e) => {
const min = minDbGiFromRestoreDump;
const val = Math.max(min, Math.min(100, parseInt(e.target.value, 10) || min));
setForm({ ...form, dbStorageSize: String(val) });
}}
className="w-16 text-center py-2 border-x border-gray-300 text-sm font-semibold focus:outline-none"
/>
<button
type="button"
onClick={() => {
const current = parseInt(form.dbStorageSize || '1', 10);
if (current < 100) setForm({ ...form, dbStorageSize: String(current + 1) });
}}
disabled={parseInt(form.dbStorageSize || '1', 10) >= 100}
className="px-3 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 font-bold disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
+
</button>
</div>
<span className="text-sm font-medium text-gray-700">GB</span>
{dbDumpFile && (
<span className="text-xs text-blue-500">
Suggested based on dump size ({(dbDumpFile.size / (1024 * 1024 * 1024)).toFixed(2)} GB)
</span>
)}
</div>
<p className="mt-1 text-xs text-gray-400">
Minimum {minDbGiFromRestoreDump} GB for this configuration
{dbDumpFile ? ' (must fit the uploaded dump)' : ''} Only expansion is allowed after creation
</p> </p>
</div> </div>
</div>
)} )}
{/* Optional Services Section */} {/* Optional Services Section */}
@@ -2444,6 +2438,48 @@ export default function DeployPage() {
</div> </div>
{form.databaseType !== 'none' && form.optionalServiceResources?.database && (
<div className="space-y-4 pt-2 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 uppercase tracking-wide">{dw.databaseResources}</h3>
<WorkloadResourceFields
title={
form.databaseType === 'postgresql' ? 'PostgreSQL'
: form.databaseType === 'mysql' ? 'MySQL'
: form.databaseType === 'mariadb' ? 'MariaDB'
: 'MongoDB'
}
accentClass="text-blue-500"
borderClass="border-blue-200"
bgClass="bg-blue-50/50"
config={{
...form.optionalServiceResources.database,
storageGi: parseInt(form.dbStorageSize || '1', 10) || 1,
}}
onChange={(patch) => {
const { storageGi, ...res } = patch;
setForm((prev) => {
const nextStorage =
storageGi !== undefined
? Math.max(minDbGiFromRestoreDump, storageGi)
: parseInt(prev.dbStorageSize || '1', 10) || 1;
return {
...prev,
dbStorageSize: String(nextStorage),
optionalServiceResources: {
...prev.optionalServiceResources,
database: {
...prev.optionalServiceResources!.database!,
...res,
storageGi: nextStorage,
},
},
};
});
}}
/>
</div>
)}
{form.runtime !== 'wordpress' && (form.enableRedis || form.enableRabbitmq) && ( {form.runtime !== 'wordpress' && (form.enableRedis || form.enableRabbitmq) && (
<div className="space-y-4 pt-2 border-t border-gray-200"> <div className="space-y-4 pt-2 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 uppercase tracking-wide">{dw.optionalServices}</h3> <h3 className="text-sm font-semibold text-gray-900 uppercase tracking-wide">{dw.optionalServices}</h3>
@@ -2529,6 +2565,16 @@ export default function DeployPage() {
<span className="text-sm text-gray-500">{dw.dbStorage}</span> <span className="text-sm text-gray-500">{dw.dbStorage}</span>
<span className="text-sm font-medium">{form.dbStorageSize || '1'} GB</span> <span className="text-sm font-medium">{form.dbStorageSize || '1'} GB</span>
</div> </div>
{form.optionalServiceResources?.database && (
<div className="flex justify-between">
<span className="text-sm text-gray-500">{dw.databaseResources}</span>
<span className="text-sm font-medium">
{form.optionalServiceResources.database.cpuRequest} / {form.optionalServiceResources.database.cpuLimit}
{' · '}
{form.optionalServiceResources.database.memoryRequest} / {form.optionalServiceResources.database.memoryLimit}
</span>
</div>
)}
{dbDumpFile && ( {dbDumpFile && (
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-sm text-gray-500">{dw.dbDump}</span> <span className="text-sm text-gray-500">{dw.dbDump}</span>
+2
View File
@@ -1231,6 +1231,8 @@ const en: Dictionary = {
databaseCredentials: 'Database Credentials', databaseCredentials: 'Database Credentials',
dbUsername: 'DB Username', dbPassword: 'DB Password', dbStorage: 'DB Storage', dbDump: 'DB Dump', dbUsername: 'DB Username', dbPassword: 'DB Password', dbStorage: 'DB Storage', dbDump: 'DB Dump',
databaseStorageSize: 'Database Storage Size', databaseStorageSize: 'Database Storage Size',
databaseResources: 'Database resources',
databaseResourcesHint: 'Configure the database CPU, RAM and disk on the Resources step.',
username: 'Username', password: 'Password', autoGenerated: 'Auto-generated', username: 'Username', password: 'Password', autoGenerated: 'Auto-generated',
generatePassword: 'Generate random password', generatePassword: 'Generate random password',
dbCredsNote: 'These credentials are used for internal cluster communication only. The database is not exposed externally.', dbCredsNote: 'These credentials are used for internal cluster communication only. The database is not exposed externally.',
+2
View File
@@ -1243,6 +1243,8 @@ const fa = {
databaseCredentials: 'اطلاعات ورود دیتابیس', databaseCredentials: 'اطلاعات ورود دیتابیس',
dbUsername: 'نام کاربری دیتابیس', dbPassword: 'رمز دیتابیس', dbStorage: 'فضای دیتابیس', dbDump: 'dump دیتابیس', dbUsername: 'نام کاربری دیتابیس', dbPassword: 'رمز دیتابیس', dbStorage: 'فضای دیتابیس', dbDump: 'dump دیتابیس',
databaseStorageSize: 'اندازهٔ فضای ذخیرهٔ دیتابیس', databaseStorageSize: 'اندازهٔ فضای ذخیرهٔ دیتابیس',
databaseResources: 'منابع دیتابیس',
databaseResourcesHint: 'پردازنده، حافظه و دیسک دیتابیس را در مرحلهٔ منابع پیکربندی کن.',
username: 'نام کاربری', password: 'رمز عبور', autoGenerated: 'تولید خودکار', username: 'نام کاربری', password: 'رمز عبور', autoGenerated: 'تولید خودکار',
generatePassword: 'تولید رمز تصادفی', generatePassword: 'تولید رمز تصادفی',
dbCredsNote: 'این اطلاعات فقط برای ارتباط داخلی کلاستر استفاده می‌شوند. دیتابیس به‌صورت خارجی expose نمی‌شود.', dbCredsNote: 'این اطلاعات فقط برای ارتباط داخلی کلاستر استفاده می‌شوند. دیتابیس به‌صورت خارجی expose نمی‌شود.',
@@ -7,6 +7,13 @@ import type {
export type OptionalServiceKey = keyof OptionalServiceResourcesMap; export type OptionalServiceKey = keyof OptionalServiceResourcesMap;
const FALLBACK_OPTIONAL_RESOURCES: Record<OptionalServiceKey, OptionalServiceResourceConfig> = { const FALLBACK_OPTIONAL_RESOURCES: Record<OptionalServiceKey, OptionalServiceResourceConfig> = {
database: {
cpuRequest: '100m',
cpuLimit: '500m',
memoryRequest: '256Mi',
memoryLimit: '512Mi',
storageGi: 1,
},
redis: { redis: {
cpuRequest: '50m', cpuRequest: '50m',
cpuLimit: '200m', cpuLimit: '200m',
+1
View File
@@ -267,6 +267,7 @@ export interface OptionalServiceResourceConfig {
} }
export interface OptionalServiceResourcesMap { export interface OptionalServiceResourcesMap {
database?: OptionalServiceResourceConfig;
redis?: OptionalServiceResourceConfig; redis?: OptionalServiceResourceConfig;
rabbitmq?: OptionalServiceResourceConfig; rabbitmq?: OptionalServiceResourceConfig;
} }