Skip to content
Merged
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
5 changes: 5 additions & 0 deletions frontend/packages/console-shared/src/hooks/previous.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { useMemo } from 'react';

export const usePrevious = <P = any>(value: P, deps: any[] = []): P =>
// eslint-disable-next-line react-hooks/exhaustive-deps
useMemo(() => value, deps);
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const HOST_STATUS_ACTIONS = {
BareMetalHostModel,
host.metadata.name,
host.metadata.namespace,
)}/edit`}
)}/edit?powerMgmt`}
>
Add credentials
</Link>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import * as React from 'react';
import * as Yup from 'yup';
import * as _ from 'lodash';
import { Formik } from 'formik';
import { history, resourcePathFromModel, FirehoseResult } from '@console/internal/components/utils';
import { Formik, FormikHelpers } from 'formik';
import {
history,
resourcePathFromModel,
LoadingBox,
LoadError,
} from '@console/internal/components/utils';
import { nameValidationSchema } from '@console/dev-console/src/components/import/validation-schema';
import { getName } from '@console/shared/src';
import { K8sResourceKind } from '@console/internal/module/k8s';
import { usePrevious } from '@console/shared/src/hooks/previous';
import { referenceForModel, SecretKind } from '@console/internal/module/k8s';
import { createBareMetalHost, updateBareMetalHost } from '../../../k8s/requests/bare-metal-host';
import { BareMetalHostModel } from '../../../models';
import { BareMetalHostKind } from '../../../types';
Expand All @@ -17,15 +23,20 @@ import {
isHostOnline,
} from '../../../selectors';
import { getSecretPassword, getSecretUsername } from '../../../selectors/secret';
import { getLoadedData } from '../../../utils';
import { usePrevious } from '../../../hooks';
import AddBareMetalHostForm from './AddBareMetalHostForm';
import { AddBareMetalHostFormValues } from './types';
import { MAC_REGEX, BMC_ADDRESS_REGEX } from './utils';
import {
useK8sWatchResource,
WatchK8sResource,
} from '@console/internal/components/utils/k8s-watch-hook';
import { SecretModel } from '@console/internal/models';

const getInitialValues = (
host: BareMetalHostKind,
secret: K8sResourceKind,
secret: SecretKind,
isEditing: boolean,
enablePowerMgmt: boolean,
): AddBareMetalHostFormValues => ({
name: getName(host) || '',
BMCAddress: getHostBMCAddress(host) || '',
Expand All @@ -35,67 +46,112 @@ const getInitialValues = (
bootMACAddress: getHostBootMACAddress(host) || '',
online: isHostOnline(host) || true,
description: getHostDescription(host) || '',
enablePowerManagement: isEditing ? !!host?.spec?.bmc || enablePowerMgmt : true,
});

type AddBareMetalHostProps = {
namespace: string;
isEditing: boolean;
loaded?: boolean;
hosts?: FirehoseResult<BareMetalHostKind[]>;
host?: FirehoseResult<BareMetalHostKind>;
secret?: FirehoseResult<K8sResourceKind>;
name?: string;
enablePowerMgmt: boolean;
};

const AddBareMetalHost: React.FC<AddBareMetalHostProps> = ({
namespace,
isEditing,
hosts,
host: resultHost,
secret: resultSecret,
name,
enablePowerMgmt,
}) => {
const [reload, setReload] = React.useState<boolean>(false);
const hostNames = _.flatMap(getLoadedData(hosts, []), (host) => getName(host));
const initialHost = getLoadedData(resultHost);
const initialSecret = getLoadedData(resultSecret);
const prevInitialHost = usePrevious(initialHost);
const prevInitialSecret = usePrevious(initialSecret);
const bmhResource = React.useMemo<WatchK8sResource>(
() =>
name
? {
kind: referenceForModel(BareMetalHostModel),
namespace,
name,
}
: undefined,
[name, namespace],
);
const bmhResources = React.useMemo<WatchK8sResource>(
() =>
!name
? {
kind: referenceForModel(BareMetalHostModel),
namespace,
isList: true,
}
: undefined,
[name, namespace],
);
const [host, hostLoaded, hostError] = useK8sWatchResource<BareMetalHostKind>(bmhResource);
const [hosts, hostsLoaded, hostsError] = useK8sWatchResource<BareMetalHostKind[]>(bmhResources);

const initialValues = getInitialValues(initialHost, initialSecret);
const prevInitialValues = getInitialValues(prevInitialHost, prevInitialSecret);
const credentialsName = host?.spec?.bmc?.credentialsName;
const secretResource = React.useMemo<WatchK8sResource>(
() =>
credentialsName
? {
kind: SecretModel.kind,
namespace,
name: credentialsName,
}
: undefined,
[credentialsName, namespace],
);
const [secret, secretLoaded, secretError] = useK8sWatchResource<SecretKind>(secretResource);

const [reload, setReload] = React.useState<boolean>(false);
React.useEffect(() => {
if (reload) {
setReload(false);
}
}, [reload, setReload]);

const showUpdated =
isEditing &&
prevInitialHost &&
prevInitialSecret &&
!_.isEqual(prevInitialValues, initialValues);

const addHostValidationSchema = Yup.object().shape({
name: Yup.mixed()
.test(
'unique-name',
'Name "${value}" is already taken.', // eslint-disable-line no-template-curly-in-string
(value) => !hostNames.includes(value),
)
.concat(nameValidationSchema),
BMCAddress: Yup.string()
.matches(BMC_ADDRESS_REGEX, 'Value provided is not a valid BMC address')
.required('Required.'),
username: Yup.string().required('Required.'),
password: Yup.string().required('Required.'),
bootMACAddress: Yup.string()
.matches(MAC_REGEX, 'Value provided is not a valid MAC Address.')
.required('Required.'),
});

const handleSubmit = (values, actions) => {
const initialHost = usePrevious<BareMetalHostKind>(host, [hostLoaded, reload]);
const initialSecret = usePrevious<SecretKind>(secret, [secretLoaded, reload]);

if (name ? !hostLoaded || (secretResource ? !secretLoaded : false) : !hostsLoaded) {
return <LoadingBox />;
}

if (hostError || secretError || hostsError) {
return <LoadError label="resources" />;
}

const hostNames = !name ? hosts.map(getName) : [];

const initialValues = getInitialValues(host, secret, !!name, enablePowerMgmt);
const prevInitialValues = getInitialValues(initialHost, initialSecret, !!name, enablePowerMgmt);

const showUpdated = initialHost && !_.isEqual(prevInitialValues, initialValues);

const addHostValidationSchema = Yup.lazy(({ enablePowerManagement }) =>
Yup.object().shape({
name: Yup.mixed()
.test(
'unique-name',
'Name "${value}" is already taken.', // eslint-disable-line no-template-curly-in-string
(value) => !hostNames.includes(value),
)
.concat(nameValidationSchema),
BMCAddress: enablePowerManagement
? Yup.string()
.matches(BMC_ADDRESS_REGEX, 'Value provided is not a valid BMC address')
.required('Required.')
: undefined,
username: enablePowerManagement ? Yup.string().required('Required.') : undefined,
password: enablePowerManagement ? Yup.string().required('Required.') : undefined,
bootMACAddress: Yup.string()
.matches(MAC_REGEX, 'Value provided is not a valid MAC Address.')
.required('Required.'),
}),
);

const handleSubmit = (
values: AddBareMetalHostFormValues,
actions: FormikHelpers<AddBareMetalHostFormValues>,
) => {
const opts = { ...values, namespace };
const promise = isEditing
const promise = name
? updateBareMetalHost(initialHost, initialSecret, opts)
: createBareMetalHost(opts);

Expand All @@ -113,14 +169,12 @@ const AddBareMetalHost: React.FC<AddBareMetalHostProps> = ({
return (
<Formik
initialValues={initialValues}
enableReinitialize={isEditing && (reload || !prevInitialHost || !prevInitialSecret)}
enableReinitialize={!!name && reload}
onSubmit={handleSubmit}
onReset={() => setReload(true)}
validationSchema={addHostValidationSchema}
>
{(formikProps) => (
<AddBareMetalHostForm {...formikProps} isEditing={isEditing} showUpdated={showUpdated} />
)}
{(props) => <AddBareMetalHostForm {...props} isEditing={!!name} showUpdated={showUpdated} />}
</Formik>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const AddBareMetalHostForm: React.FC<AddBareMetalHostFormProps> = ({
dirty,
isEditing,
showUpdated,
values,
}) => (
<Form onSubmit={handleSubmit}>
<InputField
Expand All @@ -34,7 +35,7 @@ const AddBareMetalHostForm: React.FC<AddBareMetalHostFormProps> = ({
name="name"
label="Name"
placeholder="openshift-worker"
helpText="Provide unique name for the new Bare Metal Host."
helpText="Provide a unique name for the new Bare Metal Host."
required
isDisabled={isEditing}
/>
Expand All @@ -43,34 +44,6 @@ const AddBareMetalHostForm: React.FC<AddBareMetalHostFormProps> = ({
name="description"
label="Description"
/>
<InputField
type={TextInputTypes.text}
data-test-id="add-baremetal-host-form-bmc-address-input"
name="BMCAddress"
label="BMC Address"
helpText="The URL for communicating with the BMC (Baseboard Management Controller) on the host, based on the provider being used."
required
/>
<CheckboxField
data-test-id="add-baremetal-host-form-disable-certificate-verification-input"
name="disableCertificateVerification"
label="Disable Certificate Verification"
helpText="Disable verification of server certificates when using HTTPS to connect to the BMC. This is required when the server certificate is self-signed, but is insecure because it allows a man-in-the-middle to intercept the connection."
/>
<InputField
type={TextInputTypes.text}
data-test-id="add-baremetal-host-form-username-input"
name="username"
label="BMC Username"
required
/>
<InputField
type={TextInputTypes.password}
data-test-id="add-baremetal-host-form-password-input"
name="password"
label="BMC Password"
required
/>
<InputField
type={TextInputTypes.text}
data-test-id="add-baremetal-host-form-boot-mac-address-input"
Expand All @@ -79,12 +52,50 @@ const AddBareMetalHostForm: React.FC<AddBareMetalHostFormProps> = ({
helpText="The MAC address of the NIC connected to the network that will be used to provision the host."
required
/>
{!isEditing && (
<SwitchField
name="online"
data-test-id="add-baremetal-host-form-online-switch"
label="Power host on after creation"
/>
<CheckboxField
data-test-id="add-baremetal-host-form-enable-power-mgmt-input"
name="enablePowerManagement"
label="Enable power management"
helpText="Provide credentials for the host's baseboard management controller (BMC) device to enable OpenShift to control its power state. This is required for automatic machine health check remediation."
/>
{values.enablePowerManagement && (
<>
<InputField
type={TextInputTypes.text}
data-test-id="add-baremetal-host-form-bmc-address-input"
name="BMCAddress"
label="Baseboard Management Console (BMC) Address"
helpText="The URL for communicating with the host's baseboard management controller device."
required
/>
<CheckboxField
data-test-id="add-baremetal-host-form-disable-certificate-verification-input"
name="disableCertificateVerification"
label="Disable Certificate Verification"
helpText="Disable verification of server certificates when using HTTPS to connect to the BMC. This is required when the server certificate is self-signed, but is insecure because it allows a man-in-the-middle to intercept the connection."
/>
<InputField
type={TextInputTypes.text}
data-test-id="add-baremetal-host-form-username-input"
name="username"
label="BMC Username"
required
/>
<InputField
type={TextInputTypes.password}
data-test-id="add-baremetal-host-form-password-input"
name="password"
label="BMC Password"
required
/>
{!isEditing && (
<SwitchField
name="online"
data-test-id="add-baremetal-host-form-online-switch"
label="Power host on after creation"
/>
)}
</>
)}
<FormFooter
isSubmitting={isSubmitting}
Expand Down
Loading