import { CloudProvider, COMPUTE_MAX_IOPS, COMPUTE_MAX_THROUGHPUT, computeInstanceAddonVariantIdSchema, } from 'shared-data' import { z } from 'zod' import { calculateDiskSizeRequiredForIopsWithGp3, calculateDiskSizeRequiredForIopsWithIo2, calculateIopsRequiredForThroughput, calculateMaxIopsAllowedForDiskSizeWithGp3, calculateMaxIopsAllowedForDiskSizeWithio2, calculateMaxThroughput, formatNumber, } from './DiskManagement.utils' import { DISK_LIMITS, DiskType } from './ui/DiskManagement.constants' const baseSchema = z.object({ storageType: z.enum(['io2', 'gp3']).describe('Type of storage: io2 or gp3'), totalSize: z.number().int('Value must be an integer').describe('Allocated disk size in GB'), provisionedIOPS: z.number().describe('Provisioned IOPS for storage type'), throughput: z.number().optional().describe('Throughput in MB/s for gp3'), computeSize: computeInstanceAddonVariantIdSchema .describe('Compute size') .optional() .default('ci_micro'), growthPercent: z .number() .int('Value must be an integer') .min(10, 'Growth percent must be at least 10%') .max(100, 'Growth percent cannot exceed 100%') .optional() .nullable(), minIncrementGb: z .number() .int('Value must be an integer') .min(1, 'Minimum increment must be at least 1 GB') .max(200, 'Minimum increment cannot exceed 200 GB') .optional() .nullable(), maxSizeGb: z .number() .int('Value must be an integer') .max(60000, 'Maximum size cannot exceed 60TB') .optional() .nullable(), }) export const CreateDiskStorageSchema = ({ defaultTotalSize, cloudProvider, isSpendCapEnabled, }: { defaultTotalSize: number cloudProvider: CloudProvider isSpendCapEnabled: boolean }) => { const isFlyProject = cloudProvider === 'FLY' const isAwsNimbusProject = cloudProvider === 'AWS_NIMBUS' const isAwsK8sProject = cloudProvider === 'AWS_K8S' const validateDiskConfiguration = !isFlyProject && !isAwsNimbusProject && !isAwsK8sProject const schema = baseSchema.superRefine((data, ctx) => { const { storageType, totalSize, provisionedIOPS, throughput, maxSizeGb, computeSize } = data const computeMaxIops = (() => { const parsedCompute = computeInstanceAddonVariantIdSchema.safeParse(computeSize) if (!parsedCompute.success) return Number.POSITIVE_INFINITY return COMPUTE_MAX_IOPS[parsedCompute.data] ?? Number.POSITIVE_INFINITY })() if (validateDiskConfiguration && totalSize < 8 && totalSize !== defaultTotalSize) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'New disk size must be at least 8 GB.', path: ['totalSize'], }) } if ( validateDiskConfiguration && isSpendCapEnabled && totalSize > 8 && totalSize !== defaultTotalSize ) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Disable spend cap to increase disk above 8 GB.', path: ['totalSize'], }) } if (validateDiskConfiguration && totalSize < defaultTotalSize) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Disk size cannot be reduced in size. Reduce your database size and then head to the Infrastructure settings and go through a Postgres version upgrade to right-size your disk.`, path: ['totalSize'], }) } // Validate maxSizeGb cannot be lower than totalSize if (validateDiskConfiguration && !!maxSizeGb && maxSizeGb < totalSize) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Max disk size cannot be lower than the current disk size. Must be at least ${formatNumber(totalSize)} GB.`, path: ['maxSizeGb'], }) } if (validateDiskConfiguration && storageType === 'io2') { // Validation rules for io2 const maxIopsForIo2 = Math.min(DISK_LIMITS[DiskType.IO2].maxIops, computeMaxIops) if (provisionedIOPS > maxIopsForIo2) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `IOPS cannot exceed ${formatNumber(maxIopsForIo2)} for io2 Disk type and the selected compute size.`, path: ['provisionedIOPS'], }) } const maxIOPSforDiskSizeWithio2 = calculateMaxIopsAllowedForDiskSizeWithio2(totalSize) if (provisionedIOPS < DISK_LIMITS[DiskType.IO2].minIops) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Provisioned IOPS must be at least ${formatNumber(DISK_LIMITS[DiskType.IO2].minIops)}`, path: ['provisionedIOPS'], }) } else if (provisionedIOPS > maxIOPSforDiskSizeWithio2) { if (totalSize >= 8) { const diskSizeRequiredForIopsWithIo2 = calculateDiskSizeRequiredForIopsWithIo2(provisionedIOPS) if (diskSizeRequiredForIopsWithIo2 > totalSize) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Larger Disk size of at least ${formatNumber(diskSizeRequiredForIopsWithIo2)} GB required. Current max is ${formatNumber(maxIOPSforDiskSizeWithio2)} IOPS.`, path: ['provisionedIOPS'], }) } } else { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Invalid IOPS value due to small disk size`, path: ['provisionedIOPS'], }) } } if (throughput !== undefined) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Throughput is not configurable for io2.', path: ['throughput'], }) } if (totalSize > DISK_LIMITS[DiskType.IO2].maxStorage) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Allocated disksize must not exceed ${formatNumber(DISK_LIMITS[DiskType.IO2].maxStorage)} GB `, path: ['totalSize'], }) } } if (validateDiskConfiguration && storageType === 'gp3') { const maxIopsAllowedForDiskSizeWithGp3 = calculateMaxIopsAllowedForDiskSizeWithGp3(totalSize) const maxIopsForGp3 = Math.min(DISK_LIMITS[DiskType.GP3].maxIops, computeMaxIops) const computeMaxThroughput = (() => { const parsedCompute = computeInstanceAddonVariantIdSchema.safeParse(computeSize) if (!parsedCompute.success) return DISK_LIMITS[DiskType.GP3].maxThroughput return COMPUTE_MAX_THROUGHPUT[parsedCompute.data] ?? DISK_LIMITS[DiskType.GP3].maxThroughput })() if (provisionedIOPS > maxIopsForGp3) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `IOPS cannot exceed ${formatNumber(maxIopsForGp3)} for GP3 Disk and the selected compute size.`, path: ['provisionedIOPS'], }) } if (provisionedIOPS < DISK_LIMITS[DiskType.GP3].minIops) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `IOPS must be at least ${formatNumber(DISK_LIMITS[DiskType.GP3].minIops)}`, path: ['provisionedIOPS'], }) } else if (provisionedIOPS > maxIopsAllowedForDiskSizeWithGp3) { const diskSizeRequiredForIopsWithGp3 = calculateDiskSizeRequiredForIopsWithGp3(provisionedIOPS) ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Larger Disk size of at least ${formatNumber(diskSizeRequiredForIopsWithGp3)} GB required. Current max is ${formatNumber(maxIopsAllowedForDiskSizeWithGp3)} IOPS.`, path: ['provisionedIOPS'], }) } if (throughput !== undefined) { // gp3 throughput scales with provisioned IOPS (capped by gp3 max) const iopsThroughputLimit = calculateMaxThroughput(provisionedIOPS) if (throughput > DISK_LIMITS[DiskType.GP3].maxThroughput) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Throughput cannot exceed ${formatNumber(DISK_LIMITS[DiskType.GP3].maxThroughput)} MB/s for GP3 disk type.`, path: ['throughput'], }) } else if (throughput > computeMaxThroughput) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Throughput cannot exceed ${formatNumber(computeMaxThroughput)} MB/s for the selected compute size.`, path: ['throughput'], }) } else if (throughput > iopsThroughputLimit) { const iopsRequiredForThroughput = calculateIopsRequiredForThroughput(throughput) ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Need at least ${formatNumber(iopsRequiredForThroughput)} IOPS to support ${formatNumber(throughput)} MB/s.`, path: ['throughput'], }) } if (throughput < DISK_LIMITS[DiskType.GP3].minThroughput) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Throughput must be at least ${formatNumber(DISK_LIMITS[DiskType.GP3].minThroughput)} MB/s`, path: ['throughput'], }) } } if (totalSize > DISK_LIMITS[DiskType.GP3].maxStorage) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Allocated disksize must not exceed ${formatNumber(DISK_LIMITS[DiskType.GP3].maxStorage)} GB`, path: ['totalSize'], }) } } }) return schema } export type DiskStorageSchemaType = z.infer>