@openshift-assisted/ui-lib 2.47.1 → 2.47.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/@types/common/components/clusterConfiguration/utils.d.ts +1 -3
- package/build/@types/common/components/clusterConfiguration/utils.d.ts.map +1 -1
- package/build/@types/common/components/ui/formik/validationSchemas.d.ts.map +1 -1
- package/build/@types/ocm/components/clusterConfiguration/networkConfiguration/AdvancedNetworkFields.d.ts.map +1 -1
- package/build/@types/ocm/components/clusterConfiguration/networkConfiguration/AvailableSubnetsControl.d.ts.map +1 -1
- package/build/@types/ocm/components/clusterConfiguration/networkConfiguration/StackTypeControl.d.ts.map +1 -1
- package/build/@types/ocm/components/clusterConfiguration/networkConfiguration/SubnetsDropdown.d.ts +2 -1
- package/build/@types/ocm/components/clusterConfiguration/networkConfiguration/SubnetsDropdown.d.ts.map +1 -1
- package/build/@types/ocm/components/clusterConfiguration/networkConfiguration/networkConfigurationValidation.d.ts.map +1 -1
- package/build/@types/ocm/components/clusterConfiguration/networkConfiguration/reorderNetworks.d.ts +5 -0
- package/build/@types/ocm/components/clusterConfiguration/networkConfiguration/reorderNetworks.d.ts.map +1 -0
- package/build/@types/ocm/components/clusterConfiguration/review/ReviewNetworkingTable.d.ts.map +1 -1
- package/build/@types/ocm/components/clusterDetail/ClusterProperties.d.ts.map +1 -1
- package/build/cjs/common/components/clusterConfiguration/utils.js.map +1 -1
- package/build/cjs/common/components/ui/formik/validationSchemas.js +110 -6
- package/build/cjs/common/components/ui/formik/validationSchemas.js.map +1 -1
- package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/AdvancedNetworkFields.js +1 -50
- package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/AdvancedNetworkFields.js.map +1 -1
- package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/AvailableSubnetsControl.js +41 -3
- package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/AvailableSubnetsControl.js.map +1 -1
- package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/StackTypeControl.js +26 -4
- package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/StackTypeControl.js.map +1 -1
- package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/SubnetsDropdown.js +6 -2
- package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/SubnetsDropdown.js.map +1 -1
- package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/networkConfigurationValidation.js +32 -37
- package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/networkConfigurationValidation.js.map +1 -1
- package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/reorderNetworks.js +60 -0
- package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/reorderNetworks.js.map +1 -0
- package/build/cjs/ocm/components/clusterConfiguration/review/ReviewNetworkingTable.js +27 -9
- package/build/cjs/ocm/components/clusterConfiguration/review/ReviewNetworkingTable.js.map +1 -1
- package/build/cjs/ocm/components/clusterDetail/ClusterProperties.js +2 -2
- package/build/cjs/ocm/components/clusterDetail/ClusterProperties.js.map +1 -1
- package/build/cjs/ocm/components/clusters/AssistedInstallerHeader.js +1 -1
- package/build/cjs/ocm/components/clusters/AssistedInstallerHeader.js.map +1 -1
- package/lib/common/components/clusterConfiguration/utils.ts +1 -3
- package/lib/common/components/ui/formik/validationSchemas.ts +132 -16
- package/lib/ocm/components/clusterConfiguration/networkConfiguration/AdvancedNetworkFields.tsx +1 -54
- package/lib/ocm/components/clusterConfiguration/networkConfiguration/AvailableSubnetsControl.tsx +43 -1
- package/lib/ocm/components/clusterConfiguration/networkConfiguration/StackTypeControl.tsx +28 -3
- package/lib/ocm/components/clusterConfiguration/networkConfiguration/SubnetsDropdown.tsx +7 -1
- package/lib/ocm/components/clusterConfiguration/networkConfiguration/networkConfigurationValidation.ts +6 -14
- package/lib/ocm/components/clusterConfiguration/networkConfiguration/reorderNetworks.ts +72 -0
- package/lib/ocm/components/clusterConfiguration/review/ReviewNetworkingTable.tsx +39 -14
- package/lib/ocm/components/clusterDetail/ClusterProperties.tsx +2 -2
- package/lib/ocm/components/clusters/AssistedInstallerHeader.tsx +1 -1
- package/package.json +3 -3
|
@@ -173,6 +173,17 @@ export const ipValidationSchema = Yup.string().test(
|
|
|
173
173
|
(value?: string) => Address4.isValid(value || '') || Address6.isValid(value || ''),
|
|
174
174
|
);
|
|
175
175
|
|
|
176
|
+
// Helpers to classify literal IPs more robustly
|
|
177
|
+
const isIPv4Address = (ip?: string) => {
|
|
178
|
+
if (!ip) return false;
|
|
179
|
+
return ip.includes('.') && Address4.isValid(ip);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const isIPv6Address = (ip?: string) => {
|
|
183
|
+
if (!ip) return false;
|
|
184
|
+
return ip.includes(':') && Address6.isValid(ip);
|
|
185
|
+
};
|
|
186
|
+
|
|
176
187
|
export const ipNoSuffixValidationSchema = Yup.string().test(
|
|
177
188
|
'ip-validation-no-suffix',
|
|
178
189
|
'Not a valid IP address',
|
|
@@ -202,7 +213,6 @@ export const vipRangeValidationSchema = (
|
|
|
202
213
|
} catch (err) {
|
|
203
214
|
return true;
|
|
204
215
|
}
|
|
205
|
-
|
|
206
216
|
const foundHostSubnets = [];
|
|
207
217
|
if (machineNetworks) {
|
|
208
218
|
const cidrs = machineNetworks?.map((network) => network.cidr);
|
|
@@ -303,12 +313,51 @@ export const vipNoSuffixValidationSchema = (
|
|
|
303
313
|
vipDhcpAllocation: NetworkConfigurationValues['vipDhcpAllocation'],
|
|
304
314
|
managedNetworkingType: NetworkConfigurationValues['managedNetworkingType'],
|
|
305
315
|
) => !vipDhcpAllocation && managedNetworkingType !== 'userManaged',
|
|
306
|
-
then: () =>
|
|
307
|
-
|
|
316
|
+
then: () => {
|
|
317
|
+
// Per-item schema for VIPs (apiVips[*].ip / ingressVips[*].ip)
|
|
318
|
+
// 1) Require value once set
|
|
319
|
+
// 2) Validate IP without suffix
|
|
320
|
+
// 3) Ensure VIP family matches machineNetworks[idx] family
|
|
321
|
+
// 4) Validate range vs selected subnets
|
|
322
|
+
// 5) No broadcast/network address
|
|
323
|
+
// 6) Uniqueness across API/Ingress
|
|
324
|
+
const vipFamilyMatchSchema = Yup.string().test(
|
|
325
|
+
'vip-family-match-machine-network',
|
|
326
|
+
'IP family must match the corresponding machine network family.',
|
|
327
|
+
function (value?: string) {
|
|
328
|
+
if (!value) {
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
// this.path looks like: "apiVips[0].ip" or "ingressVips[1].ip"
|
|
332
|
+
const path = this.path || '';
|
|
333
|
+
const match = path.match(/\[(\d+)\]/);
|
|
334
|
+
const index = match ? parseInt(match[1], 10) : NaN;
|
|
335
|
+
if (Number.isNaN(index)) {
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
const cidr = values.machineNetworks?.[index]?.cidr || '';
|
|
339
|
+
const mnIsV4 = isCIDR.v4(cidr);
|
|
340
|
+
const mnIsV6 = isCIDR.v6(cidr);
|
|
341
|
+
// If machine network at index is not selected/valid, don't block validation here
|
|
342
|
+
if (!mnIsV4 && !mnIsV6) {
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
const ipIsV4 = isIPv4Address(value);
|
|
346
|
+
const ipIsV6 = isIPv6Address(value);
|
|
347
|
+
if (!ipIsV4 && !ipIsV6) {
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
return mnIsV4 ? ipIsV4 : ipIsV6;
|
|
351
|
+
},
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
return requiredOnceSet(head(initialValues)?.ip, 'Required. Please provide an IP address')
|
|
308
355
|
.concat(ipNoSuffixValidationSchema)
|
|
356
|
+
.concat(vipFamilyMatchSchema)
|
|
309
357
|
.concat(vipRangeValidationSchema(hostSubnets, values, false))
|
|
310
358
|
.concat(vipBroadcastValidationSchema(values))
|
|
311
|
-
.concat(vipUniqueValidationSchema(values, true))
|
|
359
|
+
.concat(vipUniqueValidationSchema(values, true));
|
|
360
|
+
},
|
|
312
361
|
});
|
|
313
362
|
|
|
314
363
|
export const vipArrayValidationSchema = <T extends Yup.Maybe<Yup.AnyObject>>(
|
|
@@ -324,11 +373,39 @@ export const vipArrayValidationSchema = <T extends Yup.Maybe<Yup.AnyObject>>(
|
|
|
324
373
|
}),
|
|
325
374
|
)
|
|
326
375
|
: Yup.array<T>()
|
|
327
|
-
)
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
376
|
+
)
|
|
377
|
+
.test(
|
|
378
|
+
'vips-length',
|
|
379
|
+
'Both API and ingress APIs must be provided.',
|
|
380
|
+
(_value) => values.apiVips?.length === values.ingressVips?.length,
|
|
381
|
+
)
|
|
382
|
+
.test(
|
|
383
|
+
'vips-match-machine-networks',
|
|
384
|
+
'Primary API and Ingress IPs must match the primary machine network family; secondary must match the secondary machine network family.',
|
|
385
|
+
(_vips?: { ip?: string }[]) => {
|
|
386
|
+
const machineNetworks = values.machineNetworks || [];
|
|
387
|
+
const validateIndex = (idx: number) => {
|
|
388
|
+
const cidr = machineNetworks[idx]?.cidr || '';
|
|
389
|
+
const mIsIpv4 = isCIDR.v4(cidr);
|
|
390
|
+
const mIsIpv6 = isCIDR.v6(cidr);
|
|
391
|
+
if (!mIsIpv4 && !mIsIpv6) {
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
const api = values.apiVips?.[idx]?.ip || '';
|
|
395
|
+
const ing = values.ingressVips?.[idx]?.ip || '';
|
|
396
|
+
if (!api || !ing) {
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
const apiOk = mIsIpv4 ? isIPv4Address(api) : isIPv6Address(api);
|
|
400
|
+
const ingOk = mIsIpv4 ? isIPv4Address(ing) : isIPv6Address(ing);
|
|
401
|
+
return apiOk && ingOk;
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const primaryOk = validateIndex(0);
|
|
405
|
+
const secondaryOk = validateIndex(1);
|
|
406
|
+
return primaryOk && secondaryOk;
|
|
407
|
+
},
|
|
408
|
+
);
|
|
332
409
|
|
|
333
410
|
export const ipBlockValidationSchema = (reservedCidrs: string | string[] | undefined) =>
|
|
334
411
|
Yup.string()
|
|
@@ -440,11 +517,11 @@ export const hostPrefixValidationSchema = (
|
|
|
440
517
|
const errorMsgIPv4 = `${errorMsgPrefix} (${netBlockNumber}) and 25.`;
|
|
441
518
|
const errorMsgIPv6 = `${errorMsgPrefix} (8) and 128.`;
|
|
442
519
|
|
|
443
|
-
if (
|
|
520
|
+
if (isCIDR.v6(clusterNetworkCidr || '')) {
|
|
444
521
|
return Yup.number().required(requiredText).min(8, errorMsgIPv6).max(128, errorMsgIPv6);
|
|
445
522
|
}
|
|
446
523
|
|
|
447
|
-
if (
|
|
524
|
+
if (isCIDR.v4(clusterNetworkCidr || '')) {
|
|
448
525
|
return Yup.number()
|
|
449
526
|
.required(requiredText)
|
|
450
527
|
.min(netBlockNumber, errorMsgIPv4)
|
|
@@ -715,13 +792,52 @@ export const dualStackValidationSchema = (field: string, openshiftVersion?: stri
|
|
|
715
792
|
(values?: { cidr: MachineNetwork['cidr'] }[]): boolean => {
|
|
716
793
|
// For OCP versions > 4.11, allow IPv6 as primary network
|
|
717
794
|
if (openshiftVersion && isMajorMinorVersionEqualOrGreater(openshiftVersion, '4.12')) {
|
|
718
|
-
return (
|
|
719
|
-
!!values?.[0].cidr &&
|
|
720
|
-
(Address4.isValid(values[0].cidr) || Address6.isValid(values[0].cidr))
|
|
721
|
-
);
|
|
795
|
+
return !!values?.[0].cidr && (isCIDR.v4(values[0].cidr) || isCIDR.v6(values[0].cidr));
|
|
722
796
|
}
|
|
723
797
|
// For older versions, require IPv4 as primary network
|
|
724
|
-
return !!values?.[0].cidr &&
|
|
798
|
+
return !!values?.[0].cidr && isCIDR.v4(values[0].cidr);
|
|
799
|
+
},
|
|
800
|
+
)
|
|
801
|
+
.test(
|
|
802
|
+
'dual-stack-unique-cidrs',
|
|
803
|
+
`Provided ${field} subnets must be unique.`,
|
|
804
|
+
(values?: { cidr?: MachineNetwork['cidr'] }[]) => {
|
|
805
|
+
if (!values || values.length < 2) {
|
|
806
|
+
return true;
|
|
807
|
+
}
|
|
808
|
+
const first = values[0]?.cidr || '';
|
|
809
|
+
const second = values[1]?.cidr || '';
|
|
810
|
+
if (!first || !second) {
|
|
811
|
+
return true;
|
|
812
|
+
}
|
|
813
|
+
const firstIsCidr = isCIDR.v4(first) || isCIDR.v6(first);
|
|
814
|
+
const secondIsCidr = isCIDR.v4(second) || isCIDR.v6(second);
|
|
815
|
+
if (!firstIsCidr || !secondIsCidr) {
|
|
816
|
+
return true;
|
|
817
|
+
}
|
|
818
|
+
return first !== second;
|
|
819
|
+
},
|
|
820
|
+
)
|
|
821
|
+
.test(
|
|
822
|
+
'dual-stack-opposite-families',
|
|
823
|
+
`When two ${field} are provided, one must be IPv4 and the other IPv6.`,
|
|
824
|
+
(values?: { cidr?: MachineNetwork['cidr'] }[]) => {
|
|
825
|
+
if (!values || values.length < 2) {
|
|
826
|
+
return true;
|
|
827
|
+
}
|
|
828
|
+
const a = values[0]?.cidr || '';
|
|
829
|
+
const b = values[1]?.cidr || '';
|
|
830
|
+
if (!a || !b) {
|
|
831
|
+
return true;
|
|
832
|
+
}
|
|
833
|
+
const a4 = isCIDR.v4(a);
|
|
834
|
+
const a6 = isCIDR.v6(a);
|
|
835
|
+
const b4 = isCIDR.v4(b);
|
|
836
|
+
const b6 = isCIDR.v6(b);
|
|
837
|
+
if (!((a4 || a6) && (b4 || b6))) {
|
|
838
|
+
return true;
|
|
839
|
+
}
|
|
840
|
+
return (a4 && b6) || (a6 && b4);
|
|
725
841
|
},
|
|
726
842
|
);
|
|
727
843
|
|
package/lib/ocm/components/clusterConfiguration/networkConfiguration/AdvancedNetworkFields.tsx
CHANGED
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
Grid,
|
|
9
9
|
} from '@patternfly/react-core';
|
|
10
10
|
import { FieldArray, useFormikContext } from 'formik';
|
|
11
|
-
import { Address6 } from 'ip-address';
|
|
12
11
|
import {
|
|
13
12
|
DUAL_STACK,
|
|
14
13
|
PREFIX_MAX_RESTRICTION,
|
|
@@ -33,62 +32,10 @@ const serviceCidrHelperText =
|
|
|
33
32
|
'Enter only 1 IP address pool. If you need to access the services from an external network, configure load balancers and routers to manage the traffic.';
|
|
34
33
|
|
|
35
34
|
const AdvancedNetworkFields = () => {
|
|
36
|
-
const { values, errors
|
|
35
|
+
const { values, errors } = useFormikContext<NetworkConfigurationValues>();
|
|
37
36
|
|
|
38
37
|
const isDualStack = values.stackType === DUAL_STACK;
|
|
39
38
|
|
|
40
|
-
// Reorder Cluster Networks and Service Networks when Machine Network primary changes
|
|
41
|
-
React.useEffect(() => {
|
|
42
|
-
if (!isDualStack || !values.machineNetworks?.[0]?.cidr) return;
|
|
43
|
-
|
|
44
|
-
const primaryMachineNetworkCidr = values.machineNetworks[0].cidr;
|
|
45
|
-
const isPrimaryIPv6 = Address6.isValid(primaryMachineNetworkCidr);
|
|
46
|
-
|
|
47
|
-
// Check Cluster Networks
|
|
48
|
-
if (values.clusterNetworks && values.clusterNetworks.length >= 2) {
|
|
49
|
-
const firstClusterCidr = values.clusterNetworks[0].cidr;
|
|
50
|
-
if (!firstClusterCidr) return;
|
|
51
|
-
|
|
52
|
-
const isFirstClusterIPv6 = Address6.isValid(firstClusterCidr);
|
|
53
|
-
|
|
54
|
-
// If primary machine is IPv6 but first cluster is IPv4, swap them
|
|
55
|
-
if (isPrimaryIPv6 && !isFirstClusterIPv6) {
|
|
56
|
-
const reordered = [values.clusterNetworks[1], values.clusterNetworks[0]];
|
|
57
|
-
setFieldValue('clusterNetworks', reordered, false);
|
|
58
|
-
}
|
|
59
|
-
// If primary machine is IPv4 but first cluster is IPv6, swap them
|
|
60
|
-
else if (!isPrimaryIPv6 && isFirstClusterIPv6) {
|
|
61
|
-
const reordered = [values.clusterNetworks[1], values.clusterNetworks[0]];
|
|
62
|
-
setFieldValue('clusterNetworks', reordered, false);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Check Service Networks
|
|
67
|
-
if (values.serviceNetworks && values.serviceNetworks.length >= 2) {
|
|
68
|
-
const firstServiceCidr = values.serviceNetworks[0].cidr;
|
|
69
|
-
if (!firstServiceCidr) return;
|
|
70
|
-
|
|
71
|
-
const isFirstServiceIPv6 = Address6.isValid(firstServiceCidr);
|
|
72
|
-
|
|
73
|
-
// If primary machine is IPv6 but first service is IPv4, swap them
|
|
74
|
-
if (isPrimaryIPv6 && !isFirstServiceIPv6) {
|
|
75
|
-
const reordered = [values.serviceNetworks[1], values.serviceNetworks[0]];
|
|
76
|
-
setFieldValue('serviceNetworks', reordered, false);
|
|
77
|
-
}
|
|
78
|
-
// If primary machine is IPv4 but first service is IPv6, swap them
|
|
79
|
-
else if (!isPrimaryIPv6 && isFirstServiceIPv6) {
|
|
80
|
-
const reordered = [values.serviceNetworks[1], values.serviceNetworks[0]];
|
|
81
|
-
setFieldValue('serviceNetworks', reordered, false);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}, [
|
|
85
|
-
isDualStack,
|
|
86
|
-
values.machineNetworks,
|
|
87
|
-
values.clusterNetworks,
|
|
88
|
-
values.serviceNetworks,
|
|
89
|
-
setFieldValue,
|
|
90
|
-
]);
|
|
91
|
-
|
|
92
39
|
const clusterNetworkCidrPrefix = (index: number) =>
|
|
93
40
|
parseInt(
|
|
94
41
|
((values.clusterNetworks && values.clusterNetworks[index].cidr) || '').split('/')[1],
|
package/lib/ocm/components/clusterConfiguration/networkConfiguration/AvailableSubnetsControl.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from '../../../../common';
|
|
14
14
|
import { selectCurrentClusterPermissionsState } from '../../../store/slices/current-cluster/selectors';
|
|
15
15
|
import { SubnetsDropdown } from './SubnetsDropdown';
|
|
16
|
+
import { reorderNetworksForPrimary } from './reorderNetworks';
|
|
16
17
|
import { Cluster, MachineNetwork } from '@openshift-assisted/types/assisted-installer-service';
|
|
17
18
|
|
|
18
19
|
const subnetSort = (subA: HostSubnet, subB: HostSubnet) =>
|
|
@@ -26,7 +27,7 @@ const useAutoSelectSingleAvailableSubnet = (
|
|
|
26
27
|
) => {
|
|
27
28
|
useEffect(() => {
|
|
28
29
|
if (autoSelectNetwork) {
|
|
29
|
-
setFieldValue('machineNetworks', [{ cidr, clusterId }],
|
|
30
|
+
setFieldValue('machineNetworks', [{ cidr, clusterId }], false);
|
|
30
31
|
}
|
|
31
32
|
}, [autoSelectNetwork, cidr, clusterId, setFieldValue]);
|
|
32
33
|
};
|
|
@@ -72,6 +73,41 @@ export const AvailableSubnetsControl = ({
|
|
|
72
73
|
const autoSelectNetwork = !isViewerMode && hasEmptySelection;
|
|
73
74
|
useAutoSelectSingleAvailableSubnet(autoSelectNetwork, setFieldValue, cidr, clusterId);
|
|
74
75
|
|
|
76
|
+
// Ensure primary and secondary machine networks are not duplicated.
|
|
77
|
+
// If the user switches primary to the same CIDR as secondary, adjust secondary to the opposite family or clear it.
|
|
78
|
+
React.useEffect(() => {
|
|
79
|
+
if (!isDualStack) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (!values.machineNetworks || values.machineNetworks.length < 2) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const first = values.machineNetworks?.[0]?.cidr || '';
|
|
86
|
+
const second = values.machineNetworks?.[1]?.cidr || '';
|
|
87
|
+
if (!first || !second) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (first === second) {
|
|
91
|
+
if (Address6.isValid(first)) {
|
|
92
|
+
const nextIPv4 = IPv4Subnets[0]?.subnet || NO_SUBNET_SET;
|
|
93
|
+
const replacement = nextIPv4 !== first ? nextIPv4 : NO_SUBNET_SET;
|
|
94
|
+
if (replacement !== second) {
|
|
95
|
+
setFieldValue('machineNetworks.1.cidr', replacement, false);
|
|
96
|
+
}
|
|
97
|
+
} else if (Address4.isValid(first)) {
|
|
98
|
+
const nextIPv6 = IPv6Subnets[0]?.subnet || NO_SUBNET_SET;
|
|
99
|
+
const replacement = nextIPv6 !== first ? nextIPv6 : NO_SUBNET_SET;
|
|
100
|
+
if (replacement !== second) {
|
|
101
|
+
setFieldValue('machineNetworks.1.cidr', replacement, false);
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
if (second !== NO_SUBNET_SET) {
|
|
105
|
+
setFieldValue('machineNetworks.1.cidr', NO_SUBNET_SET, false);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}, [isDualStack, values.machineNetworks, IPv4Subnets, IPv6Subnets, setFieldValue]);
|
|
110
|
+
|
|
75
111
|
return (
|
|
76
112
|
<>
|
|
77
113
|
<FormGroup
|
|
@@ -95,6 +131,9 @@ export const AvailableSubnetsControl = ({
|
|
|
95
131
|
name={`machineNetworks.${index}.cidr`}
|
|
96
132
|
machineSubnets={machineSubnets}
|
|
97
133
|
isDisabled={isDisabled}
|
|
134
|
+
onAfterSelect={(newSelection) =>
|
|
135
|
+
reorderNetworksForPrimary(newSelection, values, setFieldValue)
|
|
136
|
+
}
|
|
98
137
|
data-testid={`subnets-dropdown-toggle-${index ? 'ipv6' : 'ipv4'}`}
|
|
99
138
|
/>
|
|
100
139
|
</StackItem>
|
|
@@ -106,6 +145,9 @@ export const AvailableSubnetsControl = ({
|
|
|
106
145
|
name={`machineNetworks.0.cidr`}
|
|
107
146
|
machineSubnets={IPv4Subnets}
|
|
108
147
|
isDisabled={isDisabled}
|
|
148
|
+
onAfterSelect={(newSelection) =>
|
|
149
|
+
reorderNetworksForPrimary(newSelection, values, setFieldValue)
|
|
150
|
+
}
|
|
109
151
|
data-testid={'subnets-dropdown-toggle-primary'}
|
|
110
152
|
/>
|
|
111
153
|
</StackItem>
|
|
@@ -18,6 +18,7 @@ import { ConfirmationModal, PopoverIcon } from '../../../../common/components/ui
|
|
|
18
18
|
import { useDefaultConfiguration } from '../ClusterDefaultConfigurationContext';
|
|
19
19
|
import { selectCurrentClusterPermissionsState } from '../../../store/slices/current-cluster/selectors';
|
|
20
20
|
import { OcmRadioField } from '../../ui/OcmFormFields';
|
|
21
|
+
import { reorderNetworksByCurrentPrimary } from './reorderNetworks';
|
|
21
22
|
import {
|
|
22
23
|
Cluster,
|
|
23
24
|
ClusterNetwork,
|
|
@@ -68,11 +69,12 @@ export const StackTypeControlGroup = ({
|
|
|
68
69
|
const setSingleStack = React.useCallback(() => {
|
|
69
70
|
setFieldValue('stackType', IPV4_STACK);
|
|
70
71
|
|
|
72
|
+
// Determine whether the first machine network is IPv4
|
|
73
|
+
const firstNetwork = values.machineNetworks?.[0];
|
|
74
|
+
const isFirstIPv4 = Boolean(firstNetwork?.cidr && Address4.isValid(firstNetwork.cidr));
|
|
75
|
+
|
|
71
76
|
if (values.machineNetworks && values.machineNetworks?.length >= 2) {
|
|
72
77
|
// For single-stack IPv4, prefer IPv4 machine network
|
|
73
|
-
const firstNetwork = values.machineNetworks[0];
|
|
74
|
-
const isFirstIPv4 = firstNetwork?.cidr && Address4.isValid(firstNetwork.cidr);
|
|
75
|
-
|
|
76
78
|
if (isFirstIPv4) {
|
|
77
79
|
// Keep the first network if it's IPv4
|
|
78
80
|
setFieldValue('machineNetworks', [firstNetwork]);
|
|
@@ -123,6 +125,24 @@ export const StackTypeControlGroup = ({
|
|
|
123
125
|
setFieldValue('serviceNetworks', [ipv4Service]);
|
|
124
126
|
}
|
|
125
127
|
|
|
128
|
+
// Keep only IPv4 VIPs when switching to single-stack (unless DHCP allocation is used)
|
|
129
|
+
if (!values.vipDhcpAllocation) {
|
|
130
|
+
const pruneVips = (
|
|
131
|
+
vips?: { ip?: string; clusterId?: Cluster['id'] }[],
|
|
132
|
+
): { ip: string; clusterId: Cluster['id'] }[] => {
|
|
133
|
+
if (!vips || vips.length === 0) return [];
|
|
134
|
+
// Choose which index to keep based on whether the first network is IPv4
|
|
135
|
+
const keepIndex = isFirstIPv4 ? 0 : 1;
|
|
136
|
+
const chosen = vips[keepIndex];
|
|
137
|
+
return chosen && chosen.ip
|
|
138
|
+
? [{ ip: chosen.ip, clusterId: chosen.clusterId || clusterId }]
|
|
139
|
+
: [];
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
setFieldValue('apiVips', pruneVips(values.apiVips));
|
|
143
|
+
setFieldValue('ingressVips', pruneVips(values.ingressVips));
|
|
144
|
+
}
|
|
145
|
+
|
|
126
146
|
void validateForm();
|
|
127
147
|
}, [
|
|
128
148
|
setFieldValue,
|
|
@@ -133,6 +153,9 @@ export const StackTypeControlGroup = ({
|
|
|
133
153
|
values.clusterNetworks,
|
|
134
154
|
values.machineNetworks,
|
|
135
155
|
values.serviceNetworks,
|
|
156
|
+
values.apiVips,
|
|
157
|
+
values.ingressVips,
|
|
158
|
+
values.vipDhcpAllocation,
|
|
136
159
|
]);
|
|
137
160
|
|
|
138
161
|
const setDualStack = () => {
|
|
@@ -193,6 +216,8 @@ export const StackTypeControlGroup = ({
|
|
|
193
216
|
false,
|
|
194
217
|
);
|
|
195
218
|
}
|
|
219
|
+
// Ensure cluster/service networks ordering matches the new primary family
|
|
220
|
+
reorderNetworksByCurrentPrimary(values, setFieldValue);
|
|
196
221
|
};
|
|
197
222
|
|
|
198
223
|
const setStackType = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
@@ -22,6 +22,7 @@ type SubnetsDropdownProps = {
|
|
|
22
22
|
name: string;
|
|
23
23
|
machineSubnets: HostSubnet[];
|
|
24
24
|
isDisabled: boolean;
|
|
25
|
+
onAfterSelect?: (selectedValue: string) => void;
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
const toFormSelectOptions = (subnets: HostSubnet[]) => {
|
|
@@ -51,6 +52,7 @@ export const SubnetsDropdown = ({
|
|
|
51
52
|
name,
|
|
52
53
|
machineSubnets,
|
|
53
54
|
isDisabled,
|
|
55
|
+
onAfterSelect,
|
|
54
56
|
...props
|
|
55
57
|
}: SubnetsDropdownProps & MenuToggleProps) => {
|
|
56
58
|
const [field, , { setValue }] = useField(name);
|
|
@@ -142,8 +144,12 @@ export const SubnetsDropdown = ({
|
|
|
142
144
|
}, [machineSubnets, ipv4Subnets, ipv6Subnets, itemsSubnets]);
|
|
143
145
|
|
|
144
146
|
const onSelect = (event?: React.MouseEvent<Element, MouseEvent>): void => {
|
|
145
|
-
|
|
147
|
+
const nextValue = event?.currentTarget.id;
|
|
148
|
+
setValue(nextValue);
|
|
146
149
|
setOpen(false);
|
|
150
|
+
if (onAfterSelect && nextValue) {
|
|
151
|
+
onAfterSelect(nextValue);
|
|
152
|
+
}
|
|
147
153
|
};
|
|
148
154
|
|
|
149
155
|
const currentItem = itemsSubnets.find((i) => i.label === currentDisplayValue);
|
|
@@ -34,7 +34,7 @@ export const getNetworkInitialValues = (
|
|
|
34
34
|
isClusterManagedNetworkingUnsupported: boolean,
|
|
35
35
|
): NetworkConfigurationValues => {
|
|
36
36
|
const isSNOCluster = isSNO(cluster);
|
|
37
|
-
const isDualStackType = isDualStack(
|
|
37
|
+
const isDualStackType = isDualStack(cluster);
|
|
38
38
|
|
|
39
39
|
return {
|
|
40
40
|
apiVips: cluster.apiVips,
|
|
@@ -74,17 +74,9 @@ export const getNetworkConfigurationValidationSchema = (
|
|
|
74
74
|
hostSubnets: HostSubnets,
|
|
75
75
|
openshiftVersion?: string,
|
|
76
76
|
) =>
|
|
77
|
-
Yup.lazy((values: NetworkConfigurationValues) =>
|
|
78
|
-
|
|
79
|
-
values.
|
|
80
|
-
? vipArrayValidationSchema<ApiVip>(hostSubnets, values, initialValues.apiVips).min(
|
|
81
|
-
2,
|
|
82
|
-
'Provide both Primary and Secondary API IPs.',
|
|
83
|
-
)
|
|
84
|
-
: vipArrayValidationSchema<ApiVip>(hostSubnets, values, initialValues.apiVips);
|
|
85
|
-
|
|
86
|
-
return Yup.object<NetworkConfigurationValues>().shape({
|
|
87
|
-
apiVips: apiVipSchema,
|
|
77
|
+
Yup.lazy((values: NetworkConfigurationValues) =>
|
|
78
|
+
Yup.object<NetworkConfigurationValues>().shape({
|
|
79
|
+
apiVips: vipArrayValidationSchema<ApiVip>(hostSubnets, values, initialValues.apiVips),
|
|
88
80
|
ingressVips: vipArrayValidationSchema<IngressVip>(
|
|
89
81
|
hostSubnets,
|
|
90
82
|
values,
|
|
@@ -124,5 +116,5 @@ export const getNetworkConfigurationValidationSchema = (
|
|
|
124
116
|
)
|
|
125
117
|
: Yup.array(),
|
|
126
118
|
}),
|
|
127
|
-
})
|
|
128
|
-
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Address6 } from 'ip-address';
|
|
2
|
+
import { DUAL_STACK, NetworkConfigurationValues } from '../../../../common';
|
|
3
|
+
import type { FormikHelpers } from 'formik';
|
|
4
|
+
|
|
5
|
+
const reorderByPrimaryCidr = (
|
|
6
|
+
primaryCidr: string,
|
|
7
|
+
values: NetworkConfigurationValues,
|
|
8
|
+
setFieldValue: FormikHelpers<NetworkConfigurationValues>['setFieldValue'],
|
|
9
|
+
) => {
|
|
10
|
+
const isDualStack = values.stackType === DUAL_STACK;
|
|
11
|
+
if (!isDualStack || !primaryCidr) return;
|
|
12
|
+
|
|
13
|
+
const isPrimaryIPv6 = Address6.isValid(primaryCidr);
|
|
14
|
+
|
|
15
|
+
// Reorder Cluster Networks
|
|
16
|
+
if (Array.isArray(values.clusterNetworks) && values.clusterNetworks.length >= 2) {
|
|
17
|
+
const [first, second] = values.clusterNetworks;
|
|
18
|
+
const firstClusterCidr = first?.cidr;
|
|
19
|
+
const secondClusterCidr = second?.cidr;
|
|
20
|
+
if (firstClusterCidr && secondClusterCidr) {
|
|
21
|
+
const isFirstClusterIPv6 = Address6.isValid(firstClusterCidr);
|
|
22
|
+
const isSecondClusterIPv6 = Address6.isValid(secondClusterCidr);
|
|
23
|
+
if (isFirstClusterIPv6 !== isSecondClusterIPv6) {
|
|
24
|
+
if (
|
|
25
|
+
(isPrimaryIPv6 && !isFirstClusterIPv6 && isSecondClusterIPv6) ||
|
|
26
|
+
(!isPrimaryIPv6 && isFirstClusterIPv6 && !isSecondClusterIPv6)
|
|
27
|
+
) {
|
|
28
|
+
const reordered = [values.clusterNetworks[1], values.clusterNetworks[0]];
|
|
29
|
+
setFieldValue('clusterNetworks', reordered, false);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Reorder Service Networks
|
|
36
|
+
if (Array.isArray(values.serviceNetworks) && values.serviceNetworks.length >= 2) {
|
|
37
|
+
const [first, second] = values.serviceNetworks;
|
|
38
|
+
const firstServiceCidr = first?.cidr;
|
|
39
|
+
const secondServiceCidr = second?.cidr;
|
|
40
|
+
if (firstServiceCidr && secondServiceCidr) {
|
|
41
|
+
const isFirstServiceIPv6 = Address6.isValid(firstServiceCidr);
|
|
42
|
+
const isSecondServiceIPv6 = Address6.isValid(secondServiceCidr);
|
|
43
|
+
if (isFirstServiceIPv6 !== isSecondServiceIPv6) {
|
|
44
|
+
if (
|
|
45
|
+
(isPrimaryIPv6 && !isFirstServiceIPv6 && isSecondServiceIPv6) ||
|
|
46
|
+
(!isPrimaryIPv6 && isFirstServiceIPv6 && !isSecondServiceIPv6)
|
|
47
|
+
) {
|
|
48
|
+
const reordered = [values.serviceNetworks[1], values.serviceNetworks[0]];
|
|
49
|
+
setFieldValue('serviceNetworks', reordered, false);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const reorderNetworksForPrimary = (
|
|
57
|
+
nextPrimaryCidr: string,
|
|
58
|
+
values: NetworkConfigurationValues,
|
|
59
|
+
setFieldValue: FormikHelpers<NetworkConfigurationValues>['setFieldValue'],
|
|
60
|
+
) => {
|
|
61
|
+
if (!nextPrimaryCidr) return;
|
|
62
|
+
reorderByPrimaryCidr(nextPrimaryCidr, values, setFieldValue);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const reorderNetworksByCurrentPrimary = (
|
|
66
|
+
values: NetworkConfigurationValues,
|
|
67
|
+
setFieldValue: FormikHelpers<NetworkConfigurationValues>['setFieldValue'],
|
|
68
|
+
) => {
|
|
69
|
+
const primaryCidr = values.machineNetworks?.[0]?.cidr;
|
|
70
|
+
if (!primaryCidr) return;
|
|
71
|
+
reorderByPrimaryCidr(primaryCidr, values, setFieldValue);
|
|
72
|
+
};
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import { Title } from '@patternfly/react-core';
|
|
2
2
|
import { Table, TableVariant, Tbody, Td, Tr } from '@patternfly/react-table';
|
|
3
3
|
import React from 'react';
|
|
4
|
-
import {
|
|
5
|
-
genericTableRowKey,
|
|
6
|
-
isDualStack,
|
|
7
|
-
selectApiVip,
|
|
8
|
-
selectIngressVip,
|
|
9
|
-
} from '../../../../common';
|
|
4
|
+
import { genericTableRowKey, isDualStack } from '../../../../common';
|
|
10
5
|
import {
|
|
11
6
|
getManagementType,
|
|
12
7
|
getStackTypeLabel,
|
|
@@ -58,7 +53,7 @@ export const ReviewNetworkingTable = ({ cluster }: { cluster: Cluster }) => {
|
|
|
58
53
|
)),
|
|
59
54
|
props: { 'data-testid': 'machine-networks' },
|
|
60
55
|
},
|
|
61
|
-
isDualStack(
|
|
56
|
+
isDualStack(cluster)
|
|
62
57
|
? {
|
|
63
58
|
title: cluster.machineNetworks?.map((network, index) => (
|
|
64
59
|
<span key={network.cidr}>
|
|
@@ -78,9 +73,24 @@ export const ReviewNetworkingTable = ({ cluster }: { cluster: Cluster }) => {
|
|
|
78
73
|
cells: [
|
|
79
74
|
{ title: 'API IP' },
|
|
80
75
|
{
|
|
81
|
-
title:
|
|
82
|
-
|
|
76
|
+
title: cluster.apiVips?.map((apiVip) => (
|
|
77
|
+
<span key={apiVip.ip}>
|
|
78
|
+
{apiVip.ip}
|
|
79
|
+
<br />
|
|
80
|
+
</span>
|
|
81
|
+
)),
|
|
82
|
+
props: { 'data-testid': 'api-vip' },
|
|
83
83
|
},
|
|
84
|
+
isDualStack(cluster)
|
|
85
|
+
? {
|
|
86
|
+
title: cluster.apiVips?.map((apiVip, index) => (
|
|
87
|
+
<span key={apiVip.ip}>
|
|
88
|
+
{index === 0 ? 'Primary' : 'Secondary'}
|
|
89
|
+
<br />
|
|
90
|
+
</span>
|
|
91
|
+
)),
|
|
92
|
+
}
|
|
93
|
+
: { title: '' },
|
|
84
94
|
],
|
|
85
95
|
});
|
|
86
96
|
|
|
@@ -91,9 +101,24 @@ export const ReviewNetworkingTable = ({ cluster }: { cluster: Cluster }) => {
|
|
|
91
101
|
cells: [
|
|
92
102
|
{ title: 'Ingress IP' },
|
|
93
103
|
{
|
|
94
|
-
title:
|
|
95
|
-
|
|
104
|
+
title: cluster.ingressVips?.map((ingressVip) => (
|
|
105
|
+
<span key={ingressVip.ip}>
|
|
106
|
+
{ingressVip.ip}
|
|
107
|
+
<br />
|
|
108
|
+
</span>
|
|
109
|
+
)),
|
|
110
|
+
props: { 'data-testid': 'ingress-vip' },
|
|
96
111
|
},
|
|
112
|
+
isDualStack(cluster)
|
|
113
|
+
? {
|
|
114
|
+
title: cluster.ingressVips?.map((ingressVip, index) => (
|
|
115
|
+
<span key={ingressVip.ip}>
|
|
116
|
+
{index === 0 ? 'Primary' : 'Secondary'}
|
|
117
|
+
<br />
|
|
118
|
+
</span>
|
|
119
|
+
)),
|
|
120
|
+
}
|
|
121
|
+
: { title: '' },
|
|
97
122
|
],
|
|
98
123
|
});
|
|
99
124
|
|
|
@@ -115,7 +140,7 @@ export const ReviewNetworkingTable = ({ cluster }: { cluster: Cluster }) => {
|
|
|
115
140
|
)),
|
|
116
141
|
props: { 'data-testid': 'cluster-network-cidr' },
|
|
117
142
|
},
|
|
118
|
-
isDualStack(
|
|
143
|
+
isDualStack(cluster) && {
|
|
119
144
|
title: cluster.clusterNetworks?.map((network, index) => (
|
|
120
145
|
<span key={network.cidr}>
|
|
121
146
|
{index === 0 ? 'Primary' : 'Secondary'}
|
|
@@ -138,7 +163,7 @@ export const ReviewNetworkingTable = ({ cluster }: { cluster: Cluster }) => {
|
|
|
138
163
|
)),
|
|
139
164
|
props: { 'data-testid': 'cluster-network-prefix' },
|
|
140
165
|
},
|
|
141
|
-
isDualStack(
|
|
166
|
+
isDualStack(cluster) && {
|
|
142
167
|
title: cluster.clusterNetworks?.map((network, index) => (
|
|
143
168
|
<span key={network.hostPrefix}>
|
|
144
169
|
{index === 0 ? 'Primary' : 'Secondary'}
|
|
@@ -161,7 +186,7 @@ export const ReviewNetworkingTable = ({ cluster }: { cluster: Cluster }) => {
|
|
|
161
186
|
)),
|
|
162
187
|
props: { 'data-testid': 'service-network-cidr' },
|
|
163
188
|
},
|
|
164
|
-
isDualStack(
|
|
189
|
+
isDualStack(cluster) && {
|
|
165
190
|
title: cluster.serviceNetworks?.map((network, index) => (
|
|
166
191
|
<span key={network.cidr}>
|
|
167
192
|
{index === 0 ? 'Primary' : 'Secondary'}
|
|
@@ -41,7 +41,7 @@ export const getManagementType = ({ userManagedNetworking }: Cluster): string =>
|
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
export const getStackTypeLabel = (cluster: Cluster): string =>
|
|
44
|
-
isDualStack(
|
|
44
|
+
isDualStack(cluster) ? 'Dual-stack' : 'IPv4';
|
|
45
45
|
|
|
46
46
|
export const getDiskEncryptionEnabledOnStatus = (diskEncryption: DiskEncryption['enableOn']) => {
|
|
47
47
|
let diskEncryptionType = null;
|
|
@@ -67,7 +67,7 @@ export const getDiskEncryptionEnabledOnStatus = (diskEncryption: DiskEncryption[
|
|
|
67
67
|
|
|
68
68
|
const ClusterProperties = ({ cluster, externalMode = false }: ClusterPropertiesProps) => {
|
|
69
69
|
const { t } = useTranslation();
|
|
70
|
-
const isDualStackType = isDualStack(
|
|
70
|
+
const isDualStackType = isDualStack(cluster);
|
|
71
71
|
const featureSupportLevelContext = useNewFeatureSupportLevel();
|
|
72
72
|
|
|
73
73
|
const activeFeatureConfiguration = featureSupportLevelContext.activeFeatureConfiguration;
|