DiskManagement.schema.ts249 lines · main
1import {
2 CloudProvider,
3 COMPUTE_MAX_IOPS,
4 COMPUTE_MAX_THROUGHPUT,
5 computeInstanceAddonVariantIdSchema,
6} from 'shared-data'
7import { z } from 'zod'
8
9import {
10 calculateDiskSizeRequiredForIopsWithGp3,
11 calculateDiskSizeRequiredForIopsWithIo2,
12 calculateIopsRequiredForThroughput,
13 calculateMaxIopsAllowedForDiskSizeWithGp3,
14 calculateMaxIopsAllowedForDiskSizeWithio2,
15 calculateMaxThroughput,
16 formatNumber,
17} from './DiskManagement.utils'
18import { DISK_LIMITS, DiskType } from './ui/DiskManagement.constants'
19
20const baseSchema = z.object({
21 storageType: z.enum(['io2', 'gp3']).describe('Type of storage: io2 or gp3'),
22 totalSize: z.number().int('Value must be an integer').describe('Allocated disk size in GB'),
23 provisionedIOPS: z.number().describe('Provisioned IOPS for storage type'),
24 throughput: z.number().optional().describe('Throughput in MB/s for gp3'),
25 computeSize: computeInstanceAddonVariantIdSchema
26 .describe('Compute size')
27 .optional()
28 .default('ci_micro'),
29 growthPercent: z
30 .number()
31 .int('Value must be an integer')
32 .min(10, 'Growth percent must be at least 10%')
33 .max(100, 'Growth percent cannot exceed 100%')
34 .optional()
35 .nullable(),
36 minIncrementGb: z
37 .number()
38 .int('Value must be an integer')
39 .min(1, 'Minimum increment must be at least 1 GB')
40 .max(200, 'Minimum increment cannot exceed 200 GB')
41 .optional()
42 .nullable(),
43 maxSizeGb: z
44 .number()
45 .int('Value must be an integer')
46 .max(60000, 'Maximum size cannot exceed 60TB')
47 .optional()
48 .nullable(),
49})
50
51export const CreateDiskStorageSchema = ({
52 defaultTotalSize,
53 cloudProvider,
54 isSpendCapEnabled,
55}: {
56 defaultTotalSize: number
57 cloudProvider: CloudProvider
58 isSpendCapEnabled: boolean
59}) => {
60 const isFlyProject = cloudProvider === 'FLY'
61 const isAwsNimbusProject = cloudProvider === 'AWS_NIMBUS'
62 const isAwsK8sProject = cloudProvider === 'AWS_K8S'
63
64 const validateDiskConfiguration = !isFlyProject && !isAwsNimbusProject && !isAwsK8sProject
65
66 const schema = baseSchema.superRefine((data, ctx) => {
67 const { storageType, totalSize, provisionedIOPS, throughput, maxSizeGb, computeSize } = data
68 const computeMaxIops = (() => {
69 const parsedCompute = computeInstanceAddonVariantIdSchema.safeParse(computeSize)
70 if (!parsedCompute.success) return Number.POSITIVE_INFINITY
71 return COMPUTE_MAX_IOPS[parsedCompute.data] ?? Number.POSITIVE_INFINITY
72 })()
73
74 if (validateDiskConfiguration && totalSize < 8 && totalSize !== defaultTotalSize) {
75 ctx.addIssue({
76 code: z.ZodIssueCode.custom,
77 message: 'New disk size must be at least 8 GB.',
78 path: ['totalSize'],
79 })
80 }
81
82 if (
83 validateDiskConfiguration &&
84 isSpendCapEnabled &&
85 totalSize > 8 &&
86 totalSize !== defaultTotalSize
87 ) {
88 ctx.addIssue({
89 code: z.ZodIssueCode.custom,
90 message: 'Disable spend cap to increase disk above 8 GB.',
91 path: ['totalSize'],
92 })
93 }
94
95 if (validateDiskConfiguration && totalSize < defaultTotalSize) {
96 ctx.addIssue({
97 code: z.ZodIssueCode.custom,
98 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.`,
99 path: ['totalSize'],
100 })
101 }
102
103 // Validate maxSizeGb cannot be lower than totalSize
104 if (validateDiskConfiguration && !!maxSizeGb && maxSizeGb < totalSize) {
105 ctx.addIssue({
106 code: z.ZodIssueCode.custom,
107 message: `Max disk size cannot be lower than the current disk size. Must be at least ${formatNumber(totalSize)} GB.`,
108 path: ['maxSizeGb'],
109 })
110 }
111
112 if (validateDiskConfiguration && storageType === 'io2') {
113 // Validation rules for io2
114
115 const maxIopsForIo2 = Math.min(DISK_LIMITS[DiskType.IO2].maxIops, computeMaxIops)
116 if (provisionedIOPS > maxIopsForIo2) {
117 ctx.addIssue({
118 code: z.ZodIssueCode.custom,
119 message: `IOPS cannot exceed ${formatNumber(maxIopsForIo2)} for io2 Disk type and the selected compute size.`,
120 path: ['provisionedIOPS'],
121 })
122 }
123
124 const maxIOPSforDiskSizeWithio2 = calculateMaxIopsAllowedForDiskSizeWithio2(totalSize)
125
126 if (provisionedIOPS < DISK_LIMITS[DiskType.IO2].minIops) {
127 ctx.addIssue({
128 code: z.ZodIssueCode.custom,
129 message: `Provisioned IOPS must be at least ${formatNumber(DISK_LIMITS[DiskType.IO2].minIops)}`,
130 path: ['provisionedIOPS'],
131 })
132 } else if (provisionedIOPS > maxIOPSforDiskSizeWithio2) {
133 if (totalSize >= 8) {
134 const diskSizeRequiredForIopsWithIo2 =
135 calculateDiskSizeRequiredForIopsWithIo2(provisionedIOPS)
136
137 if (diskSizeRequiredForIopsWithIo2 > totalSize) {
138 ctx.addIssue({
139 code: z.ZodIssueCode.custom,
140 message: `Larger Disk size of at least ${formatNumber(diskSizeRequiredForIopsWithIo2)} GB required. Current max is ${formatNumber(maxIOPSforDiskSizeWithio2)} IOPS.`,
141 path: ['provisionedIOPS'],
142 })
143 }
144 } else {
145 ctx.addIssue({
146 code: z.ZodIssueCode.custom,
147 message: `Invalid IOPS value due to small disk size`,
148 path: ['provisionedIOPS'],
149 })
150 }
151 }
152
153 if (throughput !== undefined) {
154 ctx.addIssue({
155 code: z.ZodIssueCode.custom,
156 message: 'Throughput is not configurable for io2.',
157 path: ['throughput'],
158 })
159 }
160
161 if (totalSize > DISK_LIMITS[DiskType.IO2].maxStorage) {
162 ctx.addIssue({
163 code: z.ZodIssueCode.custom,
164 message: `Allocated disksize must not exceed ${formatNumber(DISK_LIMITS[DiskType.IO2].maxStorage)} GB `,
165 path: ['totalSize'],
166 })
167 }
168 }
169
170 if (validateDiskConfiguration && storageType === 'gp3') {
171 const maxIopsAllowedForDiskSizeWithGp3 = calculateMaxIopsAllowedForDiskSizeWithGp3(totalSize)
172 const maxIopsForGp3 = Math.min(DISK_LIMITS[DiskType.GP3].maxIops, computeMaxIops)
173 const computeMaxThroughput = (() => {
174 const parsedCompute = computeInstanceAddonVariantIdSchema.safeParse(computeSize)
175 if (!parsedCompute.success) return DISK_LIMITS[DiskType.GP3].maxThroughput
176 return COMPUTE_MAX_THROUGHPUT[parsedCompute.data] ?? DISK_LIMITS[DiskType.GP3].maxThroughput
177 })()
178
179 if (provisionedIOPS > maxIopsForGp3) {
180 ctx.addIssue({
181 code: z.ZodIssueCode.custom,
182 message: `IOPS cannot exceed ${formatNumber(maxIopsForGp3)} for GP3 Disk and the selected compute size.`,
183 path: ['provisionedIOPS'],
184 })
185 }
186
187 if (provisionedIOPS < DISK_LIMITS[DiskType.GP3].minIops) {
188 ctx.addIssue({
189 code: z.ZodIssueCode.custom,
190 message: `IOPS must be at least ${formatNumber(DISK_LIMITS[DiskType.GP3].minIops)}`,
191 path: ['provisionedIOPS'],
192 })
193 } else if (provisionedIOPS > maxIopsAllowedForDiskSizeWithGp3) {
194 const diskSizeRequiredForIopsWithGp3 =
195 calculateDiskSizeRequiredForIopsWithGp3(provisionedIOPS)
196
197 ctx.addIssue({
198 code: z.ZodIssueCode.custom,
199 message: `Larger Disk size of at least ${formatNumber(diskSizeRequiredForIopsWithGp3)} GB required. Current max is ${formatNumber(maxIopsAllowedForDiskSizeWithGp3)} IOPS.`,
200 path: ['provisionedIOPS'],
201 })
202 }
203
204 if (throughput !== undefined) {
205 // gp3 throughput scales with provisioned IOPS (capped by gp3 max)
206 const iopsThroughputLimit = calculateMaxThroughput(provisionedIOPS)
207 if (throughput > DISK_LIMITS[DiskType.GP3].maxThroughput) {
208 ctx.addIssue({
209 code: z.ZodIssueCode.custom,
210 message: `Throughput cannot exceed ${formatNumber(DISK_LIMITS[DiskType.GP3].maxThroughput)} MB/s for GP3 disk type.`,
211 path: ['throughput'],
212 })
213 } else if (throughput > computeMaxThroughput) {
214 ctx.addIssue({
215 code: z.ZodIssueCode.custom,
216 message: `Throughput cannot exceed ${formatNumber(computeMaxThroughput)} MB/s for the selected compute size.`,
217 path: ['throughput'],
218 })
219 } else if (throughput > iopsThroughputLimit) {
220 const iopsRequiredForThroughput = calculateIopsRequiredForThroughput(throughput)
221 ctx.addIssue({
222 code: z.ZodIssueCode.custom,
223 message: `Need at least ${formatNumber(iopsRequiredForThroughput)} IOPS to support ${formatNumber(throughput)} MB/s.`,
224 path: ['throughput'],
225 })
226 }
227 if (throughput < DISK_LIMITS[DiskType.GP3].minThroughput) {
228 ctx.addIssue({
229 code: z.ZodIssueCode.custom,
230 message: `Throughput must be at least ${formatNumber(DISK_LIMITS[DiskType.GP3].minThroughput)} MB/s`,
231 path: ['throughput'],
232 })
233 }
234 }
235
236 if (totalSize > DISK_LIMITS[DiskType.GP3].maxStorage) {
237 ctx.addIssue({
238 code: z.ZodIssueCode.custom,
239 message: `Allocated disksize must not exceed ${formatNumber(DISK_LIMITS[DiskType.GP3].maxStorage)} GB`,
240 path: ['totalSize'],
241 })
242 }
243 }
244 })
245
246 return schema
247}
248
249export type DiskStorageSchemaType = z.infer<ReturnType<typeof CreateDiskStorageSchema>>