Skip to content

Commit 7f17026

Browse files
Jethro-Mavoidik
authored andcommitted
[PLAT-18616] Add warning when restoring with old backups during HA failover
Summary: Ideally we want to avoid older backups when restoring during HA failover because of the increased likelyhood of missing YBA context. Ex. If a TLS toggle off occurred, but we restored to a backup prior to this operation, then YBA would assume TLS is still enabled leading to broken connectivity and inability to perform basic operations like backup. This diff adds an extra warning which appears when users try to promote their standby using a backup which is older than 24 hours. Users can accept the risk and proceed if needed. Test Plan: Set up a HA config between 2 YBA platforms and try promoting standby with backups > 24 hours old and <= 24 hours old. {F410712} {F410713} Reviewers: kkannan, rmadhavan Reviewed By: rmadhavan Subscribers: yugaware Differential Revision: https://phorge.dev.yugabyte.com/D47494
1 parent d5f546c commit 7f17026

File tree

2 files changed

+137
-41
lines changed

2 files changed

+137
-41
lines changed

managed/ui/src/components/ha/modals/PromoteInstanceModal.tsx

Lines changed: 115 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ import { handleServerError } from '../../../utils/errorHandlingUtils';
1717
import InfoIcon from '../../../redesign/assets/info-message.svg';
1818
import { YBInput, YBTooltip } from '../../../redesign/components';
1919
import { formatDatetime, YBTimeFormats } from '../../../redesign/helpers/DateUtils';
20+
import {
21+
getHaBackupFileTimestamp,
22+
getIsHaBackupOld,
23+
HA_BACKUP_OLD_THRESHOLD_HOURS
24+
} from '../utils';
25+
import { ReactComponent as WarningIcon } from '@app/redesign/assets/alert.svg';
2026

2127
import './PromoteInstanceModal.scss';
2228

@@ -41,12 +47,16 @@ const validationSchema = Yup.object().shape({
4147
backupFile: Yup.object().nullable().required('Backup file is required')
4248
});
4349

44-
const adaptHaBackupToFormFieldOption = (value: string, currentUserTimezone?: string): PromoteInstanceFormValues['backupFile'] => {
45-
// backup_21-02-20-00-40.tgz --> 21-02-20-00-40
46-
const timestamp = value.replace('backup_', '').replace('.tgz', '');
47-
// we always get the backup list time in UTC
48-
const formattedTimestamp = moment.utc(timestamp, 'YY-MM-DD-HH:mm').toDate();
49-
const label = formatDatetime(formattedTimestamp, YBTimeFormats.YB_DEFAULT_TIMESTAMP , currentUserTimezone);
50+
const adaptHaBackupToFormFieldOption = (
51+
value: string,
52+
currentUserTimezone?: string
53+
): PromoteInstanceFormValues['backupFile'] => {
54+
const formattedTimestamp = moment.utc(getHaBackupFileTimestamp(value), 'YY-MM-DD-HH:mm').toDate();
55+
const label = formatDatetime(
56+
formattedTimestamp,
57+
YBTimeFormats.YB_DEFAULT_TIMESTAMP,
58+
currentUserTimezone
59+
);
5060

5161
return { value, label };
5262
};
@@ -60,6 +70,17 @@ const useStyles = makeStyles((theme) => ({
6070
},
6171
fieldLabel: {
6272
marginBottom: theme.spacing(1)
73+
},
74+
warningBanner: {
75+
display: 'flex',
76+
alignItems: 'center',
77+
gap: theme.spacing(1),
78+
79+
marginTop: theme.spacing(2),
80+
padding: theme.spacing(1),
81+
82+
backgroundColor: theme.palette.warning[100],
83+
borderRadius: theme.shape.borderRadius
6384
}
6485
}));
6586

@@ -70,11 +91,17 @@ export const PromoteInstanceModal: FC<PromoteInstanceModalProps> = ({
7091
instanceId
7192
}) => {
7293
const [confirmationText, setConfirmationText] = useState<string>('');
94+
const [
95+
isRestoreOldBackupConfirmationModalOpen,
96+
setIsRestoreOldBackupConfirmationModalOpen
97+
] = useState(false);
7398
const formik = useRef({} as FormikProps<PromoteInstanceFormValues>);
7499
const theme = useTheme();
75100
const classes = useStyles();
76101
const queryClient = useQueryClient();
77-
const currentUserTimezone = useSelector((state: any) => state?.customer?.currentUser?.data?.timezone);
102+
const currentUserTimezone = useSelector(
103+
(state: any) => state?.customer?.currentUser?.data?.timezone
104+
);
78105

79106
const { isLoading, data } = useQuery(
80107
[QUERY_KEY.getHABackups, configId],
@@ -84,7 +111,10 @@ export const PromoteInstanceModal: FC<PromoteInstanceModalProps> = ({
84111
onSuccess: (data) => {
85112
// pre-select first backup file from the list
86113
if (Array.isArray(data) && data.length) {
87-
formik.current.setFieldValue('backupFile', adaptHaBackupToFormFieldOption(data[0], currentUserTimezone));
114+
formik.current.setFieldValue(
115+
'backupFile',
116+
adaptHaBackupToFormFieldOption(data[0], currentUserTimezone)
117+
);
88118
}
89119
}
90120
}
@@ -106,7 +136,9 @@ export const PromoteInstanceModal: FC<PromoteInstanceModalProps> = ({
106136
}
107137
);
108138

109-
const backupsList = (data ?? []).map(backup => adaptHaBackupToFormFieldOption(backup, currentUserTimezone));
139+
const backupsList = (data ?? []).map((backup) =>
140+
adaptHaBackupToFormFieldOption(backup, currentUserTimezone)
141+
);
110142

111143
const closeModal = () => {
112144
if (!formik.current.isSubmitting) {
@@ -115,7 +147,7 @@ export const PromoteInstanceModal: FC<PromoteInstanceModalProps> = ({
115147
}
116148
};
117149

118-
const submitForm = async (
150+
const handleSubmit = async (
119151
formValues: PromoteInstanceFormValues,
120152
actions: FormikActions<PromoteInstanceFormValues>
121153
) => {
@@ -137,7 +169,7 @@ export const PromoteInstanceModal: FC<PromoteInstanceModalProps> = ({
137169
showCancelButton
138170
title="Make Active"
139171
onHide={closeModal}
140-
onFormSubmit={submitForm}
172+
onFormSubmit={handleSubmit}
141173
isSubmitDisabled={isSubmitDisabled}
142174
footerAccessory={
143175
<Box display="flex" gridGap={theme.spacing(1)}>
@@ -164,38 +196,80 @@ export const PromoteInstanceModal: FC<PromoteInstanceModalProps> = ({
164196
render={(formikProps: FormikProps<PromoteInstanceFormValues>) => {
165197
// workaround for outdated version of Formik to access form methods outside of <Formik>
166198
formik.current = formikProps;
167-
199+
const isHaBackupOld =
200+
!!formik.current.values.backupFile?.value &&
201+
getIsHaBackupOld(formik.current.values.backupFile.value);
168202
return (
169-
<div data-testid="ha-make-active-modal">
170-
{isLoading ? (
171-
<YBLoading />
172-
) : (
173-
<div className="ha-promote-instance-modal">
174-
<Alert bsStyle="warning">
175-
Note: promotion will replace all existing data on this platform instance with
176-
the data from the selected backup. After promotion succeeds you will need to
177-
re-sign in with the credentials of the previously active platform instance.
178-
</Alert>
179-
<Typography variant="body2" className={classes.fieldLabel}>
180-
<Field
181-
name="backupFile"
182-
component={YBFormSelect}
183-
options={backupsList}
184-
label="Select the backup to restore from"
185-
isSearchable
203+
<>
204+
<div data-testid="ha-make-active-modal">
205+
{isLoading ? (
206+
<YBLoading />
207+
) : (
208+
<div className="ha-promote-instance-modal">
209+
<Box
210+
display="flex"
211+
flexDirection="column"
212+
gridGap={theme.spacing(1)}
213+
marginBottom={2}
214+
>
215+
<div className={classes.warningBanner}>
216+
<Box width={24} height={24}>
217+
<WarningIcon width={24} height={24} color={theme.palette.warning[700]} />
218+
</Box>
219+
<Typography variant="body2">
220+
<b>Note! </b>After promotion, all existing data from this platform
221+
instance will be replaced with the data from teh selected backup. After
222+
promotion succeeds, you will need to log-in again with the credentials of
223+
the previous active platform instance. Contact Yugabyte Support for
224+
assistance.
225+
</Typography>
226+
</div>
227+
{isHaBackupOld && (
228+
<div className={classes.warningBanner}>
229+
<Box width={24} height={24}>
230+
<WarningIcon
231+
width={24}
232+
height={24}
233+
color={theme.palette.warning[700]}
234+
/>
235+
</Box>
236+
<Typography variant="body2">
237+
<p>
238+
<b>
239+
{`Note! The selected backup is more than ${HA_BACKUP_OLD_THRESHOLD_HOURS} hours old. Restoring from this backup could cause serious issues, including
240+
data loss.`}
241+
</b>{' '}
242+
</p>
243+
<p>Contact Yugabyte Support for assistance.</p>
244+
</Typography>
245+
</div>
246+
)}
247+
</Box>
248+
<Typography variant="body2" className={classes.fieldLabel}>
249+
<Field
250+
name="backupFile"
251+
component={YBFormSelect}
252+
options={backupsList}
253+
label="Select the backup to restore from"
254+
isSearchable
255+
menuPortalTarget={document.body}
256+
styles={{
257+
menuPortal: (base: any) => ({ ...base, zIndex: 4000 })
258+
}}
259+
/>
260+
Please type PROMOTE to confirm.
261+
</Typography>
262+
<YBInput
263+
className={classes.confirmTextInputBox}
264+
inputProps={{ 'data-testid': 'PromoteInstanceModal-ConfirmTextInputField' }}
265+
placeholder="PROMOTE"
266+
value={confirmationText}
267+
onChange={(event) => setConfirmationText(event.target.value)}
186268
/>
187-
Please type PROMOTE to confirm.
188-
</Typography>
189-
<YBInput
190-
className={classes.confirmTextInputBox}
191-
inputProps={{ 'data-testid': 'PromoteInstanceModal-ConfirmTextInputField' }}
192-
placeholder="PROMOTE"
193-
value={confirmationText}
194-
onChange={(event) => setConfirmationText(event.target.value)}
195-
/>
196-
</div>
197-
)}
198-
</div>
269+
</div>
270+
)}
271+
</div>
272+
</>
199273
);
200274
}}
201275
/>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import moment from 'moment';
2+
3+
/**
4+
* Extracts the timestamp from a platform HA backup file name.
5+
* Backup filenames are in the format of backup_YYYY-MM-DD-HH-mm.tgz. Ex. backup_2025-01-01-00-00.tgz.
6+
* Returns the extracted timestamp.
7+
*/
8+
export const getHaBackupFileTimestamp = (backupFileName: string) =>
9+
backupFileName.replace('backup_', '').replace('.tgz', '');
10+
11+
export const HA_BACKUP_OLD_THRESHOLD_HOURS = 24;
12+
/**
13+
* Checks if a given timestamp is older than HA_BACKUP_OLD_THRESHOLD_HOURS from the current time.
14+
* True if the timestamp is older than HA_BACKUP_OLD_THRESHOLD_HOURS, false otherwise.
15+
*/
16+
export const getIsHaBackupOld = (backupFileName: string): boolean => {
17+
const backupTimestamp = moment.utc(getHaBackupFileTimestamp(backupFileName), 'YYYY-MM-DD-HH-mm');
18+
const now = moment.utc();
19+
const hoursDiff = now.diff(backupTimestamp, 'hours');
20+
21+
return hoursDiff > HA_BACKUP_OLD_THRESHOLD_HOURS;
22+
};

0 commit comments

Comments
 (0)