Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PCUI] Support On-Demand Capacity Reservations and Capacity Blocks for PCUI #347

Merged
merged 5 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions frontend/locales/en/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -1082,7 +1082,24 @@
"description": "Prevent multiple threads from running on an EC2 instance core."
}
},
"enableEfa": "Use EFA"
"enableEfa": "Use EFA",
"capacityReservationTarget": {
"title": "Capacity reservations",
"label": "CapacityReservationTarget",
"help": {
"title": "Capacity Reservations and Capacity Blocks",
"description": "Configure your cluster to use Capacity Reservations or Capacity Blocks for better resource management and availability.",
"odcrLink": "Launch instances using On-Demand Capacity Reservations (ODCR)",
"capacityBlocksLink": "Launch instances using Capacity Blocks",
"note": "When selecting CapacityReservationId, the specified instance type will automatically apply."
}
},
"capacityReservationResourceGroupArn": {
"label": "CapacityReservationResourceGroupArn"
},
"capacityReservationId": {
"label": "CapacityReservationId"
}
},
"advancedOptions": {
"label": "Advanced options",
Expand Down Expand Up @@ -1448,4 +1465,4 @@
"label": "Choose file"
}
}
}
}
88 changes: 87 additions & 1 deletion frontend/src/old-pages/Configure/Components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
// OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and
// limitations under the License.

// Fameworks
// Frameworks
import React, {
ReactElement,
useCallback,
Expand Down Expand Up @@ -677,6 +677,91 @@ function IamPoliciesEditor({basePath}: any) {
)
}

function OdcrCbSelect({
selectedOption,
onChange,
inputValue,
onInputChange,
}: {
selectedOption: string
onChange: (event: any) => void
inputValue: string
onInputChange: (event: any) => void
}) {
const { t } = useTranslation()

const options = [
{ label: 'none', value: 'none' },
{ label: t('wizard.queues.computeResource.capacityReservationId.label'), value: 'capacityReservationId' },
{ label: t('wizard.queues.computeResource.capacityReservationResourceGroupArn.label'), value: 'capacityReservationResourceGroupArn' },
]

const getPlaceholder = () => {
if (selectedOption === 'capacityReservationId') {
return "cr-<reservation-id>"
}
if (selectedOption === 'capacityReservationResourceGroupArn') {
return "arn:aws:resource-groups:<region>:<account-id>:group/<resource-group-name>"
}
return ""
}

return (
<SpaceBetween direction="vertical" size="xs">
<FormField
label={t('wizard.queues.computeResource.capacityReservationTarget.label')}
info={
<InfoLink
helpPanel={
<TitleDescriptionHelpPanel
title={t('wizard.queues.computeResource.capacityReservationTarget.help.title')}
description={
<>
<p>{t('wizard.queues.computeResource.capacityReservationTarget.help.description')}</p>
<ul>
<li>
<a href="https://docs.aws.amazon.com/parallelcluster/latest/ug/launch-instances-odcr-v3.html" target="_blank" rel="noopener noreferrer">
{t('wizard.queues.computeResource.capacityReservationTarget.help.odcrLink')}
</a>
</li>
<li>
<a href="https://docs.aws.amazon.com/parallelcluster/latest/ug/launch-instances-capacity-blocks.html" target="_blank" rel="noopener noreferrer">
{t('wizard.queues.computeResource.capacityReservationTarget.help.capacityBlocksLink')}
</a>
</li>
</ul>
<p>{t('wizard.queues.computeResource.capacityReservationTarget.help.note')}</p>
</>
}
/>
}
/>
}
>
<Select
selectedOption={
options.find(option => option.value === selectedOption) || {
label: 'none',
value: 'none',
}
}
onChange={onChange}
options={options}
/>
</FormField>
{(selectedOption === 'capacityReservationId' || selectedOption === 'capacityReservationResourceGroupArn') && (
<FormField label={`${t(`wizard.queues.computeResource.${selectedOption}.label`)}`}>
<Input
placeholder={getPlaceholder()}
value={inputValue}
onChange={onInputChange}
/>
</FormField>
)}
</SpaceBetween>
)
}

type HelpTextInputProps = {
name: string
path: string[]
Expand Down Expand Up @@ -788,4 +873,5 @@ export {
IamPoliciesEditor,
HelpTextInput,
CheckboxWithHelpPanel,
OdcrCbSelect,
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as React from 'react'
import {
ColumnLayout,
FormField,
Expand All @@ -17,6 +18,7 @@ import {clearState, setState, useState} from '../../../store'
import {
CheckboxWithHelpPanel,
HelpTextInput,
OdcrCbSelect,
useInstanceGroups,
} from '../Components'
import {
Expand Down Expand Up @@ -207,6 +209,30 @@ export function ComputeResource({
[efaInstances, instanceTypePath, setEnableEFA],
)

const [odcrCbOption, setOdcrCbOption] = React.useState('none')
const [odcrCbInput, setOdcrCbInput] = React.useState('')

const capacityReservationTargetPath = useMemo(
() => [...path, 'CapacityReservationTarget'],
[path],
)

React.useEffect(() => {
if (odcrCbOption === 'none') {
clearState(capacityReservationTargetPath)
} else {
const updateData = {
CapacityReservationId: odcrCbOption === 'capacityReservationId' ? odcrCbInput : undefined,
CapacityReservationResourceGroupArn: odcrCbOption === 'capacityReservationResourceGroupArn' ? odcrCbInput : undefined,
}
setState(capacityReservationTargetPath, updateData)

if (odcrCbOption === 'capacityReservationId') {
clearState(instanceTypePath)
}
}
}, [odcrCbOption, odcrCbInput])

return (
<SpaceBetween direction="vertical" size="s">
<div className={componentsStyle['space-between-wrap']}>
Expand Down Expand Up @@ -247,24 +273,27 @@ export function ComputeResource({
/>
</FormField>
</SpaceBetween>
<FormField
label={t('wizard.queues.computeResource.instanceType.label')}
errorText={typeError}
>
<Multiselect
selectedOptions={instances.map(instance => ({
value: instance.InstanceType,
label: instance.InstanceType,
}))}
placeholder={t(
'wizard.queues.computeResource.instanceType.placeholder.multiple',
)}
tokenLimit={3}
onChange={setInstances}
options={instanceOptions}
filteringType="auto"
/>
</FormField>
{/* Render the instance type selection field only when 'capacityReservationId' is not selected */}
{odcrCbOption !== 'capacityReservationId' && (
<FormField
label={t('wizard.queues.computeResource.instanceType.label')}
errorText={typeError}
>
<Multiselect
selectedOptions={instances.map(instance => ({
value: instance.InstanceType,
label: instance.InstanceType,
}))}
placeholder={t(
'wizard.queues.computeResource.instanceType.placeholder.multiple',
)}
tokenLimit={3}
onChange={setInstances}
options={instanceOptions}
filteringType="auto"
/>
</FormField>
)}
{enableMemoryBasedScheduling && (
<HelpTextInput
name={t('wizard.queues.schedulableMemory.name')}
Expand All @@ -282,6 +311,17 @@ export function ComputeResource({
)}
</ColumnLayout>
<SpaceBetween direction="vertical" size="s">
<OdcrCbSelect
selectedOption={odcrCbOption}
onChange={({detail}) => {
setOdcrCbOption(detail.selectedOption.value)
if (detail.selectedOption.value === 'none') {
setOdcrCbInput('')
}
}}
inputValue={odcrCbInput}
onInputChange={({detail}) => setOdcrCbInput(detail.value)}
/>
<CheckboxWithHelpPanel
checked={multithreadingDisabled}
disabled={hpcInstanceSelected}
Expand Down Expand Up @@ -347,7 +387,10 @@ export function validateComputeResources(
computeResources: MultiInstanceComputeResource[],
): [boolean, QueueValidationErrors] {
let errors = computeResources.reduce<QueueValidationErrors>((acc, cr, i) => {
if (!cr.Instances || !cr.Instances.length) {
const hasCapacityReservationId = cr.CapacityReservationTarget?.CapacityReservationId

// Skip instance type validation if CapacityReservationId is set
if (!hasCapacityReservationId && (!cr.Instances || !cr.Instances.length)) {
acc[i] = 'instance_types_empty'
}
return acc
Expand Down
24 changes: 16 additions & 8 deletions frontend/src/old-pages/Configure/Queues/Queues.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -387,15 +387,16 @@ function Queue({index}: any) {
const subnetError = useState([...errorsPath, 'subnet'])
const nameError = useState([...errorsPath, 'name'])

const allocationStrategy: AllocationStrategy = useState([
...queuesPath,
index,
'AllocationStrategy',
])
const allocationStrategyPath = React.useMemo(
() => [...queuesPath, index, 'AllocationStrategy'],
[index],
)
const allocationStrategy: AllocationStrategy = useState(allocationStrategyPath)

const capacityTypes: [string, string, string][] = [
['ONDEMAND', 'On-Demand', '/pcui/img/od.svg'],
['SPOT', 'Spot', '/pcui/img/spot.svg'],
['CAPACITY_BLOCK', 'Capacity Block', '/pcui/img/cb.svg'],
]
const capacityTypePath = [...queuesPath, index, 'CapacityType']
const capacityType: string = useState(capacityTypePath) || 'ONDEMAND'
Expand All @@ -404,6 +405,12 @@ function Queue({index}: any) {
const subnetsList = useState(subnetPath) || []
const isMultiAZActive = useFeatureFlag('multi_az')

React.useEffect(() => {
if (capacityType === 'CAPACITY_BLOCK') {
clearState(allocationStrategyPath)
}
}, [capacityType])

const remove = () => {
setState(
[...queuesPath],
Expand Down Expand Up @@ -458,11 +465,11 @@ function Queue({index}: any) {
const setAllocationStrategy = React.useCallback(
({detail}) => {
setState(
[...queuesPath, index, 'AllocationStrategy'],
allocationStrategyPath,
detail.selectedOption.value,
)
},
[index],
[allocationStrategyPath],
)

const defaultAllocationStrategy = useDefaultAllocationStrategy()
Expand Down Expand Up @@ -659,7 +666,8 @@ function Queue({index}: any) {
options={capacityTypes.map(itemToOption)}
/>
</FormField>
{isMultiInstanceTypesActive ? (
{/* If the type is CAPACITY_BLOCK, do not display the AllocationStrategy */}
{isMultiInstanceTypesActive && capacityType !== 'CAPACITY_BLOCK' ? (
<FormField
label={t('wizard.queues.allocationStrategy.title')}
info={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const hasMultipleInstanceTypes = (queues: Queue[]): boolean => {
computeResources.map(computeResource => computeResource.Instances),
)
.flat()
.filter(instances => instances.length > 1).length > 0
.filter(instances => instances && instances.length > 1).length > 0
)
}

Expand Down
24 changes: 17 additions & 7 deletions frontend/src/old-pages/Configure/Queues/queues.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,29 @@ import {
function mapComputeResource(
computeResource: SingleInstanceComputeResource | MultiInstanceComputeResource,
): MultiInstanceComputeResource {
if ('Instances' in computeResource) {
// If it's already a MultiInstanceComputeResource and Instances exist, return it directly
if ('Instances' in computeResource && computeResource.Instances?.length) {
return computeResource
}

const {InstanceType, ...otherComputeResourceConfig} = computeResource
// If it's of SingleInstanceComputeResource type, convert it to MultiInstanceComputeResource
if ('InstanceType' in computeResource) {
const {InstanceType, ...otherComputeResourceConfig} = computeResource

return {
...otherComputeResourceConfig,
Instances: [
{
InstanceType,
},
],
}
}

// If Instances are cleared or do not exist, return the other configurations
const {Instances, ...otherComputeResourceConfig} = computeResource
return {
...otherComputeResourceConfig,
Instances: [
{
InstanceType,
},
],
}
}

Expand Down
8 changes: 7 additions & 1 deletion frontend/src/old-pages/Configure/Queues/queues.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,14 @@ export type AllocationStrategy = 'lowest-price' | 'capacity-optimized'

export type ComputeResourceInstance = {InstanceType: string}

export type CapacityReservationTarget = {
CapacityReservationId?: string
CapacityReservationResourceGroupArn?: string
}

export type MultiInstanceComputeResource = ComputeResource & {
Instances: ComputeResourceInstance[]
Instances?: ComputeResourceInstance[]
CapacityReservationTarget?: CapacityReservationTarget
}

export type Tag = {
Expand Down
Loading