From 7002f58e65a484efd778b3b76ab3445fb8cad6aa Mon Sep 17 00:00:00 2001 From: Lior Soffer Date: Fri, 27 Feb 2026 18:41:25 +0200 Subject: [PATCH 1/2] MGMT-23299: Make secondary VIP fields optional Signed-off-by: Lior Soffer --- libs/locales/lib/en/translation.json | 3 ++- .../validationSchemas/addressValidation.tsx | 27 +++++++++++-------- .../NetworkConfigurationForm.tsx | 9 ++++--- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/libs/locales/lib/en/translation.json b/libs/locales/lib/en/translation.json index 2102cc3bf8..0eafbc6141 100644 --- a/libs/locales/lib/en/translation.json +++ b/libs/locales/lib/en/translation.json @@ -870,7 +870,6 @@ "ai:Tang servers' URLs or thumbprints": "Tang servers' URLs or thumbprints", "ai:Technology Preview": "Technology Preview", "ai:Technology preview features provide early access to upcoming product innovations, enabling you to test functionality and provide feedback during the development process.": "Technology preview features provide early access to upcoming product innovations, enabling you to test functionality and provide feedback during the development process.", - "ai:The {{label}} Ingress and API IP addresses cannot be the same.": "The {{label}} Ingress and API IP addresses cannot be the same.", "ai:The agent is not bound to a cluster.": "The agent is not bound to a cluster.", "ai:The agent ran successfully": "The agent ran successfully", "ai:The block must not overlap with existing physical networks. To access the Pods from an external network, configure load balancers and routers to manage the traffic.": "The block must not overlap with existing physical networks. To access the Pods from an external network, configure load balancers and routers to manage the traffic.", @@ -902,10 +901,12 @@ "ai:The IP address that hosts will use to communicate with the bootstrap node during installation.": "The IP address that hosts will use to communicate with the bootstrap node during installation.", "ai:The MAC address of the host's network connected NIC that will be used to provision the host.": "The MAC address of the host's network connected NIC that will be used to provision the host.", "ai:The output displays the following:": "The output displays the following:", + "ai:The primary Ingress and API IP addresses cannot be the same.": "The primary Ingress and API IP addresses cannot be the same.", "ai:The resource has been saved and the YAML is now read only.": "The resource has been saved and the YAML is now read only.", "ai:The resource you are changing is already in use by hosts in the infrastructure environment. A change will require booting the hosts with a new discovery ISO file.": "The resource you are changing is already in use by hosts in the infrastructure environment. A change will require booting the hosts with a new discovery ISO file.", "ai:The resource you are changing is already in use by hosts in the infrastructure environment. A change will require booting the hosts with a new discovery ISO file. Hosts will be rebooted automatically after the change is applied if using BMC.": "The resource you are changing is already in use by hosts in the infrastructure environment. A change will require booting the hosts with a new discovery ISO file. Hosts will be rebooted automatically after the change is applied if using BMC.", "ai:The resource you are changing is already in use by hosts in the infrastructure environment. The hosts will be rebooted automatically after the change is applied.": "The resource you are changing is already in use by hosts in the infrastructure environment. The hosts will be rebooted automatically after the change is applied.", + "ai:The secondary Ingress and API IP addresses cannot be the same.": "The secondary Ingress and API IP addresses cannot be the same.", "ai:The specified CIDR is invalid because its resulting routing prefix matches the unspecified address.": "The specified CIDR is invalid because its resulting routing prefix matches the unspecified address.", "ai:The storage sizes will be used to store different files and data for cluster creation.": "The storage sizes will be used to store different files and data for cluster creation.", "ai:The subnet prefix length to assign to each individual node.": "The subnet prefix length to assign to each individual node.", diff --git a/libs/ui-lib/lib/common/validationSchemas/addressValidation.tsx b/libs/ui-lib/lib/common/validationSchemas/addressValidation.tsx index ac6466479d..374ee4d505 100644 --- a/libs/ui-lib/lib/common/validationSchemas/addressValidation.tsx +++ b/libs/ui-lib/lib/common/validationSchemas/addressValidation.tsx @@ -7,13 +7,7 @@ import { getAddress } from '../components/ui/formik/utils'; import { getSubnet } from '../components/clusterConfiguration/utils'; import { HostSubnets, NetworkConfigurationValues } from '../types'; import { IP_V4_ZERO, IP_V6_ZERO, MAC_REGEX } from './regexes'; -import { - alwaysRequired, - getArrayIndexFromPath, - isIPorDN, - isIPv4Address, - isIPv6Address, -} from './utils'; +import { getArrayIndexFromPath, isIPorDN, isIPv4Address, isIPv6Address } from './utils'; import { NO_SUBNET_SET } from '../config'; import { overlap } from 'cidr-tools'; import parseUrl from 'parse-url'; @@ -28,6 +22,7 @@ export const ipValidationSchema = (t: TFunction) => export const ipNoSuffixValidationSchema = (t: TFunction) => Yup.string().test('ip-validation-no-suffix', t('ai:Not a valid IP address'), (value?: string) => { + if (!value) return true; const address = getAddress(value || ''); return !!address && address.address === address.addressMinusSuffix; }); @@ -173,15 +168,25 @@ export const vipNoSuffixValidationSchema = ( } if (apiVip === ingressVip) { return this.createError({ - message: t('ai:The {{label}} Ingress and API IP addresses cannot be the same.', { - label: index === 0 ? t('ai:primary') : t('ai:secondary'), - }), + message: + index === 0 + ? t('ai:The primary Ingress and API IP addresses cannot be the same.') + : t('ai:The secondary Ingress and API IP addresses cannot be the same.'), }); } return true; }); + const requiredField = (message: string) => + Yup.string().test('required-field', message, function (value?: string) { + const index = getArrayIndexFromPath(this.path || ''); + const api1 = (values.apiVips?.[1]?.ip ?? '').toString().trim(); + const ingress1 = (values.ingressVips?.[1]?.ip ?? '').toString().trim(); + // Dual-stack: secondary VIPs (index 1) are optional; skip required when both are empty + if (index === 1 && api1 === '' && ingress1 === '') return true; + return !!value; + }); - return alwaysRequired(t('ai:Required field')) + return requiredField(t('ai:Required field')) .concat(ipNoSuffixValidationSchema(t)) .concat(vipFamilyMatchSchema) .concat(vipRangeValidationSchema(hostSubnets, values, false, t)) diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/networkConfiguration/NetworkConfigurationForm.tsx b/libs/ui-lib/lib/ocm/components/clusterConfiguration/networkConfiguration/NetworkConfigurationForm.tsx index ddc4874491..f86a32d27e 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/networkConfiguration/NetworkConfigurationForm.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/networkConfiguration/NetworkConfigurationForm.tsx @@ -236,9 +236,13 @@ const NetworkConfigurationPage = ({ cluster }: { cluster: Cluster }) => { const isMultiNodeCluster = !isSNO(cluster); const isUserManagedNetworking = values.managedNetworkingType === 'userManaged'; + // Only send VIPs that have a non-empty IP; + const apiVips = (values.apiVips || []).filter((v) => (v?.ip ?? '').trim() !== ''); + const ingressVips = (values.ingressVips || []).filter((v) => (v?.ip ?? '').trim() !== ''); + const params: V2ClusterUpdateParams = { - apiVips: values.apiVips, - ingressVips: values.ingressVips, + apiVips, + ingressVips, sshPublicKey: values.sshPublicKey, vipDhcpAllocation: values.vipDhcpAllocation, networkType: values.networkType, @@ -247,7 +251,6 @@ const NetworkConfigurationPage = ({ cluster }: { cluster: Cluster }) => { serviceNetworks: values.serviceNetworks, userManagedNetworking: isUserManagedNetworking, }; - if (params.userManagedNetworking) { params.apiVips = []; params.ingressVips = []; From 47e5f3b11d43aaa245f56ba13eec4371b4e8cf2d Mon Sep 17 00:00:00 2001 From: Lior Soffer Date: Mon, 2 Mar 2026 10:54:19 +0200 Subject: [PATCH 2/2] remove * Signed-off-by: Lior Soffer --- .../clusterWizard/networkingSteps/VirtualIPControlGroup.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/libs/ui-lib/lib/common/components/clusterWizard/networkingSteps/VirtualIPControlGroup.tsx b/libs/ui-lib/lib/common/components/clusterWizard/networkingSteps/VirtualIPControlGroup.tsx index 4eaf044d46..990d4654c1 100644 --- a/libs/ui-lib/lib/common/components/clusterWizard/networkingSteps/VirtualIPControlGroup.tsx +++ b/libs/ui-lib/lib/common/components/clusterWizard/networkingSteps/VirtualIPControlGroup.tsx @@ -243,7 +243,6 @@ export const VirtualIPControlGroup = ({ } name="apiVips.1.ip" maxLength={45} - isRequired isDisabled={isVipInputDisabled || isViewerMode} labelInfo={t('ai:Secondary')} /> @@ -275,7 +274,6 @@ export const VirtualIPControlGroup = ({ } maxLength={45} - isRequired isDisabled={isVipInputDisabled || isViewerMode} labelInfo={t('ai:Secondary')} />