-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cd8ks): volume expansion support (#126)
- Loading branch information
Showing
7 changed files
with
338 additions
and
69 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
import * as schema from '@chainfile/schema'; | ||
import { describe, expect, it } from '@workspace/jest/globals'; | ||
|
||
import { calculateStorage } from './volume'; | ||
|
||
it('should calculate volume without expansion', async () => { | ||
const volume: schema.Volume = { | ||
type: 'persistent', | ||
size: '100Gi', | ||
}; | ||
|
||
expect( | ||
calculateStorage(volume, { | ||
min: 3, | ||
max: 6, | ||
today: new Date('2021-09-01'), | ||
}), | ||
).toEqual({ value: '102400Mi' }); | ||
}); | ||
|
||
describe('expansion', () => { | ||
it.each([ | ||
{ | ||
size: '10000Mi', | ||
rate: '10Mi', | ||
startFrom: '2024-01-01', | ||
min: 1, | ||
max: 12, | ||
today: '2024-01-01', | ||
expected: '10220Mi', | ||
}, | ||
{ | ||
size: '10000Mi', | ||
rate: '10Mi', | ||
startFrom: '2024-01-01', | ||
min: 3, | ||
max: 9, | ||
today: '2024-06-01', | ||
expected: '10180Mi', | ||
}, | ||
{ | ||
size: '10000Mi', | ||
rate: '10Mi', | ||
startFrom: '2024-01-01', | ||
min: 3, | ||
max: 6, | ||
today: '2024-06-01', | ||
expected: '10120Mi', | ||
}, | ||
{ | ||
size: '10000Mi', | ||
rate: '10Mi', | ||
startFrom: '2024-01-01', | ||
min: 3, | ||
max: 6, | ||
today: '2024-07-01', | ||
expected: '10120Mi', | ||
}, | ||
{ | ||
size: '10000Mi', | ||
rate: '10Mi', | ||
startFrom: '2024-01-01', | ||
min: 3, | ||
max: 6, | ||
today: '2024-08-01', | ||
expected: '10150Mi', | ||
}, | ||
{ | ||
size: '10000Mi', | ||
rate: '10Mi', | ||
startFrom: '2024-01-01', | ||
min: 3, | ||
max: 6, | ||
today: '2024-09-01', | ||
expected: '10150Mi', | ||
}, | ||
{ | ||
size: '10000Mi', | ||
rate: '10Mi', | ||
startFrom: '2024-01-01', | ||
min: 3, | ||
max: 6, | ||
today: '2024-10-01', | ||
expected: '10150Mi', | ||
}, | ||
{ | ||
size: '10000Mi', | ||
rate: '10Mi', | ||
startFrom: '2024-01-01', | ||
min: 3, | ||
max: 6, | ||
today: '2024-11-01', | ||
expected: '10180Mi', | ||
}, | ||
{ | ||
size: '10000Mi', | ||
rate: '10Mi', | ||
startFrom: '2024-01-01', | ||
min: 4, | ||
max: 6, | ||
today: '2024-09-01', | ||
expected: '10140Mi', | ||
}, | ||
{ | ||
size: '10000Mi', | ||
rate: '10Mi', | ||
startFrom: '2024-01-01', | ||
min: 6, | ||
max: 9, | ||
today: '2024-08-01', | ||
expected: '10180Mi', | ||
}, | ||
{ | ||
size: '10000Mi', | ||
rate: '10Mi', | ||
startFrom: '2024-01-01', | ||
min: 6, | ||
max: 9, | ||
today: '2024-09-01', | ||
expected: '10180Mi', | ||
}, | ||
{ | ||
size: '10000Mi', | ||
rate: '10Mi', | ||
startFrom: '2024-01-01', | ||
min: 6, | ||
max: 9, | ||
today: '2024-10-01', | ||
expected: '10180Mi', | ||
}, | ||
{ | ||
size: '10000Mi', | ||
rate: '10Mi', | ||
startFrom: '2024-01-01', | ||
min: 6, | ||
max: 9, | ||
today: '2024-11-01', | ||
expected: '10210Mi', | ||
}, | ||
{ | ||
size: '10000Mi', | ||
rate: '10Mi', | ||
startFrom: '2024-01-01', | ||
min: 6, | ||
max: 9, | ||
today: '2024-12-01', | ||
expected: '10210Mi', | ||
}, | ||
{ | ||
size: '10000Mi', | ||
rate: '10Mi', | ||
startFrom: '2024-01-01', | ||
min: 6, | ||
max: 9, | ||
today: '2025-01-01', | ||
expected: '10210Mi', | ||
}, | ||
{ | ||
size: '10000Mi', | ||
rate: '10Mi', | ||
startFrom: '2024-01-01', | ||
min: 6, | ||
max: 9, | ||
today: '2025-02-01', | ||
expected: '10240Mi', | ||
}, | ||
])('should calculate volume with expansion %p', async ({ size, rate, startFrom, min, max, today, expected }) => { | ||
const volume: schema.Volume = { | ||
type: 'persistent', | ||
size: size, | ||
expansion: { | ||
startFrom: startFrom, | ||
monthlyRate: rate, | ||
}, | ||
}; | ||
|
||
expect( | ||
calculateStorage(volume, { | ||
min: min, | ||
max: max, | ||
today: new Date(today), | ||
}), | ||
).toEqual({ value: expected }); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import * as schema from '@chainfile/schema'; | ||
import { PersistentVolumeClaimSpec, Quantity, Volume } from 'cdk8s-plus-25/lib/imports/k8s'; | ||
|
||
export function CFEphemeralVolume(props: { name: string; volume: schema.Volume }): Volume { | ||
return { | ||
name: props.name, | ||
emptyDir: { | ||
sizeLimit: calculateStorage(props.volume, { | ||
min: 3, | ||
max: 6, | ||
}), | ||
}, | ||
}; | ||
} | ||
|
||
export function CFPersistentVolumeClaimSpec(props: { volume: schema.Volume }): PersistentVolumeClaimSpec { | ||
return { | ||
accessModes: ['ReadWriteOnce'], | ||
resources: { | ||
requests: { | ||
storage: calculateStorage(props.volume, { | ||
min: 3, | ||
max: 6, | ||
}), | ||
}, | ||
}, | ||
}; | ||
} | ||
|
||
type SizeUnit = 'M' | 'G' | 'T'; | ||
|
||
/** | ||
* Determines the storage size for a volume based on its configuration. | ||
* | ||
* If the volume has an expansion policy, the size is calculated based on the monthly rate. | ||
* The `options.min` and `options.max` parameter defines the boundary of the volume in monthly intervals. | ||
* To reduce drift, the volume size is rounded up to the next interval (interval=max-min). | ||
* This means that the volume size will be at least `options.min` and at most `options.max` after the first month. | ||
* | ||
* Although the volume can technically shrink when the schema changes. | ||
* Volume shrinking is not supported by most storage providers which will result in "kubectl apply" error. | ||
*/ | ||
export function calculateStorage( | ||
volume: schema.Volume, | ||
options: { | ||
min: number; | ||
max: number; | ||
today?: Date; | ||
}, | ||
): Quantity { | ||
const sizeMi = parseSizeAsMi(volume.size); | ||
|
||
if (volume.expansion) { | ||
const today = options.today ?? new Date(Date.now()); | ||
const startFrom = new Date(volume.expansion.startFrom); | ||
if (today >= startFrom) { | ||
const monthsSince = | ||
(today.getFullYear() - startFrom.getFullYear()) * 12 + (today.getMonth() - startFrom.getMonth()); | ||
const interval = options.max - options.min; | ||
const monthsSinceRounded = Math.ceil((monthsSince + options.max) / interval) * interval; | ||
const growthRateMi = parseSizeAsMi(volume.expansion.monthlyRate); | ||
|
||
const finalMi = sizeMi + growthRateMi * Math.max(0, monthsSinceRounded); | ||
return Quantity.fromString(`${finalMi}Mi`); | ||
} | ||
} | ||
|
||
return Quantity.fromString(`${sizeMi}Mi`); | ||
} | ||
|
||
function parseSizeAsMi(size: string): number { | ||
const match = size.match(/^(\d+)([MGT])i$/); | ||
if (!match) { | ||
throw new Error(`Invalid size format: ${size}`); | ||
} | ||
|
||
const value = parseInt(match[1], 10); | ||
const unit = match[2] as SizeUnit; | ||
|
||
return convertToMi(value, unit); | ||
} | ||
|
||
function convertToMi(value: number, unit: SizeUnit): number { | ||
switch (unit) { | ||
case 'M': | ||
return value; | ||
case 'G': | ||
return value * 1024; | ||
case 'T': | ||
return value * 1024 * 1024; | ||
default: | ||
throw new Error(`Invalid unit: ${unit}`); | ||
} | ||
} |
Oops, something went wrong.