diff --git a/frontend/packages/vsphere-plugin/locales/en/vsphere-plugin.json b/frontend/packages/vsphere-plugin/locales/en/vsphere-plugin.json index 780b7fbec1e..ce6de2dd549 100644 --- a/frontend/packages/vsphere-plugin/locales/en/vsphere-plugin.json +++ b/frontend/packages/vsphere-plugin/locales/en/vsphere-plugin.json @@ -11,7 +11,7 @@ "Failing {{reason}}": "Failing {{reason}}", "Failed to load kubecontrollermanager": "Failed to load kubecontrollermanager", "Failed to parse cloud provider config {{cm}}": "Failed to parse cloud provider config {{cm}}", - "Unknown format": "Unknown format", + "The following content was expected to be defined in the configMap: {{ expectedValues }}": "The following content was expected to be defined in the configMap: {{ expectedValues }}", "Failed to persist {{secret}}": "Failed to persist {{secret}}", "Failed to patch kubecontrollermanager": "Failed to patch kubecontrollermanager", "Failed to patch cloud provider config": "Failed to patch cloud provider config", @@ -34,6 +34,8 @@ "Warning: Updating this value will break any existing PersistentVolumes.": "Warning: Updating this value will break any existing PersistentVolumes.", "Virtual Machine Folder": "Virtual Machine Folder", "Provide <1>datacenter folder which contains VMs of the cluster.": "Provide <1>datacenter folder which contains VMs of the cluster.", + "Primary network": "Primary network", + "Provide the <1>primary network of the cluster.": "Provide the <1>primary network of the cluster.", "Saving": "Saving", "Save configuration": "Save configuration", "Cancel": "Cancel", diff --git a/frontend/packages/vsphere-plugin/src/components/VSphereConnectionForm.tsx b/frontend/packages/vsphere-plugin/src/components/VSphereConnectionForm.tsx index a55a596db2e..5e0bd8c0aaa 100644 --- a/frontend/packages/vsphere-plugin/src/components/VSphereConnectionForm.tsx +++ b/frontend/packages/vsphere-plugin/src/components/VSphereConnectionForm.tsx @@ -146,6 +146,21 @@ export const VSphereConnectionForm = () => { > + + Provide the primary network of the cluster. + + } + /> + } + fieldId="connection-network" + > + + ); }; diff --git a/frontend/packages/vsphere-plugin/src/components/VSphereConnectionModal.tsx b/frontend/packages/vsphere-plugin/src/components/VSphereConnectionModal.tsx index e1e13937598..8148af25f3c 100644 --- a/frontend/packages/vsphere-plugin/src/components/VSphereConnectionModal.tsx +++ b/frontend/packages/vsphere-plugin/src/components/VSphereConnectionModal.tsx @@ -108,6 +108,7 @@ const validationSchema = Yup.lazy((values: ConnectionFormFormikValues) => username: Yup.string().required('Username is required.'), password: Yup.string().required('Password is required.'), datacenter: Yup.string().required('Datacenter is required.'), + network: Yup.string(), defaultDatastore: Yup.string() .required('Default data store is required.') .test( diff --git a/frontend/packages/vsphere-plugin/src/components/persist.ts b/frontend/packages/vsphere-plugin/src/components/persist.ts index 799a7589683..965835b76b0 100644 --- a/frontend/packages/vsphere-plugin/src/components/persist.ts +++ b/frontend/packages/vsphere-plugin/src/components/persist.ts @@ -160,12 +160,92 @@ const updateYamlFormat = ( return dump(cmCfg); }; -const findAndReplace = (str: string, init: string, replacement: string): string | undefined => { - if (!str || !str.includes(init)) { - return undefined; +type UpdateConfigMapResult = { + config: string; + expectedValues: string[]; +}; + +const getUpdatedConfig = ( + result: UpdateConfigMapResult, + init: string, + replacement: string, +): UpdateConfigMapResult | undefined => { + const cfg = result.config; + if (!cfg || !cfg.includes(init)) { + result.expectedValues.push(init); + return result; } - return str.replace(init, replacement); + return { + config: cfg.replace(init, replacement), + expectedValues: result.expectedValues, + }; +}; + +// Updates the configMap folder value if the following conditions are met: +// 1 - The ConfigMap includes the entry for the "folder" +// 2 - The infrastructure CRD either has no "folder" entry, or it has a "folder" entry that matches the "folder" entry in the ConfigMap +const getUpdatedConfigMapFolder = ( + result: UpdateConfigMapResult, + initFolder: string, + newFolder: string, +): UpdateConfigMapResult | undefined => { + const cfg = result.config; + const folderLineMatch = cfg.match(/folder\s*=\s*["']?([^"'\n\r]+)["']?/); + if (folderLineMatch) { + const folderLine = folderLineMatch[0]; + const folderValue = folderLineMatch[1].trim(); + + if (!initFolder || initFolder === folderValue) { + return { + config: cfg.replace(folderLine, `folder = "${newFolder}"`), + expectedValues: result.expectedValues, + }; + } + } + return getUpdatedConfig(result, `folder = "${initFolder}"`, `folder = "${newFolder}"`); +}; + +// Updates the configMap resourcepool-path value if the following conditions are met: +// 1 - The ConfigMap includes the entry for the "resourcepool-path" +// 2 - The existing value for "resourcepool-path" in the ConfigMap starts with the pattern "/${datacenter}/host/${vCenterCluster}/Resources" +// Additionally, the resourcepool-path may contain additional path segments after "/Resources", which will be preserved. +const getUpdatedConfigMapResourcePool = ( + result: UpdateConfigMapResult, + initDatacenter: string, + initVCenterCluster: string, + datacenter: string, + vCenterCluster: string, +): UpdateConfigMapResult | undefined => { + const cfg = result.config; + + // Find the starting pattern in the "resourcepool-path" entry in the ConfigMap + const resourcePoolMatch = cfg.match(/resourcepool-path\s*=\s*["']?([^"'\n\r]+)["']?/); + if (resourcePoolMatch) { + const resourcePoolPathLine = resourcePoolMatch[0]; + const resourcePoolPathValue = resourcePoolMatch[1].trim(); + + // Check only the starting pattern, to prevent the additional path segments from breaking the exact match comparison + const poolPathStartingPattern = `/${initDatacenter}/host/${initVCenterCluster}/Resources`; + if (resourcePoolPathValue.startsWith(poolPathStartingPattern)) { + // Extract any additional path segments after /Resources to preserve them + const additionalSegments = resourcePoolPathValue.substring(poolPathStartingPattern.length); + const newResourcePoolPath = `/${datacenter}/host/${vCenterCluster}/Resources${additionalSegments}`; + return { + config: cfg.replace(resourcePoolPathLine, `resourcepool-path = "${newResourcePoolPath}"`), + expectedValues: result.expectedValues, + }; + } + } + + // As a fallback, only exact matches are supported (this would not preserve additional path segments) + const initResourcePoolPath = `/${initDatacenter}/host/${initVCenterCluster}/Resources`; + const newResourcePoolPath = `/${datacenter}/host/${vCenterCluster}/Resources`; + return getUpdatedConfig( + result, + `resourcepool-path = "${initResourcePoolPath}"`, + `resourcepool-path = "${newResourcePoolPath}"`, + ); }; const updateIniFormat = ( @@ -174,52 +254,63 @@ const updateIniFormat = ( initValues: ConnectionFormFormikValues, cloudProviderConfig: ConfigMap, ): string => { - let cfg = cloudProviderConfig.data.config; + const cfg = cloudProviderConfig.data.config; const initVCenter = initValues.vcenter || 'vcenterplaceholder'; const initVCenterCluster = initValues.vCenterCluster || 'clusterplaceholder'; const initDatacenter = initValues.datacenter || 'datacenterplaceholder'; const initDatastore = initValues.defaultDatastore || '/datacenterplaceholder/datastore/defaultdatastoreplaceholder'; - const initFolder = initValues.folder || '/datacenterplaceholder/vm/folderplaceholder'; - cfg = findAndReplace( - cfg, + let result: UpdateConfigMapResult = { config: cfg, expectedValues: [] }; + + result = getUpdatedConfig( + result, `[VirtualCenter "${initVCenter}"]`, `[VirtualCenter "${values.vcenter}"]`, ); - cfg = findAndReplace(cfg, `server = "${initVCenter}"`, `server = "${values.vcenter}"`); - cfg = findAndReplace( - cfg, + result = getUpdatedConfig(result, `server = "${initVCenter}"`, `server = "${values.vcenter}"`); + result = getUpdatedConfig( + result, `datacenters = "${initDatacenter}"`, `datacenters = "${values.datacenter}"`, ); - cfg = findAndReplace( - cfg, + result = getUpdatedConfig( + result, `datacenter = "${initDatacenter}"`, `datacenter = "${values.datacenter}"`, ); - cfg = findAndReplace( - cfg, + result = getUpdatedConfig( + result, `default-datastore = "${initDatastore}"`, `default-datastore = "${values.defaultDatastore}"`, ); - cfg = findAndReplace(cfg, `folder = "${initFolder}"`, `folder = "${values.folder}"`); - cfg = findAndReplace( - cfg, - `resourcepool-path = "/${initDatacenter}/host/${initVCenterCluster}/Resources"`, - `resourcepool-path = "/${values.datacenter}/host/${values.vCenterCluster}/Resources"`, + + // "folder" is handled differently, as it can be absent from the "topology" section of the Infrastructure CRD. + result = getUpdatedConfigMapFolder(result, initValues.folder, values.folder); + + // "resourcepool-path" is handled differently, as it can take additional path segments that need to be preserved + result = getUpdatedConfigMapResourcePool( + result, + initDatacenter, + initVCenterCluster, + values.datacenter, + values.vCenterCluster, ); - if (!cfg) { + if (result.expectedValues.length > 0) { throw new PersistError( - t('Failed to parse cloud provider config {{cm}}', { cm: cloudProviderConfig.metadata.name }), - t('Unknown format'), + t('Failed to parse cloud provider config {{cm}}', { + cm: cloudProviderConfig.metadata.name, + }), + t('The following content was expected to be defined in the configMap: {{ expectedValues }}', { + expectedValues: result.expectedValues.join(', '), + }), ); } - return cfg; + return result.config; }; // https://issues.redhat.com/browse/OCPBUGS-54434 @@ -374,6 +465,27 @@ const getAddTaintsOps = async (nodesModel: K8sModel): Promise => { return patchRequests; }; +// Gets the updated resource pool path for the infrastructure CRD in the format: +// /{datacenter}/host/{vCenterCluster}/Resources/{additionalSegments} +// Additional segments present in the value are respected. +const getInfrastructureResourcePoolPath = ( + values: ConnectionFormFormikValues, + initValues: ConnectionFormFormikValues, + originalResourcePool: string, +): string => { + const initDatacenter = initValues.datacenter || 'datacenterplaceholder'; + const initVCenterCluster = initValues.vCenterCluster || 'clusterplaceholder'; + const expectedResourcePoolPattern = `/${initDatacenter}/host/${initVCenterCluster}/Resources`; + + let newResourcePool = `/${values.datacenter}/host/${values.vCenterCluster}/Resources`; + if (originalResourcePool && originalResourcePool.startsWith(expectedResourcePoolPattern)) { + // Preserve additional path segments after /Resources + const additionalSegments = originalResourcePool.substring(expectedResourcePoolPattern.length); + newResourcePool = `/${values.datacenter}/host/${values.vCenterCluster}/Resources${additionalSegments}`; + } + return newResourcePool; +}; + const getPersistInfrastructureOp = async ( infrastructureModel: K8sModel, values: ConnectionFormFormikValues, @@ -394,9 +506,16 @@ const getPersistInfrastructureOp = async ( vCenterDomainCfg.topology.computeCluster = `/${values.datacenter}/host/${values.vCenterCluster}`; vCenterDomainCfg.topology.datacenter = values.datacenter; vCenterDomainCfg.topology.datastore = values.defaultDatastore; - vCenterDomainCfg.topology.networks = [values.vCenterCluster]; + if (values.network) { + vCenterDomainCfg.topology.networks = [values.network]; + } vCenterDomainCfg.topology.folder = values.folder; - vCenterDomainCfg.topology.resourcePool = `/${values.datacenter}/host/${values.vCenterCluster}/Resources`; + + vCenterDomainCfg.topology.resourcePool = getInfrastructureResourcePoolPath( + values, + initValues, + vCenterDomainCfg.topology.resourcePool, + ); const vCenterCfg = initValues.vcenter ? infrastructure.spec.platformSpec.vsphere.vcenters.find((c) => c.server === initValues.vcenter) diff --git a/frontend/packages/vsphere-plugin/src/components/types.ts b/frontend/packages/vsphere-plugin/src/components/types.ts index 823bd0f0d91..a5ce95c8594 100644 --- a/frontend/packages/vsphere-plugin/src/components/types.ts +++ b/frontend/packages/vsphere-plugin/src/components/types.ts @@ -9,6 +9,7 @@ export type ConnectionFormFormikValues = { defaultDatastore: string; folder: string; vCenterCluster: string; + network: string; // Primary network name isInit?: boolean; }; diff --git a/frontend/packages/vsphere-plugin/src/hooks/use-connection-form.ts b/frontend/packages/vsphere-plugin/src/hooks/use-connection-form.ts index b3c8dbd4e80..818c6b7444e 100644 --- a/frontend/packages/vsphere-plugin/src/hooks/use-connection-form.ts +++ b/frontend/packages/vsphere-plugin/src/hooks/use-connection-form.ts @@ -40,6 +40,7 @@ const initialLoad = async ( username: '', vcenter: '', vCenterCluster: '', + network: '', isInit: vCenterServer === 'vcenterplaceholder', }; } @@ -47,8 +48,13 @@ const initialLoad = async ( const datacenter = vSphereFailureDomain.topology?.datacenter || ''; const defaultDatastore = vSphereFailureDomain.topology?.datastore || ''; const folder = vSphereFailureDomain.topology?.folder || ''; - const vcenter = vSphereCfg.vcenters?.[0]?.server || ''; - const vCenterCluster = vSphereFailureDomain.topology.networks[0] || ''; + + // Extract cluster name from computeCluster path (format: /{datacenter}/host/{cluster}) + const computeCluster = vSphereFailureDomain.topology?.computeCluster || ''; + const vCenterCluster = computeCluster.match(/\/.*?\/host\/(.+)/)?.[1] || ''; + + // Load the primary network (first network in the networks array) + const network = vSphereFailureDomain.topology?.networks?.[0] || ''; let username = ''; let password = ''; @@ -65,8 +71,8 @@ const initialLoad = async ( } const secretKeyValues = secret.data || {}; - username = decodeBase64(secretKeyValues[`${vcenter}.username`]); - password = decodeBase64(secretKeyValues[`${vcenter}.password`]); + username = decodeBase64(secretKeyValues[`${vCenterServer}.username`]); + password = decodeBase64(secretKeyValues[`${vCenterServer}.password`]); } catch (e) { // It should be there if referenced // eslint-disable-next-line no-console @@ -80,8 +86,9 @@ const initialLoad = async ( datacenter, defaultDatastore, folder, - vcenter, + vcenter: vCenterServer, vCenterCluster, + network, password, username, };