Skip to content

Commit

Permalink
Merge pull request #5007 from rohitkrai03/helm-rollback
Browse files Browse the repository at this point in the history
Add support for Helm release rollback
  • Loading branch information
openshift-merge-robot committed Apr 15, 2020
2 parents 1eec032 + e95b193 commit 9b70973
Show file tree
Hide file tree
Showing 22 changed files with 454 additions and 84 deletions.
@@ -1,61 +1,27 @@
import * as React from 'react';
import { useField } from 'formik';
import { FormGroup, Radio } from '@patternfly/react-core';
import { RadioButtonProps } from './field-types';
import { useField, useFormikContext, FormikValues } from 'formik';
import { Radio } from '@patternfly/react-core';
import { RadioButtonFieldProps } from './field-types';
import { getFieldId } from './field-utils';
import './RadioButtonField.scss';

const RadioButtonField: React.FC<RadioButtonProps> = ({
label,
options,
helpText,
required,
...props
}) => {
const [field, { touched, error }] = useField(props.name);
const fieldId = getFieldId(props.name, 'radiobutton');
const RadioButtonField: React.FC<RadioButtonFieldProps> = ({ name, label, value, ...props }) => {
const [field, { touched, error }] = useField(name);
const { setFieldValue } = useFormikContext<FormikValues>();
const fieldId = getFieldId(`${name}-${value}`, 'radiobutton');
const isValid = !(touched && error);
const errorMessage = !isValid ? error : '';
return (
<FormGroup
className="odc-radio-button"
fieldId={fieldId}
helperText={helpText}
helperTextInvalid={errorMessage}
isValid={isValid}
isRequired={required}
<Radio
{...field}
{...props}
id={fieldId}
value={value}
label={label}
>
{options.map((option) => {
const activeChild = field.value === option.value && option.activeChildren;
const staticChild = option.children;

return (
<React.Fragment key={option.value}>
<Radio
{...field}
{...props}
id={getFieldId(option.value, 'radiobutton')}
value={option.value}
label={option.label}
isChecked={field.value === option.value}
isValid={isValid}
isDisabled={option.isDisabled}
aria-describedby={`${fieldId}-helper`}
onChange={(val, event) => {
field.onChange(event);
}}
/>
{(activeChild || staticChild) && (
<div className="odc-radio-button__children">
{staticChild}
{activeChild}
</div>
)}
</React.Fragment>
);
})}
</FormGroup>
isChecked={field.value === value}
isValid={isValid}
isDisabled={props.isDisabled}
aria-label={`${fieldId}-${label}`}
onChange={() => setFieldValue(name, value)}
/>
);
};

Expand Down
@@ -1,11 +1,11 @@
.odc-radio-button {
.ocs-radio-group-field {
padding-left: var(--pf-global--spacer--sm);

input[type='radio'] {
margin-top: 0;
}

&__children {
padding: var(--pf-global--spacer--md) 0 var(--pf-global--spacer--md) 1.8rem;
padding: var(--pf-global--spacer--md) 0;
}
}
@@ -0,0 +1,59 @@
import * as React from 'react';
import { useField } from 'formik';
import { FormGroup } from '@patternfly/react-core';
import { RadioGroupFieldProps } from './field-types';
import { getFieldId } from './field-utils';
import RadioButtonField from './RadioButtonField';
import './RadioGroupField.scss';

const RadioGroupField: React.FC<RadioGroupFieldProps> = ({
label,
options,
helpText,
required,
...props
}) => {
const [field, { touched, error }] = useField(props.name);
const fieldId = getFieldId(props.name, 'radiogroup');
const isValid = !(touched && error);
const errorMessage = !isValid ? error : '';
return (
<FormGroup
className="ocs-radio-group-field"
fieldId={fieldId}
helperText={helpText}
helperTextInvalid={errorMessage}
isValid={isValid}
isRequired={required}
label={label}
>
{options.map((option) => {
const activeChild = field.value === option.value && option.activeChildren;
const staticChild = option.children;

const description = (activeChild || staticChild) && (
<div className="ocs-radio-group-field__children">
{staticChild}
{activeChild}
</div>
);

return (
<React.Fragment key={option.value}>
<RadioButtonField
{...field}
{...props}
value={option.value}
label={option.label}
isDisabled={option.isDisabled}
aria-describedby={`${fieldId}-helper`}
description={description}
/>
</React.Fragment>
);
})}
</FormGroup>
);
};

export default RadioGroupField;
Expand Up @@ -3,7 +3,7 @@ import { K8sResourceKind } from '@console/internal/module/k8s';

export interface FieldProps {
name: string;
label?: string;
label?: React.ReactNode;
helpText?: React.ReactNode;
helpTextInvalid?: React.ReactNode;
required?: boolean;
Expand Down Expand Up @@ -99,12 +99,17 @@ export interface SecretKeyRef {
};
}

export interface RadioButtonProps extends FieldProps {
options: RadioOption[];
export interface RadioButtonFieldProps extends FieldProps {
value: string | number;
description?: React.ReactNode;
}

export interface RadioOption {
value: string;
export interface RadioGroupFieldProps extends FieldProps {
options: RadioGroupOption[];
}

export interface RadioGroupOption {
value: string | number;
label: React.ReactNode;
isDisabled?: boolean;
children?: React.ReactNode;
Expand Down
@@ -1,3 +1,3 @@
export const getFieldId = (fieldName: string, fieldType: string) => {
return `form-${fieldType}-${fieldName.replace(/\./g, '-')}-field`;
return `form-${fieldType}-${fieldName?.replace(/\./g, '-')}-field`;
};
Expand Up @@ -7,6 +7,7 @@ export { default as MultiColumnField } from './multi-column-field/MultiColumnFie
export { default as NSDropdownField } from './NSDropdownField';
export { default as NumberSpinnerField } from './NumberSpinnerField';
export { default as RadioButtonField } from './RadioButtonField';
export { default as RadioGroupField } from './RadioGroupField';
export { default as ResourceDropdownField } from './ResourceDropdownField';
export { default as ResourceLimitField } from './ResourceLimitField';
export { default as SwitchField } from './SwitchField';
Expand Down
Expand Up @@ -26,3 +26,10 @@ export const upgradeHelmRelease = (releaseName: string, namespace: string) => ({
history.push(`/helm-releases/ns/${namespace}/${releaseName}/upgrade`);
},
});

export const rollbackHelmRelease = (releaseName: string, namespace: string) => ({
label: 'Rollback',
callback: () => {
history.push(`/helm-releases/ns/${namespace}/${releaseName}/rollback`);
},
});
@@ -0,0 +1,87 @@
import * as React from 'react';
import Helmet from 'react-helmet';
import { Formik } from 'formik';
import { RouteComponentProps } from 'react-router';
import { PageBody } from '@console/shared';
import { coFetchJSON } from '@console/internal/co-fetch';
import { PageHeading, history } from '@console/internal/components/utils';

import { HelmRelease } from './helm-types';
import NamespacedPage, { NamespacedPageVariants } from '../NamespacedPage';
import HelmReleaseRollbackForm from './form/HelmReleaseRollbackForm';

type HelmReleaseRollbackPageProps = RouteComponentProps<{
ns?: string;
releaseName?: string;
}>;

type HelmRollbackFormData = {
revision: number;
};

const HelmReleaseRollbackPage: React.FC<HelmReleaseRollbackPageProps> = ({ match }) => {
const { releaseName, ns: namespace } = match.params;
const [releaseHistory, setReleaseHistory] = React.useState<HelmRelease[]>(null);

React.useEffect(() => {
let ignore = false;

const fetchReleaseHistory = async () => {
let res: HelmRelease[];
try {
res = await coFetchJSON(`/api/helm/release/history?ns=${namespace}&name=${releaseName}`);
} catch {} // eslint-disable-line no-empty
if (ignore) return;

res?.length > 0 && setReleaseHistory(res);
};

fetchReleaseHistory();

return () => {
ignore = true;
};
}, [namespace, releaseName]);

const initialValues: HelmRollbackFormData = {
revision: -1,
};

const handleSubmit = (values, actions) => {
actions.setStatus({ isSubmitting: true });
const payload = {
namespace,
name: releaseName,
version: values.revision,
};

coFetchJSON
.put('/api/helm/release', payload)
.then(() => {
actions.setStatus({ isSubmitting: false });
history.push(`/helm-releases/ns/${namespace}`);
})
.catch((err) => {
actions.setSubmitting(false);
actions.setStatus({ submitError: err.message });
});
};

return (
<NamespacedPage variant={NamespacedPageVariants.light} disabled hideApplications>
<Helmet>
<title>Rollback Helm Release</title>
</Helmet>
<PageHeading title="Rollback Helm Release">
Select the version to rollback <strong>{releaseName}</strong> to, from the table below:
</PageHeading>
<PageBody>
<Formik initialValues={initialValues} onSubmit={handleSubmit} onReset={history.goBack}>
{(props) => <HelmReleaseRollbackForm {...props} releaseHistory={releaseHistory} />}
</Formik>
</PageBody>
</NamespacedPage>
);
};

export default HelmReleaseRollbackPage;
Expand Up @@ -17,7 +17,11 @@ import { fetchHelmReleases } from '../helm-utils';
import HelmReleaseResources from './resources/HelmReleaseResources';
import HelmReleaseOverview from './overview/HelmReleaseOverview';
import HelmReleaseHistory from './history/HelmReleaseHistory';
import { deleteHelmRelease, upgradeHelmRelease } from '../../../actions/modify-helm-release';
import {
deleteHelmRelease,
upgradeHelmRelease,
rollbackHelmRelease,
} from '../../../actions/modify-helm-release';
import HelmReleaseNotes from './HelmReleaseNotes';
import { HelmRelease } from '../helm-types';

Expand Down Expand Up @@ -49,23 +53,31 @@ export const LoadedHelmReleaseDetails: React.FC<LoadedHelmReleaseDetailsProps> =
return <StatusBox loaded={secret.loaded} loadError={secret.loadError} />;
}

const secretResource = secret.data;
const secretResources = secret.data;

if (_.isEmpty(secretResources)) return <ErrorPage404 />;

if (_.isEmpty(secretResource)) return <ErrorPage404 />;
const sortedSecretResources = _.orderBy(
secretResources,
(o) => Number(o.metadata.labels.version),
'desc',
);

const secretName = secretResource[0]?.metadata.name;
const latestReleaseSecret = sortedSecretResources[0];
const secretName = latestReleaseSecret?.metadata.name;
const releaseName = helmReleaseData?.name;

const title = (
<>
{releaseName}
<Badge isRead style={{ verticalAlign: 'middle', marginLeft: 'var(--pf-global--spacer--md)' }}>
<Status status={_.capitalize(helmReleaseData?.info?.status)} />
<Status status={_.capitalize(latestReleaseSecret?.metadata.labels.status)} />
</Badge>
</>
);

const menuActions = [
() => rollbackHelmRelease(releaseName, namespace),
() => upgradeHelmRelease(releaseName, namespace),
() => deleteHelmRelease(releaseName, namespace, `/helm-releases/ns/${namespace}`),
];
Expand Down Expand Up @@ -96,7 +108,7 @@ export const LoadedHelmReleaseDetails: React.FC<LoadedHelmReleaseDetailsProps> =
},
{
href: 'history',
name: 'History',
name: 'Revision History',
component: HelmReleaseHistory,
},
{
Expand Down
Expand Up @@ -2,29 +2,36 @@ import * as React from 'react';
import { match as RMatch } from 'react-router';
import { coFetchJSON } from '@console/internal/co-fetch';
import { SortByDirection } from '@patternfly/react-table';
import { K8sResourceKind } from '@console/internal/module/k8s';
import { useDeepCompareMemoize } from '@console/shared';

import { HelmRelease } from '../../helm-types';
import CustomResourceList from '../../../custom-resource-list/CustomResourceList';
import HelmReleaseHistoryRow from './HelmReleaseHistoryRow';
import HelmReleaseHistoryHeader from './HelmReleaseHistoryHeader';
import { HelmRelease } from '../../helm-types';

interface HelmReleaseHistoryProps {
match: RMatch<{
ns?: string;
name?: string;
}>;
obj: K8sResourceKind;
}

const HelmReleaseHistory: React.FC<HelmReleaseHistoryProps> = ({ match }) => {
const HelmReleaseHistory: React.FC<HelmReleaseHistoryProps> = ({ match, obj }) => {
const namespace = match.params.ns;
const helmReleaseName = match.params.name;

const memoizedObj = useDeepCompareMemoize(obj);

const getHelmReleaseRevisions = (): Promise<HelmRelease[]> => {
return coFetchJSON(`/api/helm/release/history?ns=${namespace}&name=${helmReleaseName}`);
};

return (
<CustomResourceList
fetchCustomResources={getHelmReleaseRevisions}
dependentResource={memoizedObj}
sortBy="version"
sortOrder={SortByDirection.desc}
resourceRow={HelmReleaseHistoryRow}
Expand Down

0 comments on commit 9b70973

Please sign in to comment.