Skip to content

Commit

Permalink
[PLAT-13987] Add ability to skip bootstrapping
Browse files Browse the repository at this point in the history
Summary:
**Context**
We would like to give some advanced users with special use cases the ability to skip the bootstrapping (backup & restore)
step when they have a legitimate reason to do so.

c1fd042 added a new boolean field in the Bootstrap params called `allowBootstrap` which
simplifies the request body for clients. Instead of requiring the user to specify exactly which tables they wish to allow
bootstrapping for, the YBA backend's new `allowBootstrap` field allows us to simply allow bootstrapping for all tables and
databases in the universe. On the UI and docs we already inform the user that bootstrapping (aka creating a full copy) is
done at the database level for YSQL. Since the user is already aware and continued to submit the form anyways, we can just
convey that intention simply by passing `allowBootstrap` = true to the backend.

**Changes**
- Add a new runtime config flag called `yb.ui.xcluster.enable_skip_bootstrapping`. When set to true,
  the user will see a secondary button appear at the bottom of the create xCluster modal as well as
  the bottom of the select tables modal (when used for xCluster replication and not xCluster DR).
  This 'Skip Creating Full Copy' button will allow the user to submit their request without specifying
  backup storage config parameters. This will cause the YBA backend to skip the backup & restore step.

- Instead of just passing tables in the `bootstrapParams`, the YBA UI will now always pass `allowBootstrapping` = true.
  We already inform the user that we will bootstrap the entire database for YSQL and the user is okay with this.
  Specifying the whole tables list again under `bootstrapParams` doesn't need to be done anymore now that the backend
  lets us specify that the user is okay with bootstrapping whatever is needed. We do continue passing a non-empty tables list
  because `tables` is still a required field.

- Minor styling changes applied to the shared YBModal component. The `footerAccessory` prop will now expand
  to fit the remaining space in the footer.

Test Plan:
- Create two universes. On the desired source universe, add the following:
   - At least one database with tables containing no data
   - At least one database with tables containing data
   Create the same databases and tables on the target universe.
- Create xCluster config

Verfiy that the user is able to skip bootstrapping when `yb.ui.xcluster.enable_skip_bootstrapping` is true.
{F255505}
{F255506}
Verify that the 'skip creating full copy' button is not shown for DR.
{F255507}
Verify that the 'skip creating full copy' button is not shown when `yb.ui.xcluster.enable_skip_bootstrapping` is false.
{F255508}
- Check the form handles back and forwards navigation after the skip bootstrapping button is pressed.
  (ex. verify that the back button on the next page doesn't bring the user to the configure bootstrap page)

Reviewers: cwang, hzare, vbansal, rmadhavan

Reviewed By: hzare

Subscribers: yugaware

Differential Revision: https://phorge.dev.yugabyte.com/D35768
  • Loading branch information
Jethro-M committed Jun 15, 2024
1 parent ccc3f8e commit 468f5bb
Show file tree
Hide file tree
Showing 12 changed files with 249 additions and 135 deletions.
1 change: 1 addition & 0 deletions managed/RUNTIME-FLAGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
| "Use Redesigned Provider UI" | "yb.ui.feature_flags.provider_redesign" | "CUSTOMER" | "The redesigned provider UI adds a provider list view, a provider details view and improves the provider creation form for AWS, AZU, GCP, and K8s" | "Boolean" |
| "Enable partial editing of in use providers" | "yb.ui.feature_flags.edit_in_use_provider" | "CUSTOMER" | "A subset of fields from in use providers can be edited. Users can edit in use providers directly through the YBA API. This config is used to enable this functionality through YBA UI as well." | "Boolean" |
| "Show underlying xCluster configs from DR setup" | "yb.ui.xcluster.dr.show_xcluster_config" | "CUSTOMER" | "YBA creates an underlying transactional xCluster config when setting up an active-active single-master disaster recovery (DR) config. During regular operation you should manage the DR config through the DR UI instead of the xCluster UI. This feature flag serves as a way to expose the underlying xCluster config for troubleshooting." | "Boolean" |
| "Enable the option to skip creating a full copy for xCluster operations" | "yb.ui.xcluster.enable_skip_bootstrapping" | "CUSTOMER" | "Enabling this runtime config will expose an option in the create xCluster modal and select tables modal to skip creating a full copy for xCluster replication configs." | "Boolean" |
| "Enforce User Tags" | "yb.universe.user_tags.is_enforced" | "CUSTOMER" | "Prevents universe creation when the enforced tags are not provided." | "Boolean" |
| "Enforced User Tags List" | "yb.universe.user_tags.enforced_tags" | "CUSTOMER" | "A list of enforced user tag and accepted value pairs during universe creation. Pass '*' to accept all values for a tag. Ex: [\"yb_task:dev\",\"yb_task:test\",\"yb_owner:*\",\"yb_dept:eng\",\"yb_dept:qa\", \"yb_dept:product\", \"yb_dept:sales\"]" | "Key Value SetMultimap" |
| "Enable IMDSv2" | "yb.aws.enable_imdsv2_support" | "CUSTOMER" | "Enable IMDSv2 support for AWS providers" | "Boolean" |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,17 @@ public class CustomerConfKeys extends RuntimeConfigKeysModule {
ConfDataType.BooleanType,
ImmutableList.of(ConfKeyTags.PUBLIC));

public static final ConfKeyInfo<Boolean> enableSkipBootstrapping =
new ConfKeyInfo<>(
"yb.ui.xcluster.enable_skip_bootstrapping",
ScopeType.CUSTOMER,
"Enable the option to skip creating a full copy for xCluster operations",
"Enabling this runtime config will expose an option in the create xCluster modal and"
+ " select tables modal to skip creating a full copy for xCluster replication"
+ " configs.",
ConfDataType.BooleanType,
ImmutableList.of(ConfKeyTags.PUBLIC));

public static final ConfKeyInfo<Boolean> enforceUserTags =
new ConfKeyInfo<>(
"yb.universe.user_tags.is_enforced",
Expand Down
1 change: 1 addition & 0 deletions managed/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ yb {
}

xcluster {
enable_skip_bootstrapping=false
dr: {
# Show underlying xCluster configs used in DR on the xCluster config tab
# Relevant xCluster operations can and should be done through the DR UI instead.
Expand Down
59 changes: 32 additions & 27 deletions managed/ui/src/actions/xClusterReplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,28 +37,42 @@ export function fetchTablesInUniverse(
}
return Promise.reject('Querying universe tables failed: No universe UUID provided.');
}
export interface CreateXClusterConfigRequest {
name: string;
sourceUniverseUUID: string;
targetUniverseUUID: string;
configType: XClusterConfigType;
tables: string[];

export function createXClusterReplication(
targetUniverseUUID: string,
sourceUniverseUUID: string,
name: string,
configType: XClusterConfigType,
tables: string[],
bootstrapParams: {
bootstrapParams?: {
tables: string[];
backupRequestParams: any;
} | null
) {
allowBootstrapping: boolean;
backupRequestParams: {
storageConfigUUID: string;
};
};
}

export interface EditXClusterConfigTablesRequest {
tables: string[];

autoIncludeIndexTables?: boolean;
bootstrapParams?: {
tables: string[];
allowBootstrapping: boolean;
backupRequestParams: {
storageConfigUUID: string;
};
};
}

export function createXClusterConfig(createxClusterConfigRequest: CreateXClusterConfigRequest) {
const customerId = localStorage.getItem('customerId');
return axios
.post<YBPTask>(`${ROOT_URL}/customers/${customerId}/xcluster_configs`, {
sourceUniverseUUID,
targetUniverseUUID,
name,
configType,
tables,
...(bootstrapParams && { bootstrapParams })
})
.post<YBPTask>(
`${ROOT_URL}/customers/${customerId}/xcluster_configs`,
createxClusterConfigRequest
)
.then((response) => response.data);
}

Expand Down Expand Up @@ -135,15 +149,6 @@ export function editXclusterName(replication: XClusterConfig) {
});
}

interface EditXClusterConfigTablesRequest {
tables: string[];
autoIncludeIndexTables?: boolean;
bootstrapParams?: {
tables: string[];
backupRequestParams: any;
};
}

export function editXClusterConfigTables(
xClusterUUID: string,
{ tables, autoIncludeIndexTables, bootstrapParams }: EditXClusterConfigTablesRequest
Expand Down
137 changes: 99 additions & 38 deletions managed/ui/src/components/xcluster/createConfig/CreateConfigModal.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { useState } from 'react';
import { AxiosError } from 'axios';
import { Box, Typography, useTheme } from '@material-ui/core';
import { Box, makeStyles, Typography, useTheme } from '@material-ui/core';
import { toast } from 'react-toastify';
import { useMutation, useQuery, useQueryClient } from 'react-query';
import { useTranslation } from 'react-i18next';
import { FormProvider, SubmitHandler, useForm } from 'react-hook-form';

import {
createXClusterReplication,
createXClusterConfig,
CreateXClusterConfigRequest,
fetchTablesInUniverse,
fetchTaskUntilItCompletes,
fetchUniverseDiskUsageMetric
Expand All @@ -19,7 +20,12 @@ import {
parseFloatIfDefined
} from '../ReplicationUtils';
import { assertUnreachableCase, handleServerError } from '../../../utils/errorHandlingUtils';
import { api, drConfigQueryKey, universeQueryKey } from '../../../redesign/helpers/api';
import {
api,
drConfigQueryKey,
runtimeConfigQueryKey,
universeQueryKey
} from '../../../redesign/helpers/api';
import { YBButton, YBModal, YBModalProps } from '../../../redesign/components';
import { StorageConfigOption } from '../sharedComponents/ReactSelectStorageConfig';
import { CurrentFormStep } from './CurrentFormStep';
Expand All @@ -29,6 +35,7 @@ import {
XClusterConfigType,
XCLUSTER_UNIVERSE_TABLE_FILTERS
} from '../constants';
import { RuntimeConfigKey } from '../../../redesign/helpers/constants';

import { XClusterTableType } from '../XClusterTypes';
import { TableType, TableTypeLabel, Universe, YBTable } from '../../../redesign/helpers/dtos';
Expand Down Expand Up @@ -72,6 +79,12 @@ export const FormStep = {
} as const;
export type FormStep = typeof FormStep[keyof typeof FormStep];

const useStyles = makeStyles(() => ({
secondarySubmitButton: {
marginLeft: 'auto'
}
}));

const MODAL_NAME = 'CreateConfigModal';
const FIRST_FORM_STEP = FormStep.SELECT_TARGET_UNIVERSE;
const TRANSLATION_KEY_PREFIX = 'clusterDetail.xCluster.createConfigModal';
Expand All @@ -89,7 +102,7 @@ export const CreateConfigModal = ({ modalProps, sourceUniverseUuid }: CreateConf
} | null>(null);
const [bootstrapRequiredTableUUIDs, setBootstrapRequiredTableUUIDs] = useState<string[]>([]);
const [isTableSelectionValidated, setIsTableSelectionValidated] = useState<boolean>(false);

const [skipBootstrapping, setSkipBootStrapping] = useState<boolean>(false);
// The purpose of committedTargetUniverse is to store the targetUniverse field value prior
// to the user submitting their target universe step.
// This value updates whenever the user submits SelectTargetUniverseStep with a new
Expand All @@ -100,24 +113,32 @@ export const CreateConfigModal = ({ modalProps, sourceUniverseUuid }: CreateConf
const { t } = useTranslation('translation', { keyPrefix: TRANSLATION_KEY_PREFIX });
const queryClient = useQueryClient();
const theme = useTheme();
const classes = useStyles();

const xClusterConfigMutation = useMutation(
(formValues: CreateXClusterConfigFormValues) => {
return createXClusterReplication(
formValues.targetUniverse.value.universeUUID,
sourceUniverseUuid,
formValues.configName,
formValues.isTransactionalConfig ? XClusterConfigType.TXN : XClusterConfigType.BASIC,
formValues.tableUuids.map(formatUuidForXCluster),
bootstrapRequiredTableUUIDs.length > 0
? {
const createXClusterConfigRequest: CreateXClusterConfigRequest = {
name: formValues.configName,
sourceUniverseUUID: sourceUniverseUuid,
targetUniverseUUID: formValues.targetUniverse.value.universeUUID,
configType: formValues.isTransactionalConfig
? XClusterConfigType.TXN
: XClusterConfigType.BASIC,
tables: formValues.tableUuids.map(formatUuidForXCluster),

...(!skipBootstrapping &&
bootstrapRequiredTableUUIDs.length > 0 && {
bootstrapParams: {
tables: bootstrapRequiredTableUUIDs,
allowBootstrapping: true,

backupRequestParams: {
storageConfigUUID: formValues.storageConfig.value.uuid
}
}
: null
);
})
};
return createXClusterConfig(createXClusterConfigRequest);
},
{
onSuccess: async (response, values) => {
Expand Down Expand Up @@ -182,6 +203,11 @@ export const CreateConfigModal = ({ modalProps, sourceUniverseUuid }: CreateConf
api.fetchUniverse(sourceUniverseUuid)
);

const customerUuid = localStorage.getItem('customerId') ?? '';
const runtimeConfigQuery = useQuery(runtimeConfigQueryKey.customerScope(customerUuid), () =>
api.fetchRuntimeConfigs(sourceUniverseUuid, true)
);

const formMethods = useForm<CreateXClusterConfigFormValues>({
defaultValues: {
namespaceUuids: [],
Expand All @@ -195,19 +221,18 @@ export const CreateConfigModal = ({ modalProps, sourceUniverseUuid }: CreateConf
});

const modalTitle = t('title');
const cancelLabel = t('cancel', { keyPrefix: 'common' });
if (
sourceUniverseTablesQuery.isLoading ||
sourceUniverseTablesQuery.isIdle ||
sourceUniverseQuery.isLoading ||
sourceUniverseQuery.isIdle
sourceUniverseQuery.isIdle ||
runtimeConfigQuery.isLoading ||
runtimeConfigQuery.isIdle
) {
return (
<YBModal
title={modalTitle}
cancelLabel={cancelLabel}
submitTestId={`${MODAL_NAME}-SubmitButton`}
cancelTestId={`${MODAL_NAME}-CancelButton`}
maxWidth="xl"
size="md"
overrideWidth="960px"
Expand All @@ -218,24 +243,27 @@ export const CreateConfigModal = ({ modalProps, sourceUniverseUuid }: CreateConf
);
}

if (sourceUniverseTablesQuery.isError || sourceUniverseQuery.isError) {
if (
sourceUniverseTablesQuery.isError ||
sourceUniverseQuery.isError ||
runtimeConfigQuery.isError
) {
const errorMessage = runtimeConfigQuery.isError
? t('failedToFetchCustomerRuntimeConfig', { keyPrefix: 'queryError' })
: t('failedToFetchSourceUniverse', {
keyPrefix: 'clusterDetail.xCluster.error',
universeUuid: sourceUniverseUuid
});
return (
<YBModal
title={modalTitle}
cancelLabel={cancelLabel}
submitTestId={`${MODAL_NAME}-SubmitButton`}
cancelTestId={`${MODAL_NAME}-CancelButton`}
maxWidth="xl"
size="md"
overrideWidth="960px"
{...modalProps}
>
<YBErrorIndicator
customErrorMessage={t('failedToFetchSourceUniverse', {
keyPrefix: 'clusterDetail.xCluster.error',
universeUuid: sourceUniverseUuid
})}
/>
<YBErrorIndicator customErrorMessage={errorMessage} />
</YBModal>
);
}
Expand Down Expand Up @@ -278,7 +306,11 @@ export const CreateConfigModal = ({ modalProps, sourceUniverseUuid }: CreateConf
};

const sourceUniverseTables = sourceUniverseTablesQuery.data;
const onSubmit: SubmitHandler<CreateXClusterConfigFormValues> = async (formValues) => {

const onSubmit = async (
formValues: CreateXClusterConfigFormValues,
skipBootstrapping: boolean
) => {
// When the user changes target universe or table type, the old table selection is no longer valid.
const isTableSelectionInvalidated =
formValues.targetUniverse.value.universeUUID !== committedTargetUniverseUuid ||
Expand Down Expand Up @@ -319,6 +351,12 @@ export const CreateConfigModal = ({ modalProps, sourceUniverseUuid }: CreateConf
return;
}

setSkipBootStrapping(skipBootstrapping);
if (skipBootstrapping) {
setCurrentFormStep(FormStep.CONFIRM_ALERT);
return;
}

if (!isTableSelectionValidated) {
let bootstrapTableUuids: string[] | null = null;
const hasSelectionError = false;
Expand Down Expand Up @@ -411,7 +449,11 @@ export const CreateConfigModal = ({ modalProps, sourceUniverseUuid }: CreateConf
return assertUnreachableCase(currentFormStep);
}
};

const onFormSubmit: SubmitHandler<CreateXClusterConfigFormValues> = async (formValues) =>
onSubmit(formValues, false);
const onSkipBootstrapAndSubmit: SubmitHandler<CreateXClusterConfigFormValues> = async (
formValues
) => onSubmit(formValues, true);
const handleBackNavigation = () => {
// We can clear errors here because prior steps have already been validated
// and future steps will be revalidated when the user clicks the next page button.
Expand All @@ -427,7 +469,7 @@ export const CreateConfigModal = ({ modalProps, sourceUniverseUuid }: CreateConf
setCurrentFormStep(FormStep.SELECT_TABLES);
return;
case FormStep.CONFIRM_ALERT:
if (bootstrapRequiredTableUUIDs.length > 0) {
if (bootstrapRequiredTableUUIDs.length > 0 && !skipBootstrapping) {
setCurrentFormStep(FormStep.CONFIGURE_BOOTSTRAP);
} else {
setCurrentFormStep(FormStep.SELECT_TABLES);
Expand Down Expand Up @@ -481,16 +523,20 @@ export const CreateConfigModal = ({ modalProps, sourceUniverseUuid }: CreateConf
const isTransactionalConfig = formMethods.watch('isTransactionalConfig');

const isFormDisabled = formMethods.formState.isSubmitting;
const runtimeConfigEntries = runtimeConfigQuery.data.configEntries ?? [];
const isSkipBootstrappingEnabled = runtimeConfigEntries.some(
(config: any) =>
config.key === RuntimeConfigKey.ENABLE_XCLUSTER_SKIP_BOOTSTRAPPING && config.value === 'true'
);
return (
<YBModal
title={modalTitle}
submitLabel={submitLabel}
cancelLabel={cancelLabel}
buttonProps={{ primary: { disabled: isFormDisabled } }}
onSubmit={formMethods.handleSubmit(onSubmit)}
onSubmit={formMethods.handleSubmit(onFormSubmit)}
submitTestId={`${MODAL_NAME}-SubmitButton`}
cancelTestId={`${MODAL_NAME}-CancelButton`}
isSubmitting={formMethods.formState.isSubmitting}
showSubmitSpinner={currentFormStep !== FormStep.SELECT_TABLES || !skipBootstrapping}
maxWidth="xl"
size={
([FormStep.SELECT_TARGET_UNIVERSE, FormStep.SELECT_TABLES] as FormStep[]).includes(
Expand All @@ -501,11 +547,26 @@ export const CreateConfigModal = ({ modalProps, sourceUniverseUuid }: CreateConf
}
overrideWidth="960px"
footerAccessory={
currentFormStep !== FIRST_FORM_STEP && (
<YBButton variant="secondary" onClick={handleBackNavigation}>
{t('back', { keyPrefix: 'common' })}
</YBButton>
)
<>
{currentFormStep !== FIRST_FORM_STEP && (
<YBButton variant="secondary" onClick={handleBackNavigation}>
{t('back', { keyPrefix: 'common' })}
</YBButton>
)}
{currentFormStep === FormStep.SELECT_TABLES &&
isBootstrapStepRequired &&
isSkipBootstrappingEnabled && (
<YBButton
className={classes.secondarySubmitButton}
variant="secondary"
onClick={formMethods.handleSubmit(onSkipBootstrapAndSubmit)}
showSpinner={formMethods.formState.isSubmitting && skipBootstrapping}
disabled={isFormDisabled}
>
{t('step.selectTables.submitButton.skipBootstrapping')}
</YBButton>
)}
</>
}
{...modalProps}
>
Expand Down
Loading

0 comments on commit 468f5bb

Please sign in to comment.