DiskManagement.schema.ts 9.2 KB

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