Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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</1> folder which contains VMs of the cluster.": "Provide <1>datacenter</1> folder which contains VMs of the cluster.",
"Primary network": "Primary network",
"Provide the <1>primary network</1> of the cluster.": "Provide the <1>primary network</1> of the cluster.",
"Saving": "Saving",
"Save configuration": "Save configuration",
"Cancel": "Cancel",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,21 @@ export const VSphereConnectionForm = () => {
>
<TextField name="folder" />
</FormGroup>
<FormGroup
label={t('Primary network')}
labelHelp={
<PopoverHelpButton
content={
<Trans t={t}>
Provide the <b>primary network</b> of the cluster.
</Trans>
}
/>
}
fieldId="connection-network"
>
<TextField name="network" />
</FormGroup>
</Form>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
171 changes: 145 additions & 26 deletions frontend/packages/vsphere-plugin/src/components/persist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand All @@ -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
Expand Down Expand Up @@ -374,6 +465,27 @@ const getAddTaintsOps = async (nodesModel: K8sModel): Promise<PersistOp[]> => {
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,
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions frontend/packages/vsphere-plugin/src/components/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type ConnectionFormFormikValues = {
defaultDatastore: string;
folder: string;
vCenterCluster: string;
network: string; // Primary network name
isInit?: boolean;
};

Expand Down
17 changes: 12 additions & 5 deletions frontend/packages/vsphere-plugin/src/hooks/use-connection-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,21 @@ const initialLoad = async (
username: '',
vcenter: '',
vCenterCluster: '',
network: '',
isInit: vCenterServer === 'vcenterplaceholder',
};
}

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 = '';
Expand All @@ -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
Expand All @@ -80,8 +86,9 @@ const initialLoad = async (
datacenter,
defaultDatastore,
folder,
vcenter,
vcenter: vCenterServer,
vCenterCluster,
network,
password,
username,
};
Expand Down