Skip to content
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
148 changes: 132 additions & 16 deletions libs/ui-lib/lib/common/components/ui/formik/validationSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,17 @@ export const ipValidationSchema = Yup.string().test(
(value?: string) => Address4.isValid(value || '') || Address6.isValid(value || ''),
);

// Helpers to classify literal IPs more robustly
const isIPv4Address = (ip?: string) => {
if (!ip) return false;
return ip.includes('.') && Address4.isValid(ip);
};

const isIPv6Address = (ip?: string) => {
if (!ip) return false;
return ip.includes(':') && Address6.isValid(ip);
};

export const ipNoSuffixValidationSchema = Yup.string().test(
'ip-validation-no-suffix',
'Not a valid IP address',
Expand Down Expand Up @@ -202,7 +213,6 @@ export const vipRangeValidationSchema = (
} catch (err) {
return true;
}

const foundHostSubnets = [];
if (machineNetworks) {
const cidrs = machineNetworks?.map((network) => network.cidr);
Expand Down Expand Up @@ -303,12 +313,51 @@ export const vipNoSuffixValidationSchema = (
vipDhcpAllocation: NetworkConfigurationValues['vipDhcpAllocation'],
managedNetworkingType: NetworkConfigurationValues['managedNetworkingType'],
) => !vipDhcpAllocation && managedNetworkingType !== 'userManaged',
then: () =>
requiredOnceSet(head(initialValues)?.ip, 'Required. Please provide an IP address')
then: () => {
// Per-item schema for VIPs (apiVips[*].ip / ingressVips[*].ip)
// 1) Require value once set
// 2) Validate IP without suffix
// 3) Ensure VIP family matches machineNetworks[idx] family
// 4) Validate range vs selected subnets
// 5) No broadcast/network address
// 6) Uniqueness across API/Ingress
const vipFamilyMatchSchema = Yup.string().test(
'vip-family-match-machine-network',
'IP family must match the corresponding machine network family.',
function (value?: string) {
if (!value) {
return true;
}
// this.path looks like: "apiVips[0].ip" or "ingressVips[1].ip"
const path = this.path || '';
const match = path.match(/\[(\d+)\]/);
const index = match ? parseInt(match[1], 10) : NaN;
if (Number.isNaN(index)) {
return true;
}
const cidr = values.machineNetworks?.[index]?.cidr || '';
const mnIsV4 = isCIDR.v4(cidr);
const mnIsV6 = isCIDR.v6(cidr);
// If machine network at index is not selected/valid, don't block validation here
if (!mnIsV4 && !mnIsV6) {
return true;
}
const ipIsV4 = isIPv4Address(value);
const ipIsV6 = isIPv6Address(value);
if (!ipIsV4 && !ipIsV6) {
return true;
}
return mnIsV4 ? ipIsV4 : ipIsV6;
},
);

return requiredOnceSet(head(initialValues)?.ip, 'Required. Please provide an IP address')
.concat(ipNoSuffixValidationSchema)
.concat(vipFamilyMatchSchema)
.concat(vipRangeValidationSchema(hostSubnets, values, false))
.concat(vipBroadcastValidationSchema(values))
.concat(vipUniqueValidationSchema(values, true)),
.concat(vipUniqueValidationSchema(values, true));
},
});

export const vipArrayValidationSchema = <T extends Yup.Maybe<Yup.AnyObject>>(
Expand All @@ -324,11 +373,39 @@ export const vipArrayValidationSchema = <T extends Yup.Maybe<Yup.AnyObject>>(
}),
)
: Yup.array<T>()
).test(
'vips-length',
'Both API and ingress APIs must be provided.',
(_value) => values.apiVips?.length === values.ingressVips?.length,
);
)
.test(
'vips-length',
'Both API and ingress APIs must be provided.',
(_value) => values.apiVips?.length === values.ingressVips?.length,
)
.test(
'vips-match-machine-networks',
'Primary API and Ingress IPs must match the primary machine network family; secondary must match the secondary machine network family.',
(_vips?: { ip?: string }[]) => {
const machineNetworks = values.machineNetworks || [];
const validateIndex = (idx: number) => {
const cidr = machineNetworks[idx]?.cidr || '';
const mIsIpv4 = isCIDR.v4(cidr);
const mIsIpv6 = isCIDR.v6(cidr);
if (!mIsIpv4 && !mIsIpv6) {
return true;
}
const api = values.apiVips?.[idx]?.ip || '';
const ing = values.ingressVips?.[idx]?.ip || '';
if (!api || !ing) {
return true;
}
const apiOk = mIsIpv4 ? isIPv4Address(api) : isIPv6Address(api);
const ingOk = mIsIpv4 ? isIPv4Address(ing) : isIPv6Address(ing);
return apiOk && ingOk;
};

const primaryOk = validateIndex(0);
const secondaryOk = validateIndex(1);
return primaryOk && secondaryOk;
},
);

export const ipBlockValidationSchema = (reservedCidrs: string | string[] | undefined) =>
Yup.string()
Expand Down Expand Up @@ -440,11 +517,11 @@ export const hostPrefixValidationSchema = (
const errorMsgIPv4 = `${errorMsgPrefix} (${netBlockNumber}) and 25.`;
const errorMsgIPv6 = `${errorMsgPrefix} (8) and 128.`;

if (Address6.isValid(clusterNetworkCidr || '')) {
if (isCIDR.v6(clusterNetworkCidr || '')) {
return Yup.number().required(requiredText).min(8, errorMsgIPv6).max(128, errorMsgIPv6);
}

if (Address4.isValid(clusterNetworkCidr || '')) {
if (isCIDR.v4(clusterNetworkCidr || '')) {
return Yup.number()
.required(requiredText)
.min(netBlockNumber, errorMsgIPv4)
Expand Down Expand Up @@ -715,13 +792,52 @@ export const dualStackValidationSchema = (field: string, openshiftVersion?: stri
(values?: { cidr: MachineNetwork['cidr'] }[]): boolean => {
// For OCP versions > 4.11, allow IPv6 as primary network
if (openshiftVersion && isMajorMinorVersionEqualOrGreater(openshiftVersion, '4.12')) {
return (
!!values?.[0].cidr &&
(Address4.isValid(values[0].cidr) || Address6.isValid(values[0].cidr))
);
return !!values?.[0].cidr && (isCIDR.v4(values[0].cidr) || isCIDR.v6(values[0].cidr));
}
// For older versions, require IPv4 as primary network
return !!values?.[0].cidr && Address4.isValid(values[0].cidr);
return !!values?.[0].cidr && isCIDR.v4(values[0].cidr);
},
)
.test(
'dual-stack-unique-cidrs',
`Provided ${field} subnets must be unique.`,
(values?: { cidr?: MachineNetwork['cidr'] }[]) => {
if (!values || values.length < 2) {
return true;
}
const first = values[0]?.cidr || '';
const second = values[1]?.cidr || '';
if (!first || !second) {
return true;
}
const firstIsCidr = isCIDR.v4(first) || isCIDR.v6(first);
const secondIsCidr = isCIDR.v4(second) || isCIDR.v6(second);
if (!firstIsCidr || !secondIsCidr) {
return true;
}
return first !== second;
},
)
.test(
'dual-stack-opposite-families',
`When two ${field} are provided, one must be IPv4 and the other IPv6.`,
(values?: { cidr?: MachineNetwork['cidr'] }[]) => {
if (!values || values.length < 2) {
return true;
}
const a = values[0]?.cidr || '';
const b = values[1]?.cidr || '';
if (!a || !b) {
return true;
}
const a4 = isCIDR.v4(a);
const a6 = isCIDR.v6(a);
const b4 = isCIDR.v4(b);
const b6 = isCIDR.v6(b);
if (!((a4 || a6) && (b4 || b6))) {
return true;
}
return (a4 && b6) || (a6 && b4);
},
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,35 @@ export const AvailableSubnetsControl = ({
const autoSelectNetwork = !isViewerMode && hasEmptySelection;
useAutoSelectSingleAvailableSubnet(autoSelectNetwork, setFieldValue, cidr, clusterId);

// Ensure primary and secondary machine networks are not duplicated.
// If the user switches primary to the same CIDR as secondary, adjust secondary to the opposite family or clear it.
React.useEffect(() => {
if (!isDualStack) {
return;
}
if (!values.machineNetworks || values.machineNetworks.length < 2) {
return;
}
const first = values.machineNetworks?.[0]?.cidr || '';
const second = values.machineNetworks?.[1]?.cidr || '';
if (!first || !second) {
return;
}
if (first === second) {
if (Address6.isValid(first)) {
const nextIPv4 = IPv4Subnets[0]?.subnet || NO_SUBNET_SET;
const replacement = nextIPv4 !== first ? nextIPv4 : NO_SUBNET_SET;
setFieldValue('machineNetworks.1.cidr', replacement, true);
} else if (Address4.isValid(first)) {
const nextIPv6 = IPv6Subnets[0]?.subnet || NO_SUBNET_SET;
const replacement = nextIPv6 !== first ? nextIPv6 : NO_SUBNET_SET;
setFieldValue('machineNetworks.1.cidr', replacement, true);
} else {
setFieldValue('machineNetworks.1.cidr', NO_SUBNET_SET, true);
}
}
}, [isDualStack, values.machineNetworks, IPv4Subnets, IPv6Subnets, setFieldValue]);

return (
<>
<FormGroup
Expand Down
Loading