Skip to content

Commit

Permalink
[PCUI] Support On-Demand Capacity Reservations and Capacity Blocks fo…
Browse files Browse the repository at this point in the history
…r PCUI (#347)

* feat: Support ODCR(On-Demand Capacity Reservations) and CB(Capacity Blocks) for PCUI

- Added `CapacityReservationTarget` select component at the compute resource level, allowing users to choose between `CapacityReservationId`, `CapacityReservationResourceGroupArn`, or none.
- Implemented dynamic placeholders to guide users on the required inputs for `CapacityReservationId` and `CapacityReservationResourceGroupArn`.
- Introduced `CAPACITY_BLOCK` as a new purchase type option in the UI.
- Automatically hide the Allocation Strategy selection when `CAPACITY_BLOCK` is selected, following the expected behavior in ParallelCluster.
- Implemented logic to automatically hide the instance type selection and exclude the `Instances` section in the YAML when `CapacityReservationId` is selected.
- Made the `Instances` property of `MultiInstanceComputeResource` optional.
- Updated the mechanism in `queues.mapper.ts` to resolve the conversion conflict.
- Added `CapacityReservationTarget` in queue types.
- Updated `validateComputeResources` to adapt to the new changes, allowing `Instances` to be empty if `CapacityReservationId` is selected. 
- Added info panel for Capacity Reservation and Capacity Block, included documentation links and note on instance type usage.
  • Loading branch information
hehe7318 authored Aug 26, 2024
1 parent 9ea1d0d commit 81cffc4
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 39 deletions.
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

0 comments on commit 81cffc4

Please sign in to comment.