diff --git a/frontend/packages/console-shared/src/hooks/previous.ts b/frontend/packages/console-shared/src/hooks/previous.ts new file mode 100644 index 00000000000..2c220f01c37 --- /dev/null +++ b/frontend/packages/console-shared/src/hooks/previous.ts @@ -0,0 +1,5 @@ +import { useMemo } from 'react'; + +export const usePrevious =

(value: P, deps: any[] = []): P => + // eslint-disable-next-line react-hooks/exhaustive-deps + useMemo(() => value, deps); diff --git a/frontend/packages/metal3-plugin/src/components/baremetal-hosts/BareMetalHostStatus.tsx b/frontend/packages/metal3-plugin/src/components/baremetal-hosts/BareMetalHostStatus.tsx index 3a404227e47..27b0517b2f8 100644 --- a/frontend/packages/metal3-plugin/src/components/baremetal-hosts/BareMetalHostStatus.tsx +++ b/frontend/packages/metal3-plugin/src/components/baremetal-hosts/BareMetalHostStatus.tsx @@ -34,7 +34,7 @@ export const HOST_STATUS_ACTIONS = { BareMetalHostModel, host.metadata.name, host.metadata.namespace, - )}/edit`} + )}/edit?powerMgmt`} > Add credentials diff --git a/frontend/packages/metal3-plugin/src/components/baremetal-hosts/add-baremetal-host/AddBareMetalHost.tsx b/frontend/packages/metal3-plugin/src/components/baremetal-hosts/add-baremetal-host/AddBareMetalHost.tsx index 6c37e3a47e7..9cc7bfb2977 100644 --- a/frontend/packages/metal3-plugin/src/components/baremetal-hosts/add-baremetal-host/AddBareMetalHost.tsx +++ b/frontend/packages/metal3-plugin/src/components/baremetal-hosts/add-baremetal-host/AddBareMetalHost.tsx @@ -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'; @@ -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) || '', @@ -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; - host?: FirehoseResult; - secret?: FirehoseResult; + name?: string; + enablePowerMgmt: boolean; }; const AddBareMetalHost: React.FC = ({ namespace, - isEditing, - hosts, - host: resultHost, - secret: resultSecret, + name, + enablePowerMgmt, }) => { - const [reload, setReload] = React.useState(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( + () => + name + ? { + kind: referenceForModel(BareMetalHostModel), + namespace, + name, + } + : undefined, + [name, namespace], + ); + const bmhResources = React.useMemo( + () => + !name + ? { + kind: referenceForModel(BareMetalHostModel), + namespace, + isList: true, + } + : undefined, + [name, namespace], + ); + const [host, hostLoaded, hostError] = useK8sWatchResource(bmhResource); + const [hosts, hostsLoaded, hostsError] = useK8sWatchResource(bmhResources); - const initialValues = getInitialValues(initialHost, initialSecret); - const prevInitialValues = getInitialValues(prevInitialHost, prevInitialSecret); + const credentialsName = host?.spec?.bmc?.credentialsName; + const secretResource = React.useMemo( + () => + credentialsName + ? { + kind: SecretModel.kind, + namespace, + name: credentialsName, + } + : undefined, + [credentialsName, namespace], + ); + const [secret, secretLoaded, secretError] = useK8sWatchResource(secretResource); + const [reload, setReload] = React.useState(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(host, [hostLoaded, reload]); + const initialSecret = usePrevious(secret, [secretLoaded, reload]); + + if (name ? !hostLoaded || (secretResource ? !secretLoaded : false) : !hostsLoaded) { + return ; + } + + if (hostError || secretError || hostsError) { + return ; + } + + 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, + ) => { const opts = { ...values, namespace }; - const promise = isEditing + const promise = name ? updateBareMetalHost(initialHost, initialSecret, opts) : createBareMetalHost(opts); @@ -113,14 +169,12 @@ const AddBareMetalHost: React.FC = ({ return ( setReload(true)} validationSchema={addHostValidationSchema} > - {(formikProps) => ( - - )} + {(props) => } ); }; diff --git a/frontend/packages/metal3-plugin/src/components/baremetal-hosts/add-baremetal-host/AddBareMetalHostForm.tsx b/frontend/packages/metal3-plugin/src/components/baremetal-hosts/add-baremetal-host/AddBareMetalHostForm.tsx index 8077f9eb1d1..33ef2220b96 100644 --- a/frontend/packages/metal3-plugin/src/components/baremetal-hosts/add-baremetal-host/AddBareMetalHostForm.tsx +++ b/frontend/packages/metal3-plugin/src/components/baremetal-hosts/add-baremetal-host/AddBareMetalHostForm.tsx @@ -26,6 +26,7 @@ const AddBareMetalHostForm: React.FC = ({ dirty, isEditing, showUpdated, + values, }) => (

= ({ 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} /> @@ -43,34 +44,6 @@ const AddBareMetalHostForm: React.FC = ({ name="description" label="Description" /> - - - - = ({ helpText="The MAC address of the NIC connected to the network that will be used to provision the host." required /> - {!isEditing && ( - + + {values.enablePowerManagement && ( + <> + + + + + {!isEditing && ( + + )} + )} ; -const AddBareMetalHostPage: React.FunctionComponent = ({ match }) => { +const AddBareMetalHostPage: React.FunctionComponent = ({ + match, + location, +}) => { const { name, ns: namespace } = match.params; - const resources: FirehoseResource[] = []; + const enablePowerMgmt = new URLSearchParams(location.search).has('powerMgmt'); const isEditing = !!name; - if (isEditing) { - resources.push( - { - kind: referenceForModel(BareMetalHostModel), - namespaced: true, - namespace, - name, - isList: false, - prop: 'host', - }, - { - kind: SecretModel.kind, - namespaced: true, - namespace, - name: getSecretName(name), - isList: false, - prop: 'secret', - }, - ); - } else { - resources.push({ - kind: referenceForModel(BareMetalHostModel), - namespaced: true, - namespace, - isList: true, - prop: 'hosts', - }); - } const title = `${isEditing ? 'Edit' : 'Add'} Bare Metal Host`; return ( <> @@ -57,12 +27,10 @@ const AddBareMetalHostPage: React.FunctionComponent = {!isEditing && (

- Expand the hardware inventory by registering new Bare Metal Host. + Expand the hardware inventory by registering a new Bare Metal Host.

)} - - - + ); diff --git a/frontend/packages/metal3-plugin/src/components/baremetal-hosts/add-baremetal-host/types.ts b/frontend/packages/metal3-plugin/src/components/baremetal-hosts/add-baremetal-host/types.ts index ee262211ad4..c9e2d1e29b9 100644 --- a/frontend/packages/metal3-plugin/src/components/baremetal-hosts/add-baremetal-host/types.ts +++ b/frontend/packages/metal3-plugin/src/components/baremetal-hosts/add-baremetal-host/types.ts @@ -7,4 +7,5 @@ export type AddBareMetalHostFormValues = { bootMACAddress: string; online: boolean; description: string; + enablePowerManagement: boolean; }; diff --git a/frontend/packages/metal3-plugin/src/components/baremetal-hosts/dashboard/StatusCard.tsx b/frontend/packages/metal3-plugin/src/components/baremetal-hosts/dashboard/StatusCard.tsx index 4c44dc5671a..d12787e90c1 100644 --- a/frontend/packages/metal3-plugin/src/components/baremetal-hosts/dashboard/StatusCard.tsx +++ b/frontend/packages/metal3-plugin/src/components/baremetal-hosts/dashboard/StatusCard.tsx @@ -119,7 +119,7 @@ const HealthCard: React.FC = ({ BareMetalHostModel, obj.metadata.name, obj.metadata.namespace, - )}/edit`} + )}/edit?powerMgmt`} > Add credentials diff --git a/frontend/packages/metal3-plugin/src/hooks/index.ts b/frontend/packages/metal3-plugin/src/hooks/index.ts deleted file mode 100644 index 188463e6d81..00000000000 --- a/frontend/packages/metal3-plugin/src/hooks/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { useEffect, useRef } from 'react'; - -export const usePrevious = (value) => { - const ref = useRef(); - useEffect(() => { - ref.current = value; - }); - return ref.current; -}; diff --git a/frontend/packages/metal3-plugin/src/k8s/objects/bare-metal-host.ts b/frontend/packages/metal3-plugin/src/k8s/objects/bare-metal-host.ts index 78397303122..4244e7b03a5 100644 --- a/frontend/packages/metal3-plugin/src/k8s/objects/bare-metal-host.ts +++ b/frontend/packages/metal3-plugin/src/k8s/objects/bare-metal-host.ts @@ -26,21 +26,27 @@ export const buildBareMetalHostObject = ( disableCertificateVerification = false, online = true, description = '', -): BareMetalHostKind => ({ - apiVersion: `${BareMetalHostModel.apiGroup}/${BareMetalHostModel.apiVersion}`, - kind: BareMetalHostModel.kind, - metadata: { - name, - namespace, - }, - spec: { - bmc: { + enablePowerManagement, +): BareMetalHostKind => { + const bmh: BareMetalHostKind = { + apiVersion: `${BareMetalHostModel.apiGroup}/${BareMetalHostModel.apiVersion}`, + kind: BareMetalHostModel.kind, + metadata: { + name, + namespace, + }, + spec: { + bootMACAddress, + description, + online, + }, + }; + if (enablePowerManagement) { + bmh.spec.bmc = { address: BMCAddress, credentialsName: getSecretName(name), disableCertificateVerification, - }, - bootMACAddress, - description, - online, - }, -}); + }; + } + return bmh; +}; diff --git a/frontend/packages/metal3-plugin/src/k8s/requests/bare-metal-host.ts b/frontend/packages/metal3-plugin/src/k8s/requests/bare-metal-host.ts index 7f871d804d8..ebcc8fe806b 100644 --- a/frontend/packages/metal3-plugin/src/k8s/requests/bare-metal-host.ts +++ b/frontend/packages/metal3-plugin/src/k8s/requests/bare-metal-host.ts @@ -1,10 +1,10 @@ import { k8sPatch, k8sCreate, - K8sResourceKind, MachineKind, MachineSetKind, k8sKill, + SecretKind, } from '@console/internal/module/k8s'; import { MachineModel, MachineSetModel, SecretModel } from '@console/internal/models'; import { PatchBuilder } from '@console/shared/src/k8s'; @@ -18,6 +18,7 @@ import { buildBareMetalHostSecret, getSecretName, } from '../objects/bare-metal-host'; +import { AddBareMetalHostFormValues } from '../../components/baremetal-hosts/add-baremetal-host/types'; export const powerOffHost = (host: BareMetalHostKind) => k8sPatch(BareMetalHostModel, host, [{ op: 'replace', path: '/spec/online', value: false }]); @@ -65,14 +66,7 @@ export const deprovision = async (machine: MachineKind, machineSet?: MachineSetK } }; -export type BareMetalHostOpts = { - name: string; - BMCAddress: string; - disableCertificateVerification: boolean; - username: string; - password: string; - bootMACAddress: string; - description: string; +export type BareMetalHostOpts = AddBareMetalHostFormValues & { namespace: string; }; @@ -86,8 +80,10 @@ export const createBareMetalHost = async ({ password, username, online, -}: BareMetalHostOpts & { online: boolean }) => { - const secret = buildBareMetalHostSecret(name, namespace, username, password); + enablePowerManagement, +}: BareMetalHostOpts) => { + const secret = + enablePowerManagement && buildBareMetalHostSecret(name, namespace, username, password); const bareMetalHost = buildBareMetalHostObject( name, namespace, @@ -96,14 +92,15 @@ export const createBareMetalHost = async ({ disableCertificateVerification, online, description, + enablePowerManagement, ); - await k8sCreate(SecretModel, secret); + enablePowerManagement && (await k8sCreate(SecretModel, secret)); await k8sCreate(BareMetalHostModel, bareMetalHost); }; export const updateBareMetalHost = async ( host: BareMetalHostKind, - secret: K8sResourceKind, + secret: SecretKind, { name, BMCAddress, @@ -113,31 +110,44 @@ export const updateBareMetalHost = async ( namespace, password, username, + enablePowerManagement, }: BareMetalHostOpts, ) => { - if (secret) { - const patches = new PatchBuilder('/data').buildAddObjectKeysPatches( - { username: btoa(username), password: btoa(password) }, - secret.data, - ); - - if (patches.length > 0) { - await k8sPatch(SecretModel, secret, patches); - } - } else { - await k8sCreate(SecretModel, buildBareMetalHostSecret(name, namespace, username, password)); - } const patches = [ ...new PatchBuilder('/spec').buildAddObjectKeysPatches( { description, bootMACAddress }, host.spec, ), - ...new PatchBuilder('/spec/bmc').buildAddObjectKeysPatches( - { address: BMCAddress, credentialsName: getSecretName(name), disableCertificateVerification }, - host.spec.bmc, - ), ]; + if (enablePowerManagement) { + if (secret) { + const secretPatch = new PatchBuilder('/data').buildAddObjectKeysPatches( + { username: btoa(username), password: btoa(password) }, + secret.data, + ); + + if (secretPatch.length > 0) { + await k8sPatch(SecretModel, secret, secretPatch); + } + } else { + await k8sCreate(SecretModel, buildBareMetalHostSecret(name, namespace, username, password)); + } + patches.push( + ...new PatchBuilder('/spec/bmc').buildAddObjectKeysPatches( + { + address: BMCAddress, + credentialsName: getSecretName(name), + disableCertificateVerification, + }, + host.spec.bmc, + ), + ); + } else if (secret) { + await k8sKill(SecretModel, secret); + patches.push(new PatchBuilder('/spec/bmc').remove().build()); + } + if (patches.length > 0) { await k8sPatch(BareMetalHostModel, host, patches); } diff --git a/frontend/packages/metal3-plugin/src/selectors/secret.ts b/frontend/packages/metal3-plugin/src/selectors/secret.ts index 93996f00db3..87c38a28fc8 100644 --- a/frontend/packages/metal3-plugin/src/selectors/secret.ts +++ b/frontend/packages/metal3-plugin/src/selectors/secret.ts @@ -1,6 +1,6 @@ -import { K8sResourceKind } from '@console/internal/module/k8s'; +import { SecretKind } from '@console/internal/module/k8s/types'; -export const getSecretUsername = (secret: K8sResourceKind): string => +export const getSecretUsername = (secret: SecretKind): string => secret && secret.data && atob(secret.data.username); -export const getSecretPassword = (secret: K8sResourceKind): string => +export const getSecretPassword = (secret: SecretKind): string => secret && secret.data && atob(secret.data.password); diff --git a/frontend/packages/metal3-plugin/src/types/host.ts b/frontend/packages/metal3-plugin/src/types/host.ts index f0141561b0c..59286c73fe2 100644 --- a/frontend/packages/metal3-plugin/src/types/host.ts +++ b/frontend/packages/metal3-plugin/src/types/host.ts @@ -42,7 +42,7 @@ export type BareMetalHostBios = { export type BareMetalHostKind = { spec?: { - bmc: { + bmc?: { address: string; credentialsName: string; disableCertificateVerification: boolean; diff --git a/frontend/packages/metal3-plugin/src/utils/index.ts b/frontend/packages/metal3-plugin/src/utils/index.ts deleted file mode 100644 index 8b256a9621c..00000000000 --- a/frontend/packages/metal3-plugin/src/utils/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { FirehoseResult } from '@console/internal/components/utils'; -import { K8sResourceKind } from '@console/internal/module/k8s'; - -export const getLoadedData = ( - result: FirehoseResult, - defaultValue = null, -) => (result && result.loaded && !result.loadError ? result.data : defaultValue); diff --git a/frontend/public/components/notification-drawer.tsx b/frontend/public/components/notification-drawer.tsx index d712e6490fb..0efc7a9e65b 100644 --- a/frontend/public/components/notification-drawer.tsx +++ b/frontend/public/components/notification-drawer.tsx @@ -29,6 +29,7 @@ import { EmptyStateVariant, Title, } from '@patternfly/react-core'; +import { usePrevious } from '@console/shared/src/hooks/previous'; import { coFetchJSON } from '../co-fetch'; import { @@ -38,7 +39,6 @@ import { referenceForModel, } from '../module/k8s'; import { ClusterVersionModel } from '../models'; -import { usePrevious } from '@console/metal3-plugin/src/hooks'; import { useK8sWatchResource, WatchK8sResource } from './utils/k8s-watch-hook'; const criticalCompare = (a: Alert): boolean => getAlertSeverity(a) === 'critical'; diff --git a/frontend/public/components/utils/k8s-watch-hook.ts b/frontend/public/components/utils/k8s-watch-hook.ts index 1e1abf3e5c0..4365f5cba01 100644 --- a/frontend/public/components/utils/k8s-watch-hook.ts +++ b/frontend/public/components/utils/k8s-watch-hook.ts @@ -89,6 +89,9 @@ export const useK8sWatchResource = { + if (!resource) { + return [undefined, true, undefined]; + } if (!resourceK8s) { const data = resource?.isList ? [] : {}; return modelsLoaded && !k8sModel