Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Add node pool firewall selection to LKE-E create flow ([#12712](https://github.com/linode/manager/pull/12712))
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export const CreateCluster = () => {
isLkeEnterpriseLAFeatureEnabled,
isLkeEnterpriseLAFlagEnabled,
isLkeEnterprisePhase2FeatureEnabled,
isLkeEnterprisePostLAFeatureEnabled,
} = useIsLkeEnterpriseEnabled();

// Use React Hook Form for node pools to make updating pools and their configs easier.
Expand Down Expand Up @@ -278,7 +279,7 @@ export const CreateCluster = () => {
setSubmitting(true);

const node_pools = nodePools.map(
pick(['type', 'count', 'update_strategy'])
pick(['type', 'count', 'update_strategy', 'firewall_id'])
) as CreateNodePoolData[];

const vpcId = form.getValues('vpc_id');
Expand Down Expand Up @@ -347,7 +348,11 @@ export const CreateCluster = () => {
: createKubernetesCluster;

// TODO: Improve error handling in M3-10429, at which point we shouldn't need this.
if (isLkeEnterprisePhase2FeatureEnabled && selectedTier === 'enterprise') {
if (
(isLkeEnterprisePostLAFeatureEnabled ||
isLkeEnterprisePhase2FeatureEnabled) &&
selectedTier === 'enterprise'
) {
// Trigger the React Hook Form validation for BYO VPC selection.
const isValid = await trigger();
// Don't submit the form while RHF errors persist.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ import { NodePoolConfigOptions } from './NodePoolConfigOptions';

import type { CreateClusterFormValues } from '../CreateCluster/CreateCluster';
import type {
CreateNodePoolData,
KubernetesTier,
NodePoolUpdateStrategy,
Region,
} from '@linode/api-v4';
import type { Theme } from '@linode/ui';
Expand All @@ -45,11 +45,6 @@ export interface Props {
selectedTier: KubernetesTier;
}

interface VersionUpdateFormFields {
nodeCount: number;
updateStrategy: NodePoolUpdateStrategy | undefined;
}

export const NodePoolConfigDrawer = (props: Props) => {
const {
onClose,
Expand All @@ -74,23 +69,24 @@ export const NodePoolConfigDrawer = (props: Props) => {
});

// Manage node pool options within this drawer form.
const { control, formState, setValue, ...form } =
useForm<VersionUpdateFormFields>({
const { control, formState, setValue, ...form } = useForm<CreateNodePoolData>(
Comment thread
mjac0bs marked this conversation as resolved.
{
defaultValues: {
nodeCount: DEFAULT_PLAN_COUNT,
count: DEFAULT_PLAN_COUNT,
},
shouldUnregister: true, // For conditionally defined fields (e.g. updateStrategy)
});
shouldUnregister: true, // For conditionally defined fields (e.g. update_strategy, firewall_id)
}
);

const typesQuery = useSpecificTypes(planId ? [planId] : []);
const planType = typesQuery[0]?.data
? extendType(typesQuery[0].data)
: undefined;

// Keep track of the node count to display an accurate price.
const nodeCountWatcher = useWatch({ control, name: 'nodeCount' });
const nodeCountWatcher = useWatch({ control, name: 'count' });
const updatedCount =
nodeCountWatcher ?? form.getValues('nodeCount') ?? DEFAULT_PLAN_COUNT;
nodeCountWatcher ?? form.getValues('count') ?? DEFAULT_PLAN_COUNT;
const pricePerNode = getLinodeRegionPrice(
planType,
selectedRegion?.toString()
Expand All @@ -107,31 +103,35 @@ export const NodePoolConfigDrawer = (props: Props) => {
}
// Ensure the update strategy resets when the tier is changed.
setValue(
'updateStrategy',
'update_strategy',
selectedTier === 'enterprise' ? 'on_recycle' : undefined
);
setValue('firewall_id', undefined);

// If we're in edit mode, set the existing config values on the pool.
if (!isAddMode && poolIndex !== undefined) {
setValue('nodeCount', _nodePools[poolIndex]?.count);
setValue('updateStrategy', _nodePools[poolIndex]?.update_strategy);
setValue('count', _nodePools[poolIndex]?.count);
setValue('update_strategy', _nodePools[poolIndex]?.update_strategy);
setValue('firewall_id', _nodePools[poolIndex]?.firewall_id);
}
}, [planId, open, selectedTier, setValue, isAddMode, poolIndex, _nodePools]);

const onSubmit = async (values: VersionUpdateFormFields) => {
const onSubmit = async (values: CreateNodePoolData) => {
try {
// If there's a pool index, the drawer is in edit mode. Else, it's in add mode.
if (poolIndex !== undefined) {
update(poolIndex, {
..._nodePools[poolIndex],
count: values.nodeCount,
update_strategy: values.updateStrategy,
count: values.count,
update_strategy: values.update_strategy,
firewall_id: values.firewall_id,
});
} else if (planId) {
append({
count: values.nodeCount,
count: values.count,
type: planId,
update_strategy: values.updateStrategy,
update_strategy: values.update_strategy,
firewall_id: values.firewall_id,
});
}
onClose();
Expand Down Expand Up @@ -181,7 +181,7 @@ export const NodePoolConfigDrawer = (props: Props) => {
<Box marginBottom={4} marginTop={2}>
<Controller
control={control}
name="nodeCount"
name="count"
render={({ field }) => (
<EnhancedNumberInput
inputLabel={`edit-quantity-${planId}`}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import * as React from 'react';
import { Controller, useFormContext } from 'react-hook-form';

import { NodePoolFirewallSelect } from '../NodePoolFirewallSelect';
import { NodePoolUpdateStrategySelect } from '../NodePoolUpdateStrategySelect';

import type { CreateNodePoolData } from '@linode/api-v4';

export const NodePoolConfigOptions = () => {
const { control } = useFormContext();
const { control } = useFormContext<CreateNodePoolData>();

return (
<Controller
control={control}
name="updateStrategy"
render={({ field }) => (
<NodePoolUpdateStrategySelect
onChange={field.onChange}
value={field.value}
/>
)}
/>
<>
<Controller
control={control}
name="update_strategy"
render={({ field }) => (
<NodePoolUpdateStrategySelect
onChange={field.onChange}
value={field.value}
/>
)}
/>
<NodePoolFirewallSelect />
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {
FormControlLabel,
Radio,
RadioGroup,
Stack,
Typography,
} from '@linode/ui';
import React from 'react';
import { Controller, useFormContext, useWatch } from 'react-hook-form';

import { FirewallSelect } from '../Firewalls/components/FirewallSelect';

import type { CreateNodePoolData } from '@linode/api-v4';

export const NodePoolFirewallSelect = () => {
const { control } = useFormContext<CreateNodePoolData>();
const watchedFirewallId = useWatch({ control, name: 'firewall_id' });

const [isUsingOwnFirewall, setIsUsingOwnFirewall] = React.useState(
Boolean(watchedFirewallId)
);

return (
<Stack marginTop={3}>
<Typography
sx={(theme) => ({
font: theme.tokens.alias.Typography.Label.Bold.S,
})}
>
Firewall
</Typography>
<RadioGroup
aria-label="Bring your own firewall"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setIsUsingOwnFirewall(e.target.value === 'yes');
}}
value={isUsingOwnFirewall}
>
<FormControlLabel
checked={!isUsingOwnFirewall}
control={<Radio />}
label="Use default firewall"
value="no"
/>
<FormControlLabel
checked={isUsingOwnFirewall}
control={<Radio />}
label="Select existing firewall"
value="yes"
/>
</RadioGroup>
{isUsingOwnFirewall && (
<Controller
control={control}
name="firewall_id"
render={({ field, fieldState }) => (
<FirewallSelect
errorText={fieldState.error?.message}
onBlur={field.onBlur}
onChange={(e, firewall) => field.onChange(firewall?.id ?? null)}
placeholder="Select firewall"
value={field.value}
/>
)}
rules={{
validate: (value) => {
if (isUsingOwnFirewall && !value) {
return 'You must either select a Firewall or select the default firewall.';
}
return true;
},
}}
/>
)}
</Stack>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ interface Props {
label?: string;
noMarginTop?: boolean;
onChange: (value: string | undefined) => void;
value: string;
value: string | undefined;
}

export const NodePoolUpdateStrategySelect = (props: Props) => {
Expand Down