diff --git a/libs/locales/lib/en/translation.json b/libs/locales/lib/en/translation.json
index aee3b3bfac..8c74218919 100644
--- a/libs/locales/lib/en/translation.json
+++ b/libs/locales/lib/en/translation.json
@@ -26,9 +26,12 @@
"ai:{{count}} worker installed": "{{count}} worker installed",
"ai:{{count}} worker installed_plural": "{{count}} workers installed",
"ai:{{cpus}} CPU cores | {{memory}} Memory": "{{cpus}} CPU cores | {{memory}} Memory",
+ "ai:{{errorMsgPrefix}} ({{netBlockNumber}}) and 25.": "{{errorMsgPrefix}} ({{netBlockNumber}}) and 25.",
+ "ai:{{errorMsgPrefix}} (8) and 128.": "{{errorMsgPrefix}} (8) and 128.",
"ai:{{operatorsCountString}} installed": "{{operatorsCountString}} installed",
"ai:{{selectedAgentsCount}} host selected out of {{matchingAgentsCount}} matching.": "{{selectedAgentsCount}} host selected out of {{matchingAgentsCount}} identified.",
"ai:{{selectedAgentsCount}} host selected out of {{matchingAgentsCount}} matching._plural": "{{selectedAgentsCount}} hosts selected out of {{matchingAgentsCount}} identified.",
+ "ai:{{value}} is not a valid CIDR": "{{value}} is not a valid CIDR",
"ai:* If you close this browser window, you will not be able to return.": "* If you close this browser window, you will not be able to return.",
"ai:1 (Single Node OpenShift - not highly available cluster)": "1 (Single Node OpenShift - not highly available cluster)",
"ai:1-{{count}} characters": "1-{{count}} characters",
@@ -90,6 +93,7 @@
"ai:An error occurred while approving agents": "An error occurred while approving agents.",
"ai:An error occurred while fetching config maps": "An error occurred while fetching config maps",
"ai:An error occurred while starting installation.": "An error occurred while starting installation.",
+ "ai:An SSH key is required to debug hosts as they register.": "An SSH key is required to debug hosts as they register.",
"ai:And verify that this is the output:": "And verify the following output:",
"ai:API connectivity failure": "API connectivity failure",
"ai:API domain name resolution": "API domain name resolution",
@@ -113,6 +117,7 @@
"ai:arm64 is not supported in this OpenShift version": "arm64 is not supported in this OpenShift version",
"ai:At least 3 hosts are required, capable of functioning as control plane nodes.": "At least 3 hosts are required that are capable of functioning as control plane nodes.",
"ai:At least one config map is required": "At least one config map is required",
+ "ai:At least one of the HTTP or HTTPS proxy URLs is required.": "At least one of the HTTP or HTTPS proxy URLs is required.",
"ai:Authentication is provided by the discovery ISO, therefore when you access your host using SSH, a password is not required. Optional -i parameter can be used to specify the private key that matches the public key provided when generating Discovery ISO.": "Authentication is provided by the Discovery ISO, so a password is not required when you access your host using SSH. The optional -i parameter can be used to specify the private key that matches the public key that is provided when generating Discovery ISO.",
"ai:Auto synchronized NTP (Network Time Protocol) sources": "Auto synchronized NTP (Network Time Protocol) sources",
"ai:Auto-assign": "Auto-assign",
@@ -188,6 +193,7 @@
"ai:Cluster installation was cancelled": "Cluster installation was cancelled.",
"ai:Cluster must have at least 3 hosts.": "Cluster must have at least 3 hosts.",
"ai:Cluster name": "Cluster name",
+ "ai:cluster network": "cluster network",
"ai:Cluster network CIDR": "Cluster network CIDR",
"ai:Cluster network host prefix": "Cluster network host prefix",
"ai:Cluster networks": "Cluster networks",
@@ -290,6 +296,7 @@
"ai:Displays events": "Displays events",
"ai:DNS": "DNS",
"ai:DNS domain": "DNS domain",
+ "ai:DNS names and IP addresses must be unique.": "DNS names and IP addresses must be unique.",
"ai:DNS wildcard not configured": "DNS wildcard not configured",
"ai:Do not use forbidden words, for example: \"localhost\".": "Do not use forbidden words, for example: \"localhost\".",
"ai:Download credentials": "Download credentials",
@@ -337,6 +344,7 @@
"ai:Error creating InfraEnv": "Error creating InfraEnv",
"ai:Error parsing cluster feature_usage field": "Error parsing cluster feature_usage field",
"ai:Events table": "Events table",
+ "ai:Every single host component in the base domain name cannot contain more than 63 characters and must not contain spaces.": "Every single host component in the base domain name cannot contain more than 63 characters and must not contain spaces.",
"ai:Exactly 1 host is required, capable of functioning both as control plane and worker node.": "Exactly 1 host is required, capable of functioning both as control plane and worker node.",
"ai:Exactly 2 hosts capable of functioning as control plane nodes, and one arbiter, are required.": "Exactly 2 hosts capable of functioning as control plane nodes, and one arbiter, are required.",
"ai:Exclude destination domain names, IP addresses, or other network CIDRs from proxying by adding them to this comma-separated list.": "Exclude destination domain names, IP addresses, or other network CIDRs from proxying by adding them to this comma-separated list.",
@@ -368,12 +376,16 @@
"ai:Failing infrastructure environment": "Failing infrastructure environment",
"ai:Fence Agents Remediation requirements": "Fence Agents Remediation requirements",
"ai:File is not structured correctly. Use the template to use the right file structure.": "File is not structured correctly. Use the template to use the right file structure.",
+ "ai:File size is too big. The file size must be less than {{value}}.": "File size is too big. The file size must be less than {{value}}.",
"ai:File size is too big. Upload a new {{maxFileSizeKb}} Kb or less.": "File size is too big. Upload a new file that is {{maxFileSizeKb}} Kb or less.",
"ai:File type is not supported. File type must be {{acceptedFiles}}.": "File type is not supported. File type must be {{acceptedFiles}}.",
+ "ai:File type is not supported. File type must be yaml, yml, json, yaml.patch. or yml.patch.": "File type is not supported. File type must be yaml, yml, json, yaml.patch. or yml.patch.",
"ai:Filter by text": "Filter by text",
"ai:Filter hosts by existing labels": "Filter hosts by existing labels",
"ai:Finalizing": "Finalizing",
"ai:Find by hostname": "Find by hostname",
+ "ai:First network has to be a valid IPv4 or IPv6 subnet.": "First network has to be a valid IPv4 or IPv6 subnet.",
+ "ai:First network has to be IPv4 subnet.": "First network has to be IPv4 subnet.",
"ai:For example, if Cluster Network Host Prefix is set to 116, then each node is assigned a /116 subnet out of the given cidr (clusterNetworkCIDR), which allows for 4,094 (2^(128 - 116) - 2) pod IPs addresses.": "For example, if Cluster Network Host Prefix is set to 116, then each node is assigned a /116 subnet out of the given cidr (clusterNetworkCIDR), which allows for 4,094 (2^(128 - 116) - 2) pod IPs addresses.",
"ai:For example, if Cluster Network Host Prefix is set to 23, then each node is assigned a /23 subnet out of the given cidr (clusterNetworkCIDR), which allows for 510 (2^(32 - 23) - 2) pod IPs addresses.": "For example, if Cluster Network Host Prefix is set to 23, then each node is assigned a /23 subnet out of the given cidr (clusterNetworkCIDR), which allows for 510 (2^(32 - 23) - 2) pod IPs addresses.",
"ai:For example: host-{{n}}": "For example: host-{{n}}",
@@ -494,10 +506,15 @@
"ai:Installing SNO will result in an OpenShift deployment that is not highly available.": "Installing SNO will result in an OpenShift deployment that is not highly available.",
"ai:Insufficient": "Insufficient",
"ai:Integrate with external partner platforms": "Integrate with external partner platforms",
+ "ai:Invalid IP address block. Expected value is a network expressed in CIDR notation (IP/netmask). For example: 123.123.123.0/24, 2055:d7a::/116": "Invalid IP address block. Expected value is a network expressed in CIDR notation (IP/netmask). For example: 123.123.123.0/24, 2055:d7a::/116",
+ "ai:Invalid pull secret format. You must use your Red Hat account's pull secret.": "Invalid pull secret format. You must use your Red Hat account's pull secret.",
"ai:IP address block from which Pod IPs are allocated This block must not overlap with existing physical networks. These IP addresses are used for the Pod network, and if you need to access the Pods from an external network, configure load balancers and routers to manage the traffic.": "IP address block from which Pod IPs are allocated. This block must not overlap with existing physical networks. These IP addresses are used for the Pod network, and if you need to access the Pods from an external network, configure load balancers and routers to manage the traffic.",
"ai:IP address blocks from which Pod IPs are allocated.": "IP address blocks from which Pod IPs are allocated.",
+ "ai:IP Address is outside of selected subnet": "IP Address is outside of selected subnet",
"ai:IP allocation from the DHCP server timed out.": "IP allocation from the DHCP server timed out.",
+ "ai:IP family must match the corresponding machine network family.": "IP family must match the corresponding machine network family.",
"ai:IPv4 address": "IPv4 address",
+ "ai:IPv4 netmask must be between 1-25 and include at least 128 addresses.\nIPv6 netmask must be between 8-128 and include at least 256 addresses.": "IPv4 netmask must be between 1-25 and include at least 128 addresses.\nIPv6 netmask must be between 8-128 and include at least 256 addresses.",
"ai:IPv6 address": "IPv6 address",
"ai:iPXE script file is ready to be downloaded": "iPXE script file is ready to be downloaded",
"ai:iPXE script file URL": "iPXE script file URL",
@@ -516,6 +533,7 @@
"ai:Kernel Module Management requirements": "Kernel Module Management requirements",
"ai:Kube Descheduler requirements": "Kube Descheduler requirements",
"ai:Kubeconfig is empty.": "Kubeconfig is empty.",
+ "ai:Label needs to be in a `key=value` form": "Label needs to be in a `key=value` form",
"ai:Labels": "Labels",
"ai:Labels matching hosts": "Labels matching hosts",
"ai:Last observed condition:": "Last observed condition:",
@@ -555,6 +573,7 @@
"ai:Maximum availability {{maxAgents}}": "Maximum availability {{maxAgents}}",
"ai:Maximum availability is based on the number of hosts available in a given namespace. The number changes dynamically depending on the filtering labels added above.": "Maximum availability is based on the number of hosts available in a given namespace. The number changes dynamically depending on the filtering labels previously added.",
"ai:Maximum hosts count {{HOSTS_MAX_COUNT}} reached.": "Maximum hosts count {{HOSTS_MAX_COUNT}} reached.",
+ "ai:Maximum number of {{field}} subnets in dual stack is 2.": "Maximum number of {{field}} subnets in dual stack is 2.",
"ai:Maximum number of hosts": "Maximum number of hosts",
"ai:MCE requirements": "MCE requirements",
"ai:Media Connected": "Media Connected",
@@ -641,6 +660,7 @@
"ai:Nodepool name": "Nodepool name",
"ai:Non overlapping subnets": "Non overlapping subnets",
"ai:None": "None",
+ "ai:Not a valid IP address": "Not a valid IP address",
"ai:Not able to access the Web Console?": "Not able to access the Web Console?",
"ai:Not available": "Not available",
"ai:Not changeable": "Not changeable",
@@ -713,9 +733,14 @@
"ai:Primary": "Primary",
"ai:Product": "Product",
"ai:Progress": "Progress",
+ "ai:Provide a comma separated list of valid DNS names or IP addresses.": "Provide a comma separated list of valid DNS names or IP addresses.",
+ "ai:Provide a valid DNS name or IP Address": "Provide a valid DNS name or IP Address",
+ "ai:Provide a valid HTTP URL.": "Provide a valid HTTP URL.",
"ai:Provide an SSH key to be able to connect to the hosts for debugging purposes during the discovery process": "Provide an SSH key to be able to connect to the hosts for debugging purposes during the discovery process",
"ai:Provide as many labels as you can to narrow the list to relevant hosts only.": "Provide as many labels as you can to narrow the list to relevant hosts.",
"ai:Provide the complete URL, including the protocol and parameters.": "Provide the complete URL, including the protocol and parameters.",
+ "ai:Provided {{field}} subnets must be unique.": "Provided {{field}} subnets must be unique.",
+ "ai:Provided CIDRs can not overlap.": "Provided CIDRs can not overlap.",
"ai:Provided cluster configuration is not valid": "Provided cluster configuration is not valid",
"ai:Provides an endpoint for application traffic flowing in from outside the cluster. If needed, contact your IT manager for more information.": "Provides an endpoint for application traffic flowing in from outside the cluster. If needed, contact your IT manager for more information.",
"ai:Provides an endpoint for users, both human and machine, to interact with and configure the platform. If needed, contact your IT manager for more information.": "Provides an endpoint for users, both human and machine, to interact with and configure the platform. If needed, contact your IT manager for more information.",
@@ -784,12 +809,14 @@
"ai:Serverless requirements": "Serverless requirements",
"ai:Service CIDR": "Service CIDR",
"ai:Service Mesh requirements": "Service Mesh requirements",
+ "ai:service network": "service network",
"ai:Service network CIDR": "Service network CIDR",
"ai:Service networks": "Service networks",
"ai:Set {{agent_location_label_key}} label in Agent resource to specify its location.": "Set {{agent_location_label_key}} label in Agent resource to specify its location.",
"ai:Set all the hosts to boot using iPXE script file": "Set all the hosts to boot using iPXE script file",
"ai:Show all available versions": "Show all available versions",
"ai:Show proxy settings": "Show proxy settings",
+ "ai:Single label of the DNS name can not be longer than 63 characters.": "Single label of the DNS name can not be longer than 63 characters.",
"ai:Single node cluster cannot contain more than 1 host.": "Single node cluster cannot contain more than 1 host.",
"ai:Single replica": "Single replica",
"ai:Single replica means components are not expected to be resilient to problems across most fault boundaries associated with high availability. This usually means running critical workloads with just 1 replica and with toleration of full disruption of the component.": "Single replica means components are not expected to be resilient to problems across most fault boundaries associated with high availability. This usually means running critical workloads with just 1 replica and with toleration of full disruption of the component.",
@@ -810,6 +837,9 @@
"ai:Speed": "Speed",
"ai:SSH into your machine": "SSH into your machine",
"ai:SSH public key": "SSH public key",
+ "ai:SSH public key must consist of \"[TYPE] key [[EMAIL]]\", supported types are: ssh-rsa, ssh-ed25519, ecdsa-[VARIANT].": "SSH public key must consist of \"[TYPE] key [[EMAIL]]\", supported types are: ssh-rsa, ssh-ed25519, ecdsa-[VARIANT].",
+ "ai:SSH public key must consist of \"[TYPE] key [[EMAIL]]\", supported types are: ssh-rsa, ssh-ed25519, ecdsa-[VARIANT]. A single key can be provided only.": "SSH public key must consist of \"[TYPE] key [[EMAIL]]\", supported types are: ssh-rsa, ssh-ed25519, ecdsa-[VARIANT]. A single key can be provided only.",
+ "ai:SSH public keys must be unique.": "SSH public keys must be unique.",
"ai:Start and end with a lowercase letter or a number.": "Start and end with a lowercase letter or a number.",
"ai:Started on": "Started on",
"ai:Starting installation": "Starting installation",
@@ -830,6 +860,7 @@
"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.",
@@ -849,9 +880,12 @@
"ai:The generated discovery ISO will contain everything needed to boot.": "The generated discovery ISO will contain everything that is needed to boot.",
"ai:The host has been discovered and needs to be approved to before further use.": "The host has been discovered and needs to be approved to before further use.",
"ai:The host machine is powered on": "The host machine is powered on",
+ "ai:The host prefix is a number between 1 and 32 for IPv4 and between 8 and 128 for IPv6.": "The host prefix is a number between 1 and 32 for IPv4 and between 8 and 128 for IPv6.",
+ "ai:The host prefix is a number between size of the cluster network CIDR range": "The host prefix is a number between size of the cluster network CIDR range",
"ai:The hostname cannot be changed.": "The hostname cannot be changed.",
"ai:The hosts you selected are using different proxy settings. Configure a proxy that will be applied for these hosts. Configure at least one of the proxy settings below.": "The hosts you selected are using different proxy settings. Configure a proxy that will be applied for these hosts. Configure at least one of the following proxy settings.",
"ai:The HTTP proxy URL that agents should use to access the discovery service.": "The HTTP proxy URL that agents should use to access the discovery service.",
+ "ai:The IP address cannot be a network or broadcast address": "The IP address cannot be a network or broadcast address",
"ai:The IP address pool to use for service IP addresses. You can enter only one IP address pool. If you need to access the services from an external network, configure load balancers and routers to manage the traffic.": "The IP address pool to use for service IP addresses. You can enter only one IP address pool. If you need to access the services from an external network, configure load balancers and routers to manage the traffic.",
"ai:The IP address pool used for service IP addresses.": "The IP address pool used for service IP addresses.",
"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.",
@@ -860,6 +894,7 @@
"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 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.",
"ai:The subnet prefix length to assign to each individual node. For example, if Cluster Network Host Prefix is set to 116, then each node is assigned a /116 subnet out of the given cidr (clusterNetworkCIDR), which allows for 4,094 (2^(128 - 116) - 2) pod IPs addresses. If you are required to provide access to nodes from an external network, configure load balancers and routers to manage the traffic.": "The subnet prefix length to assign to each individual node. For example, if Cluster Network Host Prefix is set to 116, then each node is assigned a /116 subnet out of the given cidr (clusterNetworkCIDR), which allows for 4,094 (2^(128 - 116) - 2) pod IPs addresses. If you are required to provide access to nodes from an external network, configure load balancers and routers to manage the traffic.",
@@ -949,6 +984,8 @@
"ai:Valid network type": "Valid network type",
"ai:Validating...": "Validating...",
"ai:Validations are running. If they take more than 2 minutes, please attend to the alert below.": "Validations are running. If they take more than 2 minutes, resolve the alert that is displayed.",
+ "ai:Value \"{{value}}\" is not valid DNS name. Example: basedomain.example.com": "Value \"{{value}}\" is not valid DNS name. Example: basedomain.example.com",
+ "ai:Value \"{{value}}\" is not valid MAC address.": "Value \"{{value}}\" is not valid MAC address.",
"ai:Vendor": "Vendor",
"ai:Vendor ID": "Vendor ID",
"ai:Verify that you can access your host machine using SSH, or a console such as BMC or virtual machine console. In the CLI, enter the following command:": "Verify that you can access your host machine using SSH, or a console such as BMC or virtual machine console. In the CLI, enter the following command:",
@@ -967,6 +1004,7 @@
"ai:Waiting for hosts...": "Waiting for hosts...",
"ai:Warning": "Warning",
"ai:Web Console URL": "Web Console URL",
+ "ai:When two {{field}} values are provided, one must be IPv4 and the other IPv6.": "When two {{field}} values are provided, one must be IPv4 and the other IPv6.",
"ai:When using {{executionCommand}}, please run:": "When using {{executionCommand}}, please run:",
"ai:With BMC form": "With BMC form",
"ai:With Discovery ISO": "With Discovery ISO",
diff --git a/libs/ui-lib-tests/cypress/integration/custom-manifests/2-modifying-existing-custom-manifest.cy.ts b/libs/ui-lib-tests/cypress/integration/custom-manifests/2-modifying-existing-custom-manifest.cy.ts
index 83c6d9b0c8..338b052456 100644
--- a/libs/ui-lib-tests/cypress/integration/custom-manifests/2-modifying-existing-custom-manifest.cy.ts
+++ b/libs/ui-lib-tests/cypress/integration/custom-manifests/2-modifying-existing-custom-manifest.cy.ts
@@ -90,7 +90,7 @@ describe(`Assisted Installer Custom manifests step`, () => {
.fileUploadError()
.should(
'contain.text',
- 'File type is not supported. File type must be yaml, yml ,json , yaml.patch. or yml.patch.',
+ 'File type is not supported. File type must be yaml, yml, json, yaml.patch. or yml.patch.',
);
commonActions.verifyNextIsDisabled();
});
diff --git a/libs/ui-lib/lib/cim/components/Agent/BMCForm.tsx b/libs/ui-lib/lib/cim/components/Agent/BMCForm.tsx
index 51ca212e29..4d872b72f0 100644
--- a/libs/ui-lib/lib/cim/components/Agent/BMCForm.tsx
+++ b/libs/ui-lib/lib/cim/components/Agent/BMCForm.tsx
@@ -151,14 +151,14 @@ const getValidationSchema = (usedHostnames: string[], origHostname: string, t: T
bmcAddress: bmcAddressValidationSchema(t),
username: Yup.string().required(t('ai:Required field')),
password: Yup.string().required(t('ai:Required field')),
- bootMACAddress: macAddressValidationSchema,
+ bootMACAddress: macAddressValidationSchema(t),
nmState: Yup.string(),
macMapping: Yup.array().of(
Yup.object().shape(
{
- macAddress: macAddressValidationSchema.when('name', {
+ macAddress: macAddressValidationSchema(t).when('name', {
is: (name: string) => !!name,
- then: () => macAddressValidationSchema.required(t('ai:MAC has to be specified')),
+ then: (schema) => schema.required(t('ai:MAC has to be specified')),
}),
name: Yup.string().when('macAddress', {
is: (name: string) => !!name,
diff --git a/libs/ui-lib/lib/cim/components/ClusterDeployment/clusterDetails/ClusterDetailsFormFields.tsx b/libs/ui-lib/lib/cim/components/ClusterDeployment/clusterDetails/ClusterDetailsFormFields.tsx
index 8f275f07f8..e46862ddb7 100644
--- a/libs/ui-lib/lib/cim/components/ClusterDeployment/clusterDetails/ClusterDetailsFormFields.tsx
+++ b/libs/ui-lib/lib/cim/components/ClusterDeployment/clusterDetails/ClusterDetailsFormFields.tsx
@@ -3,6 +3,7 @@ import { Form } from '@patternfly/react-core';
import { useFormikContext } from 'formik';
import {
+ acmClusterNameValidationMessages,
ExternalPlatformsDropdown,
isMajorMinorVersionEqualOrGreater,
OpenShiftVersionDropdown,
@@ -11,11 +12,7 @@ import {
import { StaticTextField } from '../../../../common/components/ui/StaticTextField';
import { PullSecret } from '../../../../common/components/clusters';
import { OpenshiftVersionOptionType, SupportedCpuArchitecture } from '../../../../common/types';
-import {
- InputField,
- RichInputField,
- acmClusterNameValidationMessages,
-} from '../../../../common/components/ui/formik';
+import { InputField, RichInputField } from '../../../../common/components/ui/formik';
import { ClusterDetailsValues } from '../../../../common/components/clusterWizard/types';
import { useTranslation } from '../../../../common/hooks/use-translation-wrapper';
import CpuArchitectureDropdown from '../../common/CpuArchitectureDropdown';
diff --git a/libs/ui-lib/lib/cim/components/ClusterDeployment/hostSelection/ClusterDeploymentHostSelectionStep.tsx b/libs/ui-lib/lib/cim/components/ClusterDeployment/hostSelection/ClusterDeploymentHostSelectionStep.tsx
index 8b64e23ae9..5674014c9a 100644
--- a/libs/ui-lib/lib/cim/components/ClusterDeployment/hostSelection/ClusterDeploymentHostSelectionStep.tsx
+++ b/libs/ui-lib/lib/cim/components/ClusterDeployment/hostSelection/ClusterDeploymentHostSelectionStep.tsx
@@ -96,9 +96,7 @@ const getValidationSchema = (agentClusterInstall: AgentClusterInstallK8sResource
selectedHostIds: values.autoSelectHosts
? Yup.array(Yup.string())
: isSNOCluster
- ? Yup.array(Yup.string())
- .min(1, t('ai:Please select one host for the cluster.'))
- .max(1, t('ai:Please select one host for the cluster.')) // TODO(jtomasek): replace this with Yup.array().length() after updating Yup
+ ? Yup.array(Yup.string()).length(1, t('ai:Please select one host for the cluster.'))
: Yup.array(Yup.string()).min(3, t('ai:Please select at least 3 hosts for the cluster.')),
});
});
diff --git a/libs/ui-lib/lib/cim/components/ClusterDeployment/use-networking-formik.ts b/libs/ui-lib/lib/cim/components/ClusterDeployment/use-networking-formik.ts
index 7e62c33e7b..1b5b0e2b99 100644
--- a/libs/ui-lib/lib/cim/components/ClusterDeployment/use-networking-formik.ts
+++ b/libs/ui-lib/lib/cim/components/ClusterDeployment/use-networking-formik.ts
@@ -1,5 +1,6 @@
import React from 'react';
import * as Yup from 'yup';
+import { TFunction } from 'i18next';
import { INFRAENV_AGENTINSTALL_LABEL_KEY } from '../common';
import { getHostSubnets } from '../../../common/components/clusterConfiguration/utils';
@@ -47,32 +48,33 @@ const getInfraEnvProxy = (infraEnvs: InfraEnvK8sResource[]) => {
const getNetworkConfigurationValidationSchema = (
initialValues: ClusterDeploymentNetworkingValues,
hostSubnets: HostSubnets,
+ t: TFunction,
openshiftVersion?: string,
) =>
Yup.lazy((values: ClusterDeploymentNetworkingValues) =>
Yup.object().shape({
- clusterNetworks: clusterNetworksValidationSchema.when('stackType', {
+ clusterNetworks: clusterNetworksValidationSchema(t).when('stackType', {
is: (stackType: NetworkConfigurationValues['stackType']) => stackType === IPV4_STACK,
- then: () => clusterNetworksValidationSchema.concat(IPv4ValidationSchema),
+ then: () => clusterNetworksValidationSchema(t).concat(IPv4ValidationSchema),
otherwise: () =>
- clusterNetworksValidationSchema.concat(
- dualStackValidationSchema('cluster network', openshiftVersion),
+ clusterNetworksValidationSchema(t).concat(
+ dualStackValidationSchema(t('ai:cluster network'), t, openshiftVersion),
),
}),
- serviceNetworks: serviceNetworkValidationSchema.when('stackType', {
+ serviceNetworks: serviceNetworkValidationSchema(t).when('stackType', {
is: (stackType: NetworkConfigurationValues['stackType']) => stackType === IPV4_STACK,
- then: () => serviceNetworkValidationSchema.concat(IPv4ValidationSchema),
+ then: () => serviceNetworkValidationSchema(t).concat(IPv4ValidationSchema),
otherwise: () =>
- serviceNetworkValidationSchema.concat(
- dualStackValidationSchema('service network', openshiftVersion),
+ serviceNetworkValidationSchema(t).concat(
+ dualStackValidationSchema(t('ai:service network'), t, openshiftVersion),
),
}),
- apiVips: vipArrayValidationSchema(hostSubnets, values),
- ingressVips: vipArrayValidationSchema(hostSubnets, values),
- sshPublicKey: sshPublicKeyListValidationSchema,
- httpProxy: httpProxyValidationSchema({ values, pairValueName: 'httpsProxy' }),
- httpsProxy: httpProxyValidationSchema({ values, pairValueName: 'httpProxy' }), // share the schema, httpS is currently not supported
- noProxy: noProxyValidationSchema,
+ apiVips: vipArrayValidationSchema(hostSubnets, values, t),
+ ingressVips: vipArrayValidationSchema(hostSubnets, values, t),
+ sshPublicKey: sshPublicKeyListValidationSchema(t),
+ httpProxy: httpProxyValidationSchema({ values, pairValueName: 'httpsProxy', t }),
+ httpsProxy: httpProxyValidationSchema({ values, pairValueName: 'httpProxy', t }), // share the schema, httpS is currently not supported
+ noProxy: noProxyValidationSchema(t),
}),
);
@@ -87,6 +89,7 @@ export const useNetworkingFormik = ({
agentClusterInstall,
agents,
}: UseNetworkingFormikArgs) => {
+ const { t } = useTranslation();
const initialValues = React.useMemo(
() => {
const cluster = getAICluster({
@@ -112,9 +115,10 @@ export const useNetworkingFormik = ({
return getNetworkConfigurationValidationSchema(
initialValues,
hostSubnets,
+ t,
cluster.openshiftVersion,
);
- }, [initialValues, clusterDeployment, agentClusterInstall, agents]);
+ }, [clusterDeployment, agentClusterInstall, agents, t, initialValues]);
return {
initialValues,
diff --git a/libs/ui-lib/lib/cim/components/Hypershift/HostedClusterWizard/DetailsStep/DetailsStep.tsx b/libs/ui-lib/lib/cim/components/Hypershift/HostedClusterWizard/DetailsStep/DetailsStep.tsx
index 870b7041c8..fbb0dce3f2 100644
--- a/libs/ui-lib/lib/cim/components/Hypershift/HostedClusterWizard/DetailsStep/DetailsStep.tsx
+++ b/libs/ui-lib/lib/cim/components/Hypershift/HostedClusterWizard/DetailsStep/DetailsStep.tsx
@@ -34,8 +34,8 @@ const useDetailsFormik: UseDetailsFormik = ({
() =>
Yup.object({
name: nameValidationSchema(t, usedClusterNames),
- baseDnsDomain: dnsNameValidationSchema.required('Required'),
- pullSecret: pullSecretValidationSchema.required('Required.'),
+ baseDnsDomain: dnsNameValidationSchema(t).required(t('ai:Required field')),
+ pullSecret: pullSecretValidationSchema(t).required(t('ai:Required field')),
}),
[usedClusterNames, t],
);
diff --git a/libs/ui-lib/lib/cim/components/Hypershift/HostedClusterWizard/NetworkStep/NetworkStep.tsx b/libs/ui-lib/lib/cim/components/Hypershift/HostedClusterWizard/NetworkStep/NetworkStep.tsx
index 2687aa5d14..5e7bf4bb34 100644
--- a/libs/ui-lib/lib/cim/components/Hypershift/HostedClusterWizard/NetworkStep/NetworkStep.tsx
+++ b/libs/ui-lib/lib/cim/components/Hypershift/HostedClusterWizard/NetworkStep/NetworkStep.tsx
@@ -18,22 +18,22 @@ import { useTranslation } from '../../../../../common/hooks/use-translation-wrap
const getValidationSchema = (t: TFunction) =>
Yup.lazy((values: NetworkFormValues) =>
Yup.object().shape({
- sshPublicKey: sshPublicKeyValidationSchema.required(t('ai:Required field')),
+ sshPublicKey: sshPublicKeyValidationSchema(t).required(t('ai:Required field')),
clusterNetworkCidr: values.isAdvanced
- ? ipBlockValidationSchema(values.serviceNetworkCidr)
+ ? ipBlockValidationSchema(values.serviceNetworkCidr, t)
: Yup.string(),
serviceNetworkCidr: values.isAdvanced
- ? ipBlockValidationSchema(values.clusterNetworkCidr)
+ ? ipBlockValidationSchema(values.clusterNetworkCidr, t)
: Yup.string(),
clusterNetworkHostPrefix: values.isAdvanced
- ? hostPrefixValidationSchema(values.clusterNetworkCidr)
+ ? hostPrefixValidationSchema(values.clusterNetworkCidr, t)
: Yup.number(),
- httpProxy: httpProxyValidationSchema({ values, pairValueName: 'httpsProxy' }),
- httpsProxy: httpProxyValidationSchema({ values, pairValueName: 'httpProxy' }), // share the schema, httpS is currently not supported
- noProxy: noProxyValidationSchema,
+ httpProxy: httpProxyValidationSchema({ values, pairValueName: 'httpsProxy', t }),
+ httpsProxy: httpProxyValidationSchema({ values, pairValueName: 'httpProxy', t }), // share the schema, httpS is currently not supported
+ noProxy: noProxyValidationSchema(t),
nodePortAddress:
values.apiPublishingStrategy === 'NodePort'
- ? day2ApiVipValidationSchema.required(t('ai:Required field'))
+ ? day2ApiVipValidationSchema(t).required(t('ai:Required field'))
: Yup.string(),
}),
);
diff --git a/libs/ui-lib/lib/cim/components/InfraEnv/InfraEnvFormPage.tsx b/libs/ui-lib/lib/cim/components/InfraEnv/InfraEnvFormPage.tsx
index 43c2848264..d68c1d7492 100644
--- a/libs/ui-lib/lib/cim/components/InfraEnv/InfraEnvFormPage.tsx
+++ b/libs/ui-lib/lib/cim/components/InfraEnv/InfraEnvFormPage.tsx
@@ -76,32 +76,34 @@ const validationSchema = (usedNames: string[], t: TFunction) =>
Yup.lazy((values: EnvironmentStepFormValues) =>
Yup.object().shape({
name: richNameValidationSchema(t, usedNames),
- location: locationValidationSchema(t).required(t('ai:Location is a required field.')),
- pullSecret: pullSecretValidationSchema.required(t('ai:Pull secret is a required field.')),
- sshPublicKey: sshPublicKeyValidationSchema,
+ location: locationValidationSchema(t),
+ pullSecret: pullSecretValidationSchema(t).required(t('ai:Required field')),
+ sshPublicKey: sshPublicKeyValidationSchema(t),
httpProxy: httpProxyValidationSchema({
values,
pairValueName: 'httpsProxy',
allowEmpty: true,
+ t,
}),
httpsProxy: httpProxyValidationSchema({
values,
pairValueName: 'httpProxy',
allowEmpty: true,
+ t,
}), // share the schema, httpS is currently not supported
- noProxy: noProxyValidationSchema,
+ noProxy: noProxyValidationSchema(t),
labels: Yup.array()
.of(Yup.string())
.test(
'label-equals-validation',
- 'Label selector needs to be in a `key=value` form',
+ t('ai:Label needs to be in a `key=value` form'),
(values) =>
(values as string[]).every((value) => {
const parts = value.split('=');
return parts.length === 2;
}),
),
- additionalNtpSources: ntpSourceValidationSchema,
+ additionalNtpSources: ntpSourceValidationSchema(t),
}),
);
diff --git a/libs/ui-lib/lib/cim/components/modals/CimConfiguration/CimConfigurationModal.tsx b/libs/ui-lib/lib/cim/components/modals/CimConfiguration/CimConfigurationModal.tsx
index 8146a86899..440d2e7564 100644
--- a/libs/ui-lib/lib/cim/components/modals/CimConfiguration/CimConfigurationModal.tsx
+++ b/libs/ui-lib/lib/cim/components/modals/CimConfiguration/CimConfigurationModal.tsx
@@ -100,9 +100,15 @@ export const CimConfigurationModal: React.FC = ({
};
const validationSchema = Yup.object({
- dbVolSize: Yup.number().min(MIN_DB_VOL_SIZE, t('ai:Minimal value is 1Gi')).required(),
- fsVolSize: Yup.number().min(MIN_FS_VOL_SIZE, t('ai:Minimal value is 1Gi')).required(),
- imgVolSize: Yup.number().min(MIN_IMG_VOL_SIZE, t('ai:Minimal value is 10Gi')).required(),
+ dbVolSize: Yup.number()
+ .min(MIN_DB_VOL_SIZE, t('ai:Minimal value is 1Gi'))
+ .required(t('ai:Required field')),
+ fsVolSize: Yup.number()
+ .min(MIN_FS_VOL_SIZE, t('ai:Minimal value is 1Gi'))
+ .required(t('ai:Required field')),
+ imgVolSize: Yup.number()
+ .min(MIN_IMG_VOL_SIZE, t('ai:Minimal value is 10Gi'))
+ .required(t('ai:Required field')),
configureLoadBalancer: Yup.boolean(),
ciscoIntersightURL: Yup.string().when('addCiscoIntersightUrl', {
is: true,
diff --git a/libs/ui-lib/lib/cim/components/modals/EditProxyModal.tsx b/libs/ui-lib/lib/cim/components/modals/EditProxyModal.tsx
index 308284fe1f..cb8df33021 100644
--- a/libs/ui-lib/lib/cim/components/modals/EditProxyModal.tsx
+++ b/libs/ui-lib/lib/cim/components/modals/EditProxyModal.tsx
@@ -1,5 +1,6 @@
import * as React from 'react';
import * as Yup from 'yup';
+import { TFunction } from 'i18next';
import {
Alert,
Button,
@@ -25,20 +26,22 @@ import {
import { getErrorMessage } from '../../../common/utils';
import { getWarningMessage } from './utils';
-const validationSchema = () =>
+const validationSchema = (t: TFunction) =>
Yup.lazy((values: ProxyFieldsType) =>
Yup.object().shape({
httpProxy: httpProxyValidationSchema({
values,
pairValueName: 'httpsProxy',
allowEmpty: true,
+ t,
}),
httpsProxy: httpProxyValidationSchema({
values,
pairValueName: 'httpProxy',
allowEmpty: true,
+ t,
}),
- noProxy: noProxyValidationSchema,
+ noProxy: noProxyValidationSchema(t),
}),
);
@@ -91,7 +94,7 @@ const EditProxyModal: React.FC = ({
noProxy: infraEnv.spec?.proxy?.noProxy,
enableProxy,
}}
- validationSchema={validationSchema}
+ validationSchema={validationSchema(t)}
onSubmit={async (values: ProxyFieldsType) => {
setError(undefined);
try {
diff --git a/libs/ui-lib/lib/cim/components/modals/EditPullSecretModal.tsx b/libs/ui-lib/lib/cim/components/modals/EditPullSecretModal.tsx
index 69dc267166..2860c58f5e 100644
--- a/libs/ui-lib/lib/cim/components/modals/EditPullSecretModal.tsx
+++ b/libs/ui-lib/lib/cim/components/modals/EditPullSecretModal.tsx
@@ -23,7 +23,7 @@ import { useTranslation } from '../../../common/hooks/use-translation-wrapper';
const validationSchema = (t: TFunction) =>
Yup.object({
- pullSecret: pullSecretValidationSchema.required(t('ai:Pull secret is a required field.')),
+ pullSecret: pullSecretValidationSchema(t).required(t('ai:Pull secret is a required field.')),
});
type EditPullSecretFormProps = {
diff --git a/libs/ui-lib/lib/cim/components/modals/EditSSHKeyModal.tsx b/libs/ui-lib/lib/cim/components/modals/EditSSHKeyModal.tsx
index d7ddcfd6b9..c9aaadb03f 100644
--- a/libs/ui-lib/lib/cim/components/modals/EditSSHKeyModal.tsx
+++ b/libs/ui-lib/lib/cim/components/modals/EditSSHKeyModal.tsx
@@ -1,5 +1,6 @@
import * as React from 'react';
import * as Yup from 'yup';
+import { TFunction } from 'i18next';
import {
Alert,
Button,
@@ -20,11 +21,12 @@ import { EditSSHKeyFormikValues } from './types';
import { getWarningMessage } from './utils';
import { useTranslation } from '../../../common/hooks/use-translation-wrapper';
-const validationSchema = Yup.object({
- sshPublicKey: sshPublicKeyValidationSchema.required(
- 'An SSH key is required to debug hosts as they register.',
- ),
-});
+const validationSchema = (t: TFunction) =>
+ Yup.object({
+ sshPublicKey: sshPublicKeyValidationSchema(t).required(
+ t('ai:An SSH key is required to debug hosts as they register.'),
+ ),
+ });
export type EditSSHKeyModalProps = {
isOpen: boolean;
@@ -61,7 +63,7 @@ const EditSSHKeyModal: React.FC = ({
initialValues={{
sshPublicKey: infraEnv.spec?.sshAuthorizedKey || '',
}}
- validationSchema={validationSchema}
+ validationSchema={validationSchema(t)}
onSubmit={async (values) => {
try {
await onSubmit(values, infraEnv);
diff --git a/libs/ui-lib/lib/common/components/clusterConfiguration/DiscoveryImageConfigForm.tsx b/libs/ui-lib/lib/common/components/clusterConfiguration/DiscoveryImageConfigForm.tsx
index 4a709bf237..840c0872de 100644
--- a/libs/ui-lib/lib/common/components/clusterConfiguration/DiscoveryImageConfigForm.tsx
+++ b/libs/ui-lib/lib/common/components/clusterConfiguration/DiscoveryImageConfigForm.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import * as Yup from 'yup';
+import { TFunction } from 'i18next';
import {
Button,
Form,
@@ -17,12 +18,12 @@ import {
ImageType,
Proxy,
} from '@openshift-assisted/types/assisted-installer-service';
+import { AlertFormikError } from '../../../common/components/ui';
import {
- AlertFormikError,
httpProxyValidationSchema,
noProxyValidationSchema,
sshPublicKeyValidationSchema,
-} from '../../../common/components/ui';
+} from '../../validationSchemas';
import { ProxyFieldsType, StatusErrorType } from '../../types';
import ProxyFields from './ProxyFields';
import UploadSSH from './UploadSSH';
@@ -55,13 +56,13 @@ export const StaticIPInfo = ({ docVersion }: { docVersion?: string }) => {
export type DiscoveryImageFormValues = ImageCreateParams & ProxyFieldsType;
-const getValidationSchema = (allowEmpty: boolean) =>
+const getValidationSchema = (allowEmpty: boolean, t: TFunction) =>
Yup.lazy((values: DiscoveryImageFormValues) =>
Yup.object().shape({
- sshPublicKey: sshPublicKeyValidationSchema,
- httpProxy: httpProxyValidationSchema({ values, pairValueName: 'httpsProxy', allowEmpty }),
- httpsProxy: httpProxyValidationSchema({ values, pairValueName: 'httpProxy', allowEmpty }), // share the schema, httpS is currently not supported
- noProxy: noProxyValidationSchema,
+ sshPublicKey: sshPublicKeyValidationSchema(t),
+ httpProxy: httpProxyValidationSchema({ values, pairValueName: 'httpsProxy', allowEmpty, t }),
+ httpsProxy: httpProxyValidationSchema({ values, pairValueName: 'httpProxy', allowEmpty, t }), // share the schema, httpS is currently not supported
+ noProxy: noProxyValidationSchema(t),
}),
);
@@ -108,7 +109,7 @@ export const DiscoveryImageConfigForm: React.FC =
{({ submitForm, isSubmitting, status }) => {
diff --git a/libs/ui-lib/lib/common/components/clusterWizard/clusterDetailsValidation.ts b/libs/ui-lib/lib/common/components/clusterWizard/clusterDetailsValidation.ts
index 5e03c7d507..5476f2b8d6 100644
--- a/libs/ui-lib/lib/common/components/clusterWizard/clusterDetailsValidation.ts
+++ b/libs/ui-lib/lib/common/components/clusterWizard/clusterDetailsValidation.ts
@@ -7,13 +7,13 @@ import {
} from '@openshift-assisted/types/assisted-installer-service';
import { getDefaultCpuArchitecture, OpenshiftVersionOptionType } from '../../types';
import { TangServer } from '../clusterConfiguration/DiskEncryptionFields/DiskEncryptionValues';
+import { getDefaultOpenShiftVersion } from '../ui';
import {
baseDomainValidationSchema,
dnsNameValidationSchema,
- getDefaultOpenShiftVersion,
nameValidationSchema,
pullSecretValidationSchema,
-} from '../ui';
+} from '../../validationSchemas';
import { ClusterDetailsValues } from './types';
const emptyTangServers = (): TangServer[] => {
@@ -98,22 +98,32 @@ export const getClusterDetailsValidationSchema = ({
t: TFunction;
}) =>
Yup.lazy((values: { baseDnsDomain: string; isSNODevPreview: boolean }) => {
- const validateName = () =>
- nameValidationSchema(t, usedClusterNames, values.baseDnsDomain, validateUniqueName, isOcm);
if (pullSecretSet) {
return Yup.object({
- name: validateName(),
+ name: nameValidationSchema(
+ t,
+ usedClusterNames,
+ values.baseDnsDomain,
+ validateUniqueName,
+ isOcm,
+ ),
baseDnsDomain: isOcm
- ? baseDomainValidationSchema.required(t('ai:Required field'))
- : dnsNameValidationSchema.required(t('ai:Required field')),
+ ? baseDomainValidationSchema(t).required(t('ai:Required field'))
+ : dnsNameValidationSchema(t).required(t('ai:Required field')),
});
}
return Yup.object({
- name: validateName(),
+ name: nameValidationSchema(
+ t,
+ usedClusterNames,
+ values.baseDnsDomain,
+ validateUniqueName,
+ isOcm,
+ ),
baseDnsDomain: isOcm
- ? baseDomainValidationSchema.required(t('ai:Required field'))
- : dnsNameValidationSchema.required(t('ai:Required field')),
- pullSecret: pullSecretValidationSchema.required(t('ai:Required field')),
+ ? baseDomainValidationSchema(t).required(t('ai:Required field'))
+ : dnsNameValidationSchema(t).required(t('ai:Required field')),
+ pullSecret: pullSecretValidationSchema(t).required(t('ai:Required field')),
diskEncryptionTangServers: Yup.array().when('diskEncryptionMode', {
is: (diskEncryptionMode: DiskEncryption['mode']) => {
return diskEncryptionMode === 'tang';
diff --git a/libs/ui-lib/lib/common/components/hosts/AdditionalNTPSourcesDialog.tsx b/libs/ui-lib/lib/common/components/hosts/AdditionalNTPSourcesDialog.tsx
index 41dd1c9142..6590f57fa0 100644
--- a/libs/ui-lib/lib/common/components/hosts/AdditionalNTPSourcesDialog.tsx
+++ b/libs/ui-lib/lib/common/components/hosts/AdditionalNTPSourcesDialog.tsx
@@ -11,11 +11,8 @@ import {
ModalVariant,
ModalHeader,
} from '@patternfly/react-core';
-import {
- ntpSourceValidationSchema,
- AdditionalNTPSourcesField,
- StatusErrorType,
-} from '../../../common';
+import { AdditionalNTPSourcesField, StatusErrorType } from '../../../common';
+import { ntpSourceValidationSchema } from '../../validationSchemas/ntpValidation';
import { AlertFormikError } from '../../../common/components/ui';
import { useTranslation } from '../../hooks/use-translation-wrapper';
import {
@@ -43,7 +40,7 @@ const AdditionalNTPSourcesForm = ({
const getValidationSchema = (t: TFunction) =>
Yup.object().shape({
- additionalNtpSource: ntpSourceValidationSchema.required(t('ai:Required field')),
+ additionalNtpSource: ntpSourceValidationSchema(t).required(t('ai:Required field')),
});
const { t } = useTranslation();
diff --git a/libs/ui-lib/lib/common/components/hosts/EditHostForm.tsx b/libs/ui-lib/lib/common/components/hosts/EditHostForm.tsx
index 3a65791dd4..faadf7d6da 100644
--- a/libs/ui-lib/lib/common/components/hosts/EditHostForm.tsx
+++ b/libs/ui-lib/lib/common/components/hosts/EditHostForm.tsx
@@ -14,14 +14,8 @@ import {
import { Formik } from 'formik';
import { TFunction } from 'i18next';
import { Host, Inventory } from '@openshift-assisted/types/assisted-installer-service';
-import {
- richHostnameValidationSchema,
- RichInputField,
- StaticTextField,
- hostnameValidationMessages,
- getRichTextValidation,
- AlertFormikError,
-} from '../ui';
+import { RichInputField, StaticTextField, getRichTextValidation, AlertFormikError } from '../ui';
+import { richHostnameValidationSchema, hostnameValidationMessages } from '../../validationSchemas';
import { canHostnameBeChanged } from './utils';
import GridGap from '../ui/GridGap';
import { EditHostFormValues } from './types';
diff --git a/libs/ui-lib/lib/common/components/hosts/MassChangeHostnameModal.tsx b/libs/ui-lib/lib/common/components/hosts/MassChangeHostnameModal.tsx
index b944c0b876..a0d6b8a7b3 100644
--- a/libs/ui-lib/lib/common/components/hosts/MassChangeHostnameModal.tsx
+++ b/libs/ui-lib/lib/common/components/hosts/MassChangeHostnameModal.tsx
@@ -23,14 +23,12 @@ import { t_global_icon_color_status_info_default as blueInfoColor } from '@patte
import { InfoCircleIcon } from '@patternfly/react-icons/dist/js/icons/info-circle-icon';
import { TFunction } from 'i18next';
+import { RichInputField, getRichTextValidation, ModalProgress } from '../ui';
import {
- RichInputField,
- getRichTextValidation,
- richHostnameValidationSchema,
- hostnameValidationMessages,
- ModalProgress,
FORBIDDEN_HOSTNAMES,
-} from '../ui';
+ hostnameValidationMessages,
+ richHostnameValidationSchema,
+} from '../../validationSchemas';
import { Host } from '@openshift-assisted/types/assisted-installer-service';
import { getHostname as getHostnameUtils, getInventory } from './utils';
import { ActionCheck } from './types';
diff --git a/libs/ui-lib/lib/common/components/ui/formik/index.tsx b/libs/ui-lib/lib/common/components/ui/formik/index.tsx
index eb78b298d7..b70feba226 100644
--- a/libs/ui-lib/lib/common/components/ui/formik/index.tsx
+++ b/libs/ui-lib/lib/common/components/ui/formik/index.tsx
@@ -21,6 +21,4 @@ export { default as SelectFieldWithSearch } from './SelectFieldWithSearch';
export * from './utils';
export * from './PullSecretField';
-export * from './validationSchemas';
export * from './LabelField';
-export * from './constants';
diff --git a/libs/ui-lib/lib/common/components/ui/formik/validationSchemas.ts b/libs/ui-lib/lib/common/components/ui/formik/validationSchemas.ts
deleted file mode 100644
index e4472fbac8..0000000000
--- a/libs/ui-lib/lib/common/components/ui/formik/validationSchemas.ts
+++ /dev/null
@@ -1,803 +0,0 @@
-import { overlap } from 'cidr-tools';
-import { Address4, Address6 } from 'ip-address';
-import isCIDR from 'is-cidr';
-import { isInSubnet } from 'is-in-subnet';
-import * as Yup from 'yup';
-import parseUrl from 'parse-url';
-
-import { TFunction } from 'i18next';
-import {
- ClusterNetwork,
- MachineNetwork,
- ServiceNetwork,
-} from '@openshift-assisted/types/assisted-installer-service';
-import { NO_SUBNET_SET } from '../../../config/constants';
-import { ProxyFieldsType } from '../../../types';
-import { HostSubnets, NetworkConfigurationValues } from '../../../types/clusters';
-import { getSubnet } from '../../clusterConfiguration/utils';
-import {
- bmcAddressValidationMessages,
- clusterNameValidationMessages,
- CLUSTER_NAME_MAX_LENGTH,
- FORBIDDEN_HOSTNAMES,
- hostnameValidationMessages,
- locationValidationMessages,
- nameValidationMessages,
-} from './constants';
-import { allSubnetsIPv4, getAddress, trimCommaSeparatedList, trimSshPublicKey } from './utils';
-import { isMajorMinorVersionEqualOrGreater } from '../../../utils';
-import { ClusterDetailsValues } from '../../clusterWizard/types';
-
-const ALPHANUMERIC_REGEX = /^[a-zA-Z0-9]+$/;
-const NAME_START_END_REGEX = /^[a-z0-9](.*[a-z0-9])?$/;
-const NAME_CHARS_REGEX = /^[a-z0-9-.]*$/;
-const CLUSTER_NAME_START_END_REGEX = /^[a-z0-9](.*[a-z0-9])?$/;
-const CLUSTER_NAME_VALID_CHARS_REGEX = /^[a-z0-9-]*$/;
-const SSH_PUBLIC_KEY_REGEX =
- /^(ssh-rsa AAAAB3NzaC1yc2|ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNT|ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzOD|ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1Mj|ssh-ed25519 AAAAC3NzaC1lZDI1NTE5|ssh-dss AAAAB3NzaC1kc3)[0-9A-Za-z+/]+[=]{0,3}( .*)?$/;
-const DNS_NAME_REGEX = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/;
-
-const PROXY_DNS_REGEX =
- /(^\.?([a-zA-Z0-9_]{1}[a-zA-Z0-9_-]{0,62}){1}(\.[a-zA-Z0-9_]{1}[a-zA-Z0-9_-]{0,62})*$)/;
-const IP_V4_ZERO = '0.0.0.0';
-const IP_V6_ZERO = '0000:0000:0000:0000:0000:0000:0000:0000';
-const MAC_REGEX = /^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})$/;
-const HOST_NAME_REGEX = /^[^.]{1,63}(?:[.][^.]{1,63})*$/;
-const LOCATION_CHARS_REGEX = /^[a-zA-Z0-9-._]*$/;
-
-export const nameValidationSchema = (
- t: TFunction,
- usedClusterNames: string[],
- baseDnsDomain = '',
- validateUniqueName?: boolean,
- isOcm = false,
-) => {
- const clusterNameValidationMessagesList = clusterNameValidationMessages(t);
- return Yup.string()
- .required(t('ai:Required field'))
- .matches(CLUSTER_NAME_VALID_CHARS_REGEX, {
- message: clusterNameValidationMessagesList.INVALID_VALUE,
- excludeEmptyString: true,
- })
- .matches(CLUSTER_NAME_START_END_REGEX, {
- message: clusterNameValidationMessagesList.INVALID_START_END,
- excludeEmptyString: true,
- })
- .min(
- isOcm ? 1 : 2,
- isOcm
- ? clusterNameValidationMessagesList.INVALID_LENGTH_OCM
- : clusterNameValidationMessagesList.INVALID_LENGTH_ACM,
- )
- .max(
- CLUSTER_NAME_MAX_LENGTH,
- isOcm
- ? clusterNameValidationMessagesList.INVALID_LENGTH_OCM
- : clusterNameValidationMessagesList.INVALID_LENGTH_ACM,
- )
- .when('useRedHatDnsService', {
- is: (useRedHatDnsService: ClusterDetailsValues['useRedHatDnsService']) =>
- useRedHatDnsService === true,
- then: (schema) =>
- schema.test(
- 'is-name-unique',
- clusterNameValidationMessagesList.NOT_UNIQUE,
- (value?: string) => {
- const clusterFullName = `${value || ''}.${baseDnsDomain}`;
- return !value || !usedClusterNames.includes(clusterFullName);
- },
- ),
- otherwise: (schema) =>
- schema.test(
- 'is-name-unique',
- clusterNameValidationMessagesList.NOT_UNIQUE,
- (value?: string) => {
- // in CIM cluster name is ClusterDeployment CR name which must be unique
- return validateUniqueName ? !value || !usedClusterNames.includes(value) : true;
- },
- ),
- });
-};
-
-export const sshPublicKeyValidationSchema = Yup.string().test(
- 'ssh-public-key',
- 'SSH public key must consist of "[TYPE] key [[EMAIL]]", supported types are: ssh-rsa, ssh-ed25519, ecdsa-[VARIANT]. A single key can be provided only.',
- (value?: string) => {
- if (!value) {
- return true;
- }
-
- return !!trimSshPublicKey(value).match(SSH_PUBLIC_KEY_REGEX);
- },
-);
-
-export const sshPublicKeyListValidationSchema = Yup.string()
- .test(
- 'ssh-public-keys',
- 'SSH public key must consist of "[TYPE] key [[EMAIL]]", supported types are: ssh-rsa, ssh-ed25519, ecdsa-[VARIANT].',
- (value?: string) => {
- if (!value) {
- return true;
- }
-
- return (
- trimSshPublicKey(value)
- .split('\n')
- .find((line: string) => !line.match(SSH_PUBLIC_KEY_REGEX)) === undefined
- );
- },
- )
- .test('ssh-public-keys-unique', 'SSH public keys must be unique.', (value?: string) => {
- if (!value) {
- return true;
- }
- const keyList = trimSshPublicKey(value).split('\n');
- return new Set(keyList).size === keyList.length;
- });
-
-export const pullSecretValidationSchema = Yup.string().test(
- 'is-well-formed-json',
- "Invalid pull secret format. You must use your Red Hat account's pull secret.",
- (value?: string) => {
- const isValid = true;
- if (!value) return isValid;
- try {
- const pullSecret = JSON.parse(value) as {
- auths: string;
- };
- return (
- pullSecret.constructor.name === 'Object' &&
- !!pullSecret?.auths &&
- pullSecret.auths.constructor.name === 'Object'
- );
- } catch {
- return !isValid;
- }
- },
-);
-
-const isValidIpWithoutSuffix = (addr: string) => {
- const address = getAddress(addr);
- return !!address && address.address === address.addressMinusSuffix;
-};
-
-export const ipValidationSchema = Yup.string().test(
- 'ip-validation',
- 'Not a valid IP address',
- (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',
- (value?: string) => {
- return isValidIpWithoutSuffix(value || '');
- },
-);
-
-export const macAddressValidationSchema = Yup.string().matches(MAC_REGEX, {
- message: 'Value "${value}" is not valid MAC address.', // eslint-disable-line no-template-curly-in-string
- excludeEmptyString: true,
-});
-
-export const vipRangeValidationSchema = (
- hostSubnets: HostSubnets,
- { machineNetworks }: NetworkConfigurationValues,
- allowSuffix: boolean,
-) =>
- Yup.string().test('vip-validation', 'IP Address is outside of selected subnet', (value) => {
- if (!value) {
- return true;
- }
-
- try {
- const validator = allowSuffix ? ipValidationSchema : ipNoSuffixValidationSchema;
- validator.validateSync(value);
- } catch (err) {
- return true;
- }
- // Find host subnets that match the selected machine networks
- const cidrs = machineNetworks?.map((network) => network.cidr) ?? [];
- const matchingSubnets = hostSubnets.filter((hostSubnet) => cidrs.includes(hostSubnet.subnet));
-
- for (const hostSubnet of matchingSubnets) {
- if (hostSubnet?.subnet) {
- // Workaround for bug in CIM backend. hostIDs are empty
- if (!hostSubnet.hostIDs.length) {
- return true;
- } else if (isInSubnet(value, hostSubnet.subnet)) {
- return true;
- }
- }
- }
- return false;
- });
-
-const vipBroadcastValidationSchema = ({ machineNetworks }: NetworkConfigurationValues) =>
- Yup.string().test(
- 'vip-no-broadcast',
- 'The IP address cannot be a network or broadcast address',
- function (value?: string) {
- if (!value) {
- return true;
- }
-
- const vipAddress = getAddress(value)?.address;
- if (!vipAddress) {
- return true;
- }
-
- const index = getArrayIndexFromPath(this.path || '');
- const machineNetworkCidr = machineNetworks?.[index]?.cidr ?? machineNetworks?.[0]?.cidr ?? '';
- if (!machineNetworkCidr) {
- return true;
- }
-
- const machineNetwork = getAddress(machineNetworkCidr);
- if (!machineNetwork) {
- return true;
- }
-
- const machineNetworkBroadcast = machineNetwork.endAddress().address;
- const machineNetworkAddress = machineNetwork.startAddress().address;
-
- return vipAddress !== machineNetworkBroadcast && vipAddress !== machineNetworkAddress;
- },
- );
-
-const alwaysRequired = (message?: string) =>
- Yup.string().test(
- 'always-required',
- message || 'The value is required.',
- (value?: string): boolean => !!value,
- );
-
-const getArrayIndexFromPath = (path: string): number => {
- const match = path.match(/\[(\d+)\][^\[]*$/); // Prefer the last [...] occurrence for nested array paths like "foo[0].bar[1].ip"
- return match ? parseInt(match[1], 10) : NaN;
-};
-
-export const hostSubnetValidationSchema = Yup.string().when(['managedNetworkingType'], {
- is: (managedNetworkingType: NetworkConfigurationValues['managedNetworkingType']) =>
- managedNetworkingType === 'clusterManaged',
- then: () => Yup.string().notOneOf([NO_SUBNET_SET], 'Host subnet must be selected.'),
-});
-
-export const vipNoSuffixValidationSchema = (
- hostSubnets: HostSubnets,
- values: NetworkConfigurationValues,
-) =>
- Yup.mixed().when(['vipDhcpAllocation', 'managedNetworkingType'], {
- is: (
- vipDhcpAllocation: NetworkConfigurationValues['vipDhcpAllocation'],
- managedNetworkingType: NetworkConfigurationValues['managedNetworkingType'],
- ) => !vipDhcpAllocation && managedNetworkingType !== 'userManaged',
- 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;
- }
- const index = getArrayIndexFromPath(this.path || '');
- 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;
- },
- );
- // Ensure API and Ingress VIPs at the same index are not the same
- const vipUniqueSchema = Yup.string().test('vip-uniqueness', function (value?: string) {
- if (!value) {
- return true;
- }
- const index = getArrayIndexFromPath(this.path || '');
- if (Number.isNaN(index)) {
- return true;
- }
- const apiVip = values.apiVips?.[index]?.ip;
- const ingressVip = values.ingressVips?.[index]?.ip;
- if (!apiVip || !ingressVip) {
- return true;
- }
- if (apiVip === ingressVip) {
- const label = index === 0 ? 'primary' : 'secondary';
- return this.createError({
- message: `The ${label} Ingress and API IP addresses cannot be the same.`,
- });
- }
- return true;
- });
-
- return alwaysRequired('Required. Please provide an IP address')
- .concat(ipNoSuffixValidationSchema)
- .concat(vipFamilyMatchSchema)
- .concat(vipRangeValidationSchema(hostSubnets, values, false))
- .concat(vipBroadcastValidationSchema(values))
- .concat(vipUniqueSchema);
- },
- });
-
-export const vipArrayValidationSchema = >(
- hostSubnets: HostSubnets,
- values: NetworkConfigurationValues,
-) =>
- values.managedNetworkingType === 'clusterManaged'
- ? Yup.array().of(
- Yup.object({
- clusterId: Yup.string(),
- ip: vipNoSuffixValidationSchema(hostSubnets, values),
- }),
- )
- : Yup.array();
-
-export const ipBlockValidationSchema = (reservedCidrs: string | string[] | undefined) =>
- Yup.string()
- .required('A value is required.')
- .test(
- 'valid-ip-address',
- 'Invalid IP address block. Expected value is a network expressed in CIDR notation (IP/netmask). For example: 123.123.123.0/24, 2055:d7a::/116',
- (value?: string): boolean => !!value && (isCIDR.v4(value) || isCIDR.v6(value)),
- )
- .test(
- 'valid-netmask',
- 'IPv4 netmask must be between 1-25 and include at least 128 addresses.\nIPv6 netmask must be between 8-128 and include at least 256 addresses.',
- (value?: string) => {
- const suffix = parseInt((value || '').split('/')[1]);
-
- return (
- (isCIDR.v4(value || '') && 0 < suffix && suffix < 26) ||
- (isCIDR.v6(value || '') && 7 < suffix && suffix < 129)
- );
- },
- )
- .test(
- 'cidr-is-not-unspecified',
- 'The specified CIDR is invalid because its resulting routing prefix matches the unspecified address.',
- (cidr: string) => {
- const ip = getSubnet(cidr);
- if (ip === null) {
- return false;
- }
-
- // The first address is used to represent the network
- const startAddress = ip.startAddress().address;
-
- return startAddress !== IP_V4_ZERO && startAddress !== IP_V6_ZERO;
- },
- )
- .test(
- 'valid-cidr-base-address',
- ({ value }) => `${value as string} is not a valid CIDR`,
- (cidr: string) => {
- const ip = getSubnet(cidr);
- if (ip === null) {
- return false;
- }
-
- const networkAddress = ip.startAddress().parsedAddress;
- const ipAddress = ip.parsedAddress;
- const result = ipAddress.every((part, idx) => part === networkAddress[idx]);
-
- return result;
- },
- )
- .test('cidrs-can-not-overlap', 'Provided CIDRs can not overlap.', (cidr: string) => {
- try {
- if (cidr && reservedCidrs && reservedCidrs.length > 0) {
- return !overlap(cidr, reservedCidrs);
- }
- } catch {
- return false;
- }
- // passing by default
- return true;
- });
-
-export const dnsNameValidationSchema = Yup.string()
- .test(
- 'dns-name-label-length',
- 'Single label of the DNS name can not be longer than 63 characters.',
- (value?: string) => (value || '').split('.').every((label: string) => label.length <= 63),
- )
- .matches(DNS_NAME_REGEX, {
- message: 'Value "${value}" is not valid DNS name. Example: basedomain.example.com', // eslint-disable-line no-template-curly-in-string
- excludeEmptyString: true,
- });
-
-export const baseDomainValidationSchema = Yup.string().test(
- 'dns-name-label-length',
- 'Every single host component in the base domain name cannot contain more than 63 characters and must not contain spaces.',
- (value?: string) => {
- // Check if the value contains any spaces
- if (/\s/.test(value as string)) {
- return false; // Value contains spaces, validation fails
- }
-
- // Check the label lengths
- const labels = (value || '').split('.');
- return labels.every((label: string) => label.length <= 63);
- },
-);
-
-export const hostPrefixValidationSchema = (
- clusterNetworkCidr: NetworkConfigurationValues['clusterNetworkCidr'],
-) => {
- const requiredText = 'The host prefix is required.';
- const minMaxText =
- 'The host prefix is a number between 1 and 32 for IPv4 and between 8 and 128 for IPv6.';
- const netBlock = (clusterNetworkCidr || '').split('/')[1];
- if (!netBlock) {
- return Yup.number().required(requiredText).min(1, minMaxText).max(32, minMaxText);
- }
-
- let netBlockNumber = parseInt(netBlock);
- if (isNaN(netBlockNumber)) {
- netBlockNumber = 1;
- }
-
- const errorMsgPrefix =
- 'The host prefix is a number between size of the cluster network CIDR range';
- const errorMsgIPv4 = `${errorMsgPrefix} (${netBlockNumber}) and 25.`;
- const errorMsgIPv6 = `${errorMsgPrefix} (8) and 128.`;
-
- if (isCIDR.v6(clusterNetworkCidr || '')) {
- return Yup.number().required(requiredText).min(8, errorMsgIPv6).max(128, errorMsgIPv6);
- }
-
- if (isCIDR.v4(clusterNetworkCidr || '')) {
- return Yup.number()
- .required(requiredText)
- .min(netBlockNumber, errorMsgIPv4)
- .max(25, errorMsgIPv4);
- }
-
- return Yup.number().required(requiredText);
-};
-
-export const richNameValidationSchema = (t: TFunction, usedNames: string[], origName?: string) => {
- const nameValidationMessagesList = nameValidationMessages(t);
- return Yup.string()
- .min(1, nameValidationMessagesList.INVALID_LENGTH)
- .max(253, nameValidationMessagesList.INVALID_LENGTH)
- .test(
- nameValidationMessagesList.INVALID_START_END,
- nameValidationMessagesList.INVALID_START_END,
- (value?: string) => {
- const trimmed = value?.trim();
- if (!trimmed) {
- return true;
- }
- return (
- !!trimmed[0].match(NAME_START_END_REGEX) &&
- (trimmed[trimmed.length - 1]
- ? !!trimmed[trimmed.length - 1].match(NAME_START_END_REGEX)
- : true)
- );
- },
- )
- .matches(HOST_NAME_REGEX, nameValidationMessagesList.INVALID_FORMAT)
- .matches(NAME_CHARS_REGEX, {
- message: nameValidationMessagesList.INVALID_VALUE,
- excludeEmptyString: true,
- })
- .test(nameValidationMessagesList.NOT_UNIQUE, nameValidationMessagesList.NOT_UNIQUE, (value) => {
- if (!value || value === origName) {
- return true;
- }
- return !usedNames.find((n) => n === value);
- })
- .notOneOf(FORBIDDEN_HOSTNAMES, hostnameValidationMessages(t).LOCALHOST_ERR);
-};
-
-export const richHostnameValidationSchema = (
- t: TFunction,
- usedNames: string[],
- origName?: string,
-) => {
- const hostnameValidationMessagesList = hostnameValidationMessages(t);
- return Yup.string()
- .min(1, hostnameValidationMessagesList.INVALID_LENGTH)
- .max(63, hostnameValidationMessagesList.INVALID_LENGTH)
- .test(
- hostnameValidationMessagesList.INVALID_START_END,
- hostnameValidationMessagesList.INVALID_START_END,
- (value?: string) => {
- const trimmed = value?.trim();
- if (!trimmed) {
- return true;
- }
- return (
- !!trimmed[0].match(NAME_START_END_REGEX) &&
- (trimmed[trimmed.length - 1]
- ? !!trimmed[trimmed.length - 1].match(NAME_START_END_REGEX)
- : true)
- );
- },
- )
- .matches(NAME_CHARS_REGEX, {
- message: hostnameValidationMessagesList.INVALID_VALUE,
- excludeEmptyString: true,
- })
- .test(
- hostnameValidationMessagesList.NOT_UNIQUE,
- hostnameValidationMessagesList.NOT_UNIQUE,
- (value) => {
- if (!value || value === origName) {
- return true;
- }
- return !usedNames.find((n) => n === value);
- },
- )
- .notOneOf(FORBIDDEN_HOSTNAMES, hostnameValidationMessagesList.LOCALHOST_ERR);
-};
-
-const httpProxyValidationMessage = 'Provide a valid HTTP URL.';
-export const httpProxyValidationSchema = ({
- values,
- pairValueName,
- allowEmpty,
-}: {
- values: ProxyFieldsType;
- pairValueName: 'httpProxy' | 'httpsProxy';
- allowEmpty?: boolean;
-}) => {
- const validation = Yup.string().test(
- 'http-proxy-validation',
- httpProxyValidationMessage,
- (value?: string) => {
- if (!value) {
- return true;
- }
-
- if (!value.startsWith('http://')) {
- return false;
- }
-
- try {
- new URL(value);
- } catch {
- return false;
- }
- return true;
- },
- );
-
- if (allowEmpty) {
- return validation;
- }
-
- return validation.test(
- 'http-proxy-no-empty-validation',
- 'At least one of the HTTP or HTTPS proxy URLs is required.',
- (value) => !values.enableProxy || !!value || !!values[pairValueName],
- );
-};
-
-const isIPorDN = (value?: string, dnsRegex = DNS_NAME_REGEX) => {
- if ((value as string).match(dnsRegex)) {
- return true;
- }
- try {
- ipValidationSchema.validateSync(value);
- return true;
- } catch (err) {
- return false;
- }
-};
-
-export const noProxyValidationSchema = Yup.string().test(
- 'no-proxy-validation',
- 'Provide a comma separated list of valid DNS names or IP addresses.',
- (value?: string) => {
- if (!value || value === '*') {
- return true;
- }
-
- // https://docs.openshift.com/container-platform/4.5/installing/installing_bare_metal/installing-restricted-networks-bare-metal.html#installation-configure-proxy_installing-restricted-networks-bare-metal
- // A comma-separated list of destination domain names, domains, IP addresses, or other network CIDRs to exclude proxying.
- // Preface a domain with . to match subdomains only. For example, .y.com matches x.y.com, but not y.com.
- // Use * to bypass proxy for all destinations.
- const noProxyList = trimCommaSeparatedList(value).split(',');
- return noProxyList.every((p) => isIPorDN(p, PROXY_DNS_REGEX));
- },
-);
-
-export const ntpSourceValidationSchema = Yup.string()
- .test(
- 'ntp-source-validation',
- 'Provide a comma separated list of valid DNS names or IP addresses.',
- (value?: string) => {
- if (!value || value === '') {
- return true;
- }
- return trimCommaSeparatedList(value)
- .split(',')
- .every((v) => isIPorDN(v));
- },
- )
- .test(
- 'ntp-source-validation-unique',
- 'DNS names and IP addresses must be unique.',
- (value?: string) => {
- if (!value || value === '') {
- return true;
- }
- const arr = trimCommaSeparatedList(value).split(',');
- return arr.length === new Set(arr).size;
- },
- );
-
-export const day2ApiVipValidationSchema = Yup.string().test(
- 'day2-api-vip',
- 'Provide a valid DNS name or IP Address',
- (value?: string) => {
- if (!value) {
- return true;
- }
- return isIPorDN(value);
- },
-);
-
-export const bmcAddressValidationSchema = (t: TFunction) => {
- const bmcAddressValidationMessagesList = bmcAddressValidationMessages(t);
-
- return Yup.string()
- .required()
- .test('valid-bmc-address', bmcAddressValidationMessagesList.INVALID_VALUE, (val) => {
- try {
- const url = parseUrl(val);
- return ['redfish-virtualmedia', 'idrac-virtualmedia'].includes(url.protocol as string);
- } catch (error) {
- return false;
- }
- });
-};
-export const locationValidationSchema = (t: TFunction) => {
- const locationValidationMessagesList = locationValidationMessages(t);
- return Yup.string()
- .min(1, locationValidationMessagesList.INVALID_LENGTH)
- .max(63, locationValidationMessagesList.INVALID_LENGTH)
- .test(
- locationValidationMessagesList.INVALID_START_END,
- locationValidationMessagesList.INVALID_START_END,
- (value?: string) => {
- const trimmed = value?.trim();
- if (!trimmed) {
- return true;
- }
- return (
- !!trimmed[0].match(ALPHANUMERIC_REGEX) &&
- (trimmed[trimmed.length - 1]
- ? !!trimmed[trimmed.length - 1].match(ALPHANUMERIC_REGEX)
- : true)
- );
- },
- )
- .matches(LOCATION_CHARS_REGEX, {
- message: locationValidationMessagesList.INVALID_VALUE,
- excludeEmptyString: true,
- });
-};
-
-export const machineNetworksValidationSchema = Yup.array().of(
- Yup.object({ cidr: hostSubnetValidationSchema, clusterId: Yup.string() }),
-);
-
-export const clusterNetworksValidationSchema = Yup.array().of(
- Yup.lazy((values: ClusterNetwork) =>
- Yup.object({
- cidr: ipBlockValidationSchema(
- undefined /* So far used in OCM only and so validated by backend */,
- ),
- hostPrefix: hostPrefixValidationSchema(values.cidr),
- clusterId: Yup.string(),
- }),
- ),
-);
-
-export const serviceNetworkValidationSchema = Yup.array().of(
- Yup.object({
- cidr: ipBlockValidationSchema(
- undefined /* So far used in OCM only and so validated by backend */,
- ),
- clusterId: Yup.string(),
- }),
-);
-
-export const dualStackValidationSchema = (field: string, openshiftVersion?: string) =>
- Yup.array()
- .max(2, `Maximum number of ${field} subnets in dual stack is 2.`)
- .test(
- 'dual-stack-ipv4',
- openshiftVersion && isMajorMinorVersionEqualOrGreater(openshiftVersion, '4.12')
- ? 'First network has to be a valid IPv4 or IPv6 subnet.'
- : 'First network has to be IPv4 subnet.',
- (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 && (isCIDR.v4(values[0].cidr) || isCIDR.v6(values[0].cidr));
- }
- // For older versions, require IPv4 as primary network
- 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);
- },
- );
-
-export const IPv4ValidationSchema = Yup.array().test(
- 'single-stack',
- `All network subnets must be IPv4.`,
- (values?: (MachineNetwork | ClusterNetwork | ServiceNetwork)[]) => allSubnetsIPv4(values),
-);
diff --git a/libs/ui-lib/lib/common/index.ts b/libs/ui-lib/lib/common/index.ts
index c44a26e950..c9e2473310 100644
--- a/libs/ui-lib/lib/common/index.ts
+++ b/libs/ui-lib/lib/common/index.ts
@@ -7,4 +7,6 @@ export * from './reducers';
export * from './selectors';
export * from './hooks';
export * from './utils';
+export * from './validationSchemas';
+
export { ResourceUIState } from './types/resource-ui-state';
diff --git a/libs/ui-lib/lib/common/utils.ts b/libs/ui-lib/lib/common/utils.ts
index b7e369b39a..26d9236ea2 100644
--- a/libs/ui-lib/lib/common/utils.ts
+++ b/libs/ui-lib/lib/common/utils.ts
@@ -2,13 +2,12 @@ import filesize from 'filesize.js';
import camelCase from 'lodash-es/camelCase.js';
import isString from 'lodash-es/isString.js';
import { loadAll } from 'js-yaml';
+import { TFunction } from 'i18next';
import { MAX_FILE_SIZE_BYTES, MAX_FILE_SIZE_OFFSET_FACTOR } from './configurations';
export const FILENAME_REGEX = /^[^\/]*\.(json|ya?ml(\.patch_?[a-zA-Z0-9_]*)?)$/;
export const FILE_TYPE_MESSAGE = 'Unsupported file type. Please provide a valid YAML file.';
-export const INCORRECT_TYPE_FILE_MESSAGE =
- 'File type is not supported. File type must be yaml, yml ,json , yaml.patch. or yml.patch.';
export const getErrorMessage = (error: unknown): string => {
if (error instanceof Error) {
@@ -60,11 +59,10 @@ export const fileSize: typeof filesize = (...args) =>
.toUpperCase()
.replace(/I/, 'i');
-export const getMaxFileSizeMessage = `File size is too big. The file size must be less than ${fileSize(
- MAX_FILE_SIZE_BYTES,
- 0,
- 'si',
-)}.`;
+export const getMaxFileSizeMessage = (t: TFunction) =>
+ t('ai:File size is too big. The file size must be less than {{value}}.', {
+ value: fileSize(MAX_FILE_SIZE_BYTES, 0, 'si'),
+ });
export const validateFileName = (fileName: string) => {
return new RegExp(FILENAME_REGEX).test(fileName || '');
diff --git a/libs/ui-lib/lib/common/components/ui/formik/_validationSchemas.test.ts b/libs/ui-lib/lib/common/validationSchemas/_validationSchemas.test.ts
similarity index 86%
rename from libs/ui-lib/lib/common/components/ui/formik/_validationSchemas.test.ts
rename to libs/ui-lib/lib/common/validationSchemas/_validationSchemas.test.ts
index fd54185288..42ab30ef61 100644
--- a/libs/ui-lib/lib/common/components/ui/formik/_validationSchemas.test.ts
+++ b/libs/ui-lib/lib/common/validationSchemas/_validationSchemas.test.ts
@@ -10,7 +10,7 @@ import {
nameValidationSchema,
noProxyValidationSchema,
pullSecretValidationSchema,
-} from './validationSchemas';
+} from './..';
import { TFunction } from 'react-i18next';
import { CLUSTER_NAME_MAX_LENGTH } from './constants';
@@ -32,7 +32,7 @@ describe('validationSchemas', () => {
await Promise.all(
valid.map((value) =>
- noProxyValidationSchema
+ noProxyValidationSchema(t)
.validate(value)
.catch(() => expect(value).toBe('was rejected but is valid')),
),
@@ -45,10 +45,12 @@ describe('validationSchemas', () => {
let counter = 0;
await Promise.all(
invalid.map((value) =>
- noProxyValidationSchema.validate(value).then(
- () => expect(value).toBe('should be rejected since it is invalid'),
- () => counter++,
- ),
+ noProxyValidationSchema(t)
+ .validate(value)
+ .then(
+ () => expect(value).toBe('should be rejected since it is invalid'),
+ () => counter++,
+ ),
),
);
@@ -106,7 +108,7 @@ describe('validationSchemas', () => {
await Promise.all(
valid.map((value) =>
- pullSecretValidationSchema
+ pullSecretValidationSchema(t)
.validate(value)
.catch((msg: string) => expect(value).toBe(`was rejected but is valid: ${msg}`)),
),
@@ -115,10 +117,12 @@ describe('validationSchemas', () => {
let counter = 0;
await Promise.all(
invalid.map((value) =>
- pullSecretValidationSchema.validate(value).then(
- () => expect(value).toBe('should be rejected since it is invalid'),
- () => counter++,
- ),
+ pullSecretValidationSchema(t)
+ .validate(value)
+ .then(
+ () => expect(value).toBe('should be rejected since it is invalid'),
+ () => counter++,
+ ),
),
);
@@ -147,7 +151,7 @@ describe('validationSchemas', () => {
await Promise.all(
valid.map((value) =>
- ipValidationSchema
+ ipValidationSchema(t)
.validate(value)
.catch((msg: string) => expect(value).toBe(`was rejected but is valid: ${msg}`)),
),
@@ -156,10 +160,12 @@ describe('validationSchemas', () => {
let counter = 0;
await Promise.all(
invalid.map((value) =>
- ipValidationSchema.validate(value).then(
- () => expect(value).toBe('should be rejected since it is invalid'),
- () => counter++,
- ),
+ ipValidationSchema(t)
+ .validate(value)
+ .then(
+ () => expect(value).toBe('should be rejected since it is invalid'),
+ () => counter++,
+ ),
),
);
@@ -182,7 +188,7 @@ describe('validationSchemas', () => {
await Promise.all(
valid.map((value) =>
- macAddressValidationSchema
+ macAddressValidationSchema(t)
.validate(value)
.catch((msg: string) => expect(value).toBe(`was rejected but is valid: ${msg}`)),
),
@@ -191,10 +197,12 @@ describe('validationSchemas', () => {
let counter = 0;
await Promise.all(
invalid.map((value) =>
- macAddressValidationSchema.validate(value).then(
- () => expect(value).toBe('should be rejected since it is invalid'),
- () => counter++,
- ),
+ macAddressValidationSchema(t)
+ .validate(value)
+ .then(
+ () => expect(value).toBe('should be rejected since it is invalid'),
+ () => counter++,
+ ),
),
);
@@ -211,7 +219,7 @@ describe('validationSchemas', () => {
'192.0.0.0/8' /* Overlap */,
];
- const validationSchema = ipBlockValidationSchema(['192.168.1.0']);
+ const validationSchema = ipBlockValidationSchema(['192.168.1.0'], t);
await Promise.all(
valid.map((value) =>
validationSchema
@@ -249,7 +257,7 @@ describe('validationSchemas', () => {
await Promise.all(
valid.map((value) =>
- dnsNameValidationSchema
+ dnsNameValidationSchema(t)
.validate(value)
.catch((msg: string) => expect(value).toBe(`was rejected but is valid: ${msg}`)),
),
@@ -258,10 +266,12 @@ describe('validationSchemas', () => {
let counter = 0;
await Promise.all(
invalid.map((value) =>
- dnsNameValidationSchema.validate(value).then(
- () => expect(value).toBe('should be rejected since it is invalid'),
- () => counter++,
- ),
+ dnsNameValidationSchema(t)
+ .validate(value)
+ .then(
+ () => expect(value).toBe('should be rejected since it is invalid'),
+ () => counter++,
+ ),
),
);
@@ -272,7 +282,7 @@ describe('validationSchemas', () => {
const valid = [16, 25];
const invalid = ['', 'a', 0, 1, 33, 15, 26];
- const validationSchema = hostPrefixValidationSchema('192.168.0.0/16');
+ const validationSchema = hostPrefixValidationSchema('192.168.0.0/16', t);
await Promise.all(
valid.map((value) =>
validationSchema
@@ -298,7 +308,7 @@ describe('validationSchemas', () => {
const valid = [8, 32, 128];
const invalid = ['', 'a', 0, 1, 7, 129];
- const validationSchema = hostPrefixValidationSchema('2002::1234:abcd:ffff:c0a8:101/64');
+ const validationSchema = hostPrefixValidationSchema('2002::1234:abcd:ffff:c0a8:101/64', t);
await Promise.all(
valid.map((value) =>
validationSchema
@@ -330,7 +340,7 @@ describe('validationSchemas', () => {
await Promise.all(
valid.map((value) =>
- baseDomainValidationSchema
+ baseDomainValidationSchema(t)
.validate(value)
.catch(() => expect(value).toBe(`was rejected but is valid`)),
),
@@ -339,10 +349,12 @@ describe('validationSchemas', () => {
let counter = 0;
await Promise.all(
invalid.map((value) =>
- baseDomainValidationSchema.validate(value).then(
- () => expect(value).toBe('should be rejected since it is invalid'),
- () => counter++,
- ),
+ baseDomainValidationSchema(t)
+ .validate(value)
+ .then(
+ () => expect(value).toBe('should be rejected since it is invalid'),
+ () => counter++,
+ ),
),
);
@@ -351,7 +363,7 @@ describe('validationSchemas', () => {
describe('dualStackValidationSchema', () => {
describe('OCP < 4.12 (legacy behavior)', () => {
- const schema = dualStackValidationSchema('machine networks', '4.11');
+ const schema = dualStackValidationSchema('machine networks', t, '4.11');
test('validates IPv4 as primary and IPv6 as secondary', async () => {
const validValues = [[{ cidr: '192.168.1.0/24' }, { cidr: '2001:db8::/64' }]];
@@ -373,7 +385,7 @@ describe('validationSchemas', () => {
});
describe('OCP >= 4.12 (new behavior)', () => {
- const schema = dualStackValidationSchema('machine networks', '4.12');
+ const schema = dualStackValidationSchema('machine networks', t, '4.12');
test('validates IPv4 as primary and IPv6 as secondary', async () => {
const validValues = [[{ cidr: '192.168.1.0/24' }, { cidr: '2001:db8::/64' }]];
@@ -393,7 +405,7 @@ describe('validationSchemas', () => {
});
describe('no version specified (defaults to legacy behavior)', () => {
- const schema = dualStackValidationSchema('machine networks');
+ const schema = dualStackValidationSchema('machine networks', t);
test('validates IPv4 as primary and IPv6 as secondary', async () => {
const validValues = [[{ cidr: '192.168.1.0/24' }, { cidr: '2001:db8::/64' }]];
@@ -416,7 +428,7 @@ describe('validationSchemas', () => {
describe('edge cases', () => {
test('validates maximum 2 networks', async () => {
- const schema = dualStackValidationSchema('machine networks', '4.12');
+ const schema = dualStackValidationSchema('machine networks', t, '4.12');
const invalidValue = [
{ cidr: '192.168.1.0/24' },
{ cidr: '2001:db8::/64' },
@@ -424,7 +436,7 @@ describe('validationSchemas', () => {
];
await expect(schema.validate(invalidValue)).rejects.toThrow(
- 'Maximum number of machine networks subnets in dual stack is 2',
+ 'ai:Maximum number of {{field}} subnets in dual stack is 2',
);
});
});
diff --git a/libs/ui-lib/lib/common/validationSchemas/addressValidation.tsx b/libs/ui-lib/lib/common/validationSchemas/addressValidation.tsx
new file mode 100644
index 0000000000..ac6466479d
--- /dev/null
+++ b/libs/ui-lib/lib/common/validationSchemas/addressValidation.tsx
@@ -0,0 +1,349 @@
+import * as Yup from 'yup';
+import { Address4, Address6 } from 'ip-address';
+import isCIDR from 'is-cidr';
+import { isInSubnet } from 'is-in-subnet';
+
+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 { NO_SUBNET_SET } from '../config';
+import { overlap } from 'cidr-tools';
+import parseUrl from 'parse-url';
+import { TFunction } from 'i18next';
+
+export const ipValidationSchema = (t: TFunction) =>
+ Yup.string().test(
+ 'ip-validation',
+ t('ai:Not a valid IP address'),
+ (value?: string) => Address4.isValid(value || '') || Address6.isValid(value || ''),
+ );
+
+export const ipNoSuffixValidationSchema = (t: TFunction) =>
+ Yup.string().test('ip-validation-no-suffix', t('ai:Not a valid IP address'), (value?: string) => {
+ const address = getAddress(value || '');
+ return !!address && address.address === address.addressMinusSuffix;
+ });
+
+export const macAddressValidationSchema = (t: TFunction) =>
+ Yup.string().matches(MAC_REGEX, {
+ message: (value) => t('ai:Value "{{value}}" is not valid MAC address.', { value }),
+ excludeEmptyString: true,
+ });
+
+export const vipRangeValidationSchema = (
+ hostSubnets: HostSubnets,
+ { machineNetworks }: NetworkConfigurationValues,
+ allowSuffix: boolean,
+ t: TFunction,
+) =>
+ Yup.string().test('vip-validation', t('ai:IP Address is outside of selected subnet'), (value) => {
+ if (!value) {
+ return true;
+ }
+
+ try {
+ const validator = allowSuffix ? ipValidationSchema : ipNoSuffixValidationSchema;
+ validator(t).validateSync(value);
+ } catch (err) {
+ return true;
+ }
+ // Find host subnets that match the selected machine networks
+ const cidrs = machineNetworks?.map((network) => network.cidr) ?? [];
+ const matchingSubnets = hostSubnets.filter((hostSubnet) => cidrs.includes(hostSubnet.subnet));
+
+ for (const hostSubnet of matchingSubnets) {
+ if (hostSubnet?.subnet) {
+ // Workaround for bug in CIM backend. hostIDs are empty
+ if (!hostSubnet.hostIDs.length) {
+ return true;
+ } else if (isInSubnet(value, hostSubnet.subnet)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ });
+
+const vipBroadcastValidationSchema = (
+ { machineNetworks }: NetworkConfigurationValues,
+ t: TFunction,
+) =>
+ Yup.string().test(
+ 'vip-no-broadcast',
+ t('ai:The IP address cannot be a network or broadcast address'),
+ function (value?: string) {
+ if (!value) {
+ return true;
+ }
+
+ const vipAddress = getAddress(value)?.address;
+ if (!vipAddress) {
+ return true;
+ }
+
+ const index = getArrayIndexFromPath(this.path || '');
+ const machineNetworkCidr = machineNetworks?.[index]?.cidr ?? machineNetworks?.[0]?.cidr ?? '';
+ if (!machineNetworkCidr) {
+ return true;
+ }
+
+ const machineNetwork = getAddress(machineNetworkCidr);
+ if (!machineNetwork) {
+ return true;
+ }
+
+ const machineNetworkBroadcast = machineNetwork.endAddress().address;
+ const machineNetworkAddress = machineNetwork.startAddress().address;
+
+ return vipAddress !== machineNetworkBroadcast && vipAddress !== machineNetworkAddress;
+ },
+ );
+
+export const hostSubnetValidationSchema = Yup.string().when(['managedNetworkingType'], {
+ is: (managedNetworkingType: NetworkConfigurationValues['managedNetworkingType']) =>
+ managedNetworkingType === 'clusterManaged',
+ then: () => Yup.string().notOneOf([NO_SUBNET_SET], 'Host subnet must be selected.'),
+});
+
+export const vipNoSuffixValidationSchema = (
+ hostSubnets: HostSubnets,
+ values: NetworkConfigurationValues,
+ t: TFunction,
+) =>
+ Yup.mixed().when(['vipDhcpAllocation', 'managedNetworkingType'], {
+ is: (
+ vipDhcpAllocation: NetworkConfigurationValues['vipDhcpAllocation'],
+ managedNetworkingType: NetworkConfigurationValues['managedNetworkingType'],
+ ) => !vipDhcpAllocation && managedNetworkingType !== 'userManaged',
+ 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',
+ t('ai:IP family must match the corresponding machine network family.'),
+ function (value?: string) {
+ if (!value) {
+ return true;
+ }
+ const index = getArrayIndexFromPath(this.path || '');
+ 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;
+ },
+ );
+ // Ensure API and Ingress VIPs at the same index are not the same
+ const vipUniqueSchema = Yup.string().test('vip-uniqueness', function (value?: string) {
+ if (!value) {
+ return true;
+ }
+ const index = getArrayIndexFromPath(this.path || '');
+ if (Number.isNaN(index)) {
+ return true;
+ }
+ const apiVip = values.apiVips?.[index]?.ip;
+ const ingressVip = values.ingressVips?.[index]?.ip;
+ if (!apiVip || !ingressVip) {
+ return true;
+ }
+ 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'),
+ }),
+ });
+ }
+ return true;
+ });
+
+ return alwaysRequired(t('ai:Required field'))
+ .concat(ipNoSuffixValidationSchema(t))
+ .concat(vipFamilyMatchSchema)
+ .concat(vipRangeValidationSchema(hostSubnets, values, false, t))
+ .concat(vipBroadcastValidationSchema(values, t))
+ .concat(vipUniqueSchema);
+ },
+ });
+
+export const vipArrayValidationSchema = >(
+ hostSubnets: HostSubnets,
+ values: NetworkConfigurationValues,
+ t: TFunction,
+) =>
+ values.managedNetworkingType === 'clusterManaged'
+ ? Yup.array().of(
+ Yup.object({
+ clusterId: Yup.string(),
+ ip: vipNoSuffixValidationSchema(hostSubnets, values, t),
+ }),
+ )
+ : Yup.array();
+
+export const ipBlockValidationSchema = (
+ reservedCidrs: string | string[] | undefined,
+ t: TFunction,
+) =>
+ Yup.string()
+ .required('A value is required.')
+ .test(
+ 'valid-ip-address',
+ t(
+ 'ai:Invalid IP address block. Expected value is a network expressed in CIDR notation (IP/netmask). For example: 123.123.123.0/24, 2055:d7a::/116',
+ ),
+ (value?: string): boolean => !!value && (isCIDR.v4(value) || isCIDR.v6(value)),
+ )
+ .test(
+ 'valid-netmask',
+ t(
+ 'ai:IPv4 netmask must be between 1-25 and include at least 128 addresses.\nIPv6 netmask must be between 8-128 and include at least 256 addresses.',
+ ),
+ (value?: string) => {
+ const suffix = parseInt((value || '').split('/')[1]);
+
+ return (
+ (isCIDR.v4(value || '') && 0 < suffix && suffix < 26) ||
+ (isCIDR.v6(value || '') && 7 < suffix && suffix < 129)
+ );
+ },
+ )
+ .test(
+ 'cidr-is-not-unspecified',
+ t(
+ 'ai:The specified CIDR is invalid because its resulting routing prefix matches the unspecified address.',
+ ),
+ (cidr: string) => {
+ const ip = getSubnet(cidr);
+ if (ip === null) {
+ return false;
+ }
+
+ // The first address is used to represent the network
+ const startAddress = ip.startAddress().address;
+
+ return startAddress !== IP_V4_ZERO && startAddress !== IP_V6_ZERO;
+ },
+ )
+ .test(
+ 'valid-cidr-base-address',
+ ({ value }) => t('ai:{{value}} is not a valid CIDR', { value: value as string }),
+ (cidr: string) => {
+ const ip = getSubnet(cidr);
+ if (ip === null) {
+ return false;
+ }
+
+ const networkAddress = ip.startAddress().parsedAddress;
+ const ipAddress = ip.parsedAddress;
+ const result = ipAddress.every((part, idx) => part === networkAddress[idx]);
+
+ return result;
+ },
+ )
+ .test('cidrs-can-not-overlap', t('ai:Provided CIDRs can not overlap.'), (cidr: string) => {
+ try {
+ if (cidr && reservedCidrs && reservedCidrs.length > 0) {
+ return !overlap(cidr, reservedCidrs);
+ }
+ } catch {
+ return false;
+ }
+ // passing by default
+ return true;
+ });
+
+export const hostPrefixValidationSchema = (
+ clusterNetworkCidr: NetworkConfigurationValues['clusterNetworkCidr'],
+ t: TFunction,
+) => {
+ const requiredText = t('ai:Required field');
+ const minMaxText = t(
+ 'ai:The host prefix is a number between 1 and 32 for IPv4 and between 8 and 128 for IPv6.',
+ );
+ const netBlock = (clusterNetworkCidr || '').split('/')[1];
+ if (!netBlock) {
+ return Yup.number().required(requiredText).min(1, minMaxText).max(32, minMaxText);
+ }
+
+ let netBlockNumber = parseInt(netBlock);
+ if (isNaN(netBlockNumber)) {
+ netBlockNumber = 1;
+ }
+
+ const errorMsgPrefix = t(
+ 'ai:The host prefix is a number between size of the cluster network CIDR range',
+ );
+ const errorMsgIPv4 = t('ai:{{errorMsgPrefix}} ({{netBlockNumber}}) and 25.', {
+ errorMsgPrefix,
+ netBlockNumber,
+ });
+ const errorMsgIPv6 = t('ai:{{errorMsgPrefix}} (8) and 128.', { errorMsgPrefix });
+
+ if (isCIDR.v6(clusterNetworkCidr || '')) {
+ return Yup.number().required(requiredText).min(8, errorMsgIPv6).max(128, errorMsgIPv6);
+ }
+
+ if (isCIDR.v4(clusterNetworkCidr || '')) {
+ return Yup.number()
+ .required(requiredText)
+ .min(netBlockNumber, errorMsgIPv4)
+ .max(25, errorMsgIPv4);
+ }
+
+ return Yup.number().required(requiredText);
+};
+
+export const day2ApiVipValidationSchema = (t: TFunction) =>
+ Yup.string().test(
+ 'day2-api-vip',
+ t('ai:Provide a valid DNS name or IP Address'),
+ (value?: string) => {
+ if (!value) {
+ return true;
+ }
+ return isIPorDN(value);
+ },
+ );
+
+export const bmcAddressValidationSchema = (t: TFunction) => {
+ return Yup.string()
+ .required()
+ .test(
+ 'valid-bmc-address',
+ t(
+ 'ai:The Value is not valid BMC address, supported protocols are redfish-virtualmedia or idrac-virtualmedia.',
+ ),
+ (val) => {
+ try {
+ const url = parseUrl(val);
+ return ['redfish-virtualmedia', 'idrac-virtualmedia'].includes(url.protocol as string);
+ } catch (error) {
+ return false;
+ }
+ },
+ );
+};
diff --git a/libs/ui-lib/lib/common/validationSchemas/clusterDetailsValidation.tsx b/libs/ui-lib/lib/common/validationSchemas/clusterDetailsValidation.tsx
new file mode 100644
index 0000000000..55dde9bcfe
--- /dev/null
+++ b/libs/ui-lib/lib/common/validationSchemas/clusterDetailsValidation.tsx
@@ -0,0 +1,168 @@
+import { TFunction } from 'i18next';
+import * as Yup from 'yup';
+
+import {
+ CLUSTER_NAME_MAX_LENGTH,
+ clusterNameValidationMessages,
+ FORBIDDEN_HOSTNAMES,
+ hostnameValidationMessages,
+ locationValidationMessages,
+ nameValidationMessages,
+} from './constants';
+import { ClusterDetailsValues } from '../components';
+import {
+ ALPHANUMERIC_REGEX,
+ CLUSTER_NAME_START_END_REGEX,
+ CLUSTER_NAME_VALID_CHARS_REGEX,
+ DNS_NAME_REGEX,
+ HOST_NAME_REGEX,
+ LOCATION_CHARS_REGEX,
+ NAME_CHARS_REGEX,
+ NAME_START_END_REGEX,
+} from './regexes';
+
+export const nameValidationSchema = (
+ t: TFunction,
+ usedClusterNames: string[],
+ baseDnsDomain = '',
+ validateUniqueName?: boolean,
+ isOcm = false,
+) => {
+ const clusterNameValidationMessagesList = clusterNameValidationMessages(t);
+ return Yup.string()
+ .required(t('ai:Required field'))
+ .matches(CLUSTER_NAME_VALID_CHARS_REGEX, {
+ message: clusterNameValidationMessagesList.INVALID_VALUE,
+ excludeEmptyString: true,
+ })
+ .matches(CLUSTER_NAME_START_END_REGEX, {
+ message: clusterNameValidationMessagesList.INVALID_START_END,
+ excludeEmptyString: true,
+ })
+ .min(
+ isOcm ? 1 : 2,
+ isOcm
+ ? clusterNameValidationMessagesList.INVALID_LENGTH_OCM
+ : clusterNameValidationMessagesList.INVALID_LENGTH_ACM,
+ )
+ .max(
+ CLUSTER_NAME_MAX_LENGTH,
+ isOcm
+ ? clusterNameValidationMessagesList.INVALID_LENGTH_OCM
+ : clusterNameValidationMessagesList.INVALID_LENGTH_ACM,
+ )
+ .when('useRedHatDnsService', {
+ is: (useRedHatDnsService: ClusterDetailsValues['useRedHatDnsService']) =>
+ useRedHatDnsService === true,
+ then: (schema) =>
+ schema.test(
+ 'is-name-unique',
+ clusterNameValidationMessagesList.NOT_UNIQUE,
+ (value?: string) => {
+ const clusterFullName = `${value || ''}.${baseDnsDomain}`;
+ return !value || !usedClusterNames.includes(clusterFullName);
+ },
+ ),
+ otherwise: (schema) =>
+ schema.test(
+ 'is-name-unique',
+ clusterNameValidationMessagesList.NOT_UNIQUE,
+ (value?: string) => {
+ // in CIM cluster name is ClusterDeployment CR name which must be unique
+ return validateUniqueName ? !value || !usedClusterNames.includes(value) : true;
+ },
+ ),
+ });
+};
+
+export const richNameValidationSchema = (t: TFunction, usedNames: string[], origName?: string) => {
+ const nameValidationMessagesList = nameValidationMessages(t);
+ return Yup.string()
+ .min(1, nameValidationMessagesList.INVALID_LENGTH)
+ .max(253, nameValidationMessagesList.INVALID_LENGTH)
+ .test(
+ nameValidationMessagesList.INVALID_START_END,
+ nameValidationMessagesList.INVALID_START_END,
+ (value?: string) => {
+ const trimmed = value?.trim();
+ if (!trimmed) {
+ return true;
+ }
+ return (
+ !!trimmed[0].match(NAME_START_END_REGEX) &&
+ (trimmed[trimmed.length - 1]
+ ? !!trimmed[trimmed.length - 1].match(NAME_START_END_REGEX)
+ : true)
+ );
+ },
+ )
+ .matches(HOST_NAME_REGEX, nameValidationMessagesList.INVALID_FORMAT)
+ .matches(NAME_CHARS_REGEX, {
+ message: nameValidationMessagesList.INVALID_VALUE,
+ excludeEmptyString: true,
+ })
+ .test(nameValidationMessagesList.NOT_UNIQUE, nameValidationMessagesList.NOT_UNIQUE, (value) => {
+ if (!value || value === origName) {
+ return true;
+ }
+ return !usedNames.find((n) => n === value);
+ })
+ .notOneOf(FORBIDDEN_HOSTNAMES, hostnameValidationMessages(t).LOCALHOST_ERR);
+};
+
+export const dnsNameValidationSchema = (t: TFunction) =>
+ Yup.string()
+ .test(
+ 'dns-name-label-length',
+ t('ai:Single label of the DNS name can not be longer than 63 characters.'),
+ (value?: string) => (value || '').split('.').every((label: string) => label.length <= 63),
+ )
+ .matches(DNS_NAME_REGEX, {
+ message: (value) =>
+ t('ai:Value "{{value}}" is not valid DNS name. Example: basedomain.example.com', { value }),
+ excludeEmptyString: true,
+ });
+
+export const baseDomainValidationSchema = (t: TFunction) =>
+ Yup.string().test(
+ 'dns-name-label-length',
+ t(
+ 'ai:Every single host component in the base domain name cannot contain more than 63 characters and must not contain spaces.',
+ ),
+ (value?: string) => {
+ // Check if the value contains any spaces
+ if (/\s/.test(value as string)) {
+ return false; // Value contains spaces, validation fails
+ }
+
+ // Check the label lengths
+ const labels = (value || '').split('.');
+ return labels.every((label: string) => label.length <= 63);
+ },
+ );
+
+export const locationValidationSchema = (t: TFunction) =>
+ Yup.string()
+ .min(1, locationValidationMessages(t).INVALID_LENGTH)
+ .max(63, locationValidationMessages(t).INVALID_LENGTH)
+ .test(
+ locationValidationMessages(t).INVALID_START_END,
+ locationValidationMessages(t).INVALID_START_END,
+ (value?: string) => {
+ const trimmed = value?.trim();
+ if (!trimmed) {
+ return true;
+ }
+ return (
+ !!trimmed[0].match(ALPHANUMERIC_REGEX) &&
+ (trimmed[trimmed.length - 1]
+ ? !!trimmed[trimmed.length - 1].match(ALPHANUMERIC_REGEX)
+ : true)
+ );
+ },
+ )
+ .matches(LOCATION_CHARS_REGEX, {
+ message: locationValidationMessages(t).INVALID_VALUE,
+ excludeEmptyString: true,
+ })
+ .required(t('ai:Location is a required field.'));
diff --git a/libs/ui-lib/lib/common/components/ui/formik/constants.tsx b/libs/ui-lib/lib/common/validationSchemas/constants.tsx
similarity index 93%
rename from libs/ui-lib/lib/common/components/ui/formik/constants.tsx
rename to libs/ui-lib/lib/common/validationSchemas/constants.tsx
index 11623efc39..9d9cc20270 100644
--- a/libs/ui-lib/lib/common/components/ui/formik/constants.tsx
+++ b/libs/ui-lib/lib/common/validationSchemas/constants.tsx
@@ -57,12 +57,6 @@ export const locationValidationMessages = (t: TFunction) => ({
INVALID_START_END: t('ai:Must start and end with an alphanumeric character'),
});
-export const bmcAddressValidationMessages = (t: TFunction) => ({
- INVALID_VALUE: t(
- 'ai:The Value is not valid BMC address, supported protocols are redfish-virtualmedia or idrac-virtualmedia.',
- ),
-});
-
export const FORBIDDEN_HOSTNAMES = [
'localhost',
'localhost.localdomain',
@@ -71,3 +65,6 @@ export const FORBIDDEN_HOSTNAMES = [
'localhost6',
'localhost6.localdomain6',
];
+
+export const getIncorrectFileTypeMessage = (t: TFunction) =>
+ t('ai:File type is not supported. File type must be yaml, yml, json, yaml.patch. or yml.patch.');
diff --git a/libs/ui-lib/lib/common/validationSchemas/hostnameValidation.tsx b/libs/ui-lib/lib/common/validationSchemas/hostnameValidation.tsx
new file mode 100644
index 0000000000..69e6984e74
--- /dev/null
+++ b/libs/ui-lib/lib/common/validationSchemas/hostnameValidation.tsx
@@ -0,0 +1,46 @@
+import * as Yup from 'yup';
+import { TFunction } from 'i18next';
+import { FORBIDDEN_HOSTNAMES, hostnameValidationMessages } from './constants';
+import { NAME_START_END_REGEX, NAME_CHARS_REGEX } from './regexes';
+
+export const richHostnameValidationSchema = (
+ t: TFunction,
+ usedNames: string[],
+ origName?: string,
+) => {
+ const hostnameValidationMessagesList = hostnameValidationMessages(t);
+ return Yup.string()
+ .min(1, hostnameValidationMessagesList.INVALID_LENGTH)
+ .max(63, hostnameValidationMessagesList.INVALID_LENGTH)
+ .test(
+ hostnameValidationMessagesList.INVALID_START_END,
+ hostnameValidationMessagesList.INVALID_START_END,
+ (value?: string) => {
+ const trimmed = value?.trim();
+ if (!trimmed) {
+ return true;
+ }
+ return (
+ !!trimmed[0].match(NAME_START_END_REGEX) &&
+ (trimmed[trimmed.length - 1]
+ ? !!trimmed[trimmed.length - 1].match(NAME_START_END_REGEX)
+ : true)
+ );
+ },
+ )
+ .matches(NAME_CHARS_REGEX, {
+ message: hostnameValidationMessagesList.INVALID_VALUE,
+ excludeEmptyString: true,
+ })
+ .test(
+ hostnameValidationMessagesList.NOT_UNIQUE,
+ hostnameValidationMessagesList.NOT_UNIQUE,
+ (value) => {
+ if (!value || value === origName) {
+ return true;
+ }
+ return !usedNames.find((n) => n === value);
+ },
+ )
+ .notOneOf(FORBIDDEN_HOSTNAMES, hostnameValidationMessagesList.LOCALHOST_ERR);
+};
diff --git a/libs/ui-lib/lib/common/validationSchemas/index.tsx b/libs/ui-lib/lib/common/validationSchemas/index.tsx
new file mode 100644
index 0000000000..ead20916e2
--- /dev/null
+++ b/libs/ui-lib/lib/common/validationSchemas/index.tsx
@@ -0,0 +1,11 @@
+export * from './addressValidation';
+export * from './clusterDetailsValidation';
+export * from './hostnameValidation';
+export * from './networkingValidation';
+export * from './ntpValidation';
+export * from './proxyValidation';
+export * from './pullSecretValidation';
+export * from './sshValidation';
+
+export * from './constants';
+export { ALPHANUMERIC_REGEX, LOCATION_CHARS_REGEX } from './regexes';
diff --git a/libs/ui-lib/lib/common/validationSchemas/networkingValidation.tsx b/libs/ui-lib/lib/common/validationSchemas/networkingValidation.tsx
new file mode 100644
index 0000000000..4fb641ceeb
--- /dev/null
+++ b/libs/ui-lib/lib/common/validationSchemas/networkingValidation.tsx
@@ -0,0 +1,113 @@
+import * as Yup from 'yup';
+import isCIDR from 'is-cidr';
+import { TFunction } from 'i18next';
+
+import {
+ ClusterNetwork,
+ MachineNetwork,
+ ServiceNetwork,
+} from '@openshift-assisted/types/./assisted-installer-service';
+import {
+ hostPrefixValidationSchema,
+ hostSubnetValidationSchema,
+ ipBlockValidationSchema,
+} from './addressValidation';
+import { isMajorMinorVersionEqualOrGreater } from '../utils';
+import { allSubnetsIPv4 } from '../components/ui/formik/utils';
+
+export const machineNetworksValidationSchema = Yup.array().of(
+ Yup.object({ cidr: hostSubnetValidationSchema, clusterId: Yup.string() }),
+);
+
+export const clusterNetworksValidationSchema = (t: TFunction) =>
+ Yup.array().of(
+ Yup.lazy((values: ClusterNetwork) =>
+ Yup.object({
+ cidr: ipBlockValidationSchema(
+ undefined /* So far used in OCM only and so validated by backend */,
+ t,
+ ),
+ hostPrefix: hostPrefixValidationSchema(values.cidr, t),
+ clusterId: Yup.string(),
+ }),
+ ),
+ );
+
+export const serviceNetworkValidationSchema = (t: TFunction) =>
+ Yup.array().of(
+ Yup.object({
+ cidr: ipBlockValidationSchema(
+ undefined /* So far used in OCM only and so validated by backend */,
+ t,
+ ),
+ clusterId: Yup.string(),
+ }),
+ );
+
+export const dualStackValidationSchema = (field: string, t: TFunction, openshiftVersion?: string) =>
+ Yup.array()
+ .max(2, t('ai:Maximum number of {{field}} subnets in dual stack is 2.', { field }))
+ .test(
+ 'dual-stack-ipv4',
+ openshiftVersion && isMajorMinorVersionEqualOrGreater(openshiftVersion, '4.12')
+ ? t('ai:First network has to be a valid IPv4 or IPv6 subnet.')
+ : t('ai:First network has to be IPv4 subnet.'),
+ (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 && (isCIDR.v4(values[0].cidr) || isCIDR.v6(values[0].cidr));
+ }
+ // For older versions, require IPv4 as primary network
+ return !!values?.[0].cidr && isCIDR.v4(values[0].cidr);
+ },
+ )
+ .test(
+ 'dual-stack-unique-cidrs',
+ t('ai:Provided {{field}} subnets must be unique.', { field }),
+ (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',
+ t('ai:When two {{field}} values are provided, one must be IPv4 and the other IPv6.', {
+ field,
+ }),
+ (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);
+ },
+ );
+
+export const IPv4ValidationSchema = Yup.array().test(
+ 'single-stack',
+ `All network subnets must be IPv4.`,
+ (values?: (MachineNetwork | ClusterNetwork | ServiceNetwork)[]) => allSubnetsIPv4(values),
+);
diff --git a/libs/ui-lib/lib/common/validationSchemas/ntpValidation.tsx b/libs/ui-lib/lib/common/validationSchemas/ntpValidation.tsx
new file mode 100644
index 0000000000..c6415bfe23
--- /dev/null
+++ b/libs/ui-lib/lib/common/validationSchemas/ntpValidation.tsx
@@ -0,0 +1,30 @@
+import * as Yup from 'yup';
+import { trimCommaSeparatedList } from '../components/ui/formik/utils';
+import { isIPorDN } from './utils';
+import { TFunction } from 'i18next';
+
+export const ntpSourceValidationSchema = (t: TFunction) =>
+ Yup.string()
+ .test(
+ 'ntp-source-validation',
+ t('ai:Provide a comma separated list of valid DNS names or IP addresses.'),
+ (value?: string) => {
+ if (!value || value === '') {
+ return true;
+ }
+ return trimCommaSeparatedList(value)
+ .split(',')
+ .every((v) => isIPorDN(v));
+ },
+ )
+ .test(
+ 'ntp-source-validation-unique',
+ t('ai:DNS names and IP addresses must be unique.'),
+ (value?: string) => {
+ if (!value || value === '') {
+ return true;
+ }
+ const arr = trimCommaSeparatedList(value).split(',');
+ return arr.length === new Set(arr).size;
+ },
+ );
diff --git a/libs/ui-lib/lib/common/validationSchemas/proxyValidation.tsx b/libs/ui-lib/lib/common/validationSchemas/proxyValidation.tsx
new file mode 100644
index 0000000000..55103ab838
--- /dev/null
+++ b/libs/ui-lib/lib/common/validationSchemas/proxyValidation.tsx
@@ -0,0 +1,67 @@
+import * as Yup from 'yup';
+import { TFunction } from 'i18next';
+import { ProxyFieldsType } from '../types';
+import { trimCommaSeparatedList } from '../components/ui/formik/utils';
+import { PROXY_DNS_REGEX } from './regexes';
+import { isIPorDN } from './utils';
+
+export const httpProxyValidationSchema = ({
+ values,
+ pairValueName,
+ allowEmpty,
+ t,
+}: {
+ values: ProxyFieldsType;
+ pairValueName: 'httpProxy' | 'httpsProxy';
+ allowEmpty?: boolean;
+ t: TFunction;
+}) => {
+ const validation = Yup.string().test(
+ 'http-proxy-validation',
+ t('ai:Provide a valid HTTP URL.'),
+ (value?: string) => {
+ if (!value) {
+ return true;
+ }
+
+ if (!value.startsWith('http://')) {
+ return false;
+ }
+
+ try {
+ new URL(value);
+ } catch {
+ return false;
+ }
+ return true;
+ },
+ );
+
+ if (allowEmpty) {
+ return validation;
+ }
+
+ return validation.test(
+ 'http-proxy-no-empty-validation',
+ t('ai:At least one of the HTTP or HTTPS proxy URLs is required.'),
+ (value) => !values.enableProxy || !!value || !!values[pairValueName],
+ );
+};
+
+export const noProxyValidationSchema = (t: TFunction) =>
+ Yup.string().test(
+ 'no-proxy-validation',
+ t('ai:Provide a comma separated list of valid DNS names or IP addresses.'),
+ (value?: string) => {
+ if (!value || value === '*') {
+ return true;
+ }
+
+ // https://docs.openshift.com/container-platform/4.5/installing/installing_bare_metal/installing-restricted-networks-bare-metal.html#installation-configure-proxy_installing-restricted-networks-bare-metal
+ // A comma-separated list of destination domain names, domains, IP addresses, or other network CIDRs to exclude proxying.
+ // Preface a domain with . to match subdomains only. For example, .y.com matches x.y.com, but not y.com.
+ // Use * to bypass proxy for all destinations.
+ const noProxyList = trimCommaSeparatedList(value).split(',');
+ return noProxyList.every((p) => isIPorDN(p, PROXY_DNS_REGEX));
+ },
+ );
diff --git a/libs/ui-lib/lib/common/validationSchemas/pullSecretValidation.tsx b/libs/ui-lib/lib/common/validationSchemas/pullSecretValidation.tsx
new file mode 100644
index 0000000000..1ff883eab9
--- /dev/null
+++ b/libs/ui-lib/lib/common/validationSchemas/pullSecretValidation.tsx
@@ -0,0 +1,24 @@
+import * as Yup from 'yup';
+import { TFunction } from 'i18next';
+
+export const pullSecretValidationSchema = (t: TFunction) =>
+ Yup.string().test(
+ 'is-well-formed-json',
+ t("ai:Invalid pull secret format. You must use your Red Hat account's pull secret."),
+ (value?: string) => {
+ const isValid = true;
+ if (!value) return isValid;
+ try {
+ const pullSecret = JSON.parse(value) as {
+ auths: string;
+ };
+ return (
+ pullSecret.constructor.name === 'Object' &&
+ !!pullSecret?.auths &&
+ pullSecret.auths.constructor.name === 'Object'
+ );
+ } catch {
+ return !isValid;
+ }
+ },
+ );
diff --git a/libs/ui-lib/lib/common/validationSchemas/regexes.tsx b/libs/ui-lib/lib/common/validationSchemas/regexes.tsx
new file mode 100644
index 0000000000..868d3810ba
--- /dev/null
+++ b/libs/ui-lib/lib/common/validationSchemas/regexes.tsx
@@ -0,0 +1,22 @@
+export const ALPHANUMERIC_REGEX = /^[a-zA-Z0-9]+$/;
+
+export const CLUSTER_NAME_START_END_REGEX = /^[a-z0-9](.*[a-z0-9])?$/;
+export const CLUSTER_NAME_VALID_CHARS_REGEX = /^[a-z0-9-]*$/;
+
+export const DNS_NAME_REGEX = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/;
+export const NAME_START_END_REGEX = /^[a-z0-9](.*[a-z0-9])?$/;
+export const NAME_CHARS_REGEX = /^[a-z0-9-.]*$/;
+
+export const HOST_NAME_REGEX = /^[^.]{1,63}(?:[.][^.]{1,63})*$/;
+export const SSH_PUBLIC_KEY_REGEX =
+ /^(ssh-rsa AAAAB3NzaC1yc2|ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNT|ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzOD|ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1Mj|ssh-ed25519 AAAAC3NzaC1lZDI1NTE5|ssh-dss AAAAB3NzaC1kc3)[0-9A-Za-z+/]+[=]{0,3}( .*)?$/;
+
+export const PROXY_DNS_REGEX =
+ /(^\.?([a-zA-Z0-9_]{1}[a-zA-Z0-9_-]{0,62}){1}(\.[a-zA-Z0-9_]{1}[a-zA-Z0-9_-]{0,62})*$)/;
+
+export const IP_V4_ZERO = '0.0.0.0';
+export const IP_V6_ZERO = '0000:0000:0000:0000:0000:0000:0000:0000';
+
+export const MAC_REGEX = /^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})$/;
+
+export const LOCATION_CHARS_REGEX = /^[a-zA-Z0-9-._]*$/;
diff --git a/libs/ui-lib/lib/common/validationSchemas/sshValidation.tsx b/libs/ui-lib/lib/common/validationSchemas/sshValidation.tsx
new file mode 100644
index 0000000000..7702918712
--- /dev/null
+++ b/libs/ui-lib/lib/common/validationSchemas/sshValidation.tsx
@@ -0,0 +1,46 @@
+import * as Yup from 'yup';
+import { TFunction } from 'i18next';
+import { trimSshPublicKey } from '../components/ui/formik/utils';
+import { SSH_PUBLIC_KEY_REGEX } from './regexes';
+
+export const sshPublicKeyValidationSchema = (t: TFunction) =>
+ Yup.string().test(
+ 'ssh-public-key',
+ t(
+ 'ai:SSH public key must consist of "[TYPE] key [[EMAIL]]", supported types are: ssh-rsa, ssh-ed25519, ecdsa-[VARIANT]. A single key can be provided only.',
+ ),
+ (value?: string) => {
+ if (!value) {
+ return true;
+ }
+
+ return !!trimSshPublicKey(value).match(SSH_PUBLIC_KEY_REGEX);
+ },
+ );
+
+export const sshPublicKeyListValidationSchema = (t: TFunction) =>
+ Yup.string()
+ .test(
+ 'ssh-public-keys',
+ t(
+ 'ai:SSH public key must consist of "[TYPE] key [[EMAIL]]", supported types are: ssh-rsa, ssh-ed25519, ecdsa-[VARIANT].',
+ ),
+ (value?: string) => {
+ if (!value) {
+ return true;
+ }
+
+ return (
+ trimSshPublicKey(value)
+ .split('\n')
+ .find((line: string) => !line.match(SSH_PUBLIC_KEY_REGEX)) === undefined
+ );
+ },
+ )
+ .test('ssh-public-keys-unique', t('ai:SSH public keys must be unique.'), (value?: string) => {
+ if (!value) {
+ return true;
+ }
+ const keyList = trimSshPublicKey(value).split('\n');
+ return new Set(keyList).size === keyList.length;
+ });
diff --git a/libs/ui-lib/lib/common/validationSchemas/utils.tsx b/libs/ui-lib/lib/common/validationSchemas/utils.tsx
new file mode 100644
index 0000000000..73b9e7c16c
--- /dev/null
+++ b/libs/ui-lib/lib/common/validationSchemas/utils.tsx
@@ -0,0 +1,36 @@
+import * as Yup from 'yup';
+import { Address4, Address6 } from 'ip-address';
+import { DNS_NAME_REGEX } from './regexes';
+
+export const isIPorDN = (value?: string, dnsRegex = DNS_NAME_REGEX) => {
+ if ((value as string).match(dnsRegex)) {
+ return true;
+ } else if (value) {
+ return Address4.isValid(value) || Address6.isValid(value);
+ } else {
+ return false;
+ }
+};
+
+// Helpers to classify literal IPs more robustly
+export const isIPv4Address = (ip?: string) => {
+ if (!ip) return false;
+ return ip.includes('.') && Address4.isValid(ip);
+};
+
+export const isIPv6Address = (ip?: string) => {
+ if (!ip) return false;
+ return ip.includes(':') && Address6.isValid(ip);
+};
+
+export const alwaysRequired = (message?: string) =>
+ Yup.string().test(
+ 'always-required',
+ message || 'The value is required.',
+ (value?: string): boolean => !!value,
+ );
+
+export const getArrayIndexFromPath = (path: string): number => {
+ const match = path.match(/\[(\d+)\][^\[]*$/); // Prefer the last [...] occurrence for nested array paths like "foo[0].bar[1].ip"
+ return match ? parseInt(match[1], 10) : NaN;
+};
diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/OcmDiscoveryImageConfigForm.tsx b/libs/ui-lib/lib/ocm/components/clusterConfiguration/OcmDiscoveryImageConfigForm.tsx
index 7775c681a8..80d27a9a49 100644
--- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/OcmDiscoveryImageConfigForm.tsx
+++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/OcmDiscoveryImageConfigForm.tsx
@@ -13,18 +13,14 @@ import {
FlexItem,
} from '@patternfly/react-core';
import { Formik, FormikHelpers } from 'formik';
+import { TFunction } from 'i18next';
import {
HostStaticNetworkConfig,
ImageType,
InfraEnv,
Proxy,
} from '@openshift-assisted/types/assisted-installer-service';
-import {
- AlertFormikError,
- httpProxyValidationSchema,
- noProxyValidationSchema,
- sshPublicKeyValidationSchema,
-} from '../../../common/components/ui';
+import { AlertFormikError } from '../../../common/components/ui';
import {
DiscoveryImageType,
ProxyFieldsType,
@@ -37,6 +33,11 @@ import UploadSSH from '../../../common/components/clusterConfiguration/UploadSSH
import { useTranslation } from '../../../common/hooks/use-translation-wrapper';
import DiscoveryImageTypeDropdown, { discoveryImageTypes } from './DiscoveryImageTypeDropdown';
import CertificateFields from '../../../common/components/clusterConfiguration/CertificateFields';
+import {
+ httpProxyValidationSchema,
+ noProxyValidationSchema,
+ sshPublicKeyValidationSchema,
+} from '../../../common';
export interface OcmImageCreateParams {
/**
@@ -54,14 +55,15 @@ export type OcmDiscoveryImageFormValues = OcmImageCreateParams &
ProxyFieldsType &
TrustedCertificateFieldsType;
-const validationSchema = Yup.lazy((values: OcmDiscoveryImageFormValues) =>
- Yup.object().shape({
- sshPublicKey: sshPublicKeyValidationSchema,
- httpProxy: httpProxyValidationSchema({ values, pairValueName: 'httpsProxy' }),
- httpsProxy: httpProxyValidationSchema({ values, pairValueName: 'httpProxy' }), // share the schema, httpS is currently not supported
- noProxy: noProxyValidationSchema,
- }),
-);
+const validationSchema = (t: TFunction) =>
+ Yup.lazy((values: OcmDiscoveryImageFormValues) =>
+ Yup.object().shape({
+ sshPublicKey: sshPublicKeyValidationSchema(t),
+ httpProxy: httpProxyValidationSchema({ values, pairValueName: 'httpsProxy', t }),
+ httpsProxy: httpProxyValidationSchema({ values, pairValueName: 'httpProxy', t }), // share the schema, httpS is currently not supported
+ noProxy: noProxyValidationSchema(t),
+ }),
+ );
type OcmDiscoveryImageConfigFormProps = Proxy & {
onCancel: () => void;
@@ -137,7 +139,7 @@ export const OcmDiscoveryImageConfigForm = ({
{({ submitForm, isSubmitting, status, setStatus }) => {
diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/manifestsConfiguration/components/CustomManifests.tsx b/libs/ui-lib/lib/ocm/components/clusterConfiguration/manifestsConfiguration/components/CustomManifests.tsx
index 4742d80e3d..830b2d4f28 100644
--- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/manifestsConfiguration/components/CustomManifests.tsx
+++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/manifestsConfiguration/components/CustomManifests.tsx
@@ -5,6 +5,7 @@ import { getFormViewManifestsValidationSchema } from './customManifestsValidatio
import { CustomManifestsForm } from './CustomManifestsForm';
import { getEmptyManifestsValues, getManifestValues } from './utils';
import { Cluster } from '@openshift-assisted/types/assisted-installer-service';
+import { useTranslation } from '../../../../../common';
export const CustomManifests = ({
cluster,
@@ -13,12 +14,13 @@ export const CustomManifests = ({
cluster: Cluster;
onFormStateChange: (formState: CustomManifestFormState) => void;
}) => {
+ const { t } = useTranslation();
const [formProps, setFormProps] = React.useState();
React.useEffect(() => {
setFormProps({
onFormStateChange: onFormStateChange,
- validationSchema: getFormViewManifestsValidationSchema,
+ validationSchema: getFormViewManifestsValidationSchema(t),
getInitialValues: (customManifests: ListManifestsExtended) => {
return getManifestValues(customManifests);
@@ -27,7 +29,7 @@ export const CustomManifests = ({
showEmptyValues: true,
cluster: cluster,
});
- }, [cluster, onFormStateChange]);
+ }, [cluster, onFormStateChange, t]);
if (!formProps) {
return null;
diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/manifestsConfiguration/components/customManifestsValidationSchema.tsx b/libs/ui-lib/lib/ocm/components/clusterConfiguration/manifestsConfiguration/components/customManifestsValidationSchema.tsx
index ae98a38c87..6fada9d22b 100644
--- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/manifestsConfiguration/components/customManifestsValidationSchema.tsx
+++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/manifestsConfiguration/components/customManifestsValidationSchema.tsx
@@ -1,14 +1,13 @@
import * as Yup from 'yup';
+import { TFunction } from 'i18next';
import { CustomManifestValues, ManifestFormData } from '../data/dataTypes';
import {
getMaxFileSizeMessage,
validateFileSize,
validateFileName,
validateFileType,
- INCORRECT_TYPE_FILE_MESSAGE,
} from '../../../../../common/utils';
-
-import * as yup from 'yup';
+import { getIncorrectFileTypeMessage } from '../../../../../common';
/* eslint-disable */
type CustomManifestMapper = (a: CustomManifestValues) => string;
@@ -33,7 +32,7 @@ Yup.addMethod(
if (!list) return true;
const seen = new Set();
- const errors: yup.ValidationError[] = [];
+ const errors: Yup.ValidationError[] = [];
list.forEach((item, index) => {
const mappedValue = mapper(item);
@@ -49,7 +48,7 @@ Yup.addMethod(
});
if (errors.length > 0) {
- throw new yup.ValidationError(errors);
+ throw new Yup.ValidationError(errors);
}
return true;
@@ -62,31 +61,32 @@ const INCORRECT_FILENAME =
const UNIQUE_FOLDER_FILENAME = 'Ensure unique file names to avoid conflicts and errors.';
-export const getFormViewManifestsValidationSchema = Yup.object({
- manifests: Yup.array()
- .of(
- Yup.object({
- folder: Yup.mixed().required('Required'),
- filename: Yup.string()
- .required('Required')
- .min(1, 'Number of characters must be 1-255')
- .max(255, 'Number of characters must be 1-255')
- .test('not-correct-filename', INCORRECT_FILENAME, (value: string) => {
- return validateFileName(value);
+export const getFormViewManifestsValidationSchema = (t: TFunction) =>
+ Yup.object({
+ manifests: Yup.array()
+ .of(
+ Yup.object({
+ folder: Yup.mixed().required('Required'),
+ filename: Yup.string()
+ .required('Required')
+ .min(1, 'Number of characters must be 1-255')
+ .max(255, 'Number of characters must be 1-255')
+ .test('not-correct-filename', INCORRECT_FILENAME, (value: string) => {
+ return validateFileName(value);
+ }),
+ manifestYaml: Yup.string().when('filename', {
+ is: (filename: string) => !filename.includes('patch'),
+ then: () =>
+ Yup.string()
+ .required('Required')
+ .test('not-big-file', getMaxFileSizeMessage(t), validateFileSize)
+ .test('not-valid-file', getIncorrectFileTypeMessage(t), validateFileType),
+ otherwise: () =>
+ Yup.string()
+ .required('Required')
+ .test('not-big-file', getMaxFileSizeMessage(t), validateFileSize), // Validation of file content is not required if filename contains 'patch'
}),
- manifestYaml: Yup.string().when('filename', {
- is: (filename: string) => !filename.includes('patch'),
- then: () =>
- Yup.string()
- .required('Required')
- .test('not-big-file', getMaxFileSizeMessage, validateFileSize)
- .test('not-valid-file', INCORRECT_TYPE_FILE_MESSAGE, validateFileType),
- otherwise: () =>
- Yup.string()
- .required('Required')
- .test('not-big-file', getMaxFileSizeMessage, validateFileSize), // Validation of file content is not required if filename contains 'patch'
}),
- }),
- )
- .uniqueManifestFiles(UNIQUE_FOLDER_FILENAME, (val: CustomManifestValues) => val.filename),
-});
+ )
+ .uniqueManifestFiles(UNIQUE_FOLDER_FILENAME, (val: CustomManifestValues) => val.filename),
+ });
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 c4ce2ec9a2..ddc4874491 100644
--- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/networkConfiguration/NetworkConfigurationForm.tsx
+++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/networkConfiguration/NetworkConfigurationForm.tsx
@@ -17,6 +17,7 @@ import {
useAlerts,
useFormikAutoSave,
ClustersAPI,
+ useTranslation,
} from '../../../../common';
import { useDefaultConfiguration } from '../ClusterDefaultConfigurationContext';
import { useClusterWizardContext } from '../../clusterWizard/ClusterWizardContext';
@@ -163,6 +164,7 @@ const NetworkConfigurationForm: React.FC<{
};
const NetworkConfigurationPage = ({ cluster }: { cluster: Cluster }) => {
+ const { t } = useTranslation();
const pullSecret = usePullSecret();
const {
infraEnv,
@@ -201,8 +203,13 @@ const NetworkConfigurationPage = ({ cluster }: { cluster: Cluster }) => {
const memoizedValidationSchema = React.useMemo(
() =>
- getNetworkConfigurationValidationSchema(initialValues, hostSubnets, cluster.openshiftVersion),
- [hostSubnets, initialValues, cluster.openshiftVersion],
+ getNetworkConfigurationValidationSchema(
+ initialValues,
+ hostSubnets,
+ t,
+ cluster.openshiftVersion,
+ ),
+ [initialValues, hostSubnets, t, cluster.openshiftVersion],
);
React.useEffect(() => {
diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/networkConfiguration/networkConfigurationValidation.ts b/libs/ui-lib/lib/ocm/components/clusterConfiguration/networkConfiguration/networkConfigurationValidation.ts
index db4f19773a..10e9733e00 100644
--- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/networkConfiguration/networkConfigurationValidation.ts
+++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/networkConfiguration/networkConfigurationValidation.ts
@@ -1,4 +1,5 @@
import * as Yup from 'yup';
+import { TFunction } from 'i18next';
import {
clusterNetworksValidationSchema,
dualStackValidationSchema,
@@ -72,13 +73,14 @@ export const getNetworkInitialValues = (
export const getNetworkConfigurationValidationSchema = (
initialValues: NetworkConfigurationValues,
hostSubnets: HostSubnets,
+ t: TFunction,
openshiftVersion?: string,
) =>
Yup.lazy((values: NetworkConfigurationValues) =>
Yup.object().shape({
- apiVips: vipArrayValidationSchema(hostSubnets, values),
- ingressVips: vipArrayValidationSchema(hostSubnets, values),
- sshPublicKey: sshPublicKeyValidationSchema,
+ apiVips: vipArrayValidationSchema(hostSubnets, values, t),
+ ingressVips: vipArrayValidationSchema(hostSubnets, values, t),
+ sshPublicKey: sshPublicKeyValidationSchema(t),
machineNetworks:
values.managedNetworkingType === 'userManaged'
? Yup.array()
@@ -88,25 +90,21 @@ export const getNetworkConfigurationValidationSchema = (
otherwise: () =>
values.machineNetworks && values.machineNetworks?.length >= 2
? machineNetworksValidationSchema.concat(
- dualStackValidationSchema('machine networks', openshiftVersion),
+ dualStackValidationSchema('machine networks', t, openshiftVersion),
)
: Yup.array(),
}),
- clusterNetworks: clusterNetworksValidationSchema.when('stackType', {
+ clusterNetworks: clusterNetworksValidationSchema(t).when('stackType', {
is: (stackType: NetworkConfigurationValues['stackType']) => stackType === IPV4_STACK,
- then: () => clusterNetworksValidationSchema.concat(IPv4ValidationSchema),
- otherwise: () =>
- clusterNetworksValidationSchema.concat(
- dualStackValidationSchema('cluster network', openshiftVersion),
- ),
+ then: (schema) => schema.concat(IPv4ValidationSchema),
+ otherwise: (schema) =>
+ schema.concat(dualStackValidationSchema('cluster network', t, openshiftVersion)),
}),
- serviceNetworks: serviceNetworkValidationSchema.when('stackType', {
+ serviceNetworks: serviceNetworkValidationSchema(t).when('stackType', {
is: (stackType: NetworkConfigurationValues['stackType']) => stackType === IPV4_STACK,
- then: () => serviceNetworkValidationSchema.concat(IPv4ValidationSchema),
- otherwise: () =>
- serviceNetworkValidationSchema.concat(
- dualStackValidationSchema('service network', openshiftVersion),
- ),
+ then: (schema) => schema.concat(IPv4ValidationSchema),
+ otherwise: (schema) =>
+ schema.concat(dualStackValidationSchema('service network', t, openshiftVersion)),
}),
}),
);
diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewHosts/FormViewHosts.tsx b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewHosts/FormViewHosts.tsx
index 08d2684043..06e59cd213 100644
--- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewHosts/FormViewHosts.tsx
+++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewHosts/FormViewHosts.tsx
@@ -9,8 +9,10 @@ import { formViewHostsToInfraEnvField } from '../../data/formDataToInfraEnvField
import { getEmptyFormViewHostsValues } from '../../data/emptyData';
import { getFormViewHostsValues, getFormViewNetworkWideValues } from '../../data/fromInfraEnv';
import { InfraEnv } from '@openshift-assisted/types/assisted-installer-service';
+import { useTranslation } from '../../../../../../common';
export const FormViewHosts: React.FC = ({ infraEnv, ...props }) => {
+ const { t } = useTranslation();
const [protocolType, setProtocolType] = React.useState();
const [formProps, setFormProps] = React.useState>();
React.useEffect(() => {
@@ -20,7 +22,7 @@ export const FormViewHosts: React.FC = ({ infraEnv, ...props
if (networkWideValues) {
setFormProps({
infraEnv,
- validationSchema: getFormViewHostsValidationSchema(networkWideValues),
+ validationSchema: getFormViewHostsValidationSchema(networkWideValues, t),
getInitialValues: (infraEnv: InfraEnv) => {
return getFormViewHostsValues(infraEnv);
},
diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewHosts/formViewHostsValidationSchema.tsx b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewHosts/formViewHostsValidationSchema.tsx
index c32c943f07..95168286f7 100644
--- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewHosts/formViewHostsValidationSchema.tsx
+++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewHosts/formViewHostsValidationSchema.tsx
@@ -1,4 +1,5 @@
import * as Yup from 'yup';
+import { TFunction } from 'i18next';
import {
FormViewNetworkWideValues,
@@ -14,6 +15,7 @@ import {
getIpIsNotNetworkOrBroadcastAddressSchema,
} from '../../commonValidationSchemas';
import { getMachineNetworkCidr } from '../../data/machineNetwork';
+
const requiredMsg = 'A value is required';
const getAllIpv4Addresses: UniqueStringArrayExtractor = (
@@ -39,12 +41,12 @@ const getAllBondInterfaces: UniqueStringArrayExtractor = (
]);
};
-const getHostValidationSchema = (networkWideValues: FormViewNetworkWideValues) =>
+const getHostValidationSchema = (networkWideValues: FormViewNetworkWideValues, t: TFunction) =>
Yup.object({
macAddress: Yup.mixed().when('useBond', {
is: false,
then: () =>
- macAddressValidationSchema
+ macAddressValidationSchema(t)
.required(requiredMsg)
.concat(getUniqueValidationSchema(getAllMacAddresses)),
otherwise: () => Yup.mixed().notRequired(),
@@ -82,7 +84,7 @@ const getHostValidationSchema = (networkWideValues: FormViewNetworkWideValues) =
bondPrimaryInterface: Yup.mixed().when('useBond', {
is: true,
then: () =>
- macAddressValidationSchema
+ macAddressValidationSchema(t)
.required(requiredMsg)
.concat(getUniqueValidationSchema(getAllBondInterfaces)),
otherwise: () => Yup.mixed().notRequired(),
@@ -90,15 +92,18 @@ const getHostValidationSchema = (networkWideValues: FormViewNetworkWideValues) =
bondSecondaryInterface: Yup.mixed().when('useBond', {
is: true,
then: () =>
- macAddressValidationSchema
+ macAddressValidationSchema(t)
.required(requiredMsg)
.concat(getUniqueValidationSchema(getAllBondInterfaces)),
otherwise: () => Yup.mixed().notRequired(),
}),
});
-export const getFormViewHostsValidationSchema = (networkWideValues: FormViewNetworkWideValues) => {
+export const getFormViewHostsValidationSchema = (
+ networkWideValues: FormViewNetworkWideValues,
+ t: TFunction,
+) => {
return Yup.object().shape({
- hosts: Yup.array(getHostValidationSchema(networkWideValues)),
+ hosts: Yup.array(getHostValidationSchema(networkWideValues, t)),
});
};
diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/YamlView/YamlView.tsx b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/YamlView/YamlView.tsx
index 5a31950bf9..c9e2e0e130 100644
--- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/YamlView/YamlView.tsx
+++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/YamlView/YamlView.tsx
@@ -7,17 +7,21 @@ import { YamlViewFields } from './YamlViewFields';
import { getEmptyYamlValues } from '../../data/emptyData';
import { InfraEnv } from '@openshift-assisted/types/assisted-installer-service';
import { getYamlViewValues } from '../../data/fromInfraEnv';
+import { useTranslation } from '../../../../../../common';
export const YamlView: React.FC = ({ ...props }) => {
+ const { t } = useTranslation();
+
const formProps: StaticIpFormProps = {
...props,
- validationSchema: yamlViewValidationSchema,
+ validationSchema: yamlViewValidationSchema(t),
getInitialValues: (infraEnv: InfraEnv) => {
return getYamlViewValues(infraEnv);
},
getUpdateParams: (currentInfraEnv: InfraEnv, values: YamlViewValues) => values.hosts,
getEmptyValues: () => getEmptyYamlValues(),
};
+
return (
{...formProps}>
diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/YamlView/yamlViewValidationSchema.tsx b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/YamlView/yamlViewValidationSchema.tsx
index 3b03d0db91..f512aff767 100644
--- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/YamlView/yamlViewValidationSchema.tsx
+++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/YamlView/yamlViewValidationSchema.tsx
@@ -1,12 +1,15 @@
import * as Yup from 'yup';
-import { ArrayElementType, macAddressValidationSchema } from '../../../../../../common';
+import {
+ ArrayElementType,
+ getIncorrectFileTypeMessage,
+ macAddressValidationSchema,
+} from '../../../../../../common';
import { YamlViewValues } from '../../data/dataTypes';
import {
getUniqueValidationSchema,
UniqueStringArrayExtractor,
} from '../../commonValidationSchemas';
import {
- INCORRECT_TYPE_FILE_MESSAGE,
getMaxFileSizeMessage,
validateFileSize,
validateFileType,
@@ -15,13 +18,13 @@ import {
HostStaticNetworkConfig,
MacInterfaceMap,
} from '@openshift-assisted/types/assisted-installer-service';
+import { TFunction } from 'i18next';
-const requiredMsg = 'A value is required';
-
-const networkYamlValidationSchema = Yup.string()
- .required(requiredMsg)
- .test('file-size-limit', getMaxFileSizeMessage, validateFileSize)
- .test('file-type-yaml', INCORRECT_TYPE_FILE_MESSAGE, validateFileType);
+const networkYamlValidationSchema = (t: TFunction) =>
+ Yup.string()
+ .required('Required field')
+ .test('file-size-limit', getMaxFileSizeMessage(t), validateFileSize)
+ .test('file-type-yaml', getIncorrectFileTypeMessage(t), validateFileType);
const getAllMacAddresses: UniqueStringArrayExtractor = (
values: YamlViewValues,
@@ -59,24 +62,26 @@ const getInterfaceNamesInCurrentHost: UniqueStringArrayExtractor
});
};
-const macInterfaceMapValidationSchema = Yup.array().of(
- Yup.object({
- macAddress: macAddressValidationSchema
- .required(requiredMsg)
- .concat(getUniqueValidationSchema(getAllMacAddresses)),
- logicalNicName: Yup.string()
- .required(requiredMsg)
- .concat(getUniqueValidationSchema(getInterfaceNamesInCurrentHost))
- .max(15, 'Interface name must be 15 characters at most.')
- .matches(/^\S+$/, 'Interface name can not contain spaces.'),
- }),
-);
-
-export const yamlViewValidationSchema = Yup.object({
- hosts: Yup.array().of(
- Yup.object().shape({
- networkYaml: networkYamlValidationSchema,
- macInterfaceMap: macInterfaceMapValidationSchema,
+const macInterfaceMapValidationSchema = (t: TFunction) =>
+ Yup.array().of(
+ Yup.object({
+ macAddress: macAddressValidationSchema(t)
+ .required('Required field')
+ .concat(getUniqueValidationSchema(getAllMacAddresses)),
+ logicalNicName: Yup.string()
+ .required('Required field')
+ .concat(getUniqueValidationSchema(getInterfaceNamesInCurrentHost))
+ .max(15, 'Interface name must be 15 characters at most.')
+ .matches(/^\S+$/, 'Interface name can not contain spaces.'),
}),
- ),
-});
+ );
+
+export const yamlViewValidationSchema = (t: TFunction) =>
+ Yup.object({
+ hosts: Yup.array().of(
+ Yup.object().shape({
+ networkYaml: networkYamlValidationSchema(t),
+ macInterfaceMap: macInterfaceMapValidationSchema(t),
+ }),
+ ),
+ });
diff --git a/libs/ui-lib/lib/ocm/components/clusterWizard/disconnected/OptionalConfigurationsStep.tsx b/libs/ui-lib/lib/ocm/components/clusterWizard/disconnected/OptionalConfigurationsStep.tsx
index be3f90fe6c..d05ad57956 100644
--- a/libs/ui-lib/lib/ocm/components/clusterWizard/disconnected/OptionalConfigurationsStep.tsx
+++ b/libs/ui-lib/lib/ocm/components/clusterWizard/disconnected/OptionalConfigurationsStep.tsx
@@ -8,6 +8,7 @@ import {
sshPublicKeyValidationSchema,
pullSecretValidationSchema,
getFormikErrorFields,
+ useTranslation,
} from '../../../../common';
import { Split, SplitItem, Grid, GridItem, Form, Content, Checkbox } from '@patternfly/react-core';
import { useClusterWizardContext } from '../ClusterWizardContext';
@@ -44,6 +45,7 @@ const PullSecretSync: React.FC<{ pullSecret?: string }> = ({ pullSecret }) => {
};
const OptionalConfigurationsStep = () => {
+ const { t } = useTranslation();
const pullSecret = usePullSecret() || '';
const { clusterId } = useParams<{ clusterId: string }>();
const [cluster, setCluster] = React.useState(null);
@@ -77,10 +79,10 @@ const OptionalConfigurationsStep = () => {
const validationSchema = React.useMemo(
() =>
Yup.object().shape({
- sshPublicKey: sshPublicKeyValidationSchema,
- pullSecret: pullSecretValidationSchema,
+ sshPublicKey: sshPublicKeyValidationSchema(t),
+ pullSecret: pullSecretValidationSchema(t),
}),
- [],
+ [t],
);
const initialValues: OptionalConfigurationsFormValues = {
diff --git a/libs/ui-lib/lib/ocm/components/hosts/UpdateDay2ApiVipForm.tsx b/libs/ui-lib/lib/ocm/components/hosts/UpdateDay2ApiVipForm.tsx
index a067b9bcef..9ca200eed5 100644
--- a/libs/ui-lib/lib/ocm/components/hosts/UpdateDay2ApiVipForm.tsx
+++ b/libs/ui-lib/lib/ocm/components/hosts/UpdateDay2ApiVipForm.tsx
@@ -9,13 +9,9 @@ import {
ModalFooter,
} from '@patternfly/react-core';
import { Formik } from 'formik';
-import {
- AlertFormikError,
- day2ApiVipValidationSchema,
- InputField,
-} from '../../../common/components/ui';
+import { AlertFormikError, InputField } from '../../../common/components/ui';
import GridGap from '../../../common/components/ui/GridGap';
-import { StatusErrorType } from '../../../common';
+import { day2ApiVipValidationSchema, StatusErrorType } from '../../../common';
import { useTranslation } from '../../../common/hooks/use-translation-wrapper';
export type UpdateDay2ApiVipFormProps = {
@@ -46,7 +42,7 @@ const UpdateDay2ApiVipForm: React.FC = ({
const validationSchema = React.useMemo(
() =>
Yup.object().shape({
- apiVip: day2ApiVipValidationSchema.required(t('ai:Required field')),
+ apiVip: day2ApiVipValidationSchema(t).required(t('ai:Required field')),
}),
[t],
);