+
{extension}
diff --git a/src/core/packages/chrome/browser-components/tsconfig.json b/src/core/packages/chrome/browser-components/tsconfig.json
index 28faf7a8db1bc..208e980641e75 100644
--- a/src/core/packages/chrome/browser-components/tsconfig.json
+++ b/src/core/packages/chrome/browser-components/tsconfig.json
@@ -47,5 +47,7 @@
"@kbn/test-jest-helpers",
"@kbn/use-observable",
"@kbn/shared-ux-utility",
+ "@kbn/ui-chrome-layout",
+ "@kbn/app-header",
]
}
diff --git a/src/core/packages/chrome/browser-hooks/index.ts b/src/core/packages/chrome/browser-hooks/index.ts
index d5c0f8f242d87..c2e977998b815 100644
--- a/src/core/packages/chrome/browser-hooks/index.ts
+++ b/src/core/packages/chrome/browser-hooks/index.ts
@@ -10,6 +10,7 @@
export { useChromeStyle } from './use_chrome_style';
export { useActiveSolutionNavId } from './use_active_solution_nav_id';
export { useIsChromeVisible } from './use_is_chrome_visible';
+export { useIsNextChrome } from './use_is_next_chrome';
export { useSidebarWidth } from './use_sidebar_width';
export { useSideNavCollapsed } from './use_side_nav_collapsed';
export { useSideNavWidth } from './use_side_nav_width';
diff --git a/src/core/packages/chrome/browser-hooks/use_is_next_chrome.ts b/src/core/packages/chrome/browser-hooks/use_is_next_chrome.ts
new file mode 100644
index 0000000000000..27a6e0823f679
--- /dev/null
+++ b/src/core/packages/chrome/browser-hooks/use_is_next_chrome.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import { useChromeService } from '@kbn/core-chrome-browser-context';
+
+/**
+ * Returns `true` when Chrome Next is enabled.
+ * Note: this only checks the feature flag, it does not check whether current chrome is classic vs project.
+ * Combine with `useChromeStyle` to check if the current chrome is classic vs project.
+ */
+export function useIsNextChrome(): boolean {
+ const chrome = useChromeService();
+ return chrome.next.isEnabled;
+}
diff --git a/src/core/packages/chrome/browser-internal-types/index.ts b/src/core/packages/chrome/browser-internal-types/index.ts
index 6b339db60594f..1dffd50b1be38 100644
--- a/src/core/packages/chrome/browser-internal-types/index.ts
+++ b/src/core/packages/chrome/browser-internal-types/index.ts
@@ -9,15 +9,21 @@
import type { ReactNode } from 'react';
import type { Observable } from 'rxjs';
+import type { IBasePath } from '@kbn/core-http-browser';
+import type { MountPoint } from '@kbn/core-mount-utils-browser';
import type {
ChromeSetup,
ChromeStart,
+ AppHeaderConfig,
ChromeBadge,
ChromeBreadcrumb,
ChromeBreadcrumbsAppendExtension,
+ ChromeBreadcrumbsBadge,
+ ChromeNext,
ChromeProjectNavigationNode,
ChromeSetProjectBreadcrumbsParams,
ChromeUserBanner,
+ GlobalSearchConfig,
AppDeepLinkId,
NavigationTreeDefinition,
NavigationTreeDefinitionUI,
@@ -30,6 +36,15 @@ export type InternalChromeSetup = ChromeSetup;
/** @internal */
export interface InternalChromeStart extends ChromeStart {
+ /**
+ * Dependencies used by Chrome-owned React components that live outside
+ * `browser-internal`, but still render under `ChromeServiceProvider`.
+ */
+ componentDeps: {
+ readonly basePath: IBasePath;
+ readonly legacyActionMenu$: Observable
;
+ };
+
sideNav: ChromeStart['sideNav'] & {
/**
* Set the width of the side nav.
@@ -57,6 +72,11 @@ export interface InternalChromeStart extends ChromeStart {
*/
getBreadcrumbsAppendExtensionsWithBadges$(): Observable;
+ /**
+ * Get an observable of the current breadcrumbs badges set via setBreadcrumbsBadges().
+ */
+ getBreadcrumbsBadges$(): Observable;
+
/** Set global footer. Used by the developer toolbar. */
setGlobalFooter(node: ReactNode): void;
@@ -104,4 +124,24 @@ export interface InternalChromeStart extends ChromeStart {
params?: Partial
): void;
};
+
+ /** @internal Extends public `next` with `get$` for Chrome layout components. */
+ next: InternalChromeNext;
+}
+
+/** @internal */
+export interface InternalChromeNext extends ChromeNext {
+ contextSwitcher: ChromeNext['contextSwitcher'] & {
+ get$(): Observable;
+ };
+ globalSearch: ChromeNext['globalSearch'] & {
+ get$(): Observable;
+ };
+ inlineAppHeader: {
+ get$(): Observable;
+ set(mounted: boolean): void;
+ };
+ appHeader: ChromeNext['appHeader'] & {
+ get$(): Observable;
+ };
}
diff --git a/src/core/packages/chrome/browser-internal-types/moon.yml b/src/core/packages/chrome/browser-internal-types/moon.yml
index f899dc2b7a901..c603b90b99db0 100644
--- a/src/core/packages/chrome/browser-internal-types/moon.yml
+++ b/src/core/packages/chrome/browser-internal-types/moon.yml
@@ -18,6 +18,8 @@ project:
sourceRoot: src/core/packages/chrome/browser-internal-types
dependsOn:
- '@kbn/core-chrome-browser'
+ - '@kbn/core-http-browser'
+ - '@kbn/core-mount-utils-browser'
tags:
- shared-common
- package
diff --git a/src/core/packages/chrome/browser-internal-types/tsconfig.json b/src/core/packages/chrome/browser-internal-types/tsconfig.json
index e9ad692ae97d7..dbecf8b5d88e2 100644
--- a/src/core/packages/chrome/browser-internal-types/tsconfig.json
+++ b/src/core/packages/chrome/browser-internal-types/tsconfig.json
@@ -12,5 +12,7 @@
],
"kbn_references": [
"@kbn/core-chrome-browser",
+ "@kbn/core-http-browser",
+ "@kbn/core-mount-utils-browser",
]
}
diff --git a/src/core/packages/chrome/browser-internal/src/chrome_api.tsx b/src/core/packages/chrome/browser-internal/src/chrome_api.tsx
index f3bfe8580bfac..da47de08e00d1 100644
--- a/src/core/packages/chrome/browser-internal/src/chrome_api.tsx
+++ b/src/core/packages/chrome/browser-internal/src/chrome_api.tsx
@@ -10,6 +10,7 @@
import React, { type ReactNode } from 'react';
import { distinctUntilChanged, map, shareReplay } from 'rxjs';
import type { RecentlyAccessedService } from '@kbn/recently-accessed';
+import type { AppHeaderConfig } from '@kbn/core-chrome-browser';
import { SidebarServiceProvider } from '@kbn/core-chrome-sidebar-context';
import { ChromeServiceProvider } from '@kbn/core-chrome-browser-context';
import type { SidebarStart } from '@kbn/core-chrome-sidebar';
@@ -39,6 +40,7 @@ export interface ChromeApiDeps {
};
sidebar: SidebarStart;
featureFlags: FeatureFlagsStart;
+ componentDeps: InternalChromeStart['componentDeps'];
}
export function createChromeApi({
@@ -46,6 +48,7 @@ export function createChromeApi({
services,
sidebar,
featureFlags,
+ componentDeps,
}: ChromeApiDeps): InternalChromeStart {
const { projectNavigation } = services;
@@ -78,7 +81,11 @@ export function createChromeApi({
getProjectHome$: () => projectNavigation.getProjectHome$(),
};
+ let appHeaderRegistrationId = 0;
+
const chromeStart: InternalChromeStart = {
+ componentDeps,
+
withProvider: (children: ReactNode) => {
return (
@@ -117,6 +124,7 @@ export function createChromeApi({
},
getBreadcrumbsAppendExtensions$: () => state.breadcrumbs.appendExtensions.$,
getBreadcrumbsAppendExtensionsWithBadges$: () => state.breadcrumbs.appendExtensionsWithBadges$,
+ getBreadcrumbsBadges$: () => state.breadcrumbs.badges.$,
setBreadcrumbsAppendExtension: (extension) => {
state.breadcrumbs.appendExtensions.addSorted(
extension,
@@ -182,6 +190,26 @@ export function createChromeApi({
get$: () => state.globalSearch.$,
set: (config) => state.globalSearch.set(config),
},
+ contextSwitcher: {
+ get$: () => state.contextSwitcher.$,
+ set: state.contextSwitcher.set,
+ },
+ inlineAppHeader: {
+ get$: () => state.inlineAppHeader.$,
+ set: state.inlineAppHeader.set,
+ },
+ appHeader: {
+ get$: () => state.appHeader.$,
+ set: (config: AppHeaderConfig) => {
+ const registrationId = ++appHeaderRegistrationId;
+ state.appHeader.set(config);
+ return () => {
+ if (registrationId === appHeaderRegistrationId) {
+ state.appHeader.set(undefined);
+ }
+ };
+ },
+ },
},
sidebar,
};
diff --git a/src/core/packages/chrome/browser-internal/src/chrome_service.tsx b/src/core/packages/chrome/browser-internal/src/chrome_service.tsx
index 6b374dee68491..ca6db493d8428 100644
--- a/src/core/packages/chrome/browser-internal/src/chrome_service.tsx
+++ b/src/core/packages/chrome/browser-internal/src/chrome_service.tsx
@@ -187,6 +187,10 @@ export class ChromeService {
},
sidebar,
featureFlags,
+ componentDeps: {
+ basePath: http.basePath,
+ legacyActionMenu$: application.currentActionMenu$,
+ },
});
return chrome;
diff --git a/src/core/packages/chrome/browser-internal/src/side_effects/app_change_handler.ts b/src/core/packages/chrome/browser-internal/src/side_effects/app_change_handler.ts
index 8c9f499d04931..325172d7d8990 100644
--- a/src/core/packages/chrome/browser-internal/src/side_effects/app_change_handler.ts
+++ b/src/core/packages/chrome/browser-internal/src/side_effects/app_change_handler.ts
@@ -33,6 +33,8 @@ export function setupAppChangeHandler({
// Reset UI elements
state.breadcrumbs.legacyBadge.set(undefined);
state.appMenu.set(undefined);
+ state.appHeader.set(undefined);
+ state.inlineAppHeader.set(false);
// Reset breadcrumbs
state.breadcrumbs.classic.set([]);
diff --git a/src/core/packages/chrome/browser-internal/src/side_effects/handle_eui_dev_provider_warning.tsx b/src/core/packages/chrome/browser-internal/src/side_effects/handle_eui_dev_provider_warning.tsx
deleted file mode 100644
index 676d0837938c0..0000000000000
--- a/src/core/packages/chrome/browser-internal/src/side_effects/handle_eui_dev_provider_warning.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
- * Public License v 1"; you may not use this file except in compliance with, at
- * your election, the "Elastic License 2.0", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-import React from 'react';
-import { FormattedMessage } from '@kbn/i18n-react';
-import { setEuiDevProviderWarning } from '@elastic/eui';
-import { mountReactNode } from '@kbn/core-mount-utils-browser-internal';
-import type { NotificationsStart } from '@kbn/core-notifications-browser';
-
-/**
- * Ensure developers are notified if working in a context that lacks the EUI Provider.
- * @internal
- */
-export function handleEuiDevProviderWarning({
- notifications,
-}: {
- notifications: NotificationsStart;
-}) {
- setEuiDevProviderWarning((providerError) => {
- const errorObject = new Error(providerError.toString());
- // 1. show a stack trace in the console
- // eslint-disable-next-line no-console
- console.error(errorObject);
-
- // 2. store error in sessionStorage so it can be detected in testing
- const storedError = {
- message: providerError.toString(),
- stack: errorObject.stack ?? 'undefined',
- pageHref: window.location.href,
- pageTitle: document.title,
- };
- sessionStorage.setItem('dev.euiProviderWarning', JSON.stringify(storedError));
-
- // 3. error toast / popup
- notifications.toasts.addDanger({
- title: '`EuiProvider` is missing',
- text: mountReactNode(
-
-
- https://docs.elastic.dev/kibana-dev-docs/react-context
-
- ),
- }}
- />
-
- ),
- 'data-test-subj': 'core-chrome-euiDevProviderWarning-toast',
- toastLifeTimeMs: 60 * 60 * 1000, // keep message visible for up to an hour
- });
- });
-}
diff --git a/src/core/packages/chrome/browser-internal/src/side_effects/index.ts b/src/core/packages/chrome/browser-internal/src/side_effects/index.ts
index ba4d5d14fcf3b..69e8b5cd3aa5b 100644
--- a/src/core/packages/chrome/browser-internal/src/side_effects/index.ts
+++ b/src/core/packages/chrome/browser-internal/src/side_effects/index.ts
@@ -13,7 +13,6 @@ export type { CspWarningDeps } from './csp_warning';
export { setupAppChangeHandler } from './app_change_handler';
export type { AppChangeHandlerDeps } from './app_change_handler';
-export { handleEuiDevProviderWarning } from './handle_eui_dev_provider_warning';
export { handleEuiFullScreenChanges } from './handle_eui_fullscreen_changes';
export { handleSystemColorModeChange } from './handle_system_colormode_change';
diff --git a/src/core/packages/chrome/browser-internal/src/state/chrome_state.ts b/src/core/packages/chrome/browser-internal/src/state/chrome_state.ts
index 515cc5613888f..5deabdf16871e 100644
--- a/src/core/packages/chrome/browser-internal/src/state/chrome_state.ts
+++ b/src/core/packages/chrome/browser-internal/src/state/chrome_state.ts
@@ -21,6 +21,7 @@ import type {
GlobalSearchConfig,
ChromeNavLink,
ChromeUserBanner,
+ AppHeaderConfig,
} from '@kbn/core-chrome-browser';
import type { AppMenuConfig } from '@kbn/core-chrome-app-menu-components';
@@ -66,6 +67,9 @@ export interface ChromeState {
globalSearch: State;
customNavLink: State;
appMenu: State;
+ contextSwitcher: State;
+ inlineAppHeader: State;
+ appHeader: State;
/** Help system */
help: {
@@ -111,6 +115,9 @@ export function createChromeState({ application, docLinks }: ChromeStateDeps): C
const globalFooter = createState(null);
const globalSearch = createState(undefined);
const customNavLink = createState(undefined);
+ const contextSwitcher = createState(null);
+ const inlineAppHeader = createState(false);
+ const appHeader = createState(undefined);
// Help System
const helpExtension = createState(undefined);
@@ -136,10 +143,13 @@ export function createChromeState({ application, docLinks }: ChromeStateDeps): C
globalSearch,
customNavLink,
appMenu,
+ inlineAppHeader,
+ appHeader,
help: {
extension: helpExtension,
supportUrl: helpSupportUrl,
globalMenuLinks: globalHelpMenuLinks,
},
+ contextSwitcher,
};
}
diff --git a/src/core/packages/chrome/browser-mocks/moon.yml b/src/core/packages/chrome/browser-mocks/moon.yml
index efbeb88ea08b1..e3294c5747538 100644
--- a/src/core/packages/chrome/browser-mocks/moon.yml
+++ b/src/core/packages/chrome/browser-mocks/moon.yml
@@ -22,6 +22,7 @@ dependsOn:
- '@kbn/core-chrome-browser-internal-types'
- '@kbn/lazy-object'
- '@kbn/core-chrome-sidebar-mocks'
+ - '@kbn/core-mount-utils-browser'
tags:
- shared-browser
- package
diff --git a/src/core/packages/chrome/browser-mocks/src/chrome_service.mock.ts b/src/core/packages/chrome/browser-mocks/src/chrome_service.mock.ts
index 9a1342e7a205f..7e507c7f2fbb9 100644
--- a/src/core/packages/chrome/browser-mocks/src/chrome_service.mock.ts
+++ b/src/core/packages/chrome/browser-mocks/src/chrome_service.mock.ts
@@ -8,14 +8,17 @@
*/
import { BehaviorSubject, of } from 'rxjs';
+import type { Observable } from 'rxjs';
+import type { MountPoint } from '@kbn/core-mount-utils-browser';
import type { DeeplyMockedKeys } from '@kbn/utility-types-jest';
-import type { ChromeBadge, ChromeBreadcrumb } from '@kbn/core-chrome-browser';
+import type { AppHeaderConfig, ChromeBadge, ChromeBreadcrumb } from '@kbn/core-chrome-browser';
import type {
InternalChromeSetup,
InternalChromeStart,
} from '@kbn/core-chrome-browser-internal-types';
import { lazyObject } from '@kbn/lazy-object';
import { sidebarServiceMock } from '@kbn/core-chrome-sidebar-mocks';
+import type { ReactNode } from 'react';
const createSetupContractMock = (): DeeplyMockedKeys => {
return lazyObject({
@@ -24,8 +27,24 @@ const createSetupContractMock = (): DeeplyMockedKeys => {
};
const createStartContractMock = () => {
+ const nextAppHeaderState$ = new BehaviorSubject(undefined);
+ const inlineAppHeaderState$ = new BehaviorSubject(false);
+ let appHeaderRegistrationId = 0;
+
const startContract: DeeplyMockedKeys = lazyObject({
withProvider: jest.fn((children) => children),
+ componentDeps: lazyObject({
+ basePath: lazyObject({
+ get: jest.fn().mockReturnValue(''),
+ prepend: jest.fn((path: string) => path),
+ remove: jest.fn(),
+ serverBasePath: '/',
+ assetsHrefBase: '/',
+ }),
+ legacyActionMenu$: new BehaviorSubject(
+ undefined
+ ) as unknown as DeeplyMockedKeys>,
+ }),
sidebar: lazyObject(sidebarServiceMock.createStartContract()),
navLinks: lazyObject({
getNavLinks$: jest.fn().mockReturnValue(new BehaviorSubject([])),
@@ -54,7 +73,7 @@ const createStartContractMock = () => {
}),
setIsVisible: jest.fn(),
getIsVisible$: jest.fn().mockReturnValue(new BehaviorSubject(false)),
- getBadge$: jest.fn().mockReturnValue(new BehaviorSubject({} as ChromeBadge)),
+ getBadge$: jest.fn().mockReturnValue(new BehaviorSubject(undefined)),
setBadge: jest.fn(),
getBreadcrumbs$: jest.fn().mockReturnValue(new BehaviorSubject([{} as ChromeBreadcrumb])),
getBreadcrumbs: jest.fn().mockReturnValue([]),
@@ -69,6 +88,7 @@ const createStartContractMock = () => {
}),
getBreadcrumbsAppendExtensions$: jest.fn().mockReturnValue(new BehaviorSubject([])),
getBreadcrumbsAppendExtensionsWithBadges$: jest.fn().mockReturnValue(new BehaviorSubject([])),
+ getBreadcrumbsBadges$: jest.fn().mockReturnValue(new BehaviorSubject([])),
setBreadcrumbsAppendExtension: jest.fn(),
getGlobalHelpExtensionMenuLinks$: jest.fn().mockReturnValue(new BehaviorSubject([])),
registerGlobalHelpExtensionMenuLink: jest.fn(),
@@ -104,6 +124,28 @@ const createStartContractMock = () => {
set: jest.fn(),
get$: jest.fn().mockReturnValue(new BehaviorSubject(undefined)),
}),
+ contextSwitcher: lazyObject({
+ set: jest.fn((content?: ReactNode) => {
+ new BehaviorSubject(null).next(content ?? null);
+ }),
+ get$: jest.fn().mockReturnValue(new BehaviorSubject(null)),
+ }),
+ inlineAppHeader: lazyObject({
+ get$: jest.fn().mockReturnValue(inlineAppHeaderState$),
+ set: jest.fn((value: boolean) => inlineAppHeaderState$.next(value)),
+ }),
+ appHeader: lazyObject({
+ get$: jest.fn().mockReturnValue(nextAppHeaderState$),
+ set: jest.fn((config: AppHeaderConfig) => {
+ const registrationId = ++appHeaderRegistrationId;
+ nextAppHeaderState$.next(config);
+ return () => {
+ if (registrationId === appHeaderRegistrationId) {
+ nextAppHeaderState$.next(undefined);
+ }
+ };
+ }),
+ }),
}),
setGlobalFooter: jest.fn(),
getGlobalFooter$: jest.fn().mockReturnValue(new BehaviorSubject(null)),
diff --git a/src/core/packages/chrome/browser-mocks/tsconfig.json b/src/core/packages/chrome/browser-mocks/tsconfig.json
index 504f008687092..8016e7adc00f9 100644
--- a/src/core/packages/chrome/browser-mocks/tsconfig.json
+++ b/src/core/packages/chrome/browser-mocks/tsconfig.json
@@ -16,7 +16,8 @@
"@kbn/core-chrome-browser",
"@kbn/core-chrome-browser-internal-types",
"@kbn/lazy-object",
- "@kbn/core-chrome-sidebar-mocks"
+ "@kbn/core-chrome-sidebar-mocks",
+ "@kbn/core-mount-utils-browser"
],
"exclude": [
"target/**/*",
diff --git a/src/core/packages/chrome/browser/index.ts b/src/core/packages/chrome/browser/index.ts
index 4858f155b4a28..c2d21e38e51b4 100644
--- a/src/core/packages/chrome/browser/index.ts
+++ b/src/core/packages/chrome/browser/index.ts
@@ -10,6 +10,11 @@
export type {
AppDeepLinkId,
AppId,
+ AppHeaderBack,
+ AppHeaderBadge,
+ AppHeaderBadgeItem,
+ AppHeaderConfig,
+ AppHeaderTab,
ChromeBadge,
ChromeBreadcrumbsBadge,
ChromeBreadcrumb,
diff --git a/src/core/packages/chrome/browser/src/chrome_next/chrome_next.ts b/src/core/packages/chrome/browser/src/chrome_next/chrome_next.ts
index ec00cd29ed576..4b5d8763eaed9 100644
--- a/src/core/packages/chrome/browser/src/chrome_next/chrome_next.ts
+++ b/src/core/packages/chrome/browser/src/chrome_next/chrome_next.ts
@@ -7,9 +7,72 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import type { Observable } from 'rxjs';
+import type { ReactElement, ReactNode, MouseEventHandler } from 'react';
+import type { AppMenuConfig } from '@kbn/core-chrome-app-menu-components';
import type { GlobalSearchConfig } from './global_search';
+/** @public */
+export type AppHeaderBack = string | AppHeaderBackTarget;
+
+/** @public */
+export interface AppHeaderBackTarget {
+ href: string;
+ /** Click handler, called alongside href navigation when provided. */
+ onClick?: MouseEventHandler;
+ /** Destination name for accessibility (e.g. "Back to {label}"). */
+ label?: string;
+}
+
+/** @public */
+export interface AppHeaderBadge {
+ label: string;
+ /** EUI badge color. `filled` is intentionally excluded. */
+ color?: 'hollow' | 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'accent';
+ tooltip?: string;
+ onClick?: () => void;
+ onClickAriaLabel?: string;
+ 'data-test-subj'?: string;
+ /** @deprecated Used for compatibility with existing breadcrumb badge custom renderers. */
+ renderCustomBadge?: (props: { badgeText: string }) => ReactElement;
+ /** Popover menu items for badge context menus. When provided, the badge becomes a dropdown trigger. */
+ items?: AppHeaderBadgeItem[];
+ /** Width of the popover menu panel in pixels. */
+ popoverWidth?: number;
+}
+
+/** @public */
+export interface AppHeaderBadgeItem {
+ name: string;
+ icon?: string;
+ onClick?: () => void;
+ items?: AppHeaderBadgeItem[];
+ popoverWidth?: number;
+ 'data-test-subj'?: string;
+ disabled?: boolean;
+ toolTipContent?: string;
+}
+
+/** @public */
+export interface AppHeaderTab {
+ id: string;
+ label: string;
+ isSelected?: boolean;
+ onClick?: () => void;
+ href?: string;
+ badge?: number;
+ 'data-test-subj'?: string;
+}
+
+/** @public */
+export interface AppHeaderConfig {
+ title?: string;
+ back?: AppHeaderBack;
+ tabs?: AppHeaderTab[];
+ badges?: AppHeaderBadge[];
+ menu?: AppMenuConfig;
+ favorite?: ReactNode;
+}
+
/**
* Chrome Next rollout APIs.
*
@@ -30,7 +93,23 @@ export interface ChromeNext {
* Pass `undefined` to remove. Global — persists across app changes.
*/
set(config?: GlobalSearchConfig): void;
- /** Observable of the current global search config. */
- get$(): Observable;
+ };
+ /** Context switcher content. */
+ contextSwitcher: {
+ /**
+ * Set the context switcher content for the Chrome-Next header.
+ * Pass `undefined` to remove. Global — persists across app changes.
+ */
+ set(content?: ReactNode): void;
+ };
+ appHeader: {
+ /**
+ * Set the app header configuration for the Chrome Next project header.
+ * Chrome renders an application top bar with back navigation, title, tabs,
+ * badges, menu, share action, and favorite action based on this config.
+ * Pass the config to show; the returned callback removes it.
+ * Per-app, cleared on app change.
+ */
+ set(config: AppHeaderConfig): () => void;
};
}
diff --git a/src/core/packages/chrome/browser/src/chrome_next/index.ts b/src/core/packages/chrome/browser/src/chrome_next/index.ts
index 0ee6ea45d8752..93f99195b8d79 100644
--- a/src/core/packages/chrome/browser/src/chrome_next/index.ts
+++ b/src/core/packages/chrome/browser/src/chrome_next/index.ts
@@ -7,5 +7,12 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-export type { ChromeNext } from './chrome_next';
+export type {
+ AppHeaderBack,
+ AppHeaderBadge,
+ AppHeaderBadgeItem,
+ AppHeaderConfig,
+ AppHeaderTab,
+ ChromeNext,
+} from './chrome_next';
export type { GlobalSearchConfig } from './global_search';
diff --git a/src/core/packages/chrome/browser/src/index.ts b/src/core/packages/chrome/browser/src/index.ts
index 758ffe58179cd..b3a2e141393c2 100644
--- a/src/core/packages/chrome/browser/src/index.ts
+++ b/src/core/packages/chrome/browser/src/index.ts
@@ -12,7 +12,14 @@ export type {
ChromeBreadcrumb,
ChromeSetBreadcrumbsParams,
} from './breadcrumb';
-export type { ChromeNext } from './chrome_next';
+export type {
+ AppHeaderBack,
+ AppHeaderBadge,
+ AppHeaderBadgeItem,
+ AppHeaderConfig,
+ AppHeaderTab,
+ ChromeNext,
+} from './chrome_next';
export type { ChromeSetup, ChromeStart } from './contracts';
export type { ChromeDocTitle } from './doc_title';
export type {
diff --git a/src/core/packages/chrome/layout/core-chrome-layout-constants/README.md b/src/core/packages/chrome/layout/core-chrome-layout-constants/README.md
index 6edceec197bb8..94db45eb6df49 100644
--- a/src/core/packages/chrome/layout/core-chrome-layout-constants/README.md
+++ b/src/core/packages/chrome/layout/core-chrome-layout-constants/README.md
@@ -22,8 +22,9 @@ const styles = css`
## API Reference
-See [`css_variables.ts`](./src/css_variables.ts) for complete API documentation and [`css_variables.test.ts`](./src/css_variables.test.ts) for usage examples.
+See [`@kbn/ui-chrome-layout-constants`](../../../../platform/kbn-ui/chrome-layout-constants) for the implementation and usage examples.
## Related
+- [`@kbn/ui-chrome-layout-constants`](../../../../platform/kbn-ui/chrome-layout-constants) - implementation (this package re-exports it)
- [`@kbn/ui-chrome-layout`](../../../../platform/kbn-ui/chrome-layout) - React components using these variables
diff --git a/src/core/packages/chrome/layout/core-chrome-layout-constants/index.ts b/src/core/packages/chrome/layout/core-chrome-layout-constants/index.ts
index dc0062ef11eea..e5a933a4267ed 100644
--- a/src/core/packages/chrome/layout/core-chrome-layout-constants/index.ts
+++ b/src/core/packages/chrome/layout/core-chrome-layout-constants/index.ts
@@ -6,59 +6,4 @@
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-export { layoutVar, layoutVarName } from './src/css_variables';
-export type {
- LayoutVarName,
- CSSVarName,
- LayoutComponent,
- LayoutProperty,
- ApplicationComponent,
- ApplicationVarName,
-} from './src/css_variables';
-export { layoutLevels } from './src/levels';
-
-/**
- * The ID of the main scroll container in the application.
- * `document.getElementById(APP_MAIN_SCROLL_CONTAINER_ID)` can be used to find the main scroll container.
- */
-export const APP_MAIN_SCROLL_CONTAINER_ID = 'app-main-scroll';
-
-/**
- * The ID of the fixed viewport container in the application.
- * This div is rendered by the `AppFixedViewport` component on the top of the application area and can be used to render fixed elements that should not scroll with the main content.
- */
-export const APP_FIXED_VIEWPORT_ID = 'app-fixed-viewport';
-
-/**
- * Selector for an open EuiFlyout. All flyouts (core overlay service, system flyouts,
- * and inline plugin flyouts) render through EuiFlyout which applies this class and role.
- */
-export const FLYOUT_SELECTOR = '.euiFlyout[role="dialog"]';
-
-/**
- * The ID of the main content container in the application, regardless of the type of the layout used.
- * `document.querySelector(MAIN_CONTENT_SELECTORS.join(','))` can be used to find the main content container.
- *
- * TODO: Potentially allow this to be customizable per-plugin
- */
-export const MAIN_CONTENT_SELECTORS = [
- 'main', // Ideal target for all plugins using KibanaPageTemplate
- '[role="main"]', // Fallback for plugins using deprecated EuiPageContent
- '.kbnAppWrapper', // Last-ditch fallback for all plugins regardless of page template
-];
-
-/**
- * The gap (in pixels) between the secondary side navigation panel and the main app content.
- */
-export const SIDE_PANEL_CONTENT_GAP = 8;
-
-/**
- * The selector for elements that should be included in the focus trap of a flyout.
- * This will allow the flyout focus trap to include header and sidenav by default.
- */
-export const euiIncludeSelectorInFocusTrap = {
- prop: {
- 'data-eui-includes-in-flyout-focus-trap': true,
- },
- selector: `[data-eui-includes-in-flyout-focus-trap="true"]`,
-};
+export * from '@kbn/ui-chrome-layout-constants';
diff --git a/src/core/packages/chrome/layout/core-chrome-layout-constants/moon.yml b/src/core/packages/chrome/layout/core-chrome-layout-constants/moon.yml
index ae8e822307710..1599fff00d7b9 100644
--- a/src/core/packages/chrome/layout/core-chrome-layout-constants/moon.yml
+++ b/src/core/packages/chrome/layout/core-chrome-layout-constants/moon.yml
@@ -16,19 +16,16 @@ project:
channel: ''
owner: '@elastic/appex-sharedux'
sourceRoot: src/core/packages/chrome/layout/core-chrome-layout-constants
-dependsOn: []
+dependsOn:
+ - '@kbn/ui-chrome-layout-constants'
tags:
- shared-common
- package
- prod
- group-platform
- shared
- - jest-unit-tests
fileGroups:
src:
- '**/*.ts'
- - '**/*.tsx'
- '!target/**/*'
- jest-config:
- - jest.config.js
tasks: {}
diff --git a/src/core/packages/chrome/layout/core-chrome-layout-constants/tsconfig.json b/src/core/packages/chrome/layout/core-chrome-layout-constants/tsconfig.json
index 9b86dda534b6b..1f13d00e0b923 100644
--- a/src/core/packages/chrome/layout/core-chrome-layout-constants/tsconfig.json
+++ b/src/core/packages/chrome/layout/core-chrome-layout-constants/tsconfig.json
@@ -3,17 +3,16 @@
"compilerOptions": {
"outDir": "target/types",
"types": [
- "jest",
- "node",
- "react"
+ "node"
]
},
"include": [
- "**/*.ts",
- "**/*.tsx",
+ "**/*.ts"
],
"exclude": [
"target/**/*"
],
- "kbn_references": []
+ "kbn_references": [
+ "@kbn/ui-chrome-layout-constants"
+ ]
}
diff --git a/src/core/packages/chrome/layout/core-chrome-layout-utils/index.ts b/src/core/packages/chrome/layout/core-chrome-layout-utils/index.ts
index 616dd8bba4b61..4cc0ae597f0d8 100644
--- a/src/core/packages/chrome/layout/core-chrome-layout-utils/index.ts
+++ b/src/core/packages/chrome/layout/core-chrome-layout-utils/index.ts
@@ -7,22 +7,4 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-export {
- type ScrollContainer,
- getScrollContainer,
- scrollTo,
- scrollToTop,
- scrollToBottom,
- getViewportHeight,
- getViewportBoundaries,
- getScrollPosition,
- getScrollDimensions,
- scrollBy,
- isAtBottomOfPage,
-} from './src/scroll';
-
-export {
- type HighContrastSeparatorOptions,
- getHighContrastBorder,
- getHighContrastSeparator,
-} from './src/high_contrast';
+export * from '@kbn/ui-chrome-layout-utils';
diff --git a/src/core/packages/chrome/layout/core-chrome-layout-utils/moon.yml b/src/core/packages/chrome/layout/core-chrome-layout-utils/moon.yml
index 2312b33d4d0a0..7e052999b670b 100644
--- a/src/core/packages/chrome/layout/core-chrome-layout-utils/moon.yml
+++ b/src/core/packages/chrome/layout/core-chrome-layout-utils/moon.yml
@@ -17,19 +17,15 @@ project:
owner: '@elastic/appex-sharedux'
sourceRoot: src/core/packages/chrome/layout/core-chrome-layout-utils
dependsOn:
- - '@kbn/core-chrome-layout-constants'
+ - '@kbn/ui-chrome-layout-utils'
tags:
- shared-browser
- package
- prod
- group-platform
- shared
- - jest-unit-tests
fileGroups:
src:
- '**/*.ts'
- - '**/*.tsx'
- '!target/**/*'
- jest-config:
- - jest.config.js
tasks: {}
diff --git a/src/core/packages/chrome/layout/core-chrome-layout-utils/src/scroll.ts b/src/core/packages/chrome/layout/core-chrome-layout-utils/src/scroll.ts
deleted file mode 100644
index 21882d8dec3f6..0000000000000
--- a/src/core/packages/chrome/layout/core-chrome-layout-utils/src/scroll.ts
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
- * Public License v 1"; you may not use this file except in compliance with, at
- * your election, the "Elastic License 2.0", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-import { APP_MAIN_SCROLL_CONTAINER_ID } from '@kbn/core-chrome-layout-constants';
-
-export type ScrollContainer = HTMLElement;
-
-/**
- * Gets the main scroll container element for the application.
- * @returns The scroll container element (either the app scroll container or document.documentElement for window scroll)
- */
-export const getScrollContainer = (): ScrollContainer => {
- const appScroll = document.getElementById(APP_MAIN_SCROLL_CONTAINER_ID);
- if (appScroll instanceof HTMLElement) {
- return appScroll;
- }
- return document.documentElement;
-};
-
-/**
- * Gets the visible height of a scroll container's viewport.
- * @param container - The container to measure. Defaults to the main application scroll container
- * @returns The viewport height in pixels
- */
-export const getViewportHeight = (container: ScrollContainer = getScrollContainer()): number => {
- return container.clientHeight;
-};
-
-/**
- * Gets the vertical boundaries of a scroll container's viewport.
- * Useful for checking if elements are visible within the viewport.
- * @param container - The container to measure. Defaults to the main application scroll container
- * @returns An object with top and bottom pixel values relative to the document
- */
-export const getViewportBoundaries = (
- container: ScrollContainer = getScrollContainer()
-): { top: number; bottom: number } => {
- const rect = container.getBoundingClientRect();
- return {
- top: rect.top,
- bottom: rect.top + container.clientHeight,
- };
-};
-
-/**
- * Gets the current scroll position of a container.
- * @param container - The container to measure. Defaults to the main application scroll container
- * @returns The current vertical scroll position in pixels
- */
-export const getScrollPosition = (container: ScrollContainer = getScrollContainer()): number => {
- return container.scrollTop;
-};
-
-/**
- * Scrolls a container to a specific vertical position.
- * @param opts - Scroll options
- * @param opts.top - The vertical position to scroll to in pixels
- * @param opts.behavior - The scroll behavior ('auto' or 'smooth'). Default is 'auto'
- * @param container - The container to scroll. Defaults to the main application scroll container
- */
-export const scrollTo = (
- opts: {
- top: number;
- behavior?: ScrollBehavior;
- },
- container: ScrollContainer = getScrollContainer()
-) => {
- container.scrollTo({ top: opts.top, behavior: opts.behavior });
-};
-
-/**
- * Scrolls a container to the top.
- * @param opts - Scroll options
- * @param opts.behavior - The scroll behavior ('auto' or 'smooth'). Default is 'auto'
- * @param container - The container to scroll. Defaults to the main application scroll container
- */
-export const scrollToTop = (
- opts: {
- behavior?: ScrollBehavior;
- } = {},
- container: ScrollContainer = getScrollContainer()
-) => {
- scrollTo({ top: 0, behavior: opts.behavior }, container);
-};
-
-/**
- * Scrolls a container to the bottom.
- * @param opts - Scroll options
- * @param opts.behavior - The scroll behavior ('auto' or 'smooth'). Default is 'auto'
- * @param container - The container to scroll. Defaults to the main application scroll container
- */
-export const scrollToBottom = (
- opts: {
- behavior?: ScrollBehavior;
- } = {},
- container: ScrollContainer = getScrollContainer()
-) => {
- scrollTo({ top: container.scrollHeight, behavior: opts.behavior }, container);
-};
-
-/**
- * Gets all scroll dimensions of a container at once for efficiency.
- * @param container - The container to measure. Defaults to the main application scroll container
- * @returns An object with scrollTop, scrollHeight, and clientHeight
- */
-export const getScrollDimensions = (
- container: ScrollContainer = getScrollContainer()
-): { scrollTop: number; scrollHeight: number; clientHeight: number } => {
- return {
- scrollTop: container.scrollTop,
- scrollHeight: container.scrollHeight,
- clientHeight: container.clientHeight,
- };
-};
-
-/**
- * Scrolls a container by a relative amount.
- * @param opts - Scroll options
- * @param opts.top - The number of pixels to scroll (positive = down, negative = up)
- * @param opts.behavior - The scroll behavior ('auto' or 'smooth'). Default is 'auto'
- * @param container - The container to scroll. Defaults to the main application scroll container
- */
-export const scrollBy = (
- opts: {
- top: number;
- behavior?: ScrollBehavior;
- },
- container: ScrollContainer = getScrollContainer()
-) => {
- container.scrollBy({
- top: opts.top,
- behavior: opts.behavior,
- });
-};
-
-/**
- * Detects if a scroll container has reached the bottom of its scrollable area.
- * @param container - The container to check. Defaults to the main application scroll container
- * @returns true if the container is scrolled to the bottom, false otherwise
- */
-export const isAtBottomOfPage = (container: ScrollContainer = getScrollContainer()): boolean => {
- const { scrollTop, scrollHeight, clientHeight } = getScrollDimensions(container);
- return scrollHeight - clientHeight - scrollTop <= 1; // Allow 1px tolerance
-};
diff --git a/src/core/packages/chrome/layout/core-chrome-layout-utils/tsconfig.json b/src/core/packages/chrome/layout/core-chrome-layout-utils/tsconfig.json
index 2a830c29e5cda..295c92316678d 100644
--- a/src/core/packages/chrome/layout/core-chrome-layout-utils/tsconfig.json
+++ b/src/core/packages/chrome/layout/core-chrome-layout-utils/tsconfig.json
@@ -3,19 +3,16 @@
"compilerOptions": {
"outDir": "target/types",
"types": [
- "jest",
- "node",
- "react"
+ "node"
]
},
"include": [
- "**/*.ts",
- "**/*.tsx",
+ "**/*.ts"
],
"exclude": [
"target/**/*"
],
"kbn_references": [
- "@kbn/core-chrome-layout-constants",
+ "@kbn/ui-chrome-layout-utils"
]
}
diff --git a/src/core/packages/chrome/layout/core-chrome-layout/layouts/grid/grid_layout.tsx b/src/core/packages/chrome/layout/core-chrome-layout/layouts/grid/grid_layout.tsx
index 8a1e0a2a8f23a..f500b992745af 100644
--- a/src/core/packages/chrome/layout/core-chrome-layout/layouts/grid/grid_layout.tsx
+++ b/src/core/packages/chrome/layout/core-chrome-layout/layouts/grid/grid_layout.tsx
@@ -15,6 +15,7 @@ import {
ChromeComponentsProvider,
ClassicHeader,
ChromeNextGlobalHeader,
+ ChromeAppHeaderRenderer,
ProjectHeader,
GridLayoutProjectSideNav,
HeaderTopBanner,
@@ -22,6 +23,8 @@ import {
AppMenuBar,
Sidebar,
useHasAppMenu,
+ useHasChromeAppHeaderContent,
+ useHasInlineAppHeader,
} from '@kbn/core-chrome-browser-components';
import type { ChromeComponentsDeps } from '@kbn/core-chrome-browser-components';
import {
@@ -105,6 +108,8 @@ export class GridLayout implements LayoutService {
const hasHeaderBanner = useHasHeaderBanner();
const chromeStyle = useChromeStyle();
const hasAppMenu = useHasAppMenu();
+ const hasInlineAppHeader = useHasInlineAppHeader();
+ const hasChromeAppHeaderContent = useHasChromeAppHeaderContent();
const footer = useGlobalFooter();
const sidebarWidth = useSidebarWidth();
const navigationWidth = useSideNavWidth();
@@ -129,7 +134,11 @@ export class GridLayout implements LayoutService {
header = ;
} else {
header = nextChrome ? : ;
- if (!nextChrome && hasAppMenu) {
+ if (nextChrome) {
+ if (!hasInlineAppHeader && hasChromeAppHeaderContent) {
+ applicationTopBar = ;
+ }
+ } else if (hasAppMenu) {
applicationTopBar = ;
}
diff --git a/src/core/packages/chrome/sidebar/sidebar-components/src/components/sidebar_panel_header.tsx b/src/core/packages/chrome/sidebar/sidebar-components/src/components/sidebar_panel_header.tsx
index 56c8cb167d8f3..bd1655bdb7a2c 100644
--- a/src/core/packages/chrome/sidebar/sidebar-components/src/components/sidebar_panel_header.tsx
+++ b/src/core/packages/chrome/sidebar/sidebar-components/src/components/sidebar_panel_header.tsx
@@ -17,6 +17,7 @@ import {
EuiFlexItem,
EuiScreenReaderOnly,
EuiTitle,
+ EuiToolTip,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { useSidebarPanel } from '@kbn/core-chrome-sidebar-context';
@@ -75,12 +76,14 @@ export const SidebarHeader: FC = ({ title, children, onClose
{actions && {actions}}
{onClose && (
-
+
+
+
)}
diff --git a/src/core/packages/http/server-internal/src/cookie_session_storage.ts b/src/core/packages/http/server-internal/src/cookie_session_storage.ts
index 4c17260486498..ea78d27f08b8c 100644
--- a/src/core/packages/http/server-internal/src/cookie_session_storage.ts
+++ b/src/core/packages/http/server-internal/src/cookie_session_storage.ts
@@ -150,6 +150,24 @@ export async function createCookieSessionStorageFactory(
isSameSite: cookieOptions.sameSite ?? false,
isPartitioned:
cookieOptions.sameSite === 'None' && cookieOptions.isSecure && !disableEmbedding,
+ // Override @hapi/iron's defaults so cookie sealing works under FIPS-mode OpenSSL.
+ // Iron's default is iterations=1, which OpenSSL's FIPS provider rejects with
+ // "Deriving bits failed". 1000 is the SP 800-132 minimum and is sufficient given
+ // encryptionKey is already required to be a 32+ char high-entropy value.
+ iron: {
+ encryption: {
+ saltBits: 256,
+ algorithm: 'aes-256-cbc',
+ iterations: 1000,
+ minPasswordlength: 32,
+ },
+ integrity: {
+ saltBits: 256,
+ algorithm: 'sha256',
+ iterations: 1000,
+ minPasswordlength: 32,
+ },
+ },
},
validate: async (req: Request, session: T | T[]) => {
const result = cookieOptions.validate(session);
diff --git a/src/core/packages/i18n/server-internal/src/routes/translations.test.ts b/src/core/packages/i18n/server-internal/src/routes/translations.test.ts
index 834ac0f93c47d..788572d519bfe 100644
--- a/src/core/packages/i18n/server-internal/src/routes/translations.test.ts
+++ b/src/core/packages/i18n/server-internal/src/routes/translations.test.ts
@@ -7,11 +7,13 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
+import { Readable } from 'stream';
import { mockRouter } from '@kbn/core-http-router-server-mocks';
import { registerTranslationsRoute } from './translations';
jest.mock('fs/promises', () => ({
- readFile: jest.fn(),
+ ...jest.requireActual('fs/promises'),
+ open: jest.fn(),
}));
jest.mock('@kbn/i18n', () => ({
@@ -23,10 +25,10 @@ jest.mock('@kbn/i18n', () => ({
},
}));
-import { readFile } from 'fs/promises';
+import { open } from 'fs/promises';
import { i18n } from '@kbn/i18n';
-const readFileMock = readFile as jest.MockedFunction;
+const openMock = open as jest.MockedFunction;
const getTranslationMock = i18n.getTranslation as jest.Mock;
const buildHandler = (opts: Omit[0], 'router'>) => {
@@ -46,10 +48,34 @@ const makeResponse = () => {
return { ok, notFound };
};
+const makeReadable = (content: string): Readable => {
+ const readable = new Readable({ read() {} });
+ readable.push(content);
+ readable.push(null);
+ return readable;
+};
+
+const collectStream = (stream: Readable): Promise =>
+ new Promise((resolve, reject) => {
+ const chunks: Buffer[] = [];
+ stream.on('data', (chunk: Buffer) => chunks.push(chunk));
+ stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
+ stream.on('error', reject);
+ });
+
+const mockOpenWithContent = (content: string) => {
+ const handle = {
+ createReadStream: () => makeReadable(content),
+ close: jest.fn().mockResolvedValue(undefined),
+ } as unknown as Awaited>;
+ openMock.mockResolvedValue(handle);
+ return handle;
+};
+
describe('registerTranslationsRoute', () => {
beforeEach(() => {
jest.clearAllMocks();
- readFileMock.mockResolvedValue('' as any);
+ mockOpenWithContent('');
getTranslationMock.mockReturnValue({ locale: 'en', messages: {} });
});
@@ -126,42 +152,56 @@ describe('registerTranslationsRoute', () => {
const handler = buildHandler(defaultOpts);
const res = makeResponse();
await handler({}, makeRequest('en'), res);
- expect(readFileMock).not.toHaveBeenCalled();
+ expect(openMock).not.toHaveBeenCalled();
const { body } = (res.ok as jest.Mock).mock.calls[0][0];
expect(JSON.parse(body)).toEqual({ locale: 'en', messages: { key: 'value' } });
});
- test('serves non-default locale by reading file and injecting locale field', async () => {
- readFileMock.mockResolvedValue('{"formats":{},"messages":{"key":"valeur"}}' as any);
+ test('serves non-default single-file locale by streaming the file', async () => {
+ const fileContent = '{"locale":"fr-FR","formats":{},"messages":{"key":"valeur"}}';
+ mockOpenWithContent(fileContent);
const handler = buildHandler(defaultOpts);
const res = makeResponse();
await handler({}, makeRequest('fr-FR'), res);
- expect(readFileMock).toHaveBeenCalledWith('/translations/fr-FR.json', 'utf8');
+ expect(openMock).toHaveBeenCalledWith('/translations/fr-FR.json', 'r');
const { body } = (res.ok as jest.Mock).mock.calls[0][0];
- expect(JSON.parse(body)).toEqual({
+ expect(body).toBeInstanceOf(Readable);
+ const content = await collectStream(body);
+ expect(JSON.parse(content)).toEqual({
locale: 'fr-FR',
formats: {},
messages: { key: 'valeur' },
});
});
- test('produces valid JSON when translation file is empty ({})', async () => {
- readFileMock.mockResolvedValue('{}' as any);
+ test('propagates open errors before committing the response', async () => {
+ openMock.mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
+ const handler = buildHandler(defaultOpts);
+ const res = makeResponse();
+ await expect(handler({}, makeRequest('fr-FR'), res)).rejects.toThrow('ENOENT');
+ expect(res.ok).not.toHaveBeenCalled();
+ });
+
+ test('streams translation file content without modifying it', async () => {
+ mockOpenWithContent('{}');
const handler = buildHandler(defaultOpts);
const res = makeResponse();
await handler({}, makeRequest('fr-FR'), res);
const { body } = (res.ok as jest.Mock).mock.calls[0][0];
- expect(() => JSON.parse(body)).not.toThrow();
- expect(JSON.parse(body)).toEqual({ locale: 'fr-FR' });
+ const content = await collectStream(body);
+ expect(JSON.parse(content)).toEqual({});
});
test('locale lookup is case-insensitive', async () => {
- readFileMock.mockResolvedValue('{"messages":{}}' as any);
+ const fileContent = '{"locale":"fr-FR","messages":{}}';
+ mockOpenWithContent(fileContent);
const handler = buildHandler(defaultOpts);
const res = makeResponse();
await handler({}, makeRequest('fr-fr'), res);
+ expect(openMock).toHaveBeenCalledWith('/translations/fr-FR.json', 'r');
const { body } = (res.ok as jest.Mock).mock.calls[0][0];
- expect(JSON.parse(body).locale).toBe('fr-FR');
+ const content = await collectStream(body);
+ expect(JSON.parse(content).locale).toBe('fr-FR');
});
});
});
diff --git a/src/core/packages/i18n/server-internal/src/routes/translations.ts b/src/core/packages/i18n/server-internal/src/routes/translations.ts
index ceb606d307cf5..d617e93d7bc1a 100644
--- a/src/core/packages/i18n/server-internal/src/routes/translations.ts
+++ b/src/core/packages/i18n/server-internal/src/routes/translations.ts
@@ -7,7 +7,8 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import { readFile } from 'fs/promises';
+import { open } from 'fs/promises';
+import type { ReadStream } from 'fs';
import { i18n, i18nLoader } from '@kbn/i18n';
import { schema } from '@kbn/config-schema';
import type { IRouter } from '@kbn/core-http-server';
@@ -78,22 +79,17 @@ export const registerTranslationsRoute = ({
});
}
- let body: string;
+ let body: string | ReadStream;
if (canonicalLocale.toLowerCase() === locale.toLowerCase()) {
// Default locale: already in memory from server startup
body = JSON.stringify(i18n.getTranslation());
} else {
const files = localeFileMap[canonicalLocale] ?? [];
if (files.length === 1) {
- // Single pre-merged file (standard case): inject locale field via string
- // splice and serve without parsing or caching the content.
- // Strip the outer braces and only add the comma when inner content exists,
- // so an empty file ({}) doesn't produce the invalid {"locale":"xx",}.
- const raw = await readFile(files[0], 'utf8');
- const inner = raw.trim().slice(1, -1);
- body = inner
- ? `{"locale":${JSON.stringify(canonicalLocale)},${inner}}`
- : `{"locale":${JSON.stringify(canonicalLocale)}}`;
+ // Open before res.ok() so I/O errors surface as 500, not a truncated 200.
+ // autoClose: true (Node default) closes the handle when the stream ends or is destroyed.
+ const fileHandle = await open(files[0], 'r');
+ body = fileHandle.createReadStream();
} else {
// Multiple files (external plugin contributed translations): merge via
// the loader and serve without caching.
diff --git a/src/core/packages/logging/server-internal/src/appenders/otel/otel_tls.test.ts b/src/core/packages/logging/server-internal/src/appenders/otel/otel_tls.test.ts
index 525e3ce52336e..1ec968f0af643 100644
--- a/src/core/packages/logging/server-internal/src/appenders/otel/otel_tls.test.ts
+++ b/src/core/packages/logging/server-internal/src/appenders/otel/otel_tls.test.ts
@@ -10,7 +10,7 @@
import { mkdirSync, rmSync, writeFileSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
-import type { PeerCertificate } from 'tls';
+import { rootCertificates, type PeerCertificate } from 'tls';
import {
buildGrpcVerifyOptions,
@@ -137,13 +137,17 @@ describe('buildGrpcVerifyOptions', () => {
});
describe('toGrpcRootCerts', () => {
- it('returns null when no CA is configured', () => {
- expect(toGrpcRootCerts({ verificationMode: 'full' })).toBeNull();
+ it("returns Node's built-in roots when no CA is configured", () => {
+ expect(toGrpcRootCerts({ verificationMode: 'full' }).toString()).toEqual(
+ rootCertificates.join('\n')
+ );
});
it('returns a single buffer for one CA', () => {
const b = Buffer.from('ca');
- expect(toGrpcRootCerts({ verificationMode: 'full', ca: b })).toEqual(b);
+ expect(toGrpcRootCerts({ verificationMode: 'full', ca: b }).toString()).toEqual(
+ `ca\n${rootCertificates.join('\n')}`
+ );
});
it('concatenates multiple CAs', () => {
@@ -151,6 +155,6 @@ describe('toGrpcRootCerts', () => {
verificationMode: 'full',
ca: [Buffer.from('a'), Buffer.from('b')],
});
- expect(merged?.toString()).toBe('a\nb');
+ expect(merged?.toString()).toEqual(`a\nb\n${rootCertificates.join('\n')}`);
});
});
diff --git a/src/core/packages/logging/server-internal/src/appenders/otel/otel_tls.ts b/src/core/packages/logging/server-internal/src/appenders/otel/otel_tls.ts
index 1be0fe9ec86cd..90f9fdf058910 100644
--- a/src/core/packages/logging/server-internal/src/appenders/otel/otel_tls.ts
+++ b/src/core/packages/logging/server-internal/src/appenders/otel/otel_tls.ts
@@ -9,7 +9,7 @@
import { readFileSync } from 'fs';
import type { AgentOptions as HttpsAgentOptions } from 'https';
-import type { PeerCertificate } from 'tls';
+import { rootCertificates, type PeerCertificate } from 'tls';
import type { OtelAppenderTlsConfig } from '@kbn/core-logging-server';
/** Compatible with `@grpc/grpc-js` `VerifyOptions` passed to `credentials.createSsl`. */
@@ -118,11 +118,16 @@ export const buildHttpsAgentTlsOptions = (resolved: ResolvedOtelTls): HttpsAgent
return opts;
};
-export const toGrpcRootCerts = (resolved: ResolvedOtelTls): Buffer | null => {
+export const toGrpcRootCerts = (resolved: ResolvedOtelTls): Buffer => {
+ const systemRoots = Buffer.from(rootCertificates.join('\n'));
+ // Use Node's built-in roots only if no custom CAs are configured
if (resolved.ca === undefined) {
- return null;
+ return systemRoots;
}
- return Array.isArray(resolved.ca) ? concatCaBuffers(resolved.ca) : resolved.ca;
+
+ const customCa = Array.isArray(resolved.ca) ? concatCaBuffers(resolved.ca) : resolved.ca;
+ // Append to Node's built-in roots so intermediate CAs can be verified
+ return Buffer.concat([customCa, Buffer.from('\n'), systemRoots]);
};
export const buildGrpcVerifyOptions = (resolved: ResolvedOtelTls): OtelGrpcVerifyOptions => {
diff --git a/src/core/packages/rendering/browser-internal/src/rendering_service.test.tsx b/src/core/packages/rendering/browser-internal/src/rendering_service.test.tsx
index d5c8ccb7ccf56..5c3c5b246dce5 100644
--- a/src/core/packages/rendering/browser-internal/src/rendering_service.test.tsx
+++ b/src/core/packages/rendering/browser-internal/src/rendering_service.test.tsx
@@ -50,6 +50,8 @@ jest.mock('@kbn/core-chrome-browser-components', () => ({
AppMenuBar: () => App menu!
,
Sidebar: () => Sidebar!
,
useHasAppMenu: () => false,
+ useHasInlineAppHeader: () => false,
+ useHasChromeAppHeaderContent: () => false,
}));
const mockChromeVisible$ = new BehaviorSubject(false);
diff --git a/src/core/packages/saved-objects/migration-server-internal/index.ts b/src/core/packages/saved-objects/migration-server-internal/index.ts
index e5ccf43fcf052..aa2dac403d50f 100644
--- a/src/core/packages/saved-objects/migration-server-internal/index.ts
+++ b/src/core/packages/saved-objects/migration-server-internal/index.ts
@@ -25,27 +25,21 @@ export {
calculateExcludeFilters,
checkForUnknownDocs,
waitForIndexStatus,
- cloneIndex,
waitForTask,
updateAndPickupMappings,
updateMappings,
updateAliases,
transformDocs,
- setWriteBlock,
- removeWriteBlock,
- reindex,
readWithPit,
refreshIndex,
pickupUpdatedMappings,
fetchIndices,
- waitForReindexTask,
waitForPickupUpdatedMappingsTask,
checkClusterRoutingAllocationEnabled,
} from './src/actions';
export type {
OpenPitResponse,
ReadWithPit,
- ReindexResponse,
UpdateByQueryResponse,
UpdateAndPickupMappingsResponse,
EsResponseTooLargeError,
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/README.md b/src/core/packages/saved-objects/migration-server-internal/src/README.md
index edf34a6849285..0c78b3ff49d82 100644
--- a/src/core/packages/saved-objects/migration-server-internal/src/README.md
+++ b/src/core/packages/saved-objects/migration-server-internal/src/README.md
@@ -1,134 +1,35 @@
- [Introduction](#introduction)
- [Algorithm steps](#algorithm-steps)
+ - [State diagram](#state-diagram)
- [INIT](#init)
- - [Next action](#next-action)
- - [New control state](#new-control-state)
- [CREATE\_INDEX\_CHECK\_CLUSTER\_ROUTING\_ALLOCATION](#create_index_check_cluster_routing_allocation)
- - [Next action](#next-action-1)
- - [New control state](#new-control-state-1)
- [CREATE\_NEW\_TARGET](#create_new_target)
- - [Next action](#next-action-2)
- - [New control state](#new-control-state-2)
- - [LEGACY\_CHECK\_CLUSTER\_ROUTING\_ALLOCATION](#legacy_check_cluster_routing_allocation)
- - [Next action](#next-action-3)
- - [New control state](#new-control-state-3)
- - [LEGACY\_SET\_WRITE\_BLOCK](#legacy_set_write_block)
- - [Next action](#next-action-4)
- - [New control state](#new-control-state-4)
- - [LEGACY\_CREATE\_REINDEX\_TARGET](#legacy_create_reindex_target)
- - [Next action](#next-action-5)
- - [New control state](#new-control-state-5)
- - [LEGACY\_REINDEX](#legacy_reindex)
- - [Next action](#next-action-6)
- - [New control state](#new-control-state-6)
- - [LEGACY\_REINDEX\_WAIT\_FOR\_TASK](#legacy_reindex_wait_for_task)
- - [Next action](#next-action-7)
- - [New control state](#new-control-state-7)
- - [LEGACY\_DELETE](#legacy_delete)
- - [Next action](#next-action-8)
- - [New control state](#new-control-state-8)
- [WAIT\_FOR\_MIGRATION\_COMPLETION](#wait_for_migration_completion)
- - [Next action](#next-action-9)
- - [New control state](#new-control-state-9)
- [WAIT\_FOR\_YELLOW\_SOURCE](#wait_for_yellow_source)
- - [Next action](#next-action-10)
- - [New control state](#new-control-state-10)
- [UPDATE\_SOURCE\_MAPPINGS\_PROPERTIES](#update_source_mappings_properties)
- - [Next action](#next-action-11)
- - [New control state](#new-control-state-11)
+ - [COMPATIBLE\_UPDATE\_CHECK\_CLUSTER\_ROUTING\_ALLOCATION](#compatible_update_check_cluster_routing_allocation)
- [CLEANUP\_UNKNOWN\_AND\_EXCLUDED](#cleanup_unknown_and_excluded)
- - [Next action](#next-action-12)
- - [New control state](#new-control-state-12)
- [CLEANUP\_UNKNOWN\_AND\_EXCLUDED\_WAIT\_FOR\_TASK](#cleanup_unknown_and_excluded_wait_for_task)
- - [Next action](#next-action-13)
- - [New control state](#new-control-state-13)
- [PREPARE\_COMPATIBLE\_MIGRATION](#prepare_compatible_migration)
- - [Next action](#next-action-14)
- - [New control state](#new-control-state-14)
- [REFRESH\_SOURCE](#refresh_source)
- - [Next action](#next-action-15)
- - [New control state](#new-control-state-15)
- - [REINDEX\_CHECK\_CLUSTER\_ROUTING\_ALLOCATION](#reindex_check_cluster_routing_allocation)
- - [Next action](#next-action-16)
- - [New control state](#new-control-state-16)
- - [CHECK\_UNKNOWN\_DOCUMENTS](#check_unknown_documents)
- - [Next action](#next-action-17)
- - [SET\_SOURCE\_WRITE\_BLOCK](#set_source_write_block)
- - [Next action](#next-action-18)
- - [New control state](#new-control-state-17)
- - [RELOCATE\_CHECK\_CLUSTER\_ROUTING\_ALLOCATION](#relocate_check_cluster_routing_allocation)
- - [Next action](#next-action-20)
- - [New control state](#new-control-state-19)
- - [REINDEX\_SOURCE\_TO\_TEMP\_OPEN\_PIT](#reindex_source_to_temp_open_pit)
- - [Next action](#next-action-21)
- - [New control state](#new-control-state-20)
- - [REINDEX\_SOURCE\_TO\_TEMP\_READ](#reindex_source_to_temp_read)
- - [Next action](#next-action-22)
- - [New control state](#new-control-state-21)
- - [REINDEX\_SOURCE\_TO\_TEMP\_TRANSFORM](#reindex_source_to_temp_transform)
- - [Next action](#next-action-23)
- - [New control state](#new-control-state-22)
- - [REINDEX\_SOURCE\_TO\_TEMP\_INDEX\_BULK](#reindex_source_to_temp_index_bulk)
- - [Next action](#next-action-24)
- - [New control state](#new-control-state-23)
- - [REINDEX\_SOURCE\_TO\_TEMP\_CLOSE\_PIT](#reindex_source_to_temp_close_pit)
- - [Next action](#next-action-25)
- - [New control state](#new-control-state-24)
- - [SET\_TEMP\_WRITE\_BLOCK](#set_temp_write_block)
- - [Next action](#next-action-26)
- - [New control state](#new-control-state-25)
- - [CLONE\_TEMP\_TO\_TARGET](#clone_temp_to_target)
- - [Next action](#next-action-27)
- - [New control state](#new-control-state-26)
- - [REFRESH\_TARGET](#refresh_target)
- - [Next action](#next-action-28)
- - [New control state](#new-control-state-27)
- [OUTDATED\_DOCUMENTS\_SEARCH\_OPEN\_PIT](#outdated_documents_search_open_pit)
- - [Next action](#next-action-29)
- - [New control state](#new-control-state-28)
- [OUTDATED\_DOCUMENTS\_SEARCH\_READ](#outdated_documents_search_read)
- - [Next action](#next-action-30)
- - [New control state](#new-control-state-29)
- [OUTDATED\_DOCUMENTS\_TRANSFORM](#outdated_documents_transform)
- - [Next action](#next-action-31)
- - [New control state](#new-control-state-30)
- [TRANSFORMED\_DOCUMENTS\_BULK\_INDEX](#transformed_documents_bulk_index)
- - [Next action](#next-action-32)
- - [New control state](#new-control-state-31)
- [OUTDATED\_DOCUMENTS\_SEARCH\_CLOSE\_PIT](#outdated_documents_search_close_pit)
- - [Next action](#next-action-33)
- - [New control state](#new-control-state-32)
- [OUTDATED\_DOCUMENTS\_REFRESH](#outdated_documents_refresh)
- - [Next action](#next-action-34)
- - [New control state](#new-control-state-33)
- [CHECK\_TARGET\_MAPPINGS](#check_target_mappings)
- - [Next action](#next-action-35)
- - [New control state](#new-control-state-34)
- [UPDATE\_TARGET\_MAPPINGS\_PROPERTIES](#update_target_mappings_properties)
- - [Next action](#next-action-36)
- - [New control state](#new-control-state-35)
- [UPDATE\_TARGET\_MAPPINGS\_PROPERTIES\_WAIT\_FOR\_TASK](#update_target_mappings_properties_wait_for_task)
- - [Next action](#next-action-37)
- - [New control state](#new-control-state-36)
+ - [UPDATE\_TARGET\_MAPPINGS\_META](#update_target_mappings_meta)
- [CHECK\_VERSION\_INDEX\_READY\_ACTIONS](#check_version_index_ready_actions)
- - [Next action](#next-action-38)
- - [New control state](#new-control-state-37)
- [MARK\_VERSION\_INDEX\_READY](#mark_version_index_ready)
- - [Next action](#next-action-39)
- - [New control state](#new-control-state-38)
- [MARK\_VERSION\_INDEX\_READY\_CONFLICT](#mark_version_index_ready_conflict)
- - [Next action](#next-action-40)
- - [New control state](#new-control-state-39)
- [FATAL](#fatal)
- [DONE](#done)
- [Manual QA Test Plan](#manual-qa-test-plan)
- - [1. Legacy pre-migration](#1-legacy-pre-migration)
- - [2. Plugins enabled/disabled](#2-plugins-enableddisabled)
- - [Test scenario 1 (enable a plugin after migration)](#test-scenario-1-enable-a-plugin-after-migration)
- - [Test scenario 2 (disable a plugin after migration)](#test-scenario-2-disable-a-plugin-after-migration)
- - [Test scenario 3 (multiple instances, enable a plugin after migration)](#test-scenario-3-multiple-instances-enable-a-plugin-after-migration)
- - [Test scenario 4 (multiple instances, mixed plugin enabled configs)](#test-scenario-4-multiple-instances-mixed-plugin-enabled-configs)
+ - [1. Plugins enabled/disabled](#1-plugins-enableddisabled)
# Introduction
@@ -191,276 +92,251 @@ always reference a single saved object index `.kibana`. When Kibana starts up,
all the steps are also repeated for the `.kibana_task_manager` index but this
is left out of the description for brevity.
-## INIT
+## State diagram
-### Next action
+The v2 algorithm supports three main paths after `INIT`:
-`fetchIndices`
+1. **Fresh deployment** — no `.kibana` alias exists; create a new version index.
+2. **Compatible version upgrade** — mappings changes are compatible; update documents in place on the existing index.
+3. **Up-to-date restart** — version migration already completed; transform outdated documents and update mappings for newly enabled plugins.
-Fetch the saved object indices, mappings and aliases to find the source index
-and determine whether we’re migrating from a legacy index or a v1 migrations
-index.
+```mermaid
+flowchart TD
+ INIT -->|alias exists| WAIT_FOR_YELLOW_SOURCE
+ INIT -->|no alias| CREATE_INDEX_CHECK
+ INIT -->|waitForMigrationCompletion| WAIT_FOR_MIGRATION_COMPLETION
-### New control state
+ CREATE_INDEX_CHECK["CREATE_INDEX_CHECK_CLUSTER_ROUTING_ALLOCATION"] -->|routing OK| CREATE_NEW_TARGET
+ CREATE_INDEX_CHECK -->|incompatible routing| CREATE_INDEX_CHECK
-1. If `.kibana` is pointing to more than one index.
+ CREATE_NEW_TARGET --> CHECK_VERSION_INDEX_READY_ACTIONS
+ CREATE_NEW_TARGET -->|index already exists| OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT
- → [FATAL](#fatal)
+ WAIT_FOR_MIGRATION_COMPLETION -->|migration complete| DONE
+ WAIT_FOR_MIGRATION_COMPLETION -->|still waiting| WAIT_FOR_MIGRATION_COMPLETION
-2. If `.kibana` is pointing to an index that belongs to a later version of
- Kibana .e.g. a 7.11.0 instance found the `.kibana` alias pointing to
- `.kibana_7.12.0_001` fail the migration
+ WAIT_FOR_YELLOW_SOURCE --> UPDATE_SOURCE_MAPPINGS_PROPERTIES
- → [FATAL](#fatal)
+ UPDATE_SOURCE_MAPPINGS_PROPERTIES -->|compatible upgrade| COMPATIBLE_UPDATE_CHECK
+ UPDATE_SOURCE_MAPPINGS_PROPERTIES -->|already up to date| OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT
+ UPDATE_SOURCE_MAPPINGS_PROPERTIES -->|incompatible| FATAL
-3. If `waitForMigrations` was set we're running on a background-tasks node and
-we should not participate in the migration but instead wait for the ui node(s)
-to complete the migration.
+ COMPATIBLE_UPDATE_CHECK["COMPATIBLE_UPDATE_CHECK_CLUSTER_ROUTING_ALLOCATION"] -->|routing OK| CLEANUP_UNKNOWN_AND_EXCLUDED
+ COMPATIBLE_UPDATE_CHECK -->|incompatible routing| COMPATIBLE_UPDATE_CHECK
- → [WAIT_FOR_MIGRATION_COMPLETION](#wait_for_migration_completion)
+ CLEANUP_UNKNOWN_AND_EXCLUDED --> CLEANUP_WAIT_FOR_TASK
+ CLEANUP_UNKNOWN_AND_EXCLUDED -->|no cleanup needed| PREPARE_COMPATIBLE_MIGRATION
+ CLEANUP_UNKNOWN_AND_EXCLUDED -->|unknown docs, not discarded| FATAL
-4. If the `.kibana` alias exists we’re migrating from either a v1 or v2 index
-and the migration source index is the index the `.kibana` alias points to.
+ CLEANUP_WAIT_FOR_TASK["CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK"] --> PREPARE_COMPATIBLE_MIGRATION
+ CLEANUP_WAIT_FOR_TASK -->|retriable errors| CLEANUP_UNKNOWN_AND_EXCLUDED
+ CLEANUP_WAIT_FOR_TASK -->|out of retries| FATAL
- → [WAIT_FOR_YELLOW_SOURCE](#wait_for_yellow_source)
+ PREPARE_COMPATIBLE_MIGRATION -->|deleted docs| REFRESH_SOURCE
+ PREPARE_COMPATIBLE_MIGRATION -->|no deletions| OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT
-5. If `.kibana` is a concrete index, we’re migrating from a legacy index
+ REFRESH_SOURCE --> OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT
- → [LEGACY_SET_WRITE_BLOCK](#legacy_set_write_block)
+ OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT --> OUTDATED_DOCUMENTS_SEARCH_READ
+ OUTDATED_DOCUMENTS_SEARCH_READ --> OUTDATED_DOCUMENTS_TRANSFORM
+ OUTDATED_DOCUMENTS_SEARCH_READ --> OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT
+ OUTDATED_DOCUMENTS_TRANSFORM --> TRANSFORMED_DOCUMENTS_BULK_INDEX
+ OUTDATED_DOCUMENTS_TRANSFORM --> OUTDATED_DOCUMENTS_SEARCH_READ
+ TRANSFORMED_DOCUMENTS_BULK_INDEX --> OUTDATED_DOCUMENTS_SEARCH_READ
+ TRANSFORMED_DOCUMENTS_BULK_INDEX --> TRANSFORMED_DOCUMENTS_BULK_INDEX
+ OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT -->|transformed docs| OUTDATED_DOCUMENTS_REFRESH
+ OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT -->|no transformed docs| CHECK_TARGET_MAPPINGS
+ OUTDATED_DOCUMENTS_REFRESH --> CHECK_TARGET_MAPPINGS
-6. If there are no `.kibana` indices, this is a fresh deployment. Check cluster routing allocation and
- initialize a new saved objects index
+ CHECK_TARGET_MAPPINGS --> UPDATE_TARGET_MAPPINGS_PROPERTIES
+ CHECK_TARGET_MAPPINGS --> UPDATE_TARGET_MAPPINGS_META
+ CHECK_TARGET_MAPPINGS --> CHECK_VERSION_INDEX_READY_ACTIONS
- → [CREATE_INDEX_CHECK_CLUSTER_ROUTING_ALLOCATION](#create_index_check_cluster_routing_allocation)
+ UPDATE_TARGET_MAPPINGS_PROPERTIES --> UPDATE_TARGET_MAPPINGS_PROPERTIES_WAIT_FOR_TASK
+ UPDATE_TARGET_MAPPINGS_PROPERTIES_WAIT_FOR_TASK --> UPDATE_TARGET_MAPPINGS_META
+ UPDATE_TARGET_MAPPINGS_META --> CHECK_VERSION_INDEX_READY_ACTIONS
-7. If there is a new indices migrators (e.g. .kibana_alerting_cases). Check cluster routing allocation
- and reindex (this is dead code and should be removed)
-
-## CREATE_INDEX_CHECK_CLUSTER_ROUTING_ALLOCATION
-
-### Next action
-
-`checkClusterRoutingAllocationEnabled`
+ CHECK_VERSION_INDEX_READY_ACTIONS -->|fresh deployment| MARK_VERSION_INDEX_READY
+ CHECK_VERSION_INDEX_READY_ACTIONS -->|no alias actions| DONE
-Check that replica allocation is enabled from cluster settings (`cluster.routing.allocation.enabled`). Migrations will fail when replica allocation is disabled during the bulk index operation that waits for all active shards. Migrations wait for all active shards to ensure that saved objects are replicated to protect against data loss.
-
-The Elasticsearch documentation mentions switching off replica allocation when restoring a cluster and this is a setting that might be overlooked when a restore is done. Migrations will fail early if replica allocation is incorrectly set to avoid adding a write block to the old index before running into a failure later.
-
-If replica allocation is set to 'all', the migration continues to fetch the saved object indices.
-
-### New control state
-
-1. If `cluster.routing.allocation.enabled` has a compatible value.
-
- → [CREATE_NEW_TARGET](#create_new_target)
-
-2. If it has a value that will not allow creating new *saved object* indices.
-
- → [CREATE_INDEX_CHECK_CLUSTER_ROUTING_ALLOCATION](#create_index_check_cluster_routing_allocation)
+ MARK_VERSION_INDEX_READY --> DONE
+ MARK_VERSION_INDEX_READY --> MARK_VERSION_INDEX_READY_CONFLICT
+ MARK_VERSION_INDEX_READY_CONFLICT --> DONE
+ MARK_VERSION_INDEX_READY_CONFLICT --> FATAL
+```
-## CREATE_NEW_TARGET
+## INIT
### Next action
-`createIndex`
+`fetchIndices`
-Create the target index. This operation is idempotent, if the index already exist, we wait until its status turns green
+Fetch the saved object indices, mappings and aliases to find the source index
+and determine whether we're performing a fresh deployment or migrating from an
+existing v2 index.
### New control state
-1. If the action succeeds
-
- → [MARK_VERSION_INDEX_READY](#mark_version_index_ready)
+1. If `.kibana` is pointing to more than one index.
-2. If the action fails with a `index_not_green_timeout`
+ → [FATAL](#fatal)
- → [CREATE_NEW_TARGET](#create_new_target)
+2. If `.kibana` is pointing to an index that belongs to a later version of
+ Kibana .e.g. a 7.11.0 instance found the `.kibana` alias pointing to
+ `.kibana_7.12.0_001`
-## LEGACY_CHECK_CLUSTER_ROUTING_ALLOCATION
+ → [FATAL](#fatal)
-### Next action
+3. If a `.kibana_` alias exists that refers to a later version of Kibana
+ (e.g. `.kibana_8.7.0` exists while running 8.6.1)
-`checkClusterRoutingAllocationCompatible`
+ → [FATAL](#fatal)
-Same description and behavior as [CHECK\_CLUSTER\_ROUTING\_ALLOCATION](#check_cluster_routing_allocation), for legacy flow.
+4. If `waitForMigrationCompletion` was set we're running on a background-tasks node and
+ should not participate in the migration but instead wait for the ui node(s)
+ to complete the migration.
-### New control state
+ → [WAIT_FOR_MIGRATION_COMPLETION](#wait_for_migration_completion)
-1. If `cluster.routing.allocation.enabled` has a compatible value.
+5. If the `.kibana` alias exists we're migrating from an existing v2 index
+ and the migration source index is the index the `.kibana` alias points to.
- → [LEGACY_SET_WRITE_BLOCK](#legacy_set_write_block)
+ → [WAIT_FOR_YELLOW_SOURCE](#wait_for_yellow_source)
-2. If it has a value that will not allow creating new *saved object* indices.
+6. If there are no `.kibana` indices, this is a fresh deployment. Check cluster routing allocation and
+ initialize a new saved objects index.
- → [LEGACY_CHECK_CLUSTER_ROUTING_ALLOCATION](#legacy_check_cluster_routing_allocation) (retry)
+ → [CREATE_INDEX_CHECK_CLUSTER_ROUTING_ALLOCATION](#create_index_check_cluster_routing_allocation)
-## LEGACY_SET_WRITE_BLOCK
+## CREATE_INDEX_CHECK_CLUSTER_ROUTING_ALLOCATION
### Next action
-`setWriteBlock`
-
-Set a write block on the legacy index to prevent any older Kibana instances
-from writing to the index while the migration is in progress which could cause
-lost acknowledged writes.
+`checkClusterRoutingAllocationEnabled`
-This is the first of a series of `LEGACY_*` control states that will:
+Check that shard allocation is enabled from cluster settings (`cluster.routing.allocation.enable`). Migrations need replica shards to be allocatable when creating new indices and waiting for green status.
-- reindex the concrete legacy `.kibana` index into a `.kibana_pre6.5.0_001` index
-- delete the concrete `.kibana` *index* so that we're able to create a `.kibana` *alias*
+If shard allocation is set to `all` (or unset), the migration continues to create the target index.
### New control state
-1. If the write block was successfully added
+1. If `cluster.routing.allocation.enable` has a compatible value.
- → [LEGACY_CREATE_REINDEX_TARGET](#legacy_create_reindex_target)
+ → [CREATE_NEW_TARGET](#create_new_target)
-2. If the write block failed because the index doesn't exist, it means another instance already completed the legacy pre-migration. Proceed to the next step.
+2. If it has a value that will not allow creating new *saved object* indices.
- → [LEGACY_CREATE_REINDEX_TARGET](#legacy_create_reindex_target)
+ → [CREATE_INDEX_CHECK_CLUSTER_ROUTING_ALLOCATION](#create_index_check_cluster_routing_allocation) (retry)
-## LEGACY_CREATE_REINDEX_TARGET
+## CREATE_NEW_TARGET
### Next action
`createIndex`
-Create a new `.kibana_pre6.5.0_001` index into which we can reindex the legacy
-index. (Since the task manager index was converted from a data index into a
-saved objects index in 7.4 it will be reindexed into `.kibana_pre7.4.0_001`)
+Create the target index. This operation is idempotent; if the index already exists, we wait until its status turns green.
### New control state
-1. If the index creation succeeds
-
- → [LEGACY_REINDEX](#legacy_reindex)
+1. If the index was created successfully.
-2. If the index creation task failed with a `index_not_green_timeout`
-
- → [LEGACY_REINDEX_WAIT_FOR_TASK](#legacy_reindex_wait_for_task)
+ → [CHECK_VERSION_INDEX_READY_ACTIONS](#check_version_index_ready_actions)
-## LEGACY_REINDEX
+2. If the index already exists (e.g. from a previous incomplete upgrade attempt).
-### Next action
+ → [OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT](#outdated_documents_search_open_pit)
-`reindex`
+3. If the action fails with a `index_not_green_timeout`.
-Let Elasticsearch reindex the legacy index into `.kibana_pre6.5.0_001`. (For
-the task manager index we specify a `preMigrationScript` to convert the
-original task manager documents into valid saved objects)
+ → [CREATE_NEW_TARGET](#create_new_target) (retry)
-### New control state
+4. If the action fails with `cluster_shard_limit_exceeded`.
-→ [LEGACY_REINDEX_WAIT_FOR_TASK](#legacy_reindex_wait_for_task)
+ → [FATAL](#fatal)
-## LEGACY_REINDEX_WAIT_FOR_TASK
+## WAIT_FOR_MIGRATION_COMPLETION
### Next action
-`waitForReindexTask`
-
-Wait for up to 60s for the reindex task to complete.
+`fetchIndices`
### New control state
-1. If the reindex task completed
-
- → [LEGACY_DELETE](#legacy_delete)
-
-2. If the reindex task failed with a `target_index_had_write_block` or
- `index_not_found_exception` another instance already completed this step
+1. If the ui node finished the migration.
- → [LEGACY_DELETE](#legacy_delete)
+ → [DONE](#done)
-3. If the reindex task is still in progress
+2. Otherwise wait 2s and check again.
- → [LEGACY_REINDEX_WAIT_FOR_TASK](#legacy_reindex_wait_for_task)
+ → [WAIT_FOR_MIGRATION_COMPLETION](#wait_for_migration_completion)
-## LEGACY_DELETE
+## WAIT_FOR_YELLOW_SOURCE
### Next action
-`updateAliases`
+`waitForIndexStatus` (status='yellow')
-Use the updateAliases API to atomically remove the legacy index and create a
-new `.kibana` alias that points to `.kibana_pre6.5.0_001`.
+Wait for the source index to become yellow. This means the index's primary has been allocated and is ready for reading/searching. On a multi node cluster the replicas for this index might not be ready yet but since we're never writing to the source index it does not matter.
### New control state
-1. If the action succeeds
+1. If the action succeeds.
- → [SET_SOURCE_WRITE_BLOCK](#set_source_write_block)
+ → [UPDATE_SOURCE_MAPPINGS_PROPERTIES](#update_source_mappings_properties)
-2. If the action fails with `remove_index_not_a_concrete_index` or
- `index_not_found_exception` another instance has already completed this step.
+2. If the action fails with a `index_not_yellow_timeout`.
- → [SET_SOURCE_WRITE_BLOCK](#set_source_write_block)
+ → [WAIT_FOR_YELLOW_SOURCE](#wait_for_yellow_source) (retry)
-## WAIT_FOR_MIGRATION_COMPLETION
+## UPDATE_SOURCE_MAPPINGS_PROPERTIES
### Next action
-`fetchIndices`
-
-### New control state
-
-1. If the ui node finished the migration
+`updateSourceMappingsProperties`
- → [DONE](#done)
+This action checks for source mappings changes and, if there are some, tries to patch the mappings.
-2. Otherwise wait 2s and check again
+- If there were no changes or the patch was successful, that reports either the changes are compatible or the source is already up to date, depending on the version migration completion state. Either way, it does not require reindexing to a new index.
+- If the patch failed and the version migration is incomplete, it reports an incompatible state.
+- If the patch failed and the version migration is complete, it reports an error as it means an incompatible mappings change in an already migrated environment. The latter usually happens when a new plugin is enabled that brings some incompatible changes or when there are incompatible changes in the development environment.
- → [WAIT_FOR_MIGRATION_COMPLETION](#wait_for_migration_completion)
-
-## WAIT_FOR_YELLOW_SOURCE
+### New control state
-### Next action
+1. If the mappings changes are compatible and the version migration is still in progress.
-`waitForIndexStatus` (status='yellow')
+ → [COMPATIBLE_UPDATE_CHECK_CLUSTER_ROUTING_ALLOCATION](#compatible_update_check_cluster_routing_allocation)
-Wait for the source index to become yellow. This means the index's primary has been allocated and is ready for reading/searching. On a multi node cluster the replicas for this index might not be ready yet but since we're never writing to the source index it does not matter.
+2. If the mappings are already up to date (version migration already completed, or no mapping changes needed).
-### New control state
+ → [OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT](#outdated_documents_search_open_pit)
-1. If the action succeeds
+3. If the mappings are not updated due to incompatible changes and the version migration is still in progress.
- → [UPDATE_SOURCE_MAPPINGS_PROPERTIES](#update_source_mappings_properties)
+ → [FATAL](#fatal)
-2. If the action fails with a `index_not_yellow_timeout`
+4. If the mappings are not updated due to incompatible changes and the version migration is already completed.
- → [WAIT_FOR_YELLOW_SOURCE](#wait_for_yellow_source)
+ → [FATAL](#fatal)
-## UPDATE_SOURCE_MAPPINGS_PROPERTIES
+## COMPATIBLE_UPDATE_CHECK_CLUSTER_ROUTING_ALLOCATION
### Next action
-`updateSourceMappingsProperties`
+`checkClusterRoutingAllocationEnabled`
-This action checks for source mappings changes.
-And if there are some, it tries to patch the mappings.
+Same check as [CREATE_INDEX_CHECK_CLUSTER_ROUTING_ALLOCATION](#create_index_check_cluster_routing_allocation), but for the compatible upgrade path. Cleanup of unknown and excluded documents uses asynchronous delete-by-query tasks that require shard allocation. This step fails early with an explicit `[incompatible_cluster_routing_allocation]` message instead of blocking on generic Elasticsearch retries.
-- If there were no changes or the patch was successful, that reports either the changes are compatible or the source is already up to date, depending on the version migration completion state. Either way, it does not require a follow-up reindexing.
-- If the patch is failed and the version migration is incomplete, it reports an incompatible state that requires reindexing.
-- If the patch is failed and the version migration is complete, it reports an error as it means an incompatible mappings change in an already migrated environment.
-The latter usually happens when a new plugin is enabled that brings some incompatible changes or when there are incompatible changes in the development environment.
+The Elasticsearch shard allocation cluster setting `cluster.routing.allocation.enable` needs to be unset or set to `all`. When set to `primaries`, `new_primaries` or `none`, cleanup tasks cannot allocate shards and the migration will retry until allocation is re-enabled or retries are exhausted.
### New control state
-1. If the mappings are updated and the migration is already completed.
-
- → [OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT](#outdated_documents_search_open_pit)
-
-2. If the mappings are updated and the migration is still in progress.
+1. If `cluster.routing.allocation.enable` has a compatible value.
→ [CLEANUP_UNKNOWN_AND_EXCLUDED](#cleanup_unknown_and_excluded)
-3. If the mappings are not updated due to incompatible changes and the migration is still in progress.
+2. If it has a value that will not allow shard allocation.
- → [REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION](#reindex_check_cluster_routing_allocation)
-
-4. If the mappings are not updated due to incompatible changes and the migration is already completed.
-
- → [FATAL](#fatal)
+ → [COMPATIBLE_UPDATE_CHECK_CLUSTER_ROUTING_ALLOCATION](#compatible_update_check_cluster_routing_allocation) (retry)
## CLEANUP_UNKNOWN_AND_EXCLUDED
@@ -485,6 +361,10 @@ In order to allow Kibana to discard unknown saved objects, users must set the [m
→ [CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK](#cleanup_unknown_and_excluded_wait_for_task)
+3. If no cleanup is needed.
+
+ → [PREPARE_COMPATIBLE_MIGRATION](#prepare_compatible_migration)
+
## CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK
### Next action
@@ -499,15 +379,15 @@ The cleanup task on the previous step is launched asynchronously, tracked by a s
→ [PREPARE_COMPATIBLE_MIGRATION](#prepare_compatible_migration)
-2. If we hit the timeout whilst waiting for the task to be completed, but we still have some retry attempts left.
+2. If we hit the timeout whilst waiting for the task to be completed.
- → [CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK](#cleanup_unknown_and_excluded_wait_for_task)
+ → [CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK](#cleanup_unknown_and_excluded_wait_for_task) (retry)
3. If some errors occur whilst cleaning up, there could be other instances performing the cleanup in parallel, deleting the documents that we intend to delete. In that scenario, we will launch the operation again.
→ [CLEANUP_UNKNOWN_AND_EXCLUDED](#cleanup_unknown_and_excluded)
-4. If we hit the timeout and we run out of retries.
+4. If we run out of retries.
→ [FATAL](#fatal)
@@ -517,7 +397,7 @@ The cleanup task on the previous step is launched asynchronously, tracked by a s
`updateAliases`
-At this point, we have successfully updated the index mappings. We are performing a *compatible migration*, aka updating *saved objects* in place on the existing index. In order to prevent other Kibana instances from writing documents whilst we update them, we remove the previous version alias. We also set set the current version alias, which will cause other instances' migrators to directly perform an *up-to-date migration*.
+At this point, we have successfully updated the index mappings. We are performing a *compatible migration*, aka updating *saved objects* in place on the existing index. In order to prevent other Kibana instances from writing documents whilst we update them, we remove the previous version alias. We also set the current version alias, which will cause other instances' migrators to directly perform an *up-to-date migration*.
### New control state
@@ -529,252 +409,21 @@ At this point, we have successfully updated the index mappings. We are performin
→ [OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT](#outdated_documents_search_open_pit)
-3. When unexpected errors occur when updating the aliases.
-
- → [FATAL](#fatal)
-
-## REFRESH_SOURCE
-
-### Next action
-
-`refreshIndex`
-
-We are performing a *compatible migration*, and we discarded some unknown and excluded saved object documents. We must refresh the index so that subsequent queries no longer find these removed documents.
-
-### New control state
-
-1. If the index is refreshed successfully.
-
- → [OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT](#outdated_documents_search_open_pit)
-
-2. When unexpected errors occur during the refresh.
-
- → [FATAL](#fatal)
-
-## REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION
-
-### Next action
-
-`checkClusterRoutingAllocationEnabled`
-
-Check that replica allocation is enabled from cluster settings (`cluster.routing.allocation.enabled`). Migrations will fail when replica allocation is disabled during the bulk index operation that waits for all active shards. Migrations wait for all active shards to ensure that saved objects are replicated to protect against data loss.
-
-The Elasticsearch documentation mentions switching off replica allocation when restoring a cluster and this is a setting that might be overlooked when a restore is done. Migrations will fail early if replica allocation is incorrectly set to avoid adding a write block to the old index before running into a failure later.
-
-If replica allocation is set to 'all', the migration continues to fetch the saved object indices.
-
-### New control state
-
-The Elasticsearch shard allocation cluster setting `cluster.routing.allocation.enable` needs to be unset or set to 'all'. When set to 'primaries', 'new_primaries' or 'none', the migration will timeout when waiting for index green status before bulk indexing because the replica cannot be allocated.
-
-As per the Elasticsearch [docs](https://www.elastic.co/guide/en/elasticsearch/reference/8.2/restart-cluster.html#restart-cluster-rolling), when Cloud performs a rolling restart such as during an upgrade, it will temporarily disable shard allocation. Kibana therefore keeps retrying the INIT step to wait for shard allocation to be enabled again.
-
-The check only considers persistent and transient settings and does not take static configuration in `elasticsearch.yml` into account since there are no known use cases for doing so. If `cluster.routing.allocation.enable` is configured in `elaticsearch.yml` and not set to the default of 'all', the migration will timeout. Static settings can only be returned from the `nodes/info` API.
-
-1. If `cluster.routing.allocation.enabled` has a compatible value.
-
- → [CHECK_UNKNOWN_DOCUMENTS](#check_unknown_documents)
+3. If the alias was already deleted by another Kibana instance (`alias_not_found_exception`).
-2. If it has a value that will not allow creating new *saved object* indices.
-
- → [REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION](#reindex_check_cluster_routing_allocation) (retry)
-
-## CHECK_UNKNOWN_DOCUMENTS
-
-Saved objects are unknown when their type is not registered in the *typeRegistry*. This can happen when disabling plugins, or when deprecated plugins are removed during a major upgrade.
-
-During a *reindex migration*, these documents can be discarded if Kibana is configured with the [migrations.discardUnknownObjects](https://www.elastic.co/guide/en/kibana/current/resolve-migrations-failures.html#unknown-saved-object-types) flag.
+ → [REFRESH_SOURCE](#refresh_source) or [OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT](#outdated_documents_search_open_pit) (same conditions as above)
-### Next action
-
-1. If no unknown documents are found, or Kibana is configured to discard them.
-
- → [SET_SOURCE_WRITE_BLOCK](#set_source_write_block)
-
-2. If some unknown documents are found and Kibana is NOT configured to discard them.
+4. When unexpected errors occur when updating the aliases.
→ [FATAL](#fatal)
-## SET_SOURCE_WRITE_BLOCK
-
-### Next action
-
-`setWriteBlock`
-
-Set a write block on the source index to prevent any older Kibana instances from writing to the index while the migration is in progress which could cause lost acknowledged writes.
-
-### New control state
-
-→ [CREATE_REINDEX_TEMP](#create_reindex_temp)
-
-## RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION
-
-### Next action
-
-`checkClusterRoutingAllocationEnabled`
-
-Check that replica allocation is enabled from cluster settings (`cluster.routing.allocation.enabled`). Migrations will fail when replica allocation is disabled during the bulk index operation that waits for all active shards. Migrations wait for all active shards to ensure that saved objects are replicated to protect against data loss.
-
-The Elasticsearch documentation mentions switching off replica allocation when restoring a cluster and this is a setting that might be overlooked when a restore is done. Migrations will fail early if replica allocation is incorrectly set to avoid adding a write block to the old index before running into a failure later.
-
-If replica allocation is set to 'all', the migration continues to fetch the saved object indices.
-
-### New control state
-
-1. If `cluster.routing.allocation.enabled` has a compatible value.
-
- → [CREATE_REINDEX_TEMP](#create_reindex_temp)
-
-2. If it has a value that will not allow creating new *saved object* indices.
-
- → [RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION](#relocate_check_cluster_routing_allocation)
-
-## CREATE_REINDEX_TEMP
-
-### Next action
-
-`createIndex`
-
-This operation is idempotent, if the index already exist, we wait until its status turns green.
-
-- Because we will be transforming documents before writing them into this index, we can already set the mappings to the target mappings for this version. The source index might contain documents belonging to a disabled plugin. So set `dynamic: false` mappings for any unknown saved object types.
-- (Since we never query the temporary index we can potentially disable refresh to speed up indexing performance. Profile to see if gains justify complexity)
-
-### New control state
-
-1. If the action succeeds
-
- → [REINDEX_SOURCE_TO_TEMP_OPEN_PIT](#reindex_source_to_temp_open_pit)
-
-2. If the action fails with a `index_not_green_timeout`
-
- → [CREATE_REINDEX_TEMP](#create_reindex_temp)
-
-## REINDEX_SOURCE_TO_TEMP_OPEN_PIT
-
-### Next action
-
-`openPIT`
-
-Open a PIT. Since there is a write block on the source index there is basically no overhead to keeping the PIT so we can lean towards a larger `keep_alive` value like 10 minutes.
-
-### New control state
-
-→ [REINDEX_SOURCE_TO_TEMP_READ](#reindex_source_to_temp_read)
-
-## REINDEX_SOURCE_TO_TEMP_READ
-
-### Next action
-
-`readNextBatchOfSourceDocuments`
-
-Read the next batch of outdated documents from the source index by using search after with our PIT.
-
-### New control state
-
-1. If the batch contained > 0 documents
-
- → [REINDEX_SOURCE_TO_TEMP_TRANSFORM](#reindex_source_to_temp_transform)
-
-2. If there are no more documents returned
-
- → [REINDEX_SOURCE_TO_TEMP_CLOSE_PIT](#reindex_source_to_temp_close_pit)
-
-## REINDEX_SOURCE_TO_TEMP_TRANSFORM
-
-### Next action
-
-`transformRawDocs`
-
-Transform the current batch of documents
-
-In order to support sharing saved objects to multiple spaces in 8.0, the
-transforms will also regenerate document `_id`'s. To ensure that this step
-remains idempotent, the new `_id` is deterministically generated using UUIDv5
-ensuring that each Kibana instance generates the same new `_id` for the same document.
-
-### New control state
-
-→ [REINDEX_SOURCE_TO_TEMP_INDEX_BULK](#reindex_source_to_temp_index_bulk)
-
-## REINDEX_SOURCE_TO_TEMP_INDEX_BULK
-
-### Next action
-
-`bulkIndexTransformedDocuments`
-
-Use the bulk API create action to write a batch of up-to-date documents. The
-create action ensures that there will be only one write per reindexed document
-even if multiple Kibana instances are performing this step. Use
-`refresh=false` to speed up the create actions, the `UPDATE_TARGET_MAPPINGS_PROPERTIES`
-step will ensure that the index is refreshed before we start serving traffic.
-
-The following errors are ignored because it means another instance already
-completed this step:
-
-- documents already exist in the temp index
-- temp index has a write block
-- temp index is not found
-
-### New control state
-
-1. If `currentBatch` is the last batch in `bulkOperationBatches`
-
- → [REINDEX_SOURCE_TO_TEMP_READ](#reindex_source_to_temp_read)
-
-2. If there are more batches left in `bulkOperationBatches`
-
- → [REINDEX_SOURCE_TO_TEMP_INDEX_BULK](#reindex_source_to_temp_index_bulk)
-
-## REINDEX_SOURCE_TO_TEMP_CLOSE_PIT
-
-### Next action
-
-`closePIT`
-
-### New control state
-
-→ [SET_TEMP_WRITE_BLOCK](#set_temp_write_block)
-
-## SET_TEMP_WRITE_BLOCK
-
-### Next action
-
-`setWriteBlock`
-
-Set a write block on the temporary index so that we can clone it.
-
-### New control state
-
-→ [CLONE_TEMP_TO_TARGET](#clone_temp_to_target)
-
-## CLONE_TEMP_TO_TARGET
-
-### Next action
-
-`cloneIndex`
-
-Ask elasticsearch to clone the temporary index into the target index. If the target index already exists (because another node already started the clone operation), wait until the clone is complete by waiting for a green index status.
-
-We can’t use the temporary index as our target index because one instance can complete the migration, delete a document, and then a second instance starts the reindex operation and re-creates the deleted document. By cloning the temporary index and only accepting writes/deletes from the cloned target index, we prevent lost acknowledged deletes.
-
-### New control state
-
-1. If the action succeeds.
-
- → [REFRESH_TARGET](#refresh_target)
-
-2. If the action fails with an `index_not_green_timeout`.
-
- → [CLONE_TEMP_TO_TARGET](#clone_temp_to_target)
-
-## REFRESH_TARGET
+## REFRESH_SOURCE
### Next action
`refreshIndex`
-We refresh the temporary clone index, to make sure newly added documents are taken into account.
+We are performing a *compatible migration*, and we discarded some unknown and excluded saved object documents. We must refresh the index so that subsequent queries no longer find these removed documents.
### New control state
@@ -816,8 +465,7 @@ documents.
If another instance has a disabled plugin it will reindex that plugin's
documents without transforming them. Because this instance doesn't know which
-plugins were disabled by the instance that performed the
-`REINDEX_SOURCE_TO_TEMP_TRANSFORM` step, we need to search for outdated documents
+plugins were disabled by the instance that performed the migration, we need to search for outdated documents
and transform them to ensure that everything is up to date.
### New control state
@@ -864,7 +512,7 @@ and transform them to ensure that everything is up to date.
`bulkOverwriteTransformedDocuments`
-Once transformed we use an index operation to overwrite the outdated document with the up-to-date version. Optimistic concurrency control ensures that we only overwrite the document once so that any updates/writes by another instance which already completed the migration aren’t overwritten and lost. The transformed documents are split in different batches, and then each batch is bulk indexed.
+Once transformed we use an index operation to overwrite the outdated document with the up-to-date version. Optimistic concurrency control ensures that we only overwrite the document once so that any updates/writes by another instance which already completed the migration aren't overwritten and lost. The transformed documents are split in different batches, and then each batch is bulk indexed.
### New control state
@@ -876,13 +524,21 @@ Once transformed we use an index operation to overwrite the outdated document wi
→ [OUTDATED_DOCUMENTS_SEARCH_READ](#outdated_documents_search_read)
+3. If bulk indexing fails with `unavailable_shards_exception`.
+
+ → [TRANSFORMED_DOCUMENTS_BULK_INDEX](#transformed_documents_bulk_index) (retry)
+
+4. If bulk indexing fails with `request_entity_too_large_exception`.
+
+ → [FATAL](#fatal)
+
## OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT
### Next action
`closePit`
-After reading, transforming and bulk indexingn all saved objects, we can close our PIT.
+After reading, transforming and bulk indexing all saved objects, we can close our PIT.
### New control state
@@ -912,7 +568,7 @@ We updated some outdated documents, we must refresh the target index to pick up
→ [CHECK_TARGET_MAPPINGS](#check_target_mappings)
-2. When unexpected errors occur during the refresh.****
+2. When unexpected errors occur during the refresh.
→ [FATAL](#fatal)
@@ -926,11 +582,19 @@ Compare the calculated mappings' hashes against those stored in the `.map
### New control state
-1. If calculated mappings don't match, we must update them.
+1. If calculated mappings don't match because top-level properties changed (e.g. `dynamic` or `_meta`).
→ [UPDATE_TARGET_MAPPINGS_PROPERTIES](#update_target_mappings_properties)
-2. If calculated mappings and stored mappings match, we can skip directly to the next step.
+2. If calculated mappings don't match because some SO type mappings changed.
+
+ → [UPDATE_TARGET_MAPPINGS_PROPERTIES](#update_target_mappings_properties)
+
+3. If only new SO types were introduced (no existing type mappings changed).
+
+ → [UPDATE_TARGET_MAPPINGS_META](#update_target_mappings_meta)
+
+4. If calculated mappings and stored mappings match, we can skip directly to the next step.
→ [CHECK_VERSION_INDEX_READY_ACTIONS](#check_version_index_ready_actions)
@@ -940,8 +604,8 @@ Compare the calculated mappings' hashes against those stored in the `.map
`updateAndPickupMappings`
-If another instance has some plugins disabled it will disable the mappings of that plugin's types when creating the temporary index. This action will
-update the mappings and then use an update_by_query to ensure that all fields are “picked-up” and ready to be searched over.
+If another instance has some plugins disabled it will disable the mappings of that plugin's types when creating the index. This action will
+update the mappings and then use an update_by_query to ensure that all fields are "picked-up" and ready to be searched over.
### New control state
@@ -955,7 +619,35 @@ update the mappings and then use an update_by_query to ensure that all fields ar
### New control state
-→ [MARK_VERSION_INDEX_READY](#mark_version_index_ready)
+1. If the task completes successfully.
+
+ → [UPDATE_TARGET_MAPPINGS_META](#update_target_mappings_meta)
+
+2. If we hit a `wait_for_task_completion_timeout`.
+
+ → [UPDATE_TARGET_MAPPINGS_PROPERTIES_WAIT_FOR_TASK](#update_target_mappings_properties_wait_for_task) (retry)
+
+3. If the task completed with a retriable error.
+
+ → [UPDATE_TARGET_MAPPINGS_PROPERTIES](#update_target_mappings_properties) (retry)
+
+## UPDATE_TARGET_MAPPINGS_META
+
+### Next action
+
+`updateMappings`
+
+Update the mapping `_meta` information with the hashes of the mappings for each plugin. Properties were already updated on the previous step.
+
+### New control state
+
+1. If the mappings are updated successfully.
+
+ → [CHECK_VERSION_INDEX_READY_ACTIONS](#check_version_index_ready_actions)
+
+2. When unexpected errors occur.
+
+ → [FATAL](#fatal)
## CHECK_VERSION_INDEX_READY_ACTIONS
@@ -963,7 +655,7 @@ Check if the state contains some `versionIndexReadyActions` from the `INIT` acti
### Next action
-None
+`noop`
### New control state
@@ -971,7 +663,7 @@ None
→ [MARK_VERSION_INDEX_READY](#mark_version_index_ready)
-2. If there are no `versionIndexReadyActions`, another instance already completed this migration and we only transformed outdated documents and updated the mappings for in case a new plugin was enabled.
+2. If there are no `versionIndexReadyActions`, another instance already completed this migration and we only transformed outdated documents and updated the mappings in case a new plugin was enabled.
→ [DONE](#done)
@@ -981,19 +673,15 @@ None
`updateAliases`
-Atomically apply the `versionIndexReadyActions` using the _alias actions API. By performing the following actions we guarantee that if multiple versions of Kibana started the upgrade in parallel, only one version will succeed.
-
-1. verify that the current alias is still pointing to the source index
-2. Point the version alias and the current alias to the target index.
-3. Remove the temporary index
+Atomically apply the `versionIndexReadyActions` using the _alias actions API. For a fresh deployment this adds the current and version aliases to the new target index. By performing these actions we guarantee that if multiple versions of Kibana started the upgrade in parallel, only one version will succeed.
### New control state
-1. If all the actions succeed we’re ready to serve traffic
+1. If all the actions succeed we're ready to serve traffic.
→ [DONE](#done)
-2. If action (1) fails with alias_not_found_exception or action (3) fails with index_not_found_exception another instance already completed the migration
+2. If action (1) fails with `alias_not_found_exception` another instance already completed the migration.
→ [MARK_VERSION_INDEX_READY_CONFLICT](#mark_version_index_ready_conflict)
@@ -1003,17 +691,17 @@ Atomically apply the `versionIndexReadyActions` using the _alias actions API. By
`fetchIndices`
-Fetch the saved object indices
+Fetch the saved object indices.
### New control state
If another instance completed a migration from the same source we need to verify that it is running the same version.
-1. If the current and version aliases are pointing to the same index the instance that completed the migration was on the same version and it’s safe to start serving traffic.
+1. If the current and version aliases are pointing to the same index the instance that completed the migration was on the same version and it's safe to start serving traffic.
→ [DONE](#done)
-2. If the other instance was running a different version we fail the migration. Once we restart one of two things can happen: the other instance is an older version and we will restart the migration, or, it’s a newer version and we will refuse to start up.
+2. If the other instance was running a different version we fail the migration. Once we restart one of two things can happen: the other instance is an older version and we will restart the migration, or, it's a newer version and we will refuse to start up.
→ [FATAL](#fatal)
@@ -1027,54 +715,7 @@ Congratulations, this migrator finished the saved objects migration for its inde
# Manual QA Test Plan
-## 1. Legacy pre-migration
-
-When upgrading from a legacy index additional steps are required before the
-regular migration process can start.
-
-We have the following potential legacy indices:
-
-- v5.x index that wasn't upgraded -> kibana should refuse to start the migration
-- v5.x index that was upgraded to v6.x: `.kibana-6` *index* with `.kibana` *alias*
-- < v6.5 `.kibana` *index* (Saved Object Migrations were
- introduced in v6.5 )
-- TODO: Test versions which introduced the `kibana_index_template` template?
-- < v7.4 `.kibana_task_manager` *index* (Task Manager started
- using Saved Objects in v7.4 )
-
-Test plan:
-
-1. Ensure that the different versions of Kibana listed above can successfully
- upgrade to 7.11.
-2. Ensure that multiple Kibana nodes can migrate a legacy index in parallel
- (choose a representative legacy version to test with e.g. v6.4). Add a lot
- of Saved Objects to Kibana to increase the time it takes for a migration to
- complete which will make it easier to introduce failures.
- 1. If all instances are started in parallel the upgrade should succeed
- 2. If nodes are randomly restarted shortly after they start participating
- in the migration the upgrade should either succeed or never complete.
- However, if a fatal error occurs it should never result in permanent
- failure.
- 1. Start one instance, wait 500 ms
- 2. Start a second instance
- 3. If an instance starts a saved object migration, wait X ms before
- killing the process and restarting the migration.
- 4. Keep decreasing X until migrations are barely able to complete.
- 5. If a migration fails with a fatal error, start a Kibana that doesn't
- get restarted. Given enough time, it should always be able to
- successfully complete the migration.
-
-For a successful migration the following behaviour should be observed:
-
- 1. The `.kibana` index should be reindexed into a `.kibana_pre6.5.0` index
- 2. The `.kibana` index should be deleted
- 3. The `.kibana_index_template` should be deleted
- 4. The `.kibana_pre6.5.0` index should have a write block applied
- 5. Documents from `.kibana_pre6.5.0` should be migrated into `.kibana_7.11.0_001`
- 6. Once migration has completed, the `.kibana_current` and `.kibana_7.11.0`
- aliases should point to the `.kibana_7.11.0_001` index.
-
-## 2. Plugins enabled/disabled
+## 1. Plugins enabled/disabled
Kibana plugins can be disabled/enabled at any point in time. We need to ensure
that Saved Object documents are migrated for all the possible sequences of
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/__snapshots__/migrations_state_action_machine.test.ts.snap b/src/core/packages/saved-objects/migration-server-internal/src/__snapshots__/migrations_state_action_machine.test.ts.snap
index 45b3314d10589..5aa1d794bbb2b 100644
--- a/src/core/packages/saved-objects/migration-server-internal/src/__snapshots__/migrations_state_action_machine.test.ts.snap
+++ b/src/core/packages/saved-objects/migration-server-internal/src/__snapshots__/migrations_state_action_machine.test.ts.snap
@@ -7,7 +7,7 @@ Object {
"[.my-so-index] INIT RESPONSE",
],
Array [
- "[.my-so-index] INIT -> LEGACY_REINDEX. took: 0ms.",
+ "[.my-so-index] INIT -> WAIT_FOR_YELLOW_SOURCE. took: 0ms.",
Object {
"kibana": Object {
"migrations": Object {
@@ -17,7 +17,7 @@ Object {
"bulkOperationBatches": Array [
Array [],
],
- "controlState": "LEGACY_REINDEX",
+ "controlState": "WAIT_FOR_YELLOW_SOURCE",
"currentAlias": ".my-so-index",
"discardCorruptObjects": false,
"discardUnknownObjects": false,
@@ -60,28 +60,13 @@ Object {
"typeB",
"typeC",
],
- "indexTypesMap": Object {
- ".kibana": Array [
- "typeA",
- "typeB",
- "typeC",
- ],
- ".kibana_cases": Array [
- "typeD",
- "typeE",
- ],
- ".kibana_task_manager": Array [
- "task",
- ],
- },
"kibanaVersion": "7.11.0",
"knownTypes": Array [],
"latestMappingsVersions": Object {},
- "legacyIndex": ".my-so-index",
"logs": Array [
Object {
"level": "info",
- "message": "Log from LEGACY_REINDEX control state",
+ "message": "Log from WAIT_FOR_YELLOW_SOURCE control state",
},
],
"maxBatchSize": 1000,
@@ -93,16 +78,12 @@ Object {
"resolveMigrationFailures": "https://www.elastic.co/docs/troubleshoot/kibana/migration-failures",
"routingAllocationDisabled": "https://www.elastic.co/docs/troubleshoot/kibana/migration-failures#routing-allocation-disabled",
},
- "mustRelocateDocuments": true,
"outdatedDocuments": Array [],
"outdatedDocumentsQuery": Object {
"bool": Object {
"should": Array [],
},
},
- "preMigrationScript": Object {
- "_tag": "None",
- },
"retryAttempts": 5,
"retryCount": 0,
"retryDelay": 0,
@@ -110,19 +91,6 @@ Object {
"targetIndexMappings": Object {
"properties": Object {},
},
- "tempIndex": ".my-so-index_7.11.0_reindex_temp",
- "tempIndexAlias": ".my-so-index_7.11.0_reindex_temp_alias",
- "tempIndexMappings": Object {
- "dynamic": false,
- "properties": Object {
- "type": Object {
- "type": "keyword",
- },
- "typeMigrationVersion": Object {
- "type": "version",
- },
- },
- },
"versionAlias": ".my-so-index_7.11.0",
"versionIndex": ".my-so-index_7.11.0_001",
"waitForMigrationCompletion": false,
@@ -132,10 +100,10 @@ Object {
},
],
Array [
- "[.my-so-index] LEGACY_REINDEX RESPONSE",
+ "[.my-so-index] WAIT_FOR_YELLOW_SOURCE RESPONSE",
],
Array [
- "[.my-so-index] LEGACY_REINDEX -> LEGACY_DELETE. took: 0ms.",
+ "[.my-so-index] WAIT_FOR_YELLOW_SOURCE -> UPDATE_SOURCE_MAPPINGS_PROPERTIES. took: 0ms.",
Object {
"kibana": Object {
"migrations": Object {
@@ -145,7 +113,7 @@ Object {
"bulkOperationBatches": Array [
Array [],
],
- "controlState": "LEGACY_DELETE",
+ "controlState": "UPDATE_SOURCE_MAPPINGS_PROPERTIES",
"currentAlias": ".my-so-index",
"discardCorruptObjects": false,
"discardUnknownObjects": false,
@@ -188,32 +156,17 @@ Object {
"typeB",
"typeC",
],
- "indexTypesMap": Object {
- ".kibana": Array [
- "typeA",
- "typeB",
- "typeC",
- ],
- ".kibana_cases": Array [
- "typeD",
- "typeE",
- ],
- ".kibana_task_manager": Array [
- "task",
- ],
- },
"kibanaVersion": "7.11.0",
"knownTypes": Array [],
"latestMappingsVersions": Object {},
- "legacyIndex": ".my-so-index",
"logs": Array [
Object {
"level": "info",
- "message": "Log from LEGACY_REINDEX control state",
+ "message": "Log from WAIT_FOR_YELLOW_SOURCE control state",
},
Object {
"level": "info",
- "message": "Log from LEGACY_DELETE control state",
+ "message": "Log from UPDATE_SOURCE_MAPPINGS_PROPERTIES control state",
},
],
"maxBatchSize": 1000,
@@ -225,16 +178,12 @@ Object {
"resolveMigrationFailures": "https://www.elastic.co/docs/troubleshoot/kibana/migration-failures",
"routingAllocationDisabled": "https://www.elastic.co/docs/troubleshoot/kibana/migration-failures#routing-allocation-disabled",
},
- "mustRelocateDocuments": true,
"outdatedDocuments": Array [],
"outdatedDocumentsQuery": Object {
"bool": Object {
"should": Array [],
},
},
- "preMigrationScript": Object {
- "_tag": "None",
- },
"retryAttempts": 5,
"retryCount": 0,
"retryDelay": 0,
@@ -242,19 +191,6 @@ Object {
"targetIndexMappings": Object {
"properties": Object {},
},
- "tempIndex": ".my-so-index_7.11.0_reindex_temp",
- "tempIndexAlias": ".my-so-index_7.11.0_reindex_temp_alias",
- "tempIndexMappings": Object {
- "dynamic": false,
- "properties": Object {
- "type": Object {
- "type": "keyword",
- },
- "typeMigrationVersion": Object {
- "type": "version",
- },
- },
- },
"versionAlias": ".my-so-index_7.11.0",
"versionIndex": ".my-so-index_7.11.0_001",
"waitForMigrationCompletion": false,
@@ -264,10 +200,10 @@ Object {
},
],
Array [
- "[.my-so-index] LEGACY_DELETE RESPONSE",
+ "[.my-so-index] UPDATE_SOURCE_MAPPINGS_PROPERTIES RESPONSE",
],
Array [
- "[.my-so-index] LEGACY_DELETE -> LEGACY_DELETE. took: 0ms.",
+ "[.my-so-index] UPDATE_SOURCE_MAPPINGS_PROPERTIES -> UPDATE_SOURCE_MAPPINGS_PROPERTIES. took: 0ms.",
Object {
"kibana": Object {
"migrations": Object {
@@ -277,7 +213,7 @@ Object {
"bulkOperationBatches": Array [
Array [],
],
- "controlState": "LEGACY_DELETE",
+ "controlState": "UPDATE_SOURCE_MAPPINGS_PROPERTIES",
"currentAlias": ".my-so-index",
"discardCorruptObjects": false,
"discardUnknownObjects": false,
@@ -320,36 +256,21 @@ Object {
"typeB",
"typeC",
],
- "indexTypesMap": Object {
- ".kibana": Array [
- "typeA",
- "typeB",
- "typeC",
- ],
- ".kibana_cases": Array [
- "typeD",
- "typeE",
- ],
- ".kibana_task_manager": Array [
- "task",
- ],
- },
"kibanaVersion": "7.11.0",
"knownTypes": Array [],
"latestMappingsVersions": Object {},
- "legacyIndex": ".my-so-index",
"logs": Array [
Object {
"level": "info",
- "message": "Log from LEGACY_REINDEX control state",
+ "message": "Log from WAIT_FOR_YELLOW_SOURCE control state",
},
Object {
"level": "info",
- "message": "Log from LEGACY_DELETE control state",
+ "message": "Log from UPDATE_SOURCE_MAPPINGS_PROPERTIES control state",
},
Object {
"level": "info",
- "message": "Log from LEGACY_DELETE control state",
+ "message": "Log from UPDATE_SOURCE_MAPPINGS_PROPERTIES control state",
},
],
"maxBatchSize": 1000,
@@ -361,16 +282,12 @@ Object {
"resolveMigrationFailures": "https://www.elastic.co/docs/troubleshoot/kibana/migration-failures",
"routingAllocationDisabled": "https://www.elastic.co/docs/troubleshoot/kibana/migration-failures#routing-allocation-disabled",
},
- "mustRelocateDocuments": true,
"outdatedDocuments": Array [],
"outdatedDocumentsQuery": Object {
"bool": Object {
"should": Array [],
},
},
- "preMigrationScript": Object {
- "_tag": "None",
- },
"retryAttempts": 5,
"retryCount": 0,
"retryDelay": 0,
@@ -378,19 +295,6 @@ Object {
"targetIndexMappings": Object {
"properties": Object {},
},
- "tempIndex": ".my-so-index_7.11.0_reindex_temp",
- "tempIndexAlias": ".my-so-index_7.11.0_reindex_temp_alias",
- "tempIndexMappings": Object {
- "dynamic": false,
- "properties": Object {
- "type": Object {
- "type": "keyword",
- },
- "typeMigrationVersion": Object {
- "type": "version",
- },
- },
- },
"versionAlias": ".my-so-index_7.11.0",
"versionIndex": ".my-so-index_7.11.0_001",
"waitForMigrationCompletion": false,
@@ -400,10 +304,10 @@ Object {
},
],
Array [
- "[.my-so-index] LEGACY_DELETE RESPONSE",
+ "[.my-so-index] UPDATE_SOURCE_MAPPINGS_PROPERTIES RESPONSE",
],
Array [
- "[.my-so-index] LEGACY_DELETE -> DONE. took: 0ms.",
+ "[.my-so-index] UPDATE_SOURCE_MAPPINGS_PROPERTIES -> DONE. took: 0ms.",
Object {
"kibana": Object {
"migrations": Object {
@@ -456,36 +360,21 @@ Object {
"typeB",
"typeC",
],
- "indexTypesMap": Object {
- ".kibana": Array [
- "typeA",
- "typeB",
- "typeC",
- ],
- ".kibana_cases": Array [
- "typeD",
- "typeE",
- ],
- ".kibana_task_manager": Array [
- "task",
- ],
- },
"kibanaVersion": "7.11.0",
"knownTypes": Array [],
"latestMappingsVersions": Object {},
- "legacyIndex": ".my-so-index",
"logs": Array [
Object {
"level": "info",
- "message": "Log from LEGACY_REINDEX control state",
+ "message": "Log from WAIT_FOR_YELLOW_SOURCE control state",
},
Object {
"level": "info",
- "message": "Log from LEGACY_DELETE control state",
+ "message": "Log from UPDATE_SOURCE_MAPPINGS_PROPERTIES control state",
},
Object {
"level": "info",
- "message": "Log from LEGACY_DELETE control state",
+ "message": "Log from UPDATE_SOURCE_MAPPINGS_PROPERTIES control state",
},
Object {
"level": "info",
@@ -501,16 +390,12 @@ Object {
"resolveMigrationFailures": "https://www.elastic.co/docs/troubleshoot/kibana/migration-failures",
"routingAllocationDisabled": "https://www.elastic.co/docs/troubleshoot/kibana/migration-failures#routing-allocation-disabled",
},
- "mustRelocateDocuments": true,
"outdatedDocuments": Array [],
"outdatedDocumentsQuery": Object {
"bool": Object {
"should": Array [],
},
},
- "preMigrationScript": Object {
- "_tag": "None",
- },
"retryAttempts": 5,
"retryCount": 0,
"retryDelay": 0,
@@ -518,19 +403,6 @@ Object {
"targetIndexMappings": Object {
"properties": Object {},
},
- "tempIndex": ".my-so-index_7.11.0_reindex_temp",
- "tempIndexAlias": ".my-so-index_7.11.0_reindex_temp_alias",
- "tempIndexMappings": Object {
- "dynamic": false,
- "properties": Object {
- "type": Object {
- "type": "keyword",
- },
- "typeMigrationVersion": Object {
- "type": "version",
- },
- },
- },
"versionAlias": ".my-so-index_7.11.0",
"versionIndex": ".my-so-index_7.11.0_001",
"waitForMigrationCompletion": false,
@@ -544,13 +416,13 @@ Object {
"fatal": Array [],
"info": Array [
Array [
- "[.my-so-index] Log from LEGACY_REINDEX control state",
+ "[.my-so-index] Log from WAIT_FOR_YELLOW_SOURCE control state",
],
Array [
- "[.my-so-index] Log from LEGACY_DELETE control state",
+ "[.my-so-index] Log from UPDATE_SOURCE_MAPPINGS_PROPERTIES control state",
],
Array [
- "[.my-so-index] Log from LEGACY_DELETE control state",
+ "[.my-so-index] Log from UPDATE_SOURCE_MAPPINGS_PROPERTIES control state",
],
Array [
"[.my-so-index] Log from DONE control state",
@@ -569,7 +441,7 @@ Object {
"[.my-so-index] INIT RESPONSE",
],
Array [
- "[.my-so-index] INIT -> LEGACY_DELETE. took: 0ms.",
+ "[.my-so-index] INIT -> UPDATE_SOURCE_MAPPINGS_PROPERTIES. took: 0ms.",
Object {
"kibana": Object {
"migrations": Object {
@@ -585,7 +457,7 @@ Object {
},
],
],
- "controlState": "LEGACY_DELETE",
+ "controlState": "UPDATE_SOURCE_MAPPINGS_PROPERTIES",
"currentAlias": ".my-so-index",
"discardCorruptObjects": false,
"discardUnknownObjects": false,
@@ -628,28 +500,13 @@ Object {
"typeB",
"typeC",
],
- "indexTypesMap": Object {
- ".kibana": Array [
- "typeA",
- "typeB",
- "typeC",
- ],
- ".kibana_cases": Array [
- "typeD",
- "typeE",
- ],
- ".kibana_task_manager": Array [
- "task",
- ],
- },
"kibanaVersion": "7.11.0",
"knownTypes": Array [],
"latestMappingsVersions": Object {},
- "legacyIndex": ".my-so-index",
"logs": Array [
Object {
"level": "info",
- "message": "Log from LEGACY_DELETE control state",
+ "message": "Log from UPDATE_SOURCE_MAPPINGS_PROPERTIES control state",
},
],
"maxBatchSize": 1000,
@@ -661,7 +518,6 @@ Object {
"resolveMigrationFailures": "https://www.elastic.co/docs/troubleshoot/kibana/migration-failures",
"routingAllocationDisabled": "https://www.elastic.co/docs/troubleshoot/kibana/migration-failures#routing-allocation-disabled",
},
- "mustRelocateDocuments": true,
"outdatedDocuments": Array [
Object {
"_id": "1234",
@@ -672,9 +528,6 @@ Object {
"should": Array [],
},
},
- "preMigrationScript": Object {
- "_tag": "None",
- },
"reason": "the fatal reason",
"retryAttempts": 5,
"retryCount": 0,
@@ -683,19 +536,6 @@ Object {
"targetIndexMappings": Object {
"properties": Object {},
},
- "tempIndex": ".my-so-index_7.11.0_reindex_temp",
- "tempIndexAlias": ".my-so-index_7.11.0_reindex_temp_alias",
- "tempIndexMappings": Object {
- "dynamic": false,
- "properties": Object {
- "type": Object {
- "type": "keyword",
- },
- "typeMigrationVersion": Object {
- "type": "version",
- },
- },
- },
"versionAlias": ".my-so-index_7.11.0",
"versionIndex": ".my-so-index_7.11.0_001",
"waitForMigrationCompletion": false,
@@ -705,10 +545,10 @@ Object {
},
],
Array [
- "[.my-so-index] LEGACY_DELETE RESPONSE",
+ "[.my-so-index] UPDATE_SOURCE_MAPPINGS_PROPERTIES RESPONSE",
],
Array [
- "[.my-so-index] LEGACY_DELETE -> FATAL. took: 0ms.",
+ "[.my-so-index] UPDATE_SOURCE_MAPPINGS_PROPERTIES -> FATAL. took: 0ms.",
Object {
"kibana": Object {
"migrations": Object {
@@ -767,28 +607,13 @@ Object {
"typeB",
"typeC",
],
- "indexTypesMap": Object {
- ".kibana": Array [
- "typeA",
- "typeB",
- "typeC",
- ],
- ".kibana_cases": Array [
- "typeD",
- "typeE",
- ],
- ".kibana_task_manager": Array [
- "task",
- ],
- },
"kibanaVersion": "7.11.0",
"knownTypes": Array [],
"latestMappingsVersions": Object {},
- "legacyIndex": ".my-so-index",
"logs": Array [
Object {
"level": "info",
- "message": "Log from LEGACY_DELETE control state",
+ "message": "Log from UPDATE_SOURCE_MAPPINGS_PROPERTIES control state",
},
Object {
"level": "info",
@@ -804,7 +629,6 @@ Object {
"resolveMigrationFailures": "https://www.elastic.co/docs/troubleshoot/kibana/migration-failures",
"routingAllocationDisabled": "https://www.elastic.co/docs/troubleshoot/kibana/migration-failures#routing-allocation-disabled",
},
- "mustRelocateDocuments": true,
"outdatedDocuments": Array [
Object {
"_id": "1234",
@@ -815,9 +639,6 @@ Object {
"should": Array [],
},
},
- "preMigrationScript": Object {
- "_tag": "None",
- },
"reason": "the fatal reason",
"retryAttempts": 5,
"retryCount": 0,
@@ -826,19 +647,6 @@ Object {
"targetIndexMappings": Object {
"properties": Object {},
},
- "tempIndex": ".my-so-index_7.11.0_reindex_temp",
- "tempIndexAlias": ".my-so-index_7.11.0_reindex_temp_alias",
- "tempIndexMappings": Object {
- "dynamic": false,
- "properties": Object {
- "type": Object {
- "type": "keyword",
- },
- "typeMigrationVersion": Object {
- "type": "version",
- },
- },
- },
"versionAlias": ".my-so-index_7.11.0",
"versionIndex": ".my-so-index_7.11.0_001",
"waitForMigrationCompletion": false,
@@ -852,7 +660,7 @@ Object {
"fatal": Array [],
"info": Array [
Array [
- "[.my-so-index] Log from LEGACY_DELETE control state",
+ "[.my-so-index] Log from UPDATE_SOURCE_MAPPINGS_PROPERTIES control state",
],
Array [
"[.my-so-index] Log from FATAL control state",
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/actions/clone_index.test.ts b/src/core/packages/saved-objects/migration-server-internal/src/actions/clone_index.test.ts
deleted file mode 100644
index 57ab482a26ddc..0000000000000
--- a/src/core/packages/saved-objects/migration-server-internal/src/actions/clone_index.test.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
- * Public License v 1"; you may not use this file except in compliance with, at
- * your election, the "Elastic License 2.0", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-import { errors as EsErrors } from '@elastic/elasticsearch';
-import { cloneIndex } from './clone_index';
-import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
-import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks';
-import { catchRetryableEsClientErrors } from './catch_retryable_es_client_errors';
-
-jest.mock('./catch_retryable_es_client_errors');
-
-describe('cloneIndex', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- // Create a mock client that rejects all methods with a 503 status code
- // response.
- const retryableError = new EsErrors.ResponseError(
- elasticsearchClientMock.createApiResponse({
- statusCode: 503,
- body: { error: { type: 'es_type', reason: 'es_reason' } },
- })
- );
- const client = elasticsearchClientMock.createInternalClient(
- elasticsearchClientMock.createErrorTransportRequestPromise(retryableError)
- );
-
- const nonRetryableError = new Error('crash');
- const clientWithNonRetryableError = elasticsearchClientMock.createInternalClient(
- elasticsearchClientMock.createErrorTransportRequestPromise(nonRetryableError)
- );
-
- it('calls client.indices.clone with the correct parameter for default ES', async () => {
- const statefulCapabilities = elasticsearchServiceMock.createCapabilities({ serverless: false });
- const task = cloneIndex({
- client,
- source: 'my_source_index',
- target: 'my_target_index',
- esCapabilities: statefulCapabilities,
- });
- try {
- await task();
- } catch (e) {
- /** ignore */
- }
- expect(client.indices.clone.mock.calls[0][0]).toMatchInlineSnapshot(`
- Object {
- "index": "my_source_index",
- "settings": Object {
- "index": Object {
- "auto_expand_replicas": "0-1",
- "blocks.write": false,
- "mapping": Object {
- "total_fields": Object {
- "limit": 1500,
- },
- },
- "number_of_shards": 1,
- "priority": 10,
- "refresh_interval": "1s",
- },
- },
- "target": "my_target_index",
- "timeout": "300s",
- "wait_for_active_shards": "all",
- }
- `);
- });
-
- it('resolve left with operation_not_supported for serverless ES', async () => {
- const statelessCapabilities = elasticsearchServiceMock.createCapabilities({ serverless: true });
- const task = cloneIndex({
- client,
- source: 'my_source_index',
- target: 'my_target_index',
- esCapabilities: statelessCapabilities,
- });
- const result = await task();
- expect(result).toMatchInlineSnapshot(`
- Object {
- "_tag": "Left",
- "left": Object {
- "operationName": "clone",
- "type": "operation_not_supported",
- },
- }
- `);
- });
-
- it('calls catchRetryableEsClientErrors when the promise rejects', async () => {
- const task = cloneIndex({
- client,
- source: 'my_source_index',
- target: 'my_target_index',
- esCapabilities: elasticsearchServiceMock.createCapabilities(),
- });
- try {
- await task();
- } catch (e) {
- /** ignore */
- }
- expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError);
- });
-
- it('re-throws non retry-able errors', async () => {
- const task = cloneIndex({
- client: clientWithNonRetryableError,
- source: 'my_source_index',
- target: 'my_target_index',
- esCapabilities: elasticsearchServiceMock.createCapabilities(),
- });
- try {
- await task();
- } catch (e) {
- /** ignore */
- }
- expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(nonRetryableError);
- });
-});
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/actions/clone_index.ts b/src/core/packages/saved-objects/migration-server-internal/src/actions/clone_index.ts
deleted file mode 100644
index c06250ae80e87..0000000000000
--- a/src/core/packages/saved-objects/migration-server-internal/src/actions/clone_index.ts
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
- * Public License v 1"; you may not use this file except in compliance with, at
- * your election, the "Elastic License 2.0", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-import * as Either from 'fp-ts/Either';
-import * as TaskEither from 'fp-ts/TaskEither';
-import { pipe } from 'fp-ts/function';
-import type { errors as EsErrors } from '@elastic/elasticsearch';
-import type {
- ElasticsearchClient,
- ElasticsearchCapabilities,
-} from '@kbn/core-elasticsearch-server';
-import {
- catchRetryableEsClientErrors,
- type RetryableEsClientError,
-} from './catch_retryable_es_client_errors';
-import type { IndexNotFound, AcknowledgeResponse, OperationNotSupported } from '.';
-import { type IndexNotGreenTimeout, waitForIndexStatus } from './wait_for_index_status';
-import {
- DEFAULT_TIMEOUT,
- INDEX_AUTO_EXPAND_REPLICAS,
- INDEX_NUMBER_OF_SHARDS,
- WAIT_FOR_ALL_SHARDS_TO_BE_ACTIVE,
-} from './constants';
-import { isClusterShardLimitExceeded } from './es_errors';
-import type { ClusterShardLimitExceeded } from './create_index';
-
-export type CloneIndexResponse = AcknowledgeResponse;
-
-/** @internal */
-export interface CloneIndexParams {
- client: ElasticsearchClient;
- esCapabilities: ElasticsearchCapabilities;
- source: string;
- target: string;
- /** only used for testing */
- timeout?: string;
-}
-
-/**
- * Makes a clone of the source index into the target.
- *
- * @remarks
- * This method adds some additional logic to the ES clone index API:
- * - it is idempotent, if it gets called multiple times subsequent calls will
- * wait for the first clone operation to complete (up to 60s)
- * - the first call will wait up to 120s for the cluster state and all shards
- * to be updated.
- */
-export const cloneIndex = ({
- client,
- esCapabilities,
- source,
- target,
- timeout = DEFAULT_TIMEOUT,
-}: CloneIndexParams): TaskEither.TaskEither<
- | RetryableEsClientError
- | IndexNotFound
- | IndexNotGreenTimeout
- | ClusterShardLimitExceeded
- | OperationNotSupported,
- CloneIndexResponse
-> => {
- const cloneTask: TaskEither.TaskEither<
- RetryableEsClientError | IndexNotFound | ClusterShardLimitExceeded | OperationNotSupported,
- AcknowledgeResponse
- > = () => {
- // clone is not supported on serverless
- if (esCapabilities.serverless) {
- return Promise.resolve(
- Either.left({
- type: 'operation_not_supported' as const,
- operationName: 'clone',
- })
- );
- }
-
- return client.indices
- .clone({
- index: source,
- target,
- wait_for_active_shards: WAIT_FOR_ALL_SHARDS_TO_BE_ACTIVE,
- settings: {
- index: {
- // The source we're cloning from will have a write block set, so
- // we need to remove it to allow writes to our newly cloned index
- 'blocks.write': false,
- // Increase the fields limit beyond the default of 1000
- mapping: {
- total_fields: { limit: 1500 },
- },
- // The rest of the index settings should have already been applied
- // to the source index and will be copied to the clone target. But
- // we repeat it here for explicitness.
- number_of_shards: INDEX_NUMBER_OF_SHARDS,
- auto_expand_replicas: INDEX_AUTO_EXPAND_REPLICAS,
- // Set an explicit refresh interval so that we don't inherit the
- // value from incorrectly configured index templates (not required
- // after we adopt system indices)
- refresh_interval: '1s',
- // Bump priority so that recovery happens before newer indices
- priority: 10,
- },
- },
- timeout,
- })
- .then((response) => {
- /**
- * - acknowledged=false, we timed out before the cluster state was
- * updated with the newly created index, but it probably will be
- * created sometime soon.
- * - shards_acknowledged=false, we timed out before all shards were
- * started
- * - acknowledged=true, shards_acknowledged=true, cloning complete
- */
- return Either.right({
- acknowledged: response.acknowledged,
- shardsAcknowledged: response.shards_acknowledged,
- });
- })
- .catch((error: EsErrors.ResponseError) => {
- if (error?.body?.error?.type === 'index_not_found_exception') {
- return Either.left({
- type: 'index_not_found_exception' as const,
- index: error.body.error.index,
- });
- } else if (error?.body?.error?.type === 'resource_already_exists_exception') {
- /**
- * If the target index already exists it means a previous clone
- * operation had already been started. However, we can't be sure
- * that all shards were started so return shardsAcknowledged: false
- */
- return Either.right({
- acknowledged: true,
- shardsAcknowledged: false,
- });
- } else if (isClusterShardLimitExceeded(error?.body?.error)) {
- return Either.left({
- type: 'cluster_shard_limit_exceeded' as const,
- });
- } else {
- throw error;
- }
- })
- .catch(catchRetryableEsClientErrors);
- };
-
- return pipe(
- cloneTask,
- TaskEither.chainW((res) => {
- if (res.acknowledged && res.shardsAcknowledged) {
- // If the cluster state was updated and all shards ackd we're done
- return TaskEither.right(res);
- } else {
- // Otherwise, wait until the target index has a 'yellow' status.
- return pipe(
- waitForIndexStatus({ client, index: target, timeout, status: 'green' }),
- TaskEither.map((value) => {
- /** When the index status is 'yellow' we know that all shards were started */
- return { acknowledged: true, shardsAcknowledged: true };
- })
- );
- }
- })
- );
-};
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/actions/index.ts b/src/core/packages/saved-objects/migration-server-internal/src/actions/index.ts
index a7dd110a07231..1aaa8794c3031 100644
--- a/src/core/packages/saved-objects/migration-server-internal/src/actions/index.ts
+++ b/src/core/packages/saved-objects/migration-server-internal/src/actions/index.ts
@@ -28,18 +28,6 @@ export { checkClusterRoutingAllocationEnabled } from './check_cluster_routing_al
export type { FetchIndexResponse, FetchIndicesParams } from './fetch_indices';
export { fetchIndices } from './fetch_indices';
-export type { SafeWriteBlockParams } from './safe_write_block';
-export { safeWriteBlock } from './safe_write_block';
-
-export type { SetWriteBlockParams } from './set_write_block';
-export { setWriteBlock } from './set_write_block';
-
-export type { RemoveWriteBlockParams } from './remove_write_block';
-export { removeWriteBlock } from './remove_write_block';
-
-export type { CloneIndexResponse, CloneIndexParams } from './clone_index';
-export { cloneIndex } from './clone_index';
-
export type {
WaitForIndexStatusParams,
IndexNotYellowTimeout,
@@ -73,14 +61,8 @@ export { transformDocs } from './transform_docs';
export type { RefreshIndexParams } from './refresh_index';
export { refreshIndex } from './refresh_index';
-export type { ReindexResponse, ReindexParams } from './reindex';
-export { reindex } from './reindex';
-
-import type { IncompatibleMappingException } from './wait_for_reindex_task';
-
-export { waitForReindexTask } from './wait_for_reindex_task';
-
import type { AliasNotFound, RemoveIndexNotAConcreteIndex } from './update_aliases';
+import type { IncompatibleMappingException } from './update_mappings';
export type { AliasAction, UpdateAliasesParams } from './update_aliases';
export { updateAliases } from './update_aliases';
@@ -91,8 +73,6 @@ export { waitForDeleteByQueryTask } from './wait_for_delete_by_query_task';
export type { CreateIndexParams, ClusterShardLimitExceeded } from './create_index';
-export { synchronizeMigrators } from './synchronize_migrators';
-
export { createIndex } from './create_index';
export { checkTargetTypesMappings } from './check_target_mappings';
@@ -115,7 +95,6 @@ export {
import type { UnknownDocsFound } from './check_for_unknown_docs';
import type { IncompatibleClusterRoutingAllocation } from './check_cluster_routing_allocation';
import type { ClusterShardLimitExceeded } from './create_index';
-import type { SynchronizationFailed } from './synchronize_migrators';
import type { IndexMappingsIncomplete, TypesAdded, TypesChanged } from './check_target_mappings';
export type {
@@ -149,10 +128,6 @@ export interface OperationNotSupported {
operationName: string;
}
-export interface WaitForReindexTaskFailure {
- readonly cause: { type: string; reason: string };
-}
-
export interface TargetIndexHadWriteBlock {
type: 'target_index_had_write_block';
}
@@ -200,7 +175,6 @@ export interface ActionErrorTypeMap {
index_not_yellow_timeout: IndexNotYellowTimeout;
cluster_shard_limit_exceeded: ClusterShardLimitExceeded;
es_response_too_large: EsResponseTooLargeError;
- synchronization_failed: SynchronizationFailed;
index_mappings_incomplete: IndexMappingsIncomplete;
types_changed: TypesChanged;
types_added: TypesAdded;
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/actions/reindex.test.ts b/src/core/packages/saved-objects/migration-server-internal/src/actions/reindex.test.ts
deleted file mode 100644
index 6c5b8e815e62f..0000000000000
--- a/src/core/packages/saved-objects/migration-server-internal/src/actions/reindex.test.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
- * Public License v 1"; you may not use this file except in compliance with, at
- * your election, the "Elastic License 2.0", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-import * as Option from 'fp-ts/Option';
-import { catchRetryableEsClientErrors } from './catch_retryable_es_client_errors';
-import { errors as EsErrors } from '@elastic/elasticsearch';
-import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
-import { reindex } from './reindex';
-
-jest.mock('./catch_retryable_es_client_errors');
-
-describe('reindex', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- it('calls catchRetryableEsClientErrors when the promise rejects', async () => {
- // Create a mock client that rejects all methods with a 503 status code
- // response.
- const retryableError = new EsErrors.ResponseError(
- elasticsearchClientMock.createApiResponse({
- statusCode: 503,
- body: { error: { type: 'es_type', reason: 'es_reason' } },
- })
- );
- const client = elasticsearchClientMock.createInternalClient(
- elasticsearchClientMock.createErrorTransportRequestPromise(retryableError)
- );
- const task = reindex({
- client,
- sourceIndex: 'my_source_index',
- targetIndex: 'my_target_index',
- reindexScript: Option.none,
- requireAlias: false,
- excludeOnUpgradeQuery: {},
- batchSize: 1000,
- });
- try {
- await task();
- } catch (e) {
- /** ignore */
- }
- expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError);
- });
-
- it('passes options to Elasticsearch client', async () => {
- const client = elasticsearchClientMock.createInternalClient(
- elasticsearchClientMock.createSuccessTransportRequestPromise({
- hits: {
- total: 0,
- hits: [],
- },
- })
- );
- const task = reindex({
- client,
- sourceIndex: 'my_source_index',
- targetIndex: 'my_target_index',
- reindexScript: Option.some('my script'),
- requireAlias: false,
- excludeOnUpgradeQuery: { match_all: {} },
- batchSize: 99,
- });
- try {
- await task();
- } catch (e) {
- /** ignore */
- }
- expect(client.reindex).toHaveBeenCalledTimes(1);
- expect(client.reindex).toHaveBeenCalledWith({
- conflicts: 'proceed',
- source: {
- index: 'my_source_index',
- size: 99,
- query: { match_all: {} },
- },
- dest: {
- index: 'my_target_index',
- op_type: 'create',
- },
- script: { lang: 'painless', source: 'my script' },
- refresh: true,
- require_alias: false,
- wait_for_completion: false,
- });
- });
-});
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/actions/reindex.ts b/src/core/packages/saved-objects/migration-server-internal/src/actions/reindex.ts
deleted file mode 100644
index d1ae5be5450eb..0000000000000
--- a/src/core/packages/saved-objects/migration-server-internal/src/actions/reindex.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
- * Public License v 1"; you may not use this file except in compliance with, at
- * your election, the "Elastic License 2.0", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-import * as Either from 'fp-ts/Either';
-import type * as TaskEither from 'fp-ts/TaskEither';
-import * as Option from 'fp-ts/Option';
-import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
-import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
-import {
- catchRetryableEsClientErrors,
- type RetryableEsClientError,
-} from './catch_retryable_es_client_errors';
-
-/** @internal */
-export interface ReindexResponse {
- taskId: string;
-}
-
-/** @internal */
-export interface ReindexParams {
- client: ElasticsearchClient;
- sourceIndex: string;
- targetIndex: string;
- reindexScript: Option.Option;
- requireAlias: boolean;
- /* When reindexing we use a source query to exclude saved objects types which
- * are no longer used. These saved objects will still be kept in the outdated
- * index for backup purposes, but won't be available in the upgraded index.
- */
- excludeOnUpgradeQuery: QueryDslQueryContainer;
- /** Number of documents Elasticsearch will read/write in each batch */
- batchSize: number;
-}
-
-/**
- * Reindex documents from the `sourceIndex` into the `targetIndex`. Returns a
- * task ID which can be tracked for progress.
- *
- * @remarks This action is idempotent allowing several Kibana instances to run
- * this in parallel. By using `op_type: 'create', conflicts: 'proceed'` there
- * will be only one write per reindexed document.
- */
-export const reindex =
- ({
- client,
- sourceIndex,
- targetIndex,
- reindexScript,
- requireAlias,
- excludeOnUpgradeQuery,
- batchSize,
- }: ReindexParams): TaskEither.TaskEither =>
- () => {
- return client
- .reindex({
- // Require targetIndex to be an alias. Prevents a new index from being
- // created if targetIndex doesn't exist.
- require_alias: requireAlias,
- // Ignore version conflicts from existing documents
- conflicts: 'proceed',
- source: {
- index: sourceIndex,
- // Set reindex batch size
- size: batchSize,
- // Exclude saved object types
- query: excludeOnUpgradeQuery,
- },
- dest: {
- index: targetIndex,
- // Don't override existing documents, only create if missing
- op_type: 'create',
- },
- script: Option.fold(
- () => undefined,
- (script) => ({
- source: script,
- lang: 'painless',
- })
- )(reindexScript),
- // force a refresh so that we can query the target index
- refresh: true,
- // Create a task and return task id instead of blocking until complete
- wait_for_completion: false,
- })
- .then(({ task: taskId }) => {
- return Either.right({ taskId: String(taskId) });
- })
- .catch(catchRetryableEsClientErrors);
- };
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/actions/remove_write_block.test.ts b/src/core/packages/saved-objects/migration-server-internal/src/actions/remove_write_block.test.ts
deleted file mode 100644
index d291b2f228cf4..0000000000000
--- a/src/core/packages/saved-objects/migration-server-internal/src/actions/remove_write_block.test.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
- * Public License v 1"; you may not use this file except in compliance with, at
- * your election, the "Elastic License 2.0", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-import { removeWriteBlock } from './remove_write_block';
-import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
-import { catchRetryableEsClientErrors } from './catch_retryable_es_client_errors';
-import { errors as EsErrors } from '@elastic/elasticsearch';
-
-jest.mock('./catch_retryable_es_client_errors');
-
-describe('removeWriteBlock', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- // Create a mock client that rejects all methods with a 503 status code
- // response.
- const retryableError = new EsErrors.ResponseError(
- elasticsearchClientMock.createApiResponse({
- statusCode: 503,
- body: { error: { type: 'es_type', reason: 'es_reason' } },
- })
- );
- const client = elasticsearchClientMock.createInternalClient(
- elasticsearchClientMock.createErrorTransportRequestPromise(retryableError)
- );
-
- const nonRetryableError = new Error('crash');
- const clientWithNonRetryableError = elasticsearchClientMock.createInternalClient(
- elasticsearchClientMock.createErrorTransportRequestPromise(nonRetryableError)
- );
- it('calls catchRetryableEsClientErrors when the promise rejects', async () => {
- const task = removeWriteBlock({ client, index: 'my_index' });
- try {
- await task();
- } catch (e) {
- /** ignore */
- }
- expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError);
- });
- it('re-throws non retry-able errors', async () => {
- const task = removeWriteBlock({
- client: clientWithNonRetryableError,
- index: 'my_index',
- });
- await task();
- expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(nonRetryableError);
- });
-});
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/actions/remove_write_block.ts b/src/core/packages/saved-objects/migration-server-internal/src/actions/remove_write_block.ts
deleted file mode 100644
index 6bb786cad3f69..0000000000000
--- a/src/core/packages/saved-objects/migration-server-internal/src/actions/remove_write_block.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
- * Public License v 1"; you may not use this file except in compliance with, at
- * your election, the "Elastic License 2.0", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-import * as Either from 'fp-ts/Either';
-import type * as TaskEither from 'fp-ts/TaskEither';
-import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
-import {
- catchRetryableEsClientErrors,
- type RetryableEsClientError,
-} from './catch_retryable_es_client_errors';
-import { DEFAULT_TIMEOUT } from './constants';
-
-/** @internal */
-export interface RemoveWriteBlockParams {
- client: ElasticsearchClient;
- index: string;
- timeout?: string;
-}
-
-/**
- * Removes a write block from an index
- */
-export const removeWriteBlock =
- ({
- client,
- index,
- timeout = DEFAULT_TIMEOUT,
- }: RemoveWriteBlockParams): TaskEither.TaskEither<
- RetryableEsClientError,
- 'remove_write_block_succeeded'
- > =>
- () => {
- return client.indices
- .putSettings({
- index,
- // Don't change any existing settings
- preserve_existing: true,
- settings: {
- blocks: {
- write: false,
- },
- },
- timeout,
- })
- .then((res) => {
- return res.acknowledged === true
- ? Either.right('remove_write_block_succeeded' as const)
- : Either.left({
- type: 'retryable_es_client_error' as const,
- message: 'remove_write_block_failed',
- });
- })
- .catch(catchRetryableEsClientErrors);
- };
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/actions/safe_write_block.test.ts b/src/core/packages/saved-objects/migration-server-internal/src/actions/safe_write_block.test.ts
deleted file mode 100644
index 81455b777c58d..0000000000000
--- a/src/core/packages/saved-objects/migration-server-internal/src/actions/safe_write_block.test.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
- * Public License v 1"; you may not use this file except in compliance with, at
- * your election, the "Elastic License 2.0", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-import * as Either from 'fp-ts/Either';
-import * as TaskEither from 'fp-ts/TaskEither';
-import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
-import { safeWriteBlock } from './safe_write_block';
-
-jest.mock('./set_write_block');
-import { setWriteBlock } from './set_write_block';
-
-const setWriteBlockMock = setWriteBlock as jest.MockedFn;
-
-describe('safeWriteBlock', () => {
- beforeEach(() => {
- setWriteBlockMock.mockReset();
- setWriteBlockMock.mockReturnValueOnce(
- TaskEither.fromEither(Either.right('set_write_block_succeeded' as const))
- );
- });
-
- const client = elasticsearchClientMock.createInternalClient();
- it('returns a Left response if source and target indices match', async () => {
- const task = safeWriteBlock({
- client,
- sourceIndex: '.kibana_8.15.0_001',
- targetIndex: '.kibana_8.15.0_001',
- });
- const res = await task();
- expect(res).toEqual(Either.left({ type: 'source_equals_target', index: '.kibana_8.15.0_001' }));
- expect(setWriteBlockMock).not.toHaveBeenCalled();
- });
-
- it('calls setWriteBlock if indices are different', async () => {
- const task = safeWriteBlock({
- client,
- sourceIndex: '.kibana_7.13.0_001',
- targetIndex: '.kibana_8.15.0_001',
- timeout: '28s',
- });
- const res = await task();
- expect(res).toEqual(Either.right('set_write_block_succeeded' as const));
- expect(setWriteBlockMock).toHaveBeenCalledTimes(1);
- expect(setWriteBlockMock).toHaveBeenCalledWith({
- client,
- index: '.kibana_7.13.0_001',
- timeout: '28s',
- });
- });
-});
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/actions/safe_write_block.ts b/src/core/packages/saved-objects/migration-server-internal/src/actions/safe_write_block.ts
deleted file mode 100644
index a330834b214b4..0000000000000
--- a/src/core/packages/saved-objects/migration-server-internal/src/actions/safe_write_block.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
- * Public License v 1"; you may not use this file except in compliance with, at
- * your election, the "Elastic License 2.0", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-import * as Either from 'fp-ts/Either';
-import * as TaskEither from 'fp-ts/TaskEither';
-import { pipe } from 'fp-ts/function';
-import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
-import type { RetryableEsClientError } from './catch_retryable_es_client_errors';
-import { DEFAULT_TIMEOUT, type SourceEqualsTarget, type IndexNotFound } from '.';
-import { setWriteBlock } from './set_write_block';
-
-/** @internal */
-export interface SafeWriteBlockParams {
- client: ElasticsearchClient;
- sourceIndex: string;
- targetIndex: string;
- timeout?: string;
-}
-
-export const safeWriteBlock = ({
- client,
- sourceIndex,
- targetIndex,
- timeout = DEFAULT_TIMEOUT,
-}: SafeWriteBlockParams): TaskEither.TaskEither<
- SourceEqualsTarget | IndexNotFound | RetryableEsClientError,
- 'set_write_block_succeeded'
-> => {
- const assertSourceAndTargetDifferTask: TaskEither.TaskEither<
- SourceEqualsTarget,
- 'source_and_target_differ'
- > = TaskEither.fromEither(
- sourceIndex === targetIndex
- ? Either.left({ type: 'source_equals_target' as const, index: sourceIndex })
- : Either.right('source_and_target_differ' as const)
- );
-
- return pipe(
- assertSourceAndTargetDifferTask,
- TaskEither.chainW(() => setWriteBlock({ client, index: sourceIndex, timeout }))
- );
-};
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/actions/set_write_block.test.ts b/src/core/packages/saved-objects/migration-server-internal/src/actions/set_write_block.test.ts
deleted file mode 100644
index f5bb949cfe792..0000000000000
--- a/src/core/packages/saved-objects/migration-server-internal/src/actions/set_write_block.test.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
- * Public License v 1"; you may not use this file except in compliance with, at
- * your election, the "Elastic License 2.0", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-import { setWriteBlock } from './set_write_block';
-import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
-import { catchRetryableEsClientErrors } from './catch_retryable_es_client_errors';
-import { errors as EsErrors } from '@elastic/elasticsearch';
-
-jest.mock('./catch_retryable_es_client_errors');
-
-describe('setWriteBlock', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- // Create a mock client that rejects all methods with a 503 status code
- // response.
- const retryableError = new EsErrors.ResponseError(
- elasticsearchClientMock.createApiResponse({
- statusCode: 503,
- body: { error: { type: 'es_type', reason: 'es_reason' } },
- })
- );
- const client = elasticsearchClientMock.createInternalClient(
- elasticsearchClientMock.createErrorTransportRequestPromise(retryableError)
- );
-
- const nonRetryableError = new Error('crash');
- const clientWithNonRetryableError = elasticsearchClientMock.createInternalClient(
- elasticsearchClientMock.createErrorTransportRequestPromise(nonRetryableError)
- );
- it('calls catchRetryableEsClientErrors when the promise rejects', async () => {
- const task = setWriteBlock({ client, index: 'my_index' });
- try {
- await task();
- } catch (e) {
- /** ignore */
- }
- expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError);
- });
- it('re-throws non retry-able errors', async () => {
- const task = setWriteBlock({
- client: clientWithNonRetryableError,
- index: 'my_index',
- });
- await task();
- expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(nonRetryableError);
- });
-});
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/actions/set_write_block.ts b/src/core/packages/saved-objects/migration-server-internal/src/actions/set_write_block.ts
deleted file mode 100644
index 444747f183151..0000000000000
--- a/src/core/packages/saved-objects/migration-server-internal/src/actions/set_write_block.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
- * Public License v 1"; you may not use this file except in compliance with, at
- * your election, the "Elastic License 2.0", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-import * as Either from 'fp-ts/Either';
-import type * as TaskEither from 'fp-ts/TaskEither';
-import { errors as EsErrors } from '@elastic/elasticsearch';
-import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
-import {
- catchRetryableEsClientErrors,
- type RetryableEsClientError,
-} from './catch_retryable_es_client_errors';
-import type { IndexNotFound } from '.';
-import { DEFAULT_TIMEOUT } from '.';
-
-/** @internal */
-export interface SetWriteBlockParams {
- client: ElasticsearchClient;
- index: string;
- timeout?: string;
-}
-
-/**
- * Sets a write block in place for the given index. If the response includes
- * `acknowledged: true` all in-progress writes have drained and no further
- * writes to this index will be possible.
- *
- * The first time the write block is added to an index the response will
- * include `shards_acknowledged: true` but once the block is in place,
- * subsequent calls return `shards_acknowledged: false`
- */
-export const setWriteBlock =
- ({
- client,
- index,
- timeout = DEFAULT_TIMEOUT,
- }: SetWriteBlockParams): TaskEither.TaskEither<
- IndexNotFound | RetryableEsClientError,
- 'set_write_block_succeeded'
- > =>
- () => {
- return (
- client.indices
- .addBlock(
- {
- index,
- block: 'write',
- timeout,
- },
- { maxRetries: 0 /** handle retry ourselves for now */ }
- )
- // not typed yet
- .then((res) => {
- return res.acknowledged === true
- ? Either.right('set_write_block_succeeded' as const)
- : Either.left({
- type: 'retryable_es_client_error' as const,
- message: 'set_write_block_failed',
- });
- })
- .catch((e: EsErrors.ElasticsearchClientError) => {
- if (e instanceof EsErrors.ResponseError) {
- if (e.body?.error?.type === 'index_not_found_exception') {
- return Either.left({ type: 'index_not_found_exception' as const, index });
- }
- }
- throw e;
- })
- .catch(catchRetryableEsClientErrors)
- );
- };
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/actions/synchronize_migrators.test.ts b/src/core/packages/saved-objects/migration-server-internal/src/actions/synchronize_migrators.test.ts
deleted file mode 100644
index 59894649bd4db..0000000000000
--- a/src/core/packages/saved-objects/migration-server-internal/src/actions/synchronize_migrators.test.ts
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
- * Public License v 1"; you may not use this file except in compliance with, at
- * your election, the "Elastic License 2.0", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-import { synchronizeMigrators } from './synchronize_migrators';
-import { type WaitGroup, waitGroup as createWaitGroup } from '../kibana_migrator_utils';
-
-describe('synchronizeMigrators', () => {
- let waitGroups: Array>;
- let allWaitGroupsPromise: Promise;
- let migratorsWaitGroups: Array>;
-
- beforeEach(() => {
- jest.clearAllMocks();
-
- waitGroups = ['.kibana_cases', '.kibana_task_manager', '.kibana'].map(createWaitGroup);
- allWaitGroupsPromise = Promise.all(waitGroups.map(({ promise }) => promise));
-
- migratorsWaitGroups = waitGroups.map(({ resolve, reject }) => ({
- resolve: jest.fn(resolve),
- reject: jest.fn(reject),
- promise: allWaitGroupsPromise,
- }));
- });
-
- describe('when all migrators reach the synchronization point with a correct state', () => {
- it('unblocks all migrators and resolves Right', async () => {
- const tasks = migratorsWaitGroups.map((waitGroup) => synchronizeMigrators({ waitGroup }));
-
- const res = await Promise.all(tasks.map((task) => task()));
-
- migratorsWaitGroups.forEach((waitGroup) =>
- expect(waitGroup.resolve).toHaveBeenCalledTimes(1)
- );
- migratorsWaitGroups.forEach((waitGroup) => expect(waitGroup.reject).not.toHaveBeenCalled());
-
- expect(res).toEqual([
- {
- _tag: 'Right',
- right: {
- data: [undefined, undefined, undefined],
- type: 'synchronization_successful',
- },
- },
- {
- _tag: 'Right',
- right: {
- data: [undefined, undefined, undefined],
- type: 'synchronization_successful',
- },
- },
- {
- _tag: 'Right',
- right: {
- data: [undefined, undefined, undefined],
- type: 'synchronization_successful',
- },
- },
- ]);
- });
-
- it('migrators are not unblocked until the last one reaches the synchronization point', async () => {
- let resolved: number = 0;
- migratorsWaitGroups.forEach((waitGroup) => waitGroup.promise.then(() => ++resolved));
- const [casesDefer, ...otherMigratorsDefers] = migratorsWaitGroups;
-
- // we simulate that only kibana_task_manager and kibana migrators get to the sync point
- const tasks = otherMigratorsDefers.map((waitGroup) => synchronizeMigrators({ waitGroup }));
- // we don't await for them, or we would be locked forever
- Promise.all(tasks.map((task) => task()));
-
- const [taskManagerDefer, kibanaDefer] = otherMigratorsDefers;
- expect(taskManagerDefer.resolve).toHaveBeenCalledTimes(1);
- expect(kibanaDefer.resolve).toHaveBeenCalledTimes(1);
- expect(casesDefer.resolve).not.toHaveBeenCalled();
- expect(resolved).toEqual(0);
-
- // finally, the last migrator gets to the synchronization point
- await synchronizeMigrators({ waitGroup: casesDefer })();
- expect(resolved).toEqual(3);
- });
- });
-
- describe('when one migrator fails and rejects the synchronization defer', () => {
- describe('before the rest of the migrators reach the synchronization point', () => {
- it('synchronizedMigrators resolves Left for the rest of migrators', async () => {
- let resolved: number = 0;
- let errors: number = 0;
- migratorsWaitGroups.forEach((waitGroup) =>
- waitGroup.promise.then(() => ++resolved).catch(() => ++errors)
- );
- const [casesDefer, ...otherMigratorsDefers] = migratorsWaitGroups;
-
- // we first make one random migrator fail and not reach the sync point
- casesDefer.reject('Oops. The cases migrator failed unexpectedly.');
-
- // the other migrators then try to synchronize
- const tasks = otherMigratorsDefers.map((waitGroup) => synchronizeMigrators({ waitGroup }));
-
- expect(Promise.all(tasks.map((task) => task()))).resolves.toEqual([
- {
- _tag: 'Left',
- left: {
- type: 'synchronization_failed',
- error: 'Oops. The cases migrator failed unexpectedly.',
- },
- },
- {
- _tag: 'Left',
- left: {
- type: 'synchronization_failed',
- error: 'Oops. The cases migrator failed unexpectedly.',
- },
- },
- ]);
-
- // force next tick (as we did not await for Promises)
- await new Promise((resolve) => setImmediate(resolve));
- expect(resolved).toEqual(0);
- expect(errors).toEqual(3);
- });
- });
-
- describe('after the rest of the migrators reach the synchronization point', () => {
- it('synchronizedMigrators resolves Left for the rest of migrators', async () => {
- let resolved: number = 0;
- let errors: number = 0;
- migratorsWaitGroups.forEach((waitGroup) =>
- waitGroup.promise.then(() => ++resolved).catch(() => ++errors)
- );
- const [casesDefer, ...otherMigratorsDefers] = migratorsWaitGroups;
-
- // some migrators try to synchronize
- const tasks = otherMigratorsDefers.map((waitGroup) => synchronizeMigrators({ waitGroup }));
-
- // we then make one random migrator fail and not reach the sync point
- casesDefer.reject('Oops. The cases migrator failed unexpectedly.');
-
- expect(Promise.all(tasks.map((task) => task()))).resolves.toEqual([
- {
- _tag: 'Left',
- left: {
- type: 'synchronization_failed',
- error: 'Oops. The cases migrator failed unexpectedly.',
- },
- },
- {
- _tag: 'Left',
- left: {
- type: 'synchronization_failed',
- error: 'Oops. The cases migrator failed unexpectedly.',
- },
- },
- ]);
-
- // force next tick (as we did not await for Promises)
- await new Promise((resolve) => setImmediate(resolve));
- expect(resolved).toEqual(0);
- expect(errors).toEqual(3);
- });
- });
- });
-});
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/actions/synchronize_migrators.ts b/src/core/packages/saved-objects/migration-server-internal/src/actions/synchronize_migrators.ts
deleted file mode 100644
index eaf857361b98f..0000000000000
--- a/src/core/packages/saved-objects/migration-server-internal/src/actions/synchronize_migrators.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
- * Public License v 1"; you may not use this file except in compliance with, at
- * your election, the "Elastic License 2.0", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-import * as Either from 'fp-ts/Either';
-import type * as TaskEither from 'fp-ts/TaskEither';
-import type { WaitGroup } from '../kibana_migrator_utils';
-
-/** @internal */
-export interface SynchronizationFailed {
- type: 'synchronization_failed';
- error: Error;
-}
-
-/** @internal */
-export interface SynchronizationSuccessful {
- type: 'synchronization_successful';
- data: T[];
-}
-
-/** @internal */
-export interface SynchronizeMigratorsParams {
- waitGroup: WaitGroup;
- payload?: T;
-}
-
-export function synchronizeMigrators({
- waitGroup,
- payload,
-}: SynchronizeMigratorsParams): TaskEither.TaskEither<
- SynchronizationFailed,
- SynchronizationSuccessful
-> {
- return () => {
- waitGroup.resolve(payload);
- return waitGroup.promise
- .then((data: T[]) => Either.right({ type: 'synchronization_successful' as const, data }))
- .catch((error) => Either.left({ type: 'synchronization_failed' as const, error }));
- };
-}
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/actions/update_aliases.test.ts b/src/core/packages/saved-objects/migration-server-internal/src/actions/update_aliases.test.ts
index 3201e3e1f47a4..1ee64f128d3f1 100644
--- a/src/core/packages/saved-objects/migration-server-internal/src/actions/update_aliases.test.ts
+++ b/src/core/packages/saved-objects/migration-server-internal/src/actions/update_aliases.test.ts
@@ -11,7 +11,6 @@ import { catchRetryableEsClientErrors } from './catch_retryable_es_client_errors
import { errors as EsErrors } from '@elastic/elasticsearch';
import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
import { updateAliases } from './update_aliases';
-import { setWriteBlock } from './set_write_block';
jest.mock('./catch_retryable_es_client_errors');
@@ -47,10 +46,7 @@ describe('updateAliases', () => {
expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError);
});
it('re-throws non retry-able errors', async () => {
- const task = setWriteBlock({
- client: clientWithNonRetryableError,
- index: 'my_index',
- });
+ const task = updateAliases({ client: clientWithNonRetryableError, aliasActions: [] });
await task();
expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(nonRetryableError);
});
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/actions/wait_for_reindex_task.test.ts b/src/core/packages/saved-objects/migration-server-internal/src/actions/wait_for_reindex_task.test.ts
deleted file mode 100644
index 25c77f7d946ba..0000000000000
--- a/src/core/packages/saved-objects/migration-server-internal/src/actions/wait_for_reindex_task.test.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
- * Public License v 1"; you may not use this file except in compliance with, at
- * your election, the "Elastic License 2.0", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-import { catchRetryableEsClientErrors } from './catch_retryable_es_client_errors';
-import { errors as EsErrors } from '@elastic/elasticsearch';
-import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
-import { waitForReindexTask } from './wait_for_reindex_task';
-import { setWriteBlock } from './set_write_block';
-
-jest.mock('./catch_retryable_es_client_errors');
-
-describe('waitForReindexTask', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- // Create a mock client that rejects all methods with a 503 status code
- // response.
- const retryableError = new EsErrors.ResponseError(
- elasticsearchClientMock.createApiResponse({
- statusCode: 503,
- body: { error: { type: 'es_type', reason: 'es_reason' } },
- })
- );
- const client = elasticsearchClientMock.createInternalClient(
- elasticsearchClientMock.createErrorTransportRequestPromise(retryableError)
- );
-
- const nonRetryableError = new Error('crash');
- const clientWithNonRetryableError = elasticsearchClientMock.createInternalClient(
- elasticsearchClientMock.createErrorTransportRequestPromise(nonRetryableError)
- );
-
- it('calls catchRetryableEsClientErrors when the promise rejects', async () => {
- const task = waitForReindexTask({ client, taskId: 'my task id', timeout: '60s' });
- try {
- await task();
- } catch (e) {
- /** ignore */
- }
-
- expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError);
- });
- it('re-throws non retry-able errors', async () => {
- const task = setWriteBlock({
- client: clientWithNonRetryableError,
- index: 'my_index',
- });
- await task();
- expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(nonRetryableError);
- });
-});
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/actions/wait_for_reindex_task.ts b/src/core/packages/saved-objects/migration-server-internal/src/actions/wait_for_reindex_task.ts
deleted file mode 100644
index 2142de105fb6b..0000000000000
--- a/src/core/packages/saved-objects/migration-server-internal/src/actions/wait_for_reindex_task.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
- * Public License v 1"; you may not use this file except in compliance with, at
- * your election, the "Elastic License 2.0", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-import * as TaskEither from 'fp-ts/TaskEither';
-import * as Option from 'fp-ts/Option';
-import { flow } from 'fp-ts/function';
-import type { RetryableEsClientError } from './catch_retryable_es_client_errors';
-import type { IndexNotFound, TargetIndexHadWriteBlock } from '.';
-import { waitForTask, type WaitForTaskCompletionTimeout } from './wait_for_task';
-import { isWriteBlockException, isIncompatibleMappingException } from './es_errors';
-
-export interface IncompatibleMappingException {
- type: 'incompatible_mapping_exception';
-}
-
-export const waitForReindexTask = flow(
- waitForTask,
- TaskEither.chain(
- (
- res
- ): TaskEither.TaskEither<
- | IndexNotFound
- | TargetIndexHadWriteBlock
- | IncompatibleMappingException
- | RetryableEsClientError
- | WaitForTaskCompletionTimeout,
- 'reindex_succeeded'
- > => {
- if (Option.isSome(res.error)) {
- if (res.error.value.type === 'index_not_found_exception') {
- return TaskEither.left({
- type: 'index_not_found_exception' as const,
- index: res.error.value.index!,
- });
- } else {
- throw new Error('Reindex failed with the following error:\n' + JSON.stringify(res.error));
- }
- } else if (Option.isSome(res.failures)) {
- const failureCauses = res.failures.value.map((failure) => failure.cause);
- if (failureCauses.every(isWriteBlockException)) {
- return TaskEither.left({ type: 'target_index_had_write_block' as const });
- } else if (failureCauses.every(isIncompatibleMappingException)) {
- return TaskEither.left({ type: 'incompatible_mapping_exception' as const });
- } else {
- throw new Error(
- 'Reindex failed with the following failures:\n' + JSON.stringify(res.failures.value)
- );
- }
- } else {
- return TaskEither.right('reindex_succeeded' as const);
- }
- }
- )
-);
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/initial_state.test.ts b/src/core/packages/saved-objects/migration-server-internal/src/initial_state.test.ts
index 3244eb2f394a5..8b7702532bbdb 100644
--- a/src/core/packages/saved-objects/migration-server-internal/src/initial_state.test.ts
+++ b/src/core/packages/saved-objects/migration-server-internal/src/initial_state.test.ts
@@ -8,7 +8,6 @@
*/
import { ByteSizeValue } from '@kbn/config-schema';
-import * as Option from 'fp-ts/Option';
import type { DocLinksServiceSetup } from '@kbn/core-doc-links-server';
import { docLinksServiceMock } from '@kbn/core-doc-links-server-mocks';
import {
@@ -32,18 +31,10 @@ const migrationsConfig = {
maxReadBatchSizeBytes: ByteSizeValue.parse('500mb'),
} as unknown as SavedObjectsMigrationConfigType;
-const indexTypesMap = {
- '.kibana': ['typeA', 'typeB', 'typeC'],
- '.kibana_task_manager': ['task'],
- '.kibana_cases': ['typeD', 'typeE'],
-};
-
const createInitialStateCommonParams = {
kibanaVersion: '8.1.0',
waitForMigrationCompletion: false,
- mustRelocateDocuments: true,
indexTypes: ['typeA', 'typeB', 'typeC'],
- indexTypesMap,
hashToVersionMap: {
'typeA|someHash': '10.1.0',
'typeB|someHash': '10.1.0',
@@ -52,9 +43,6 @@ const createInitialStateCommonParams = {
targetIndexMappings: {
dynamic: 'strict',
properties: { my_type: { properties: { title: { type: 'text' } } } },
- _meta: {
- indexTypesMap,
- },
} as IndexMapping,
coreMigrationVersionPerType: {},
migrationVersionPerType: {},
@@ -163,20 +151,6 @@ describe('createInitialState', () => {
"typeB",
"typeC",
],
- "indexTypesMap": Object {
- ".kibana": Array [
- "typeA",
- "typeB",
- "typeC",
- ],
- ".kibana_cases": Array [
- "typeD",
- "typeE",
- ],
- ".kibana_task_manager": Array [
- "task",
- ],
- },
"kibanaVersion": "8.1.0",
"knownTypes": Array [
"foo",
@@ -186,7 +160,6 @@ describe('createInitialState', () => {
"bar": "10.2.0",
"foo": "10.0.0",
},
- "legacyIndex": ".kibana_task_manager",
"logs": Array [],
"maxBatchSize": 1000,
"maxBatchSizeBytes": 104857600,
@@ -197,36 +170,16 @@ describe('createInitialState', () => {
"resolveMigrationFailures": "https://www.elastic.co/docs/troubleshoot/kibana/migration-failures",
"routingAllocationDisabled": "https://www.elastic.co/docs/troubleshoot/kibana/migration-failures#routing-allocation-disabled",
},
- "mustRelocateDocuments": true,
"outdatedDocumentsQuery": Object {
"bool": Object {
"should": Array [],
},
},
- "preMigrationScript": Object {
- "_tag": "None",
- },
"retryAttempts": 15,
"retryCount": 0,
"retryDelay": 0,
"skipRetryReset": false,
"targetIndexMappings": Object {
- "_meta": Object {
- "indexTypesMap": Object {
- ".kibana": Array [
- "typeA",
- "typeB",
- "typeC",
- ],
- ".kibana_cases": Array [
- "typeD",
- "typeE",
- ],
- ".kibana_task_manager": Array [
- "task",
- ],
- },
- },
"dynamic": "strict",
"properties": Object {
"my_type": Object {
@@ -238,19 +191,6 @@ describe('createInitialState', () => {
},
},
},
- "tempIndex": ".kibana_task_manager_8.1.0_reindex_temp",
- "tempIndexAlias": ".kibana_task_manager_8.1.0_reindex_temp_alias",
- "tempIndexMappings": Object {
- "dynamic": false,
- "properties": Object {
- "type": Object {
- "type": "keyword",
- },
- "typeMigrationVersion": Object {
- "type": "version",
- },
- },
- },
"versionAlias": ".kibana_task_manager_8.1.0",
"versionIndex": ".kibana_task_manager_8.1.0_001",
"waitForMigrationCompletion": true,
@@ -289,35 +229,12 @@ describe('createInitialState', () => {
expect(initialState.excludeFromUpgradeFilterHooks).toEqual({ baz: fooExcludeOnUpgradeHook });
});
- it('returns state with a preMigration script', () => {
- const preMigrationScript = "ctx._id = ctx._source.type + ':' + ctx._id";
- const initialState = createInitialState({
- ...createInitialStateParams,
- preMigrationScript,
- });
-
- expect(Option.isSome(initialState.preMigrationScript)).toEqual(true);
- expect((initialState.preMigrationScript as Option.Some).value).toEqual(
- preMigrationScript
- );
- });
- it('returns state without a preMigration script', () => {
- expect(
- Option.isNone(
- createInitialState({
- ...createInitialStateParams,
- preMigrationScript: undefined,
- }).preMigrationScript
- )
- ).toEqual(true);
- });
it('returns state with an outdatedDocumentsQuery', () => {
jest.spyOn(getOutdatedDocumentsQueryModule, 'getOutdatedDocumentsQuery');
expect(
createInitialState({
...createInitialStateParams,
- preMigrationScript: "ctx._id = ctx._source.type + ':' + ctx._id",
coreMigrationVersionPerType: {},
migrationVersionPerType: { my_dashboard: '7.10.1', my_viz: '8.0.0' },
}).outdatedDocumentsQuery
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/initial_state.ts b/src/core/packages/saved-objects/migration-server-internal/src/initial_state.ts
index 38e975f8c81f7..481e28231ddf6 100644
--- a/src/core/packages/saved-objects/migration-server-internal/src/initial_state.ts
+++ b/src/core/packages/saved-objects/migration-server-internal/src/initial_state.ts
@@ -7,13 +7,11 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import * as Option from 'fp-ts/Option';
import type { DocLinksServiceStart } from '@kbn/core-doc-links-server';
import type { Logger } from '@kbn/logging';
import type { ISavedObjectTypeRegistry } from '@kbn/core-saved-objects-server';
import type {
IndexMapping,
- IndexTypesMap,
SavedObjectsMigrationConfigType,
} from '@kbn/core-saved-objects-base-server-internal';
import { getLatestMappingsVirtualVersionMap } from '@kbn/core-saved-objects-base-server-internal';
@@ -24,17 +22,13 @@ import {
} from './get_outdated_documents_query';
import type { InitState } from './state';
import { buildExcludeUnusedTypesQuery } from './core';
-import { getTempIndexName } from './model/helpers';
export interface CreateInitialStateParams extends OutdatedDocumentsQueryParams {
kibanaVersion: string;
waitForMigrationCompletion: boolean;
- mustRelocateDocuments: boolean;
indexTypes: string[];
- indexTypesMap: IndexTypesMap;
hashToVersionMap: Record;
targetIndexMappings: IndexMapping;
- preMigrationScript?: string;
indexPrefix: string;
migrationsConfig: SavedObjectsMigrationConfigType;
typeRegistry: ISavedObjectTypeRegistry;
@@ -43,28 +37,15 @@ export interface CreateInitialStateParams extends OutdatedDocumentsQueryParams {
esCapabilities: ElasticsearchCapabilities;
}
-const TEMP_INDEX_MAPPINGS: IndexMapping = {
- dynamic: false,
- properties: {
- type: { type: 'keyword' },
- typeMigrationVersion: {
- type: 'version',
- },
- },
-};
-
/**
* Construct the initial state for the model
*/
export const createInitialState = ({
kibanaVersion,
waitForMigrationCompletion,
- mustRelocateDocuments,
indexTypes,
- indexTypesMap,
hashToVersionMap,
targetIndexMappings,
- preMigrationScript,
coreMigrationVersionPerType,
migrationVersionPerType,
indexPrefix,
@@ -110,21 +91,14 @@ export const createInitialState = ({
return {
controlState: 'INIT',
waitForMigrationCompletion,
- mustRelocateDocuments,
indexTypes,
- indexTypesMap,
hashToVersionMap,
indexPrefix,
- legacyIndex: indexPrefix,
currentAlias: indexPrefix,
versionAlias: `${indexPrefix}_${kibanaVersion}`,
versionIndex: `${indexPrefix}_${kibanaVersion}_001`,
- tempIndex: getTempIndexName(indexPrefix, kibanaVersion),
- tempIndexAlias: getTempIndexName(indexPrefix, kibanaVersion) + '_alias',
kibanaVersion,
- preMigrationScript: Option.fromNullable(preMigrationScript),
targetIndexMappings,
- tempIndexMappings: TEMP_INDEX_MAPPINGS,
outdatedDocumentsQuery,
retryCount: 0,
skipRetryReset: false,
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/kibana_migrator.test.ts b/src/core/packages/saved-objects/migration-server-internal/src/kibana_migrator.test.ts
index 9231a2d46656f..cec226f803a6e 100644
--- a/src/core/packages/saved-objects/migration-server-internal/src/kibana_migrator.test.ts
+++ b/src/core/packages/saved-objects/migration-server-internal/src/kibana_migrator.test.ts
@@ -270,14 +270,6 @@ const mockOptions = (algorithm: 'v2' | 'zdt' = 'v2'): KibanaMigratorOptions => {
logger: loggingSystemMock.create().get(),
kibanaVersion: '8.2.3',
waitForMigrationCompletion: false,
- defaultIndexTypesMap: {
- '.my_index': ['testtype', 'testtype2'],
- '.task_index': ['testtasktype'],
- // this index no longer has any types registered in typeRegistry
- // but we still need a migrator for it, so that 'testtype3' documents
- // are moved over to their new index (.my_index)
- '.my_complementary_index': ['testtype3'],
- },
hashToVersionMap: {},
typeRegistry: createRegistry([
// typeRegistry depicts an updated index map:
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/kibana_migrator.ts b/src/core/packages/saved-objects/migration-server-internal/src/kibana_migrator.ts
index 85265d7f8b3b9..7249e160854e0 100644
--- a/src/core/packages/saved-objects/migration-server-internal/src/kibana_migrator.ts
+++ b/src/core/packages/saved-objects/migration-server-internal/src/kibana_migrator.ts
@@ -25,7 +25,6 @@ import type { SavedObjectUnsanitizedDoc } from '@kbn/core-saved-objects-server';
import {
type IKibanaMigrator,
type IndexMapping,
- type IndexTypesMap,
type ISavedObjectTypeRegistryInternal,
type KibanaMigratorStatus,
type MigrateDocumentOptions,
@@ -44,7 +43,6 @@ import { runV2Migration } from './run_v2_migration';
export interface KibanaMigratorOptions {
client: ElasticsearchClient;
typeRegistry: ISavedObjectTypeRegistryInternal;
- defaultIndexTypesMap: IndexTypesMap;
hashToVersionMap: Record;
soMigrationsConfig: SavedObjectsMigrationConfigType;
kibanaIndex: string;
@@ -68,7 +66,6 @@ export class KibanaMigrator implements IKibanaMigrator {
private readonly log: Logger;
private readonly mappingProperties: SavedObjectsTypeMappingDefinitions;
private readonly typeRegistry: ISavedObjectTypeRegistryInternal;
- private readonly defaultIndexTypesMap: IndexTypesMap;
private readonly hashToVersionMap: Record;
private readonly serializer: SavedObjectsSerializer;
private migrationResult?: Promise;
@@ -100,7 +97,6 @@ export class KibanaMigrator implements IKibanaMigrator {
client,
typeRegistry,
kibanaIndex,
- defaultIndexTypesMap,
hashToVersionMap,
soMigrationsConfig,
kibanaVersion,
@@ -115,7 +111,6 @@ export class KibanaMigrator implements IKibanaMigrator {
this.kibanaIndex = kibanaIndex;
this.soMigrationsConfig = soMigrationsConfig;
this.typeRegistry = typeRegistry;
- this.defaultIndexTypesMap = defaultIndexTypesMap;
this.hashToVersionMap = hashToVersionMap;
this.serializer = new SavedObjectsSerializer(this.typeRegistry);
// build mappings.properties for all types, all indices
@@ -208,7 +203,6 @@ export class KibanaMigrator implements IKibanaMigrator {
kibanaVersion: this.kibanaVersion,
kibanaIndexPrefix: this.kibanaIndex,
typeRegistry: this.typeRegistry,
- defaultIndexTypesMap: this.defaultIndexTypesMap,
hashToVersionMap: this.hashToVersionMap,
logger,
documentMigrator: this.documentMigrator,
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/kibana_migrator_constants.ts b/src/core/packages/saved-objects/migration-server-internal/src/kibana_migrator_constants.ts
index a7a050a81d454..d2650f0701c97 100644
--- a/src/core/packages/saved-objects/migration-server-internal/src/kibana_migrator_constants.ts
+++ b/src/core/packages/saved-objects/migration-server-internal/src/kibana_migrator_constants.ts
@@ -7,19 +7,6 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-export enum TypeStatus {
- Added = 'added',
- Removed = 'removed',
- Moved = 'moved',
- Untouched = 'untouched',
-}
-
-export interface TypeStatusDetails {
- currentIndex?: string;
- targetIndex?: string;
- status: TypeStatus;
-}
-
// ensure plugins don't try to convert SO namespaceTypes after 8.0.0
// see https://github.com/elastic/kibana/issues/147344
export const ALLOWED_CONVERT_VERSION = '8.0.0';
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/kibana_migrator_utils.test.ts b/src/core/packages/saved-objects/migration-server-internal/src/kibana_migrator_utils.test.ts
deleted file mode 100644
index 631546ddc95a1..0000000000000
--- a/src/core/packages/saved-objects/migration-server-internal/src/kibana_migrator_utils.test.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
- * Public License v 1"; you may not use this file except in compliance with, at
- * your election, the "Elastic License 2.0", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-import { DEFAULT_INDEX_TYPES_MAP } from '@kbn/core-saved-objects-base-server-internal';
-import {
- calculateTypeStatuses,
- createWaitGroupMap,
- getIndicesInvolvedInRelocation,
- indexMapToIndexTypesMap,
-} from './kibana_migrator_utils';
-import { INDEX_MAP_BEFORE_SPLIT } from './kibana_migrator_utils.fixtures';
-
-describe('createWaitGroupMap', () => {
- it('creates defer objects with the same Promise', () => {
- const defers = createWaitGroupMap(['.kibana', '.kibana_cases']);
- expect(Object.keys(defers)).toHaveLength(2);
- expect(defers['.kibana'].promise).toEqual(defers['.kibana_cases'].promise);
- expect(defers['.kibana'].resolve).not.toEqual(defers['.kibana_cases'].resolve);
- expect(defers['.kibana'].reject).not.toEqual(defers['.kibana_cases'].reject);
- });
-
- it('the common Promise resolves when all defers resolve', async () => {
- const defers = createWaitGroupMap(['.kibana', '.kibana_cases']);
- let resolved = 0;
- Object.values(defers).forEach((defer) => defer.promise.then(() => ++resolved));
- defers['.kibana'].resolve();
- await new Promise((resolve) => setImmediate(resolve)); // next tick
- expect(resolved).toEqual(0);
- defers['.kibana_cases'].resolve();
- await new Promise((resolve) => setImmediate(resolve)); // next tick
- expect(resolved).toEqual(2);
- });
-});
-
-describe('getIndicesInvolvedInRelocation', () => {
- it('returns the list of types that have moved to different indices', async () => {
- const indices = getIndicesInvolvedInRelocation(
- { '.my_index': ['testtype', 'testtype2', 'testtype3'], '.task_index': ['testtasktype'] },
- {
- '.my_index': ['testtype', 'testtype3'],
- '.other_index': ['testtype2'],
- '.task_index': ['testtasktype'],
- }
- );
-
- expect(indices).toEqual(['.my_index', '.other_index']);
- });
-});
-
-describe('indexMapToIndexTypesMap', () => {
- it('converts IndexMap to IndexTypesMap', () => {
- expect(indexMapToIndexTypesMap(INDEX_MAP_BEFORE_SPLIT)).toEqual(DEFAULT_INDEX_TYPES_MAP);
- });
-});
-
-describe('calculateTypeStatuses', () => {
- it('takes two indexTypesMaps and checks what types have been added, removed and relocated', () => {
- const currentIndexTypesMap = {
- '.indexA': ['type1', 'type2', 'type3'],
- '.indexB': ['type4', 'type5', 'type6'],
- };
- const desiredIndexTypesMap = {
- '.indexA': ['type2'],
- '.indexB': ['type3', 'type5'],
- '.indexC': ['type4', 'type6', 'type7'],
- '.indexD': ['type8'],
- };
-
- expect(calculateTypeStatuses(currentIndexTypesMap, desiredIndexTypesMap)).toEqual({
- type1: {
- currentIndex: '.indexA',
- status: 'removed',
- },
- type2: {
- currentIndex: '.indexA',
- status: 'untouched',
- targetIndex: '.indexA',
- },
- type3: {
- currentIndex: '.indexA',
- status: 'moved',
- targetIndex: '.indexB',
- },
- type4: {
- currentIndex: '.indexB',
- status: 'moved',
- targetIndex: '.indexC',
- },
- type5: {
- currentIndex: '.indexB',
- status: 'untouched',
- targetIndex: '.indexB',
- },
- type6: {
- currentIndex: '.indexB',
- status: 'moved',
- targetIndex: '.indexC',
- },
- type7: {
- status: 'added',
- targetIndex: '.indexC',
- },
- type8: {
- status: 'added',
- targetIndex: '.indexD',
- },
- });
- });
-});
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/kibana_migrator_utils.ts b/src/core/packages/saved-objects/migration-server-internal/src/kibana_migrator_utils.ts
deleted file mode 100644
index 3c10b6df03b9f..0000000000000
--- a/src/core/packages/saved-objects/migration-server-internal/src/kibana_migrator_utils.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
- * Public License v 1"; you may not use this file except in compliance with, at
- * your election, the "Elastic License 2.0", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-import type { IndexTypesMap } from '@kbn/core-saved-objects-base-server-internal';
-import type { IndexMap } from './core';
-import { TypeStatus, type TypeStatusDetails } from './kibana_migrator_constants';
-
-// even though this utility class is present in @kbn/kibana-utils-plugin, we can't easily import it from Core
-// aka. one does not simply reuse code
-class Defer {
- public resolve!: (data?: T) => void;
- public reject!: (error: any) => void;
- public promise: Promise = new Promise((resolve, reject) => {
- (this as any).resolve = resolve;
- (this as any).reject = reject;
- });
-}
-
-export type WaitGroup = Defer;
-
-export function waitGroup(): WaitGroup {
- return new Defer();
-}
-
-export function createWaitGroupMap(keys: string[]): Record> {
- if (!keys?.length) {
- return {};
- }
-
- const defers: Array> = keys.map(() => waitGroup());
-
- // every member of the WaitGroup will wait for all members to resolve
- const all = Promise.all(defers.map(({ promise }) => promise));
-
- return keys.reduce>>((acc, indexName, i) => {
- const { resolve, reject } = defers[i];
- acc[indexName] = { resolve, reject, promise: all };
- return acc;
- }, {});
-}
-
-export function getIndicesInvolvedInRelocation(
- currentIndexTypesMap: IndexTypesMap,
- desiredIndexTypesMap: IndexTypesMap
-): string[] {
- const indicesWithRelocatingTypesSet = new Set();
-
- const typeIndexDistribution = calculateTypeStatuses(currentIndexTypesMap, desiredIndexTypesMap);
-
- Object.values(typeIndexDistribution)
- .filter(({ status }) => status === TypeStatus.Moved)
- .forEach(({ currentIndex, targetIndex }) => {
- indicesWithRelocatingTypesSet.add(currentIndex!);
- indicesWithRelocatingTypesSet.add(targetIndex!);
- });
-
- return Array.from(indicesWithRelocatingTypesSet);
-}
-
-export function indexMapToIndexTypesMap(indexMap: IndexMap): IndexTypesMap {
- return Object.entries(indexMap).reduce((acc, [indexAlias, { typeMappings }]) => {
- acc[indexAlias] = Object.keys(typeMappings).sort();
- return acc;
- }, {});
-}
-
-export function calculateTypeStatuses(
- currentIndexTypesMap: IndexTypesMap,
- desiredIndexTypesMap: IndexTypesMap
-): Record {
- const statuses: Record = {};
-
- Object.entries(currentIndexTypesMap).forEach(([currentIndex, types]) => {
- types.forEach((type) => {
- statuses[type] = {
- currentIndex,
- status: TypeStatus.Removed, // type is removed unless we still have it
- };
- });
- });
-
- Object.entries(desiredIndexTypesMap).forEach(([targetIndex, types]) => {
- types.forEach((type) => {
- if (!statuses[type]) {
- statuses[type] = {
- targetIndex,
- status: TypeStatus.Added, // type didn't exist, it must be new
- };
- } else {
- statuses[type].targetIndex = targetIndex;
- statuses[type].status =
- statuses[type].currentIndex === targetIndex ? TypeStatus.Untouched : TypeStatus.Moved;
- }
- });
- });
-
- return statuses;
-}
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/migrations_state_action_machine.test.ts b/src/core/packages/saved-objects/migration-server-internal/src/migrations_state_action_machine.test.ts
index bafada3474358..fbcf52cb9adba 100644
--- a/src/core/packages/saved-objects/migration-server-internal/src/migrations_state_action_machine.test.ts
+++ b/src/core/packages/saved-objects/migration-server-internal/src/migrations_state_action_machine.test.ts
@@ -44,13 +44,7 @@ describe('migrationsStateActionMachine', () => {
const initialState = createInitialState({
kibanaVersion: '7.11.0',
waitForMigrationCompletion: false,
- mustRelocateDocuments: true,
indexTypes: ['typeA', 'typeB', 'typeC'],
- indexTypesMap: {
- '.kibana': ['typeA', 'typeB', 'typeC'],
- '.kibana_task_manager': ['task'],
- '.kibana_cases': ['typeD', 'typeE'],
- },
hashToVersionMap: {
'typeA|someHash': '10.1.0',
'typeB|someHash': '10.1.0',
@@ -114,7 +108,12 @@ describe('migrationsStateActionMachine', () => {
await migrationStateActionMachine({
initialState,
logger: mockLogger.get(),
- model: transitionModel(['LEGACY_REINDEX', 'LEGACY_DELETE', 'LEGACY_DELETE', 'DONE']),
+ model: transitionModel([
+ 'WAIT_FOR_YELLOW_SOURCE',
+ 'UPDATE_SOURCE_MAPPINGS_PROPERTIES',
+ 'UPDATE_SOURCE_MAPPINGS_PROPERTIES',
+ 'DONE',
+ ]),
next,
abort,
});
@@ -136,7 +135,7 @@ describe('migrationsStateActionMachine', () => {
],
} as State,
logger: mockLogger.get(),
- model: transitionModel(['LEGACY_DELETE', 'FATAL']),
+ model: transitionModel(['UPDATE_SOURCE_MAPPINGS_PROPERTIES', 'FATAL']),
next,
abort,
}).catch((err) => err);
@@ -152,7 +151,12 @@ describe('migrationsStateActionMachine', () => {
migrationStateActionMachine({
initialState,
logger,
- model: transitionModel(['LEGACY_REINDEX', 'LEGACY_DELETE', 'LEGACY_DELETE', 'DONE']),
+ model: transitionModel([
+ 'WAIT_FOR_YELLOW_SOURCE',
+ 'UPDATE_SOURCE_MAPPINGS_PROPERTIES',
+ 'UPDATE_SOURCE_MAPPINGS_PROPERTIES',
+ 'DONE',
+ ]),
next,
abort,
})
@@ -165,9 +169,9 @@ describe('migrationsStateActionMachine', () => {
expect(stateTransitionLogs).toMatchInlineSnapshot(`
Array [
- "[.my-so-index] Log from LEGACY_REINDEX control state",
- "[.my-so-index] Log from LEGACY_DELETE control state",
- "[.my-so-index] Log from LEGACY_DELETE control state",
+ "[.my-so-index] Log from WAIT_FOR_YELLOW_SOURCE control state",
+ "[.my-so-index] Log from UPDATE_SOURCE_MAPPINGS_PROPERTIES control state",
+ "[.my-so-index] Log from UPDATE_SOURCE_MAPPINGS_PROPERTIES control state",
"[.my-so-index] Log from DONE control state",
]
`);
@@ -178,7 +182,12 @@ describe('migrationsStateActionMachine', () => {
migrationStateActionMachine({
initialState,
logger: mockLogger.get(),
- model: transitionModel(['LEGACY_REINDEX', 'LEGACY_DELETE', 'LEGACY_DELETE', 'DONE']),
+ model: transitionModel([
+ 'WAIT_FOR_YELLOW_SOURCE',
+ 'UPDATE_SOURCE_MAPPINGS_PROPERTIES',
+ 'UPDATE_SOURCE_MAPPINGS_PROPERTIES',
+ 'DONE',
+ ]),
next,
abort,
})
@@ -190,7 +199,12 @@ describe('migrationsStateActionMachine', () => {
migrationStateActionMachine({
initialState: { ...initialState, ...{ sourceIndex: Option.some('source-index') } },
logger: mockLogger.get(),
- model: transitionModel(['LEGACY_REINDEX', 'LEGACY_DELETE', 'LEGACY_DELETE', 'DONE']),
+ model: transitionModel([
+ 'WAIT_FOR_YELLOW_SOURCE',
+ 'UPDATE_SOURCE_MAPPINGS_PROPERTIES',
+ 'UPDATE_SOURCE_MAPPINGS_PROPERTIES',
+ 'DONE',
+ ]),
next,
abort,
})
@@ -202,7 +216,12 @@ describe('migrationsStateActionMachine', () => {
migrationStateActionMachine({
initialState: { ...initialState, ...{ sourceIndex: Option.none } },
logger: mockLogger.get(),
- model: transitionModel(['LEGACY_REINDEX', 'LEGACY_DELETE', 'LEGACY_DELETE', 'DONE']),
+ model: transitionModel([
+ 'WAIT_FOR_YELLOW_SOURCE',
+ 'UPDATE_SOURCE_MAPPINGS_PROPERTIES',
+ 'UPDATE_SOURCE_MAPPINGS_PROPERTIES',
+ 'DONE',
+ ]),
next,
abort,
})
@@ -214,7 +233,11 @@ describe('migrationsStateActionMachine', () => {
migrationStateActionMachine({
initialState: { ...initialState, reason: 'the fatal reason' } as State,
logger: mockLogger.get(),
- model: transitionModel(['LEGACY_REINDEX', 'LEGACY_DELETE', 'FATAL']),
+ model: transitionModel([
+ 'WAIT_FOR_YELLOW_SOURCE',
+ 'UPDATE_SOURCE_MAPPINGS_PROPERTIES',
+ 'FATAL',
+ ]),
next,
abort,
})
@@ -228,7 +251,11 @@ describe('migrationsStateActionMachine', () => {
migrationStateActionMachine({
initialState: { ...initialState, reason: 'the fatal reason' } as State,
logger: mockLogger.get(),
- model: transitionModel(['LEGACY_REINDEX', 'LEGACY_DELETE', 'FATAL']),
+ model: transitionModel([
+ 'WAIT_FOR_YELLOW_SOURCE',
+ 'UPDATE_SOURCE_MAPPINGS_PROPERTIES',
+ 'FATAL',
+ ]),
next: () => {
throw new errors.ResponseError(
elasticsearchClientMock.createApiResponse({
@@ -270,7 +297,11 @@ describe('migrationsStateActionMachine', () => {
migrationStateActionMachine({
initialState: { ...initialState, reason: 'the fatal reason' } as State,
logger: mockLogger.get(),
- model: transitionModel(['LEGACY_REINDEX', 'LEGACY_DELETE', 'FATAL']),
+ model: transitionModel([
+ 'WAIT_FOR_YELLOW_SOURCE',
+ 'UPDATE_SOURCE_MAPPINGS_PROPERTIES',
+ 'FATAL',
+ ]),
next: () => {
throw new Error('this action throws');
},
@@ -301,7 +332,11 @@ describe('migrationsStateActionMachine', () => {
migrationStateActionMachine({
initialState: { ...initialState, reason: 'the fatal reason' } as State,
logger: mockLogger.get(),
- model: transitionModel(['LEGACY_REINDEX', 'LEGACY_DELETE', 'FATAL']),
+ model: transitionModel([
+ 'WAIT_FOR_YELLOW_SOURCE',
+ 'UPDATE_SOURCE_MAPPINGS_PROPERTIES',
+ 'FATAL',
+ ]),
next: () => {
throw new Error('this action throws');
},
@@ -316,7 +351,11 @@ describe('migrationsStateActionMachine', () => {
migrationStateActionMachine({
initialState: { ...initialState, reason: 'the fatal reason' } as State,
logger: mockLogger.get(),
- model: transitionModel(['LEGACY_REINDEX', 'LEGACY_DELETE', 'FATAL']),
+ model: transitionModel([
+ 'WAIT_FOR_YELLOW_SOURCE',
+ 'UPDATE_SOURCE_MAPPINGS_PROPERTIES',
+ 'FATAL',
+ ]),
next,
abort,
})
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/migrations_state_action_machine.ts b/src/core/packages/saved-objects/migration-server-internal/src/migrations_state_action_machine.ts
index cb88be39e9848..7e7884715a33f 100644
--- a/src/core/packages/saved-objects/migration-server-internal/src/migrations_state_action_machine.ts
+++ b/src/core/packages/saved-objects/migration-server-internal/src/migrations_state_action_machine.ts
@@ -18,7 +18,7 @@ import type { SavedObjectsRawDoc } from '@kbn/core-saved-objects-server';
import type { MigrationResult } from '@kbn/core-saved-objects-base-server-internal';
import { logActionResponse, logStateTransition } from './common/utils/logs';
import { type Model, type Next, stateActionMachine } from './state_action_machine';
-import type { ReindexSourceToTempTransform, ReindexSourceToTempIndexBulk, State } from './state';
+import type { OutdatedDocumentsTransform, TransformedDocumentsBulkIndex, State } from './state';
import { redactBulkOperationBatches } from './common/redact_state';
/**
@@ -65,7 +65,7 @@ export async function migrationStateActionMachine({
...newState,
...{
outdatedDocuments: (
- (newState as ReindexSourceToTempTransform).outdatedDocuments ?? []
+ (newState as OutdatedDocumentsTransform).outdatedDocuments ?? []
).map(
(doc) =>
({
@@ -75,7 +75,7 @@ export async function migrationStateActionMachine({
},
...{
bulkOperationBatches: redactBulkOperationBatches(
- (newState as ReindexSourceToTempIndexBulk).bulkOperationBatches ?? [[]]
+ (newState as TransformedDocumentsBulkIndex).bulkOperationBatches ?? [[]]
),
},
};
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/model/create_batches.test.ts b/src/core/packages/saved-objects/migration-server-internal/src/model/create_batches.test.ts
index 437c25a2e0e61..80b3ee2b2ec89 100644
--- a/src/core/packages/saved-objects/migration-server-internal/src/model/create_batches.test.ts
+++ b/src/core/packages/saved-objects/migration-server-internal/src/model/create_batches.test.ts
@@ -9,7 +9,7 @@
import * as Either from 'fp-ts/Either';
import type { SavedObjectsRawDoc } from '@kbn/core-saved-objects-server';
-import { buildTempIndexMap, createBatches } from './create_batches';
+import { createBatches } from './create_batches';
describe('createBatches', () => {
const documentToOperation = (document: SavedObjectsRawDoc) => [
@@ -19,145 +19,64 @@ describe('createBatches', () => {
const DOCUMENT_SIZE_BYTES = 77; // 76 + \n
- describe('when indexTypesMap and kibanaVersion are not provided', () => {
- it('returns right one batch if all documents fit in maxBatchSizeBytes', () => {
- const documents = [
- { _id: '', _source: { type: 'dashboard', title: 'my saved object title ¹' } },
- { _id: '', _source: { type: 'dashboard', title: 'my saved object title ²' } },
- { _id: '', _source: { type: 'dashboard', title: 'my saved object title ®' } },
- ];
+ it('returns right one batch if all documents fit in maxBatchSizeBytes', () => {
+ const documents = [
+ { _id: '', _source: { type: 'dashboard', title: 'my saved object title ¹' } },
+ { _id: '', _source: { type: 'dashboard', title: 'my saved object title ²' } },
+ { _id: '', _source: { type: 'dashboard', title: 'my saved object title ®' } },
+ ];
- expect(
- createBatches({
- documents,
- maxBatchSizeBytes: DOCUMENT_SIZE_BYTES * 3,
- })
- ).toEqual(Either.right([documents.map(documentToOperation)]));
- });
- it('creates multiple batches with each batch limited to maxBatchSizeBytes', () => {
- const documents = [
- { _id: '', _source: { type: 'dashboard', title: 'my saved object title ¹' } },
- { _id: '', _source: { type: 'dashboard', title: 'my saved object title ²' } },
- { _id: '', _source: { type: 'dashboard', title: 'my saved object title ®' } },
- { _id: '', _source: { type: 'dashboard', title: 'my saved object title 44' } },
- { _id: '', _source: { type: 'dashboard', title: 'my saved object title 55' } },
- ];
- expect(
- createBatches({
- documents,
- maxBatchSizeBytes: DOCUMENT_SIZE_BYTES * 2,
- })
- ).toEqual(
- Either.right([
- documents.slice(0, 2).map(documentToOperation),
- documents.slice(2, 4).map(documentToOperation),
- documents.slice(4).map(documentToOperation),
- ])
- );
- });
- it('creates a single empty batch if there are no documents', () => {
- const documents = [] as SavedObjectsRawDoc[];
- expect(createBatches({ documents, maxBatchSizeBytes: 100 })).toEqual(Either.right([[]]));
- });
- it('throws if any one document exceeds the maxBatchSizeBytes', () => {
- const documents = [
- { _id: 'foo', _source: { type: 'dashboard', title: 'my saved object title ¹' } },
- {
- _id: 'bar',
- _source: {
- type: 'dashboard',
- title: 'my saved object title ² with a very long title that exceeds max size bytes',
- },
- },
- { _id: 'baz', _source: { type: 'dashboard', title: 'my saved object title ®' } },
- ];
- expect(createBatches({ documents, maxBatchSizeBytes: 120 })).toEqual(
- Either.left({
- maxBatchSizeBytes: 120,
- docSizeBytes: 130,
- type: 'document_exceeds_batch_size_bytes',
- documentId: documents[1]._id,
- })
- );
- });
+ expect(
+ createBatches({
+ documents,
+ maxBatchSizeBytes: DOCUMENT_SIZE_BYTES * 3,
+ })
+ ).toEqual(Either.right([documents.map(documentToOperation)]));
});
-
- describe('when a type index map is provided', () => {
- it('creates batches that contain the target index information for each type', () => {
- const documents = [
- { _id: '', _source: { type: 'dashboard', title: 'my saved object title ¹' } },
- { _id: '', _source: { type: 'dashboard', title: 'my saved object title ²' } },
- { _id: '', _source: { type: 'cases', title: 'a case' } },
- { _id: '', _source: { type: 'cases-comments', title: 'a case comment #1' } },
- { _id: '', _source: { type: 'cases-user-actions', title: 'a case user action' } },
- ];
- expect(
- createBatches({
- documents,
- maxBatchSizeBytes: (DOCUMENT_SIZE_BYTES + 49) * 2, // add extra length for 'index' property
- typeIndexMap: buildTempIndexMap(
- {
- '.kibana': ['dashboard'],
- '.kibana_cases': ['cases', 'cases-comments', 'cases-user-actions'],
- },
- '8.8.0'
- ),
- })
- ).toEqual(
- Either.right([
- [
- [
- {
- index: {
- _id: '',
- _index: '.kibana_8.8.0_reindex_temp_alias',
- },
- },
- { type: 'dashboard', title: 'my saved object title ¹' },
- ],
- [
- {
- index: {
- _id: '',
- _index: '.kibana_8.8.0_reindex_temp_alias',
- },
- },
- { type: 'dashboard', title: 'my saved object title ²' },
- ],
- ],
- [
- [
- {
- index: {
- _id: '',
- _index: '.kibana_cases_8.8.0_reindex_temp_alias',
- },
- },
- { type: 'cases', title: 'a case' },
- ],
- [
- {
- index: {
- _id: '',
- _index: '.kibana_cases_8.8.0_reindex_temp_alias',
- },
- },
- { type: 'cases-comments', title: 'a case comment #1' },
- ],
- ],
- [
- [
- {
- index: {
- _id: '',
- _index: '.kibana_cases_8.8.0_reindex_temp_alias',
- },
- },
- { type: 'cases-user-actions', title: 'a case user action' },
- ],
- ],
- ])
- );
- });
+ it('creates multiple batches with each batch limited to maxBatchSizeBytes', () => {
+ const documents = [
+ { _id: '', _source: { type: 'dashboard', title: 'my saved object title ¹' } },
+ { _id: '', _source: { type: 'dashboard', title: 'my saved object title ²' } },
+ { _id: '', _source: { type: 'dashboard', title: 'my saved object title ®' } },
+ { _id: '', _source: { type: 'dashboard', title: 'my saved object title 44' } },
+ { _id: '', _source: { type: 'dashboard', title: 'my saved object title 55' } },
+ ];
+ expect(
+ createBatches({
+ documents,
+ maxBatchSizeBytes: DOCUMENT_SIZE_BYTES * 2,
+ })
+ ).toEqual(
+ Either.right([
+ documents.slice(0, 2).map(documentToOperation),
+ documents.slice(2, 4).map(documentToOperation),
+ documents.slice(4).map(documentToOperation),
+ ])
+ );
+ });
+ it('creates a single empty batch if there are no documents', () => {
+ const documents = [] as SavedObjectsRawDoc[];
+ expect(createBatches({ documents, maxBatchSizeBytes: 100 })).toEqual(Either.right([[]]));
+ });
+ it('throws if any one document exceeds the maxBatchSizeBytes', () => {
+ const documents = [
+ { _id: 'foo', _source: { type: 'dashboard', title: 'my saved object title ¹' } },
+ {
+ _id: 'bar',
+ _source: {
+ type: 'dashboard',
+ title: 'my saved object title ² with a very long title that exceeds max size bytes',
+ },
+ },
+ { _id: 'baz', _source: { type: 'dashboard', title: 'my saved object title ®' } },
+ ];
+ expect(createBatches({ documents, maxBatchSizeBytes: 120 })).toEqual(
+ Either.left({
+ maxBatchSizeBytes: 120,
+ docSizeBytes: 130,
+ type: 'document_exceeds_batch_size_bytes',
+ documentId: documents[1]._id,
+ })
+ );
});
});
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/model/create_batches.ts b/src/core/packages/saved-objects/migration-server-internal/src/model/create_batches.ts
index 6362dc25cb6d1..dfee39084ea2e 100644
--- a/src/core/packages/saved-objects/migration-server-internal/src/model/create_batches.ts
+++ b/src/core/packages/saved-objects/migration-server-internal/src/model/create_batches.ts
@@ -10,12 +10,7 @@
import * as Either from 'fp-ts/Either';
import type { SavedObjectsRawDoc, SavedObjectsRawDocSource } from '@kbn/core-saved-objects-server';
import type { BulkOperationContainer } from '@elastic/elasticsearch/lib/api/types';
-import type { IndexTypesMap } from '@kbn/core-saved-objects-base-server-internal';
-import {
- createBulkDeleteOperationBody,
- createBulkIndexOperationTuple,
- getTempIndexName,
-} from './helpers';
+import { createBulkDeleteOperationBody, createBulkIndexOperationTuple } from './helpers';
import type { TransformErrorObjects } from '../core';
export type BulkIndexOperationTuple = [BulkOperationContainer, SavedObjectsRawDocSource];
@@ -27,12 +22,6 @@ export interface CreateBatchesParams {
corruptDocumentIds?: string[];
transformErrors?: TransformErrorObjects[];
maxBatchSizeBytes: number;
- /** This map holds a list of temporary index names for each SO type, e.g.:
- * 'cases': '.kibana_cases_8.8.0_reindex_temp'
- * 'task': '.kibana_task_manager_8.8.0_reindex_temp'
- * ...
- */
- typeIndexMap?: Record;
}
export interface DocumentExceedsBatchSize {
@@ -42,32 +31,6 @@ export interface DocumentExceedsBatchSize {
maxBatchSizeBytes: number;
}
-/**
- * Build a relationship of temporary index names for each SO type, e.g.:
- * 'cases': '.kibana_cases_8.8.0_reindex_temp'
- * 'task': '.kibana_task_manager_8.8.0_reindex_temp'
- * ...
- *
- * @param indexTypesMap information about which types are stored in each index
- * @param kibanaVersion the target version of the indices
- */
-export function buildTempIndexMap(
- indexTypesMap: IndexTypesMap,
- kibanaVersion: string
-): Record {
- return Object.entries(indexTypesMap || {}).reduce>(
- (acc, [indexAlias, types]) => {
- const tempIndex = getTempIndexName(indexAlias, kibanaVersion!) + '_alias';
-
- types.forEach((type) => {
- acc[type] = tempIndex;
- });
- return acc;
- },
- {}
- );
-}
-
/**
* Creates batches of documents to be used by the bulk API. Each batch will
* have a request body content length that's <= maxBatchSizeBytes
@@ -77,7 +40,6 @@ export function createBatches({
corruptDocumentIds = [],
transformErrors = [],
maxBatchSizeBytes,
- typeIndexMap,
}: CreateBatchesParams): Either.Either {
/* To build up the NDJSON request body we construct an array of objects like:
* [
@@ -131,7 +93,7 @@ export function createBatches({
// create index (update) operations for all transformed documents
for (const document of documents) {
- const bulkIndexOperationBody = createBulkIndexOperationTuple(document, typeIndexMap);
+ const bulkIndexOperationBody = createBulkIndexOperationTuple(document);
// take into account that this tuple's surrounding brackets `[]` won't be present in the NDJSON
const docSizeBytes =
Buffer.byteLength(JSON.stringify(bulkIndexOperationBody), 'utf8') - BRACKETS_BYTES;
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/model/extract_errors.ts b/src/core/packages/saved-objects/migration-server-internal/src/model/extract_errors.ts
index 0ed10e9e8f38a..ba15625e6cac5 100644
--- a/src/core/packages/saved-objects/migration-server-internal/src/model/extract_errors.ts
+++ b/src/core/packages/saved-objects/migration-server-internal/src/model/extract_errors.ts
@@ -27,8 +27,13 @@ export function extractTransformFailuresReason(
// we have both the saved object Id and the stack trace in each `transformErrors` item.
const transformErrorsReason =
transformErrors.length > 0
- ? ` ${transformErrors.length} transformation errors were encountered:\n` +
+ ? ` ${transformErrors.length} transformation errors were encountered${
+ transformErrors.length > 10
+ ? ' (showing the first 10 - check the logs for the full list)'
+ : ''
+ }:\n` +
transformErrors
+ .slice(0, 10)
.map((errObj) => `- ${errObj.rawId}: ${errObj.err.stack ?? errObj.err.message}\n`)
.join('')
: '';
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/model/helpers.test.ts b/src/core/packages/saved-objects/migration-server-internal/src/model/helpers.test.ts
index 7af7e64e63edc..e10670687fc2e 100644
--- a/src/core/packages/saved-objects/migration-server-internal/src/model/helpers.test.ts
+++ b/src/core/packages/saved-objects/migration-server-internal/src/model/helpers.test.ts
@@ -8,7 +8,6 @@
*/
import type { FetchIndexResponse } from '../actions/fetch_indices';
-import type { BaseState } from '../state';
import {
addExcludedTypesToBoolQuery,
addMustClausesToBoolQuery,
@@ -18,11 +17,9 @@ import {
buildRemoveAliasActions,
versionMigrationCompleted,
MigrationType,
- getTempIndexName,
createBulkIndexOperationTuple,
hasLaterVersionAlias,
aliasVersion,
- getIndexTypes,
} from './helpers';
describe('addExcludedTypesToBoolQuery', () => {
@@ -387,27 +384,6 @@ describe('buildRemoveAliasActions', () => {
describe('createBulkIndexOperationTuple', () => {
it('creates the proper request body to bulk index a document', () => {
- const document = { _id: '', _source: { type: 'cases', title: 'a case' } };
- const typeIndexMap = {
- cases: '.kibana_cases_8.8.0_reindex_temp',
- };
- expect(createBulkIndexOperationTuple(document, typeIndexMap)).toMatchInlineSnapshot(`
- Array [
- Object {
- "index": Object {
- "_id": "",
- "_index": ".kibana_cases_8.8.0_reindex_temp",
- },
- },
- Object {
- "title": "a case",
- "type": "cases",
- },
- ]
- `);
- });
-
- it('does not include the index property if it is not specified in the typeIndexMap', () => {
const document = { _id: '', _source: { type: 'cases', title: 'a case' } };
expect(createBulkIndexOperationTuple(document)).toMatchInlineSnapshot(`
Array [
@@ -480,23 +456,3 @@ describe('getMigrationType', () => {
}
);
});
-
-describe('getTempIndexName', () => {
- it('composes a temporary index name for reindexing', () => {
- expect(getTempIndexName('.kibana_cases', '8.8.0')).toEqual('.kibana_cases_8.8.0_reindex_temp');
- });
-});
-
-describe('getIndexTypes', () => {
- it("returns the list of types that belong to a migrator's index, based on its state", () => {
- const baseState = {
- indexPrefix: '.kibana_task_manager',
- indexTypesMap: {
- '.kibana': ['foo', 'bar'],
- '.kibana_task_manager': ['task'],
- },
- };
-
- expect(getIndexTypes(baseState as unknown as BaseState)).toEqual(['task']);
- });
-});
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/model/helpers.ts b/src/core/packages/saved-objects/migration-server-internal/src/model/helpers.ts
index 3b0496417cb01..82a2c36420a7f 100644
--- a/src/core/packages/saved-objects/migration-server-internal/src/model/helpers.ts
+++ b/src/core/packages/saved-objects/migration-server-internal/src/model/helpers.ts
@@ -19,16 +19,11 @@ import type { IndexMapping } from '@kbn/core-saved-objects-base-server-internal'
import type { AliasAction, FetchIndexResponse } from '../actions';
import type { BulkIndexOperationTuple } from './create_batches';
import type {
- BaseState,
CleanupUnknownAndExcluded,
CleanupUnknownAndExcludedWaitForTaskState,
OutdatedDocumentsSearchRead,
- ReindexSourceToTempRead,
} from '../state';
-/** @internal */
-export const REINDEX_TEMP_SUFFIX = '_reindex_temp';
-
/** @internal */
export type Aliases = Partial>;
@@ -279,18 +274,12 @@ export function buildRemoveAliasActions(
/**
* Given a document, creates a valid body to index the document using the Bulk API.
*/
-export const createBulkIndexOperationTuple = (
- doc: SavedObjectsRawDoc,
- typeIndexMap: Record = {}
-): BulkIndexOperationTuple => {
+export const createBulkIndexOperationTuple = (doc: SavedObjectsRawDoc): BulkIndexOperationTuple => {
const idChanged = doc._source.originId && doc._source.originId !== doc._id;
return [
{
index: {
_id: doc._id,
- ...(typeIndexMap[doc._source.type] && {
- _index: typeIndexMap[doc._source.type],
- }),
// use optimistic concurrency control to ensure that outdated
// documents are only overwritten once with the latest version
...(typeof doc._seq_no !== 'undefined' && !idChanged && { if_seq_no: doc._seq_no }),
@@ -341,23 +330,8 @@ export function getMigrationType({
return MigrationType.Invalid;
}
-/**
- * Generate a temporary index name, to reindex documents into it
- * @param index The name of the SO index
- * @param kibanaVersion The current kibana version
- * @returns A temporary index name to reindex documents
- */
-export const getTempIndexName = (indexPrefix: string, kibanaVersion: string): string =>
- `${indexPrefix}_${kibanaVersion}${REINDEX_TEMP_SUFFIX}`;
-
/** Increase batchSize by 20% until a maximum of maxBatchSize */
-export const increaseBatchSize = (
- stateP: OutdatedDocumentsSearchRead | ReindexSourceToTempRead
-) => {
+export const increaseBatchSize = (stateP: OutdatedDocumentsSearchRead) => {
const increasedBatchSize = Math.floor(stateP.batchSize * 1.2);
return increasedBatchSize > stateP.maxBatchSize ? stateP.maxBatchSize : increasedBatchSize;
};
-
-export const getIndexTypes = (state: BaseState): string[] => {
- return state.indexTypesMap[state.indexPrefix];
-};
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/model/model.test.ts b/src/core/packages/saved-objects/migration-server-internal/src/model/model.test.ts
index 8ed8026ec8d4b..6184f9d08dd57 100644
--- a/src/core/packages/saved-objects/migration-server-internal/src/model/model.test.ts
+++ b/src/core/packages/saved-objects/migration-server-internal/src/model/model.test.ts
@@ -12,28 +12,16 @@ import * as Either from 'fp-ts/Either';
import * as Option from 'fp-ts/Option';
import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks';
import type { SavedObjectsRawDoc } from '@kbn/core-saved-objects-server';
-import {
- DEFAULT_INDEX_TYPES_MAP,
- type IndexMapping,
-} from '@kbn/core-saved-objects-base-server-internal';
+import { type IndexMapping } from '@kbn/core-saved-objects-base-server-internal';
import type {
BaseState,
- CalculateExcludeFiltersState,
UpdateSourceMappingsPropertiesState,
CheckTargetTypesMappingsState,
- CheckUnknownDocumentsState,
CheckVersionIndexReadyActions,
CleanupUnknownAndExcluded,
CleanupUnknownAndExcludedWaitForTaskState,
- CloneTempToTarget,
CreateNewTargetState,
- CreateReindexTempState,
FatalState,
- LegacyCreateReindexTargetState,
- LegacyDeleteState,
- LegacyReindexState,
- LegacyReindexWaitForTaskState,
- LegacySetWriteBlockState,
MarkVersionIndexReady,
MarkVersionIndexReadyConflict,
OutdatedDocumentsSearchClosePit,
@@ -41,27 +29,15 @@ import type {
OutdatedDocumentsSearchRead,
OutdatedDocumentsTransform,
PrepareCompatibleMigration,
- RefreshTarget,
- ReindexSourceToTempClosePit,
- ReindexSourceToTempIndexBulk,
- ReindexSourceToTempOpenPit,
- ReindexSourceToTempRead,
- ReindexSourceToTempTransform,
- SetSourceWriteBlockState,
- SetTempWriteBlock,
State,
TransformedDocumentsBulkIndex,
UpdateTargetMappingsMeta,
UpdateTargetMappingsPropertiesState,
UpdateTargetMappingsPropertiesWaitForTaskState,
WaitForYellowSourceState,
- ReadyToReindexSyncState,
- DoneReindexingSyncState,
- LegacyCheckClusterRoutingAllocationState,
- ReindexCheckClusterRoutingAllocationState,
PostInitState,
CreateIndexCheckClusterRoutingAllocationState,
- RelocateCheckClusterRoutingAllocationState,
+ CompatibleUpdateCheckClusterRoutingAllocationState,
} from '../state';
import { type TransformErrorObjects, TransformSavedObjectDocumentError } from '../core';
import type { AliasAction, RetryableEsClientError } from '../actions';
@@ -88,7 +64,6 @@ describe('migrations v2 model', () => {
};
const baseState: BaseState = {
controlState: '',
- legacyIndex: '.kibana',
kibanaVersion: '7.11.0',
logs: [],
retryCount: 0,
@@ -104,13 +79,9 @@ describe('migrations v2 model', () => {
indexPrefix: '.kibana',
outdatedDocumentsQuery: {},
targetIndexMappings: indexMapping,
- tempIndexMappings: { properties: {} },
- preMigrationScript: Option.none,
currentAlias: '.kibana',
versionAlias: '.kibana_7.11.0',
versionIndex: '.kibana_7.11.0_001',
- tempIndex: '.kibana_7.11.0_reindex_temp',
- tempIndexAlias: '.kibana_7.11.0_reindex_temp_alias',
excludeOnUpgradeQuery: {
bool: {
must_not: [
@@ -140,8 +111,6 @@ describe('migrations v2 model', () => {
clusterShardLimitExceeded: 'clusterShardLimitExceeded',
},
waitForMigrationCompletion: false,
- mustRelocateDocuments: false,
- indexTypesMap: DEFAULT_INDEX_TYPES_MAP,
esCapabilities: elasticsearchServiceMock.createCapabilities(),
};
const postInitState = {
@@ -468,7 +437,7 @@ describe('migrations v2 model', () => {
expect(newState.retryDelay).toEqual(2000);
});
- test('INIT -> WAIT_FOR_MIGRATION_COMPLETION when migrating from a legacy index (>= 6.0.0 < 6.5)', () => {
+ test('INIT -> WAIT_FOR_MIGRATION_COMPLETION when a concrete index exists without a version alias', () => {
const res: ResponseType<'INIT'> = Either.right({
'.kibana': {
aliases: {},
@@ -635,46 +604,6 @@ describe('migrations v2 model', () => {
});
});
- test('INIT -> LEGACY_CHECK_CLUSTER_ROUTING_ALLOCATION when migrating from a legacy index (>= 6.0.0 < 6.5)', () => {
- const res: ResponseType<'INIT'> = Either.right({
- '.kibana': {
- aliases: {},
- mappings: mappingsWithUnknownType,
- settings: {},
- },
- });
- const newState = model(initState, res);
-
- expect(newState).toMatchObject({
- controlState: 'LEGACY_CHECK_CLUSTER_ROUTING_ALLOCATION',
- sourceIndex: Option.some('.kibana_pre6.5.0_001'),
- targetIndex: '.kibana_7.11.0_001',
- });
- // This snapshot asserts that we disable the unknown saved object
- // type. Because it's mappings are disabled, we also don't copy the
- // `_meta.migrationMappingPropertyHashes` for the disabled type.
- expect(newState.targetIndexMappings).toMatchInlineSnapshot(`
- Object {
- "_meta": Object {
- "migrationMappingPropertyHashes": Object {
- "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0",
- },
- },
- "properties": Object {
- "new_saved_object_type": Object {
- "properties": Object {
- "value": Object {
- "type": "text",
- },
- },
- },
- },
- }
- `);
- expect(newState.retryCount).toEqual(0);
- expect(newState.retryDelay).toEqual(0);
- });
-
test('INIT -> WAIT_FOR_YELLOW_SOURCE when migrating from a custom kibana.index name (>= 6.5 < 7.11.0)', () => {
const res: ResponseType<'INIT'> = Either.right({
'my-saved-objects_3': {
@@ -731,7 +660,7 @@ describe('migrations v2 model', () => {
expect(newState.retryDelay).toEqual(0);
});
- test('INIT -> CREATE_INDEX_CHECK_CLUSTER_ROUTING_ALLOCATION when the index does not exist and the migrator is NOT involved in a relocation', () => {
+ test('INIT -> CREATE_INDEX_CHECK_CLUSTER_ROUTING_ALLOCATION when no saved object indices exist', () => {
const res: ResponseType<'INIT'> = Either.right({});
const newState = model(initState, res);
@@ -743,30 +672,6 @@ describe('migrations v2 model', () => {
expect(newState.retryCount).toEqual(0);
expect(newState.retryDelay).toEqual(0);
});
-
- test('INIT -> RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION when the index does not exist and the migrator is involved in a relocation', () => {
- const res: ResponseType<'INIT'> = Either.right({});
- const newState = model(
- {
- ...initState,
- mustRelocateDocuments: true,
- },
- res
- );
-
- expect(newState).toMatchObject({
- controlState: 'RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION',
- sourceIndex: Option.none,
- targetIndex: '.kibana_7.11.0_001',
- versionIndexReadyActions: Option.some([
- { add: { index: '.kibana_7.11.0_001', alias: '.kibana' } },
- { add: { index: '.kibana_7.11.0_001', alias: '.kibana_7.11.0' } },
- { remove_index: { index: '.kibana_7.11.0_reindex_temp' } },
- ]),
- });
- expect(newState.retryCount).toEqual(0);
- expect(newState.retryDelay).toEqual(0);
- });
});
});
@@ -817,51 +722,6 @@ describe('migrations v2 model', () => {
});
});
- describe('RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION', () => {
- const reindexCheckClusterRoutingAllocationState: RelocateCheckClusterRoutingAllocationState =
- {
- ...postInitState,
- controlState: 'RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION',
- sourceIndex: Option.some('.kibana') as Option.Some,
- sourceIndexMappings: Option.some({}) as Option.Some,
- tempIndexMappings: { properties: {} },
- };
-
- test('RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION -> RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION when cluster allocation is not compatible', () => {
- const res: ResponseType<'RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION'> = Either.left({
- type: 'incompatible_cluster_routing_allocation',
- });
- const newState = model(reindexCheckClusterRoutingAllocationState, res);
-
- expect(newState.controlState).toBe('RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION');
- expect(newState.retryCount).toEqual(1);
- expect(newState.retryDelay).toEqual(2000);
- });
-
- test('RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION -> CREATE_REINDEX_TEMP when cluster allocation is compatible', () => {
- const res: ResponseType<'RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION'> = Either.right({});
- const newState = model(
- reindexCheckClusterRoutingAllocationState,
- res
- ) as CreateReindexTempState;
-
- expect(newState.controlState).toBe('CREATE_REINDEX_TEMP');
- expect(newState.retryCount).toEqual(0);
- expect(newState.retryDelay).toEqual(0);
- // check that we are correctly "forwarding" the state
- expect(newState.tempIndex).toEqual(reindexCheckClusterRoutingAllocationState.tempIndex);
- expect(newState.tempIndexAlias).toEqual(
- reindexCheckClusterRoutingAllocationState.tempIndexAlias
- );
- expect(newState.tempIndexMappings).toEqual(
- reindexCheckClusterRoutingAllocationState.tempIndexMappings
- );
- expect(newState.esCapabilities).toEqual(
- reindexCheckClusterRoutingAllocationState.esCapabilities
- );
- });
- });
-
describe('WAIT_FOR_MIGRATION_COMPLETION', () => {
const waitForMState: State = {
...postInitState,
@@ -944,7 +804,7 @@ describe('migrations v2 model', () => {
expect(newState.retryDelay).toEqual(2000);
});
- test('WAIT_FOR_MIGRATION_COMPLETION -> WAIT_FOR_MIGRATION_COMPLETION when migrating from a legacy index (>= 6.0.0 < 6.5)', () => {
+ test('WAIT_FOR_MIGRATION_COMPLETION -> WAIT_FOR_MIGRATION_COMPLETION when the source index has no current alias', () => {
const res: ResponseType<'WAIT_FOR_MIGRATION_COMPLETION'> = Either.right({
'.kibana': {
aliases: {},
@@ -1032,265 +892,6 @@ describe('migrations v2 model', () => {
});
});
- describe('LEGACY_CHECK_CLUSTER_ROUTING_ALLOCATION', () => {
- const legacyCheckClusterRoutingAllocationState: LegacyCheckClusterRoutingAllocationState = {
- ...postInitState,
- controlState: 'LEGACY_CHECK_CLUSTER_ROUTING_ALLOCATION',
- sourceIndex: Option.some('.kibana') as Option.Some,
- sourceIndexMappings: Option.some({ properties: {} }) as Option.Some,
- legacyPreMigrationDoneActions: [],
- legacyIndex: '',
- };
-
- test('LEGACY_CHECK_CLUSTER_ROUTING_ALLOCATION -> LEGACY_CHECK_CLUSTER_ROUTING_ALLOCATION when cluster allocation is not compatible', () => {
- const res: ResponseType<'LEGACY_CHECK_CLUSTER_ROUTING_ALLOCATION'> = Either.left({
- type: 'incompatible_cluster_routing_allocation',
- });
- const newState = model(legacyCheckClusterRoutingAllocationState, res);
-
- expect(newState.controlState).toBe('LEGACY_CHECK_CLUSTER_ROUTING_ALLOCATION');
- expect(newState.retryCount).toEqual(1);
- expect(newState.retryDelay).toEqual(2000);
- });
-
- test('LEGACY_CHECK_CLUSTER_ROUTING_ALLOCATION -> LEGACY_SET_WRITE_BLOCK when cluster allocation is compatible', () => {
- const res: ResponseType<'LEGACY_CHECK_CLUSTER_ROUTING_ALLOCATION'> = Either.right({});
- const newState = model(legacyCheckClusterRoutingAllocationState, res);
-
- expect(newState.controlState).toBe('LEGACY_SET_WRITE_BLOCK');
- expect(newState.retryCount).toEqual(0);
- expect(newState.retryDelay).toEqual(0);
- });
- });
-
- describe('LEGACY_SET_WRITE_BLOCK', () => {
- const legacySetWriteBlockState: LegacySetWriteBlockState = {
- ...postInitState,
- controlState: 'LEGACY_SET_WRITE_BLOCK',
- sourceIndex: Option.some('.kibana') as Option.Some,
- sourceIndexMappings: Option.some({ properties: {} }) as Option.Some,
- legacyPreMigrationDoneActions: [],
- legacyIndex: '',
- };
-
- test('LEGACY_SET_WRITE_BLOCK -> LEGACY_SET_WRITE_BLOCK if action fails with set_write_block_failed', () => {
- const res: ResponseType<'LEGACY_SET_WRITE_BLOCK'> = Either.left({
- type: 'retryable_es_client_error',
- message: 'set_write_block_failed',
- });
- const newState = model(legacySetWriteBlockState, res);
- expect(newState.controlState).toEqual('LEGACY_SET_WRITE_BLOCK');
- expect(newState.retryCount).toEqual(1);
- expect(newState.retryDelay).toEqual(2000);
- });
-
- test('LEGACY_SET_WRITE_BLOCK -> LEGACY_CREATE_REINDEX_TARGET if action fails with index_not_found_exception', () => {
- const res: ResponseType<'LEGACY_SET_WRITE_BLOCK'> = Either.left({
- type: 'index_not_found_exception',
- index: 'legacy_index_name',
- });
- const newState = model(legacySetWriteBlockState, res);
- expect(newState.controlState).toEqual('LEGACY_CREATE_REINDEX_TARGET');
- expect(newState.retryCount).toEqual(0);
- expect(newState.retryDelay).toEqual(0);
- });
-
- test('LEGACY_SET_WRITE_BLOCK -> LEGACY_CREATE_REINDEX_TARGET if action succeeds with set_write_block_succeeded', () => {
- const res: ResponseType<'LEGACY_SET_WRITE_BLOCK'> = Either.right(
- 'set_write_block_succeeded'
- );
- const newState = model(legacySetWriteBlockState, res);
- expect(newState.controlState).toEqual('LEGACY_CREATE_REINDEX_TARGET');
- expect(newState.retryCount).toEqual(0);
- expect(newState.retryDelay).toEqual(0);
- });
- });
-
- describe('LEGACY_CREATE_REINDEX_TARGET', () => {
- const legacyCreateReindexTargetState: LegacyCreateReindexTargetState = {
- ...postInitState,
- controlState: 'LEGACY_CREATE_REINDEX_TARGET',
- sourceIndex: Option.some('.kibana') as Option.Some,
- sourceIndexMappings: Option.some({ properties: {} }) as Option.Some,
- legacyPreMigrationDoneActions: [],
- legacyIndex: '',
- };
-
- test('LEGACY_CREATE_REINDEX_TARGET -> LEGACY_REINDEX', () => {
- const res: ResponseType<'LEGACY_CREATE_REINDEX_TARGET'> =
- Either.right('create_index_succeeded');
- const newState = model(legacyCreateReindexTargetState, res);
- expect(newState.controlState).toEqual('LEGACY_REINDEX');
- expect(newState.retryCount).toEqual(0);
- expect(newState.retryDelay).toEqual(0);
- });
-
- test('LEGACY_CREATE_REINDEX_TARGET -> LEGACY_CREATE_REINDEX_TARGET if action fails with index_not_green_timeout', () => {
- const res: ResponseType<'LEGACY_CREATE_REINDEX_TARGET'> = Either.left({
- message: '[index_not_green_timeout] Timeout waiting for ...',
- type: 'index_not_green_timeout',
- });
- const newState = model(legacyCreateReindexTargetState, res);
- expect(newState.controlState).toEqual('LEGACY_CREATE_REINDEX_TARGET');
- expect(newState.retryCount).toEqual(1);
- expect(newState.retryDelay).toEqual(2000);
- expect(newState.logs[0]).toMatchInlineSnapshot(`
- Object {
- "level": "error",
- "message": "Action failed with '[index_not_green_timeout] Timeout waiting for ... Refer to repeatedTimeoutRequests for information on how to resolve the issue.'. Retrying attempt 1 in 2 seconds.",
- }
- `);
- });
-
- test('LEGACY_CREATE_REINDEX_TARGET -> LEGACY_REINDEX resets retry count and retry delay if action succeeds', () => {
- const res: ResponseType<'LEGACY_CREATE_REINDEX_TARGET'> =
- Either.right('create_index_succeeded');
- const testState = {
- ...legacyCreateReindexTargetState,
- retryCount: 1,
- retryDelay: 2000,
- };
- const newState = model(testState, res);
- expect(newState.controlState).toEqual('LEGACY_REINDEX');
- expect(newState.retryCount).toEqual(0);
- expect(newState.retryDelay).toEqual(0);
- });
-
- test('LEGACY_CREATE_REINDEX_TARGET -> FATAL if action fails with cluster_shard_limit_exceeded', () => {
- const res: ResponseType<'LEGACY_CREATE_REINDEX_TARGET'> = Either.left({
- type: 'cluster_shard_limit_exceeded',
- });
- const newState = model(legacyCreateReindexTargetState, res) as FatalState;
- expect(newState.controlState).toEqual('FATAL');
- expect(newState.reason).toMatchInlineSnapshot(
- `"[cluster_shard_limit_exceeded] Upgrading Kibana requires adding a small number of new shards. Ensure that Kibana is able to add 10 more shards by increasing the cluster.max_shards_per_node setting, or removing indices to clear up resources. See clusterShardLimitExceeded"`
- );
- });
- });
-
- describe('LEGACY_REINDEX', () => {
- const legacyReindexState: LegacyReindexState = {
- ...postInitState,
- controlState: 'LEGACY_REINDEX',
- sourceIndex: Option.some('.kibana') as Option.Some,
- sourceIndexMappings: Option.some({ properties: {} }) as Option.Some,
- legacyPreMigrationDoneActions: [],
- legacyIndex: '',
- };
-
- test('LEGACY_REINDEX -> LEGACY_REINDEX_WAIT_FOR_TASK', () => {
- const res: ResponseType<'LEGACY_REINDEX'> = Either.right({ taskId: 'task id' });
- const newState = model(legacyReindexState, res);
- expect(newState.controlState).toEqual('LEGACY_REINDEX_WAIT_FOR_TASK');
- expect(newState.retryCount).toEqual(0);
- expect(newState.retryDelay).toEqual(0);
- });
- });
-
- describe('LEGACY_REINDEX_WAIT_FOR_TASK', () => {
- const legacyReindexWaitForTaskState: LegacyReindexWaitForTaskState = {
- ...postInitState,
- controlState: 'LEGACY_REINDEX_WAIT_FOR_TASK',
- sourceIndex: Option.some('source_index_name') as Option.Some,
- sourceIndexMappings: Option.some({ properties: {} }) as Option.Some,
- legacyPreMigrationDoneActions: [],
- legacyIndex: 'legacy_index_name',
- legacyReindexTaskId: 'test_task_id',
- };
-
- test('LEGACY_REINDEX_WAIT_FOR_TASK -> LEGACY_DELETE if action succeeds', () => {
- const res: ResponseType<'LEGACY_REINDEX_WAIT_FOR_TASK'> = Either.right('reindex_succeeded');
- const newState = model(legacyReindexWaitForTaskState, res);
- expect(newState.controlState).toEqual('LEGACY_DELETE');
- expect(newState.retryCount).toEqual(0);
- expect(newState.retryDelay).toEqual(0);
- });
-
- test('LEGACY_REINDEX_WAIT_FOR_TASK -> LEGACY_DELETE if action fails with index_not_found_exception for reindex source', () => {
- const res: ResponseType<'LEGACY_REINDEX_WAIT_FOR_TASK'> = Either.left({
- type: 'index_not_found_exception',
- index: 'legacy_index_name',
- });
- const newState = model(legacyReindexWaitForTaskState, res);
- expect(newState.controlState).toEqual('LEGACY_DELETE');
- expect(newState.retryCount).toEqual(0);
- expect(newState.retryDelay).toEqual(0);
- });
-
- test('LEGACY_REINDEX_WAIT_FOR_TASK -> LEGACY_DELETE if action fails with target_index_had_write_block', () => {
- const res: ResponseType<'LEGACY_REINDEX_WAIT_FOR_TASK'> = Either.left({
- type: 'target_index_had_write_block',
- });
- const newState = model(legacyReindexWaitForTaskState, res);
- expect(newState.controlState).toEqual('LEGACY_DELETE');
- expect(newState.retryCount).toEqual(0);
- expect(newState.retryDelay).toEqual(0);
- });
-
- test('LEGACY_REINDEX_WAIT_FOR_TASK -> LEGACY_REINDEX_WAIT_FOR_TASK if action fails with wait_for_task_completion_timeout', () => {
- const res: ResponseType<'LEGACY_REINDEX_WAIT_FOR_TASK'> = Either.left({
- message: '[timeout_exception] Timeout waiting for ...',
- type: 'wait_for_task_completion_timeout',
- });
- const newState = model(legacyReindexWaitForTaskState, res);
- expect(newState.controlState).toEqual('LEGACY_REINDEX_WAIT_FOR_TASK');
- expect(newState.retryCount).toEqual(1);
- expect(newState.retryDelay).toEqual(2000);
- });
-
- test('LEGACY_REINDEX_WAIT_FOR_TASK -> LEGACY_REINDEX_WAIT_FOR_TASK with incremented retryCount if action fails with wait_for_task_completion_timeout a second time', () => {
- const state = Object.assign({}, legacyReindexWaitForTaskState, { retryCount: 1 });
- const res: ResponseType<'LEGACY_REINDEX_WAIT_FOR_TASK'> = Either.left({
- message: '[timeout_exception] Timeout waiting for ...',
- type: 'wait_for_task_completion_timeout',
- });
- const newState = model(state, res);
- expect(newState.controlState).toEqual('LEGACY_REINDEX_WAIT_FOR_TASK');
- expect(newState.retryCount).toEqual(2);
- expect(newState.retryDelay).toEqual(4000);
- });
- });
-
- describe('LEGACY_DELETE', () => {
- const legacyDeleteState: LegacyDeleteState = {
- ...postInitState,
- controlState: 'LEGACY_DELETE',
- sourceIndex: Option.some('source_index_name') as Option.Some,
- sourceIndexMappings: Option.some({ properties: {} }) as Option.Some,
- legacyPreMigrationDoneActions: [],
- legacyIndex: 'legacy_index_name',
- };
-
- test('LEGACY_DELETE -> SET_SOURCE_WRITE_BLOCK if action succeeds', () => {
- const res: ResponseType<'LEGACY_DELETE'> = Either.right('update_aliases_succeeded');
- const newState = model(legacyDeleteState, res);
- expect(newState.controlState).toEqual('SET_SOURCE_WRITE_BLOCK');
- expect(newState.retryCount).toEqual(0);
- expect(newState.retryDelay).toEqual(0);
- });
-
- test('LEGACY_DELETE -> SET_SOURCE_WRITE_BLOCK if action fails with index_not_found_exception for legacy index', () => {
- const res: ResponseType<'LEGACY_REINDEX_WAIT_FOR_TASK'> = Either.left({
- type: 'index_not_found_exception',
- index: 'legacy_index_name',
- });
- const newState = model(legacyDeleteState, res);
- expect(newState.controlState).toEqual('SET_SOURCE_WRITE_BLOCK');
- expect(newState.retryCount).toEqual(0);
- expect(newState.retryDelay).toEqual(0);
- });
-
- test('LEGACY_DELETE -> SET_SOURCE_WRITE_BLOCK if action fails with remove_index_not_a_concrete_index', () => {
- const res: ResponseType<'LEGACY_DELETE'> = Either.left({
- type: 'remove_index_not_a_concrete_index',
- });
- const newState = model(legacyDeleteState, res);
- expect(newState.controlState).toEqual('SET_SOURCE_WRITE_BLOCK');
- expect(newState.retryCount).toEqual(0);
- expect(newState.retryDelay).toEqual(0);
- });
- });
-
describe('WAIT_FOR_YELLOW_SOURCE', () => {
const waitForYellowSourceState: WaitForYellowSourceState = {
...postInitState,
@@ -1314,26 +915,13 @@ describe('migrations v2 model', () => {
expect(newState.retryDelay).toEqual(0);
});
- describe('if the migrator is NOT involved in a relocation', () => {
+ describe('when the source index is ready', () => {
test('WAIT_FOR_YELLOW_SOURCE -> UPDATE_SOURCE_MAPPINGS_PROPERTIES', () => {
const res: ResponseType<'WAIT_FOR_YELLOW_SOURCE'> = Either.right({});
const newState = model(waitForYellowSourceState, res);
expect(newState.controlState).toEqual('UPDATE_SOURCE_MAPPINGS_PROPERTIES');
});
});
-
- describe('if the migrator is involved in a relocation', () => {
- // no need to attempt to update the mappings, we are going to reindex
- test('WAIT_FOR_YELLOW_SOURCE -> REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION', () => {
- const res: ResponseType<'WAIT_FOR_YELLOW_SOURCE'> = Either.right({});
- const newState = model(
- { ...waitForYellowSourceState, mustRelocateDocuments: true },
- res
- );
-
- expect(newState.controlState).toEqual('REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION');
- });
- });
});
test('WAIT_FOR_YELLOW_SOURCE -> WAIT_FOR_YELLOW_SOURCE if action fails with index_not_yellow_timeout', () => {
@@ -1371,12 +959,14 @@ describe('migrations v2 model', () => {
};
describe('if action succeeds', () => {
- test('UPDATE_SOURCE_MAPPINGS_PROPERTIES -> CLEANUP_UNKNOWN_AND_EXCLUDED if mappings changes are compatible and index is not migrated yet', () => {
+ test('UPDATE_SOURCE_MAPPINGS_PROPERTIES -> COMPATIBLE_UPDATE_CHECK_CLUSTER_ROUTING_ALLOCATION if mappings changes are compatible and index is not migrated yet', () => {
const res: ResponseType<'UPDATE_SOURCE_MAPPINGS_PROPERTIES'> = Either.right(
'update_mappings_succeeded'
);
const newState = model(updateSourceMappingsPropertiesState, res);
- expect(newState.controlState).toEqual('CLEANUP_UNKNOWN_AND_EXCLUDED');
+ expect(newState.controlState).toEqual(
+ 'COMPATIBLE_UPDATE_CHECK_CLUSTER_ROUTING_ALLOCATION'
+ );
});
test('UPDATE_SOURCE_MAPPINGS_PROPERTIES -> OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT if mappings changes are compatible and index is already migrated', () => {
@@ -1406,12 +996,12 @@ describe('migrations v2 model', () => {
});
describe('if action fails', () => {
- test('UPDATE_SOURCE_MAPPINGS_PROPERTIES -> REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION if mappings changes are incompatible', () => {
+ test('UPDATE_SOURCE_MAPPINGS_PROPERTIES -> FATAL if mappings changes are incompatible', () => {
const res: ResponseType<'UPDATE_SOURCE_MAPPINGS_PROPERTIES'> = Either.left({
type: 'incompatible_mapping_exception',
});
- const newState = model(updateSourceMappingsPropertiesState, res);
- expect(newState.controlState).toEqual('REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION');
+ const newState = model(updateSourceMappingsPropertiesState, res) as FatalState;
+ expect(newState.controlState).toEqual('FATAL');
});
test('UPDATE_SOURCE_MAPPINGS_PROPERTIES -> FATAL', () => {
@@ -1434,1070 +1024,201 @@ describe('migrations v2 model', () => {
});
});
- describe('CLEANUP_UNKNOWN_AND_EXCLUDED', () => {
- const cleanupUnknownAndExcluded: CleanupUnknownAndExcluded = {
- ...postInitState,
- controlState: 'CLEANUP_UNKNOWN_AND_EXCLUDED',
- sourceIndex: Option.some('.kibana_7.11.0_001') as Option.Some,
- sourceIndexMappings: Option.some(
- baseState.targetIndexMappings
- ) as Option.Some,
- targetIndex: baseState.versionIndex,
- kibanaVersion: '7.12.0', // new version!
- currentAlias: '.kibana',
- versionAlias: '.kibana_7.12.0',
- };
-
- describe('if action succeeds', () => {
- test('CLEANUP_UNKNOWN_AND_EXCLUDED -> CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK', () => {
- const res: ResponseType<'CLEANUP_UNKNOWN_AND_EXCLUDED'> = Either.right({
- type: 'cleanup_started' as const,
- taskId: '1234',
- unknownDocs: [],
- errorsByType: {},
- });
- const newState = model(cleanupUnknownAndExcluded, res);
-
- expect(newState.controlState).toEqual('CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK');
- });
-
- test('CLEANUP_UNKNOWN_AND_EXCLUDED -> PREPARE_COMPATIBLE_MIGRATION', () => {
- const res: ResponseType<'CLEANUP_UNKNOWN_AND_EXCLUDED'> = Either.right({
- type: 'cleanup_not_needed' as const,
- });
- const newState = model(cleanupUnknownAndExcluded, res);
-
- expect(newState.controlState).toEqual('PREPARE_COMPATIBLE_MIGRATION');
- });
- });
+ describe('COMPATIBLE_UPDATE_CHECK_CLUSTER_ROUTING_ALLOCATION', () => {
+ const compatibleUpdateCheckClusterRoutingAllocationState: CompatibleUpdateCheckClusterRoutingAllocationState =
+ {
+ ...postInitState,
+ controlState: 'COMPATIBLE_UPDATE_CHECK_CLUSTER_ROUTING_ALLOCATION',
+ sourceIndex: Option.some('.kibana_7.11.0_001') as Option.Some,
+ sourceIndexMappings: Option.some(
+ baseState.targetIndexMappings
+ ) as Option.Some,
+ };
- test('CLEANUP_UNKNOWN_AND_EXCLUDED -> FATAL if discardUnknownObjects=false', () => {
- const res: ResponseType<'CLEANUP_UNKNOWN_AND_EXCLUDED'> = Either.left({
- type: 'unknown_docs_found' as const,
- unknownDocs: [
- { id: 'dashboard:12', type: 'dashboard' },
- { id: 'foo:17', type: 'foo' },
- ],
- });
-
- const newState = model(cleanupUnknownAndExcluded, res);
-
- expect(newState).toMatchObject({
- controlState: 'FATAL',
- reason: expect.stringContaining(
- 'Migration failed because some documents were found which use unknown saved object types'
- ),
- });
- });
- });
-
- describe('CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK', () => {
- const cleanupUnknownAndExcludedWaitForTask: CleanupUnknownAndExcludedWaitForTaskState = {
- ...postInitState,
- controlState: 'CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK',
- deleteByQueryTaskId: '1234',
- sourceIndex: Option.some('.kibana_7.11.0_001') as Option.Some,
- sourceIndexMappings: Option.some(
- baseState.targetIndexMappings
- ) as Option.Some,
- targetIndex: baseState.versionIndex,
- kibanaVersion: '7.12.0', // new version!
- currentAlias: '.kibana',
- versionAlias: '.kibana_7.12.0',
- aliases: {
- '.kibana': '.kibana_7.11.0_001',
- '.kibana_7.11.0': '.kibana_7.11.0_001',
- },
- };
-
- test('CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK -> CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK when response is left wait_for_task_completion_timeout', () => {
- const res: ResponseType<'UPDATE_TARGET_MAPPINGS_PROPERTIES_WAIT_FOR_TASK'> = Either.left({
- message: '[timeout_exception] Timeout waiting for ...',
- type: 'wait_for_task_completion_timeout',
- });
- const newState = model(cleanupUnknownAndExcludedWaitForTask, res);
- expect(newState.controlState).toEqual('CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK');
- expect(newState.retryCount).toEqual(1);
- expect(newState.retryDelay).toEqual(2000);
- });
-
- test('CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK -> PREPARE_COMPATIBLE_MIGRATION if action succeeds', () => {
- const res: ResponseType<'CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK'> = Either.right({
- type: 'cleanup_successful' as const,
- });
- const newState = model(
- cleanupUnknownAndExcludedWaitForTask,
- res
- ) as PrepareCompatibleMigration;
-
- expect(newState.controlState).toEqual('PREPARE_COMPATIBLE_MIGRATION');
- expect(newState.targetIndexMappings).toEqual(indexMapping);
- expect(newState.preTransformDocsActions).toEqual([
- {
- add: {
- alias: '.kibana_7.12.0',
- index: '.kibana_7.11.0_001',
- },
- },
- {
- remove: {
- alias: '.kibana_7.11.0',
- index: '.kibana_7.11.0_001',
- must_exist: true,
- },
- },
- ]);
- });
-
- test('CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK -> CLEANUP_UNKNOWN_AND_EXCLUDED if the deleteQuery fails and we have some attempts left', () => {
- const res: ResponseType<'CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK'> = Either.left({
- type: 'cleanup_failed' as const,
- failures: ['Failed to delete dashboard:12345', 'Failed to delete dashboard:67890'],
- versionConflicts: 12,
- });
-
- const newState = model(cleanupUnknownAndExcludedWaitForTask, res);
-
- expect(newState).toMatchObject({
- controlState: 'CLEANUP_UNKNOWN_AND_EXCLUDED',
- logs: [
- {
- level: 'warning',
- message:
- 'Errors occurred whilst deleting unwanted documents. Another instance is probably updating or deleting documents in the same index. Retrying attempt 1.',
- },
- ],
- });
- });
-
- test('CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK -> FAIL if the deleteQuery fails after N retries', () => {
- const res: ResponseType<'CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK'> = Either.left({
- type: 'cleanup_failed' as const,
- failures: ['Failed to delete dashboard:12345', 'Failed to delete dashboard:67890'],
- });
-
- const newState = model(
- {
- ...cleanupUnknownAndExcludedWaitForTask,
- retryCount: cleanupUnknownAndExcludedWaitForTask.retryAttempts,
- },
- res
- );
-
- expect(newState).toMatchObject({
- controlState: 'FATAL',
- reason: expect.stringContaining(
- 'Migration failed because it was unable to delete unwanted documents from the .kibana_7.11.0_001 system index'
- ),
- });
- });
- });
-
- describe('REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION', () => {
- const checkClusterRoutingAllocationState: ReindexCheckClusterRoutingAllocationState = {
- ...postInitState,
- controlState: 'REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION',
- sourceIndex: Option.some('.kibana') as Option.Some,
- sourceIndexMappings: Option.some({}) as Option.Some,
- };
-
- test('REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION -> REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION when cluster allocation is not compatible', () => {
- const res: ResponseType<'REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION'> = Either.left({
- type: 'incompatible_cluster_routing_allocation',
- });
- const newState = model(checkClusterRoutingAllocationState, res);
-
- expect(newState.controlState).toBe('REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION');
- expect(newState.retryCount).toEqual(1);
- expect(newState.retryDelay).toEqual(2000);
- });
-
- test('REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION -> CHECK_UNKNOWN_DOCUMENTS when cluster allocation is compatible', () => {
- const res: ResponseType<'REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION'> = Either.right({});
- const newState = model(checkClusterRoutingAllocationState, res);
-
- expect(newState.controlState).toBe('CHECK_UNKNOWN_DOCUMENTS');
- expect(newState.retryCount).toEqual(0);
- expect(newState.retryDelay).toEqual(0);
- });
- });
-
- describe('CHECK_UNKNOWN_DOCUMENTS', () => {
- const mappingsWithUnknownType = {
- properties: {
- disabled_saved_object_type: {
- properties: {
- value: { type: 'keyword' },
- },
- },
- },
- _meta: {
- migrationMappingPropertyHashes: {
- disabled_saved_object_type: '7997cf5a56cc02bdc9c93361bde732b0',
- },
- },
- } as const;
-
- test('CHECK_UNKNOWN_DOCUMENTS -> SET_SOURCE_WRITE_BLOCK if action succeeds and no unknown docs are found', () => {
- const checkUnknownDocumentsSourceState: CheckUnknownDocumentsState = {
- ...postInitState,
- controlState: 'CHECK_UNKNOWN_DOCUMENTS',
- sourceIndex: Option.some('.kibana_3') as Option.Some,
- sourceIndexMappings: Option.some(mappingsWithUnknownType) as Option.Some,
- };
-
- const res: ResponseType<'CHECK_UNKNOWN_DOCUMENTS'> = Either.right({});
- const newState = model(checkUnknownDocumentsSourceState, res);
- expect(newState.controlState).toEqual('SET_SOURCE_WRITE_BLOCK');
-
- expect(newState).toMatchObject({
- controlState: 'SET_SOURCE_WRITE_BLOCK',
- sourceIndex: Option.some('.kibana_3'),
- targetIndex: '.kibana_7.11.0_001',
- });
-
- // This snapshot asserts that we disable the unknown saved object
- // type. Because it's mappings are disabled, we also don't copy the
- // `_meta.migrationMappingPropertyHashes` for the disabled type.
- expect(newState.targetIndexMappings).toMatchInlineSnapshot(`
- Object {
- "_meta": Object {
- "migrationMappingPropertyHashes": Object {
- "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0",
- },
- },
- "properties": Object {
- "new_saved_object_type": Object {
- "properties": Object {
- "value": Object {
- "type": "text",
- },
- },
- },
- },
- }
- `);
-
- // No log message gets appended
- expect(newState.logs).toEqual([]);
- });
-
- describe('when unknown docs are found', () => {
- test('CHECK_UNKNOWN_DOCUMENTS -> SET_SOURCE_WRITE_BLOCK if discardUnknownObjects=true', () => {
- const checkUnknownDocumentsSourceState: CheckUnknownDocumentsState = {
- ...postInitState,
- discardUnknownObjects: true,
- controlState: 'CHECK_UNKNOWN_DOCUMENTS',
- sourceIndex: Option.some('.kibana_3') as Option.Some,
- sourceIndexMappings: Option.some(mappingsWithUnknownType) as Option.Some,
- };
-
- const res: ResponseType<'CHECK_UNKNOWN_DOCUMENTS'> = Either.right({
- type: 'unknown_docs_found',
- unknownDocs: [
- { id: 'dashboard:12', type: 'dashboard' },
- { id: 'foo:17', type: 'foo' },
- ],
- });
- const newState = model(checkUnknownDocumentsSourceState, res);
-
- expect(newState).toMatchObject({
- controlState: 'SET_SOURCE_WRITE_BLOCK',
- sourceIndex: Option.some('.kibana_3'),
- targetIndex: '.kibana_7.11.0_001',
- });
-
- expect(newState.excludeOnUpgradeQuery).toEqual({
- bool: {
- must_not: [
- { term: { type: 'unused-fleet-agent-events' } },
- { term: { type: 'dashboard' } },
- { term: { type: 'foo' } },
- ],
- must: [{ exists: { field: 'type' } }],
- },
- });
-
- // we should have a warning in the logs about the ignored types
- expect(
- newState.logs.find(({ level, message }) => {
- return (
- level === 'warning' && message.includes('dashboard') && message.includes('foo')
- );
- })
- ).toBeDefined();
- });
-
- test('CHECK_UNKNOWN_DOCUMENTS -> FATAL if discardUnknownObjects=false', () => {
- const checkUnknownDocumentsSourceState: CheckUnknownDocumentsState = {
- ...postInitState,
- controlState: 'CHECK_UNKNOWN_DOCUMENTS',
- sourceIndex: Option.some('.kibana_3') as Option.Some,
- sourceIndexMappings: Option.some(mappingsWithUnknownType) as Option.Some,
- };
-
- const res: ResponseType<'CHECK_UNKNOWN_DOCUMENTS'> = Either.right({
- type: 'unknown_docs_found',
- unknownDocs: [
- { id: 'dashboard:12', type: 'dashboard' },
- { id: 'foo:17', type: 'foo' },
- ],
- });
- const newState = model(checkUnknownDocumentsSourceState, res);
- expect(newState.controlState).toEqual('FATAL');
-
- expect(newState).toMatchObject({
- controlState: 'FATAL',
- reason: expect.stringContaining(
- 'Migration failed because some documents were found which use unknown saved object types'
- ),
- });
- });
- });
- });
-
- describe('SET_SOURCE_WRITE_BLOCK', () => {
- const setWriteBlockState: SetSourceWriteBlockState = {
- ...postInitState,
- controlState: 'SET_SOURCE_WRITE_BLOCK',
- sourceIndex: Option.some('.kibana') as Option.Some,
- sourceIndexMappings: Option.some({}) as Option.Some,
- };
-
- test('SET_SOURCE_WRITE_BLOCK -> SET_SOURCE_WRITE_BLOCK if action fails with set_write_block_failed', () => {
- const res: ResponseType<'SET_SOURCE_WRITE_BLOCK'> = Either.left({
- type: 'retryable_es_client_error',
- message: 'set_write_block_failed',
- });
- const newState = model(setWriteBlockState, res);
- expect(newState.controlState).toEqual('SET_SOURCE_WRITE_BLOCK');
- expect(newState.retryCount).toEqual(1);
- expect(newState.retryDelay).toEqual(2000);
- });
-
- test('SET_SOURCE_WRITE_BLOCK -> CALCULATE_EXCLUDE_FILTERS if action succeeds with set_write_block_succeeded', () => {
- const res: ResponseType<'SET_SOURCE_WRITE_BLOCK'> = Either.right(
- 'set_write_block_succeeded'
- );
- const newState = model(setWriteBlockState, res);
- expect(newState.controlState).toEqual('CALCULATE_EXCLUDE_FILTERS');
- expect(newState.retryCount).toEqual(0);
- expect(newState.retryDelay).toEqual(0);
- });
- test('SET_SOURCE_WRITE_BLOCK -> REFRESH_TARGET if source index matches target index', () => {
- const index = `.kibana_${setWriteBlockState.kibanaVersion}_001`;
- const res: ResponseType<'SET_SOURCE_WRITE_BLOCK'> = Either.left({
- type: 'source_equals_target' as const,
- index,
- });
- const newState = model(setWriteBlockState, res);
- expect(newState.controlState).toEqual('REFRESH_TARGET');
- });
- });
-
- describe('CALCULATE_EXCLUDE_FILTERS', () => {
- const state: CalculateExcludeFiltersState = {
- ...postInitState,
- controlState: 'CALCULATE_EXCLUDE_FILTERS',
- sourceIndex: Option.some('.kibana') as Option.Some,
- sourceIndexMappings: Option.some({}) as Option.Some,
- tempIndexMappings: { properties: {} },
- };
-
- test('CALCULATE_EXCLUDE_FILTERS -> CALCULATE_EXCLUDE_FILTERS if action fails with retryable error', () => {
- const res: ResponseType<'CALCULATE_EXCLUDE_FILTERS'> = Either.left({
- type: 'retryable_es_client_error',
- message: 'Something temporarily broke!',
- });
- const newState = model(state, res);
- expect(newState.controlState).toEqual('CALCULATE_EXCLUDE_FILTERS');
- });
- it('CALCULATE_EXCLUDE_FILTERS -> CREATE_REINDEX_TEMP if action succeeds with filters', () => {
- const res: ResponseType<'CALCULATE_EXCLUDE_FILTERS'> = Either.right({
- filterClauses: [{ term: { fieldA: 'abc' } }],
- errorsByType: { type1: new Error('an error!') },
- });
- const newState = model(state, res);
- expect(newState.controlState).toEqual('CREATE_REINDEX_TEMP');
-
- expect(newState.excludeOnUpgradeQuery).toEqual({
- // new filters should be added inside a must_not clause, enriching excludeOnUpgradeQuery
- bool: {
- must_not: [
- {
- term: {
- type: 'unused-fleet-agent-events',
- },
- },
- {
- term: {
- fieldA: 'abc',
- },
- },
- ],
- },
- });
- // Logs should be added for any errors encountered from excludeOnUpgrade hooks
- expect(newState.logs).toEqual([
+ test('COMPATIBLE_UPDATE_CHECK_CLUSTER_ROUTING_ALLOCATION -> COMPATIBLE_UPDATE_CHECK_CLUSTER_ROUTING_ALLOCATION when cluster allocation is not compatible', () => {
+ const res: ResponseType<'COMPATIBLE_UPDATE_CHECK_CLUSTER_ROUTING_ALLOCATION'> = Either.left(
{
- level: 'warning',
- message: `Ignoring excludeOnUpgrade hook on type [type1] that failed with error: "Error: an error!"`,
- },
- ]);
- });
- });
-
- describe('CREATE_REINDEX_TEMP', () => {
- const state: CreateReindexTempState = {
- ...postInitState,
- controlState: 'CREATE_REINDEX_TEMP',
- sourceIndex: Option.some('.kibana') as Option.Some,
- sourceIndexMappings: Option.some({}) as Option.Some,
- tempIndexMappings: { properties: {} },
- };
-
- describe('if the migrator is NOT involved in a relocation', () => {
- it('CREATE_REINDEX_TEMP -> REINDEX_SOURCE_TO_TEMP_OPEN_PIT if action succeeds', () => {
- const res: ResponseType<'CREATE_REINDEX_TEMP'> = Either.right('create_index_succeeded');
- const newState = model(state, res);
- expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_OPEN_PIT');
- expect(newState.retryCount).toEqual(0);
- expect(newState.retryDelay).toEqual(0);
- });
- });
-
- describe('if the migrator is involved in a relocation', () => {
- it('CREATE_REINDEX_TEMP -> READY_TO_REINDEX_SYNC if action succeeds', () => {
- const res: ResponseType<'CREATE_REINDEX_TEMP'> = Either.right('create_index_succeeded');
- const newState = model({ ...state, mustRelocateDocuments: true }, res);
- expect(newState.controlState).toEqual('READY_TO_REINDEX_SYNC');
- expect(newState.retryCount).toEqual(0);
- expect(newState.retryDelay).toEqual(0);
- });
- });
-
- it('CREATE_REINDEX_TEMP -> CREATE_REINDEX_TEMP if action fails with index_not_green_timeout', () => {
- const res: ResponseType<'CREATE_REINDEX_TEMP'> = Either.left({
- message: '[index_not_green_timeout] Timeout waiting for ...',
- type: 'index_not_green_timeout',
- });
- const newState = model(state, res);
- expect(newState.controlState).toEqual('CREATE_REINDEX_TEMP');
- expect(newState.retryCount).toEqual(1);
- expect(newState.retryDelay).toEqual(2000);
- expect(newState.logs[0]).toMatchInlineSnapshot(`
- Object {
- "level": "error",
- "message": "Action failed with '[index_not_green_timeout] Timeout waiting for ... Refer to repeatedTimeoutRequests for information on how to resolve the issue.'. Retrying attempt 1 in 2 seconds.",
- }
- `);
- });
- it('CREATE_REINDEX_TEMP -> REINDEX_SOURCE_TO_TEMP_OPEN_PIT resets retry count if action succeeds', () => {
- const res: ResponseType<'CREATE_REINDEX_TEMP'> = Either.right('create_index_succeeded');
- const testState = {
- ...state,
- retryCount: 1,
- retryDelay: 2000,
- };
- const newState = model(testState, res);
- expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_OPEN_PIT');
- expect(newState.retryCount).toEqual(0);
- expect(newState.retryDelay).toEqual(0);
- });
-
- test('CREATE_REINDEX_TEMP -> FATAL if action fails with cluster_shard_limit_exceeded', () => {
- const res: ResponseType<'CREATE_REINDEX_TEMP'> = Either.left({
- type: 'cluster_shard_limit_exceeded',
- });
- const newState = model(state, res) as FatalState;
- expect(newState.controlState).toEqual('FATAL');
- expect(newState.reason).toMatchInlineSnapshot(
- `"[cluster_shard_limit_exceeded] Upgrading Kibana requires adding a small number of new shards. Ensure that Kibana is able to add 10 more shards by increasing the cluster.max_shards_per_node setting, or removing indices to clear up resources. See clusterShardLimitExceeded"`
- );
- });
- });
-
- describe('READY_TO_REINDEX_SYNC', () => {
- const state: ReadyToReindexSyncState = {
- ...postInitState,
- controlState: 'READY_TO_REINDEX_SYNC',
- };
-
- describe('if the migrator source index did NOT exist', () => {
- test('READY_TO_REINDEX_SYNC -> DONE_REINDEXING_SYNC', () => {
- const res: ResponseType<'READY_TO_REINDEX_SYNC'> = Either.right({
- type: 'synchronization_successful' as const,
- data: [],
- });
- const newState = model(state, res);
- expect(newState.controlState).toEqual('DONE_REINDEXING_SYNC');
- });
- });
-
- describe('if the migrator source index did exist', () => {
- test('READY_TO_REINDEX_SYNC -> REINDEX_SOURCE_TO_TEMP_OPEN_PIT', () => {
- const res: ResponseType<'READY_TO_REINDEX_SYNC'> = Either.right({
- type: 'synchronization_successful' as const,
- data: [],
- });
- const newState = model(
- {
- ...state,
- sourceIndex: Option.fromNullable('.kibana'),
- sourceIndexMappings: Option.fromNullable({} as IndexMapping),
- },
- res
- );
- expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_OPEN_PIT');
- });
- });
-
- test('READY_TO_REINDEX_SYNC -> FATAL if the synchronization between migrators fails', () => {
- const res: ResponseType<'READY_TO_REINDEX_SYNC'> = Either.left({
- type: 'synchronization_failed',
- error: new Error('Other migrators failed to reach the synchronization point'),
- });
- const newState = model(state, res) as FatalState;
- expect(newState.controlState).toEqual('FATAL');
- expect(newState.reason).toMatchInlineSnapshot(
- `"An error occurred whilst waiting for other migrators to get to this step."`
- );
- });
- });
-
- describe('REINDEX_SOURCE_TO_TEMP_OPEN_PIT', () => {
- const state: ReindexSourceToTempOpenPit = {
- ...postInitState,
- controlState: 'REINDEX_SOURCE_TO_TEMP_OPEN_PIT',
- sourceIndex: Option.some('.kibana') as Option.Some,
- sourceIndexMappings: Option.some({}) as Option.Some,
- tempIndexMappings: { properties: {} },
- };
- it('REINDEX_SOURCE_TO_TEMP_OPEN_PIT -> REINDEX_SOURCE_TO_TEMP_READ if action succeeds', () => {
- const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_OPEN_PIT'> = Either.right({
- pitId: 'pit_id',
- });
- const newState = model(state, res) as ReindexSourceToTempRead;
- expect(newState.controlState).toBe('REINDEX_SOURCE_TO_TEMP_READ');
- expect(newState.sourceIndexPitId).toBe('pit_id');
- expect(newState.lastHitSortValue).toBe(undefined);
- expect(newState.progress.processed).toBe(undefined);
- expect(newState.progress.total).toBe(undefined);
- });
- });
-
- describe('REINDEX_SOURCE_TO_TEMP_READ', () => {
- const state: ReindexSourceToTempRead = {
- ...postInitState,
- controlState: 'REINDEX_SOURCE_TO_TEMP_READ',
- sourceIndex: Option.some('.kibana') as Option.Some,
- sourceIndexMappings: Option.some({}) as Option.Some,
- sourceIndexPitId: 'pit_id',
- targetIndex: '.kibana_7.11.0_001',
- tempIndexMappings: { properties: {} },
- lastHitSortValue: undefined,
- corruptDocumentIds: [],
- transformErrors: [],
- progress: createInitialProgress(),
- };
-
- it('REINDEX_SOURCE_TO_TEMP_READ -> REINDEX_SOURCE_TO_TEMP_TRANSFORM if the index has outdated documents to reindex', () => {
- const outdatedDocuments = [{ _id: '1', _source: { type: 'vis' } }];
- const lastHitSortValue = [123456];
- const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_READ'> = Either.right({
- outdatedDocuments,
- pitId: 'refreshed_pit_id',
- lastHitSortValue,
- totalHits: 1,
- });
- const newState = model(state, res) as ReindexSourceToTempTransform;
- expect(newState.controlState).toBe('REINDEX_SOURCE_TO_TEMP_TRANSFORM');
- expect(newState.sourceIndexPitId).toBe('refreshed_pit_id');
- expect(newState.outdatedDocuments).toBe(outdatedDocuments);
- expect(newState.lastHitSortValue).toBe(lastHitSortValue);
- expect(newState.progress.processed).toBe(undefined);
- expect(newState.progress.total).toBe(1);
- expect(newState.maxBatchSize).toBe(1000);
- expect(newState.batchSize).toBe(1000); // don't increase batchsize above default
- expect(newState.logs).toMatchInlineSnapshot(`
- Array [
- Object {
- "level": "info",
- "message": "Starting to process 1 documents.",
- },
- ]
- `);
- });
-
- it('REINDEX_SOURCE_TO_TEMP_READ -> REINDEX_SOURCE_TO_TEMP_TRANSFORM increases batchSize if < maxBatchSize', () => {
- const outdatedDocuments = [{ _id: '1', _source: { type: 'vis' } }];
- const lastHitSortValue = [123456];
- const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_READ'> = Either.right({
- outdatedDocuments,
- pitId: 'pit_id',
- lastHitSortValue,
- totalHits: 1,
- processedDocs: 1,
- });
- let newState = model({ ...state, batchSize: 500 }, res) as ReindexSourceToTempTransform;
- expect(newState.batchSize).toBe(600);
- newState = model({ ...state, batchSize: 600 }, res) as ReindexSourceToTempTransform;
- expect(newState.batchSize).toBe(720);
- newState = model({ ...state, batchSize: 720 }, res) as ReindexSourceToTempTransform;
- expect(newState.batchSize).toBe(864);
- newState = model({ ...state, batchSize: 864 }, res) as ReindexSourceToTempTransform;
- expect(newState.batchSize).toBe(1000); // + 20% would have been 1036
- expect(newState.controlState).toBe('REINDEX_SOURCE_TO_TEMP_TRANSFORM');
- expect(newState.maxBatchSize).toBe(1000);
- });
-
- it('REINDEX_SOURCE_TO_TEMP_READ -> REINDEX_SOURCE_TO_TEMP_READ if left es_response_too_large', () => {
- const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_READ'> = Either.left({
- type: 'es_response_too_large',
- contentLength: 4567,
- });
- const newState = model(state, res) as ReindexSourceToTempRead;
- expect(newState.controlState).toBe('REINDEX_SOURCE_TO_TEMP_READ');
- expect(newState.lastHitSortValue).toBe(undefined); // lastHitSortValue should not be set
- expect(newState.progress.processed).toBe(undefined); // don't increment progress
- expect(newState.batchSize).toBe(500); // halves the batch size
- expect(newState.maxBatchSize).toBe(1000); // leaves maxBatchSize unchanged
- expect(newState.logs).toMatchInlineSnapshot(`
- Array [
- Object {
- "level": "warning",
- "message": "Read a batch with a response content length of 4567 bytes which exceeds migrations.maxReadBatchSizeBytes, retrying by reducing the batch size in half to 500.",
- },
- ]
- `);
- });
-
- it('REINDEX_SOURCE_TO_TEMP_READ -> REINDEX_SOURCE_TO_TEMP_READ if left es_response_too_large will not reduce batch size below 1', () => {
- const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_READ'> = Either.left({
- type: 'es_response_too_large',
- contentLength: 2345,
- });
- const newState = model({ ...state, batchSize: 1.5 }, res) as ReindexSourceToTempRead;
- expect(newState.controlState).toBe('REINDEX_SOURCE_TO_TEMP_READ');
- expect(newState.lastHitSortValue).toBe(undefined); // lastHitSortValue should not be set
- expect(newState.progress.processed).toBe(undefined); // don't increment progress
- expect(newState.batchSize).toBe(1); // don't halve the batch size or go below 1
- expect(newState.maxBatchSize).toBe(1000); // leaves maxBatchSize unchanged
- expect(newState.logs).toMatchInlineSnapshot(`
- Array [
- Object {
- "level": "warning",
- "message": "Read a batch with a response content length of 2345 bytes which exceeds migrations.maxReadBatchSizeBytes, retrying by reducing the batch size in half to 1.",
- },
- ]
- `);
- });
-
- it('REINDEX_SOURCE_TO_TEMP_READ -> FATAL if left es_response_too_large and batchSize already 1', () => {
- const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_READ'> = Either.left({
- type: 'es_response_too_large',
- contentLength: 2345,
- });
- const newState = model({ ...state, batchSize: 1 }, res) as FatalState;
- expect(newState).toMatchObject({
- controlState: 'FATAL',
- batchSize: 1,
- maxBatchSize: 1000,
- reason:
- 'After reducing the read batch size to a single document, the Elasticsearch response content length was 2345bytes which still exceeded migrations.maxReadBatchSizeBytes. Increase migrations.maxReadBatchSizeBytes and try again.',
- });
- });
-
- it('REINDEX_SOURCE_TO_TEMP_READ -> REINDEX_SOURCE_TO_TEMP_CLOSE_PIT if no outdated documents to reindex', () => {
- const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_READ'> = Either.right({
- outdatedDocuments: [],
- pitId: 'refreshed_pit_id',
- lastHitSortValue: undefined,
- totalHits: undefined,
- });
- const newState = model(state, res) as ReindexSourceToTempClosePit;
- expect(newState.controlState).toBe('REINDEX_SOURCE_TO_TEMP_CLOSE_PIT');
- expect(newState.sourceIndexPitId).toBe('refreshed_pit_id');
- expect(newState.logs).toStrictEqual([]); // No logs because no hits
- });
-
- describe('when transform failures or corrupt documents are found', () => {
- it('REINDEX_SOURCE_TO_TEMP_READ -> FATAL if no outdated documents to reindex and transform failures seen with previous outdated documents', () => {
- const testState: ReindexSourceToTempRead = {
- ...state,
- corruptDocumentIds: ['a:b'],
- transformErrors: [],
- };
- const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_READ'> = Either.right({
- outdatedDocuments: [],
- pitId: 'pit_id',
- lastHitSortValue: undefined,
- totalHits: undefined,
- });
- const newState = model(testState, res) as FatalState;
- expect(newState.controlState).toBe('FATAL');
- expect(newState.reason).toMatchInlineSnapshot(`
- "Migrations failed. Reason: 1 corrupt saved object documents were found: a:b
-
- To allow migrations to proceed, please delete or fix these documents.
- Note that you can configure Kibana to automatically discard corrupt documents and transform errors for this migration.
- Please refer to https://someurl.co/ for more information."
- `);
- expect(newState.logs).toStrictEqual([]); // No logs because no hits
- });
-
- it('REINDEX_SOURCE_TO_TEMP_READ -> REINDEX_SOURCE_TO_TEMP_CLOSE_PIT if discardCorruptObjects=true', () => {
- const testState: ReindexSourceToTempRead = {
- ...state,
- discardCorruptObjects: true,
- corruptDocumentIds: ['a:b'],
- transformErrors: [{ rawId: 'c:d', err: new Error('Oops!') }],
- };
- const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_READ'> = Either.right({
- outdatedDocuments: [],
- pitId: 'pit_id',
- lastHitSortValue: undefined,
- totalHits: undefined,
- });
- const newState = model(testState, res) as ReindexSourceToTempClosePit;
- expect(newState.controlState).toBe('REINDEX_SOURCE_TO_TEMP_CLOSE_PIT');
- expect(newState.logs.length).toEqual(1);
- expect(newState.logs[0]).toMatchInlineSnapshot(`
- Object {
- "level": "warning",
- "message": "Kibana has been configured to discard corrupt documents and documents that cause transform errors for this migration.
- Therefore, the following documents will not be taken into account and they will not be available after the migration:
- - \\"a:b\\" (corrupt)
- - \\"c:d\\" (Oops!)
- ",
- }
- `);
- });
- });
- });
-
- describe('REINDEX_SOURCE_TO_TEMP_CLOSE_PIT', () => {
- const state: ReindexSourceToTempClosePit = {
- ...postInitState,
- controlState: 'REINDEX_SOURCE_TO_TEMP_CLOSE_PIT',
- sourceIndex: Option.some('.kibana') as Option.Some,
- sourceIndexMappings: Option.some({}) as Option.Some,
- sourceIndexPitId: 'pit_id',
- tempIndexMappings: { properties: {} },
- };
-
- describe('if the migrator is NOT involved in a relocation', () => {
- it('REINDEX_SOURCE_TO_TEMP_CLOSE_PIT -> SET_TEMP_WRITE_BLOCK if action succeeded', () => {
- const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_CLOSE_PIT'> = Either.right({});
- const newState = model(state, res) as ReindexSourceToTempTransform;
- expect(newState.controlState).toBe('SET_TEMP_WRITE_BLOCK');
- expect(newState.sourceIndex).toEqual(state.sourceIndex);
- });
- });
-
- describe('if the migrator is involved in a relocation', () => {
- it('REINDEX_SOURCE_TO_TEMP_CLOSE_PIT -> DONE_REINDEXING_SYNC if action succeeded', () => {
- const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_CLOSE_PIT'> = Either.right({});
- const newState = model(
- { ...state, mustRelocateDocuments: true },
- res
- ) as ReindexSourceToTempTransform;
- expect(newState.controlState).toBe('DONE_REINDEXING_SYNC');
- });
- });
- });
-
- describe('DONE_REINDEXING_SYNC', () => {
- const state: DoneReindexingSyncState = {
- ...postInitState,
- controlState: 'DONE_REINDEXING_SYNC',
- };
-
- test('DONE_REINDEXING_SYNC -> SET_TEMP_WRITE_BLOCK if synchronization succeeds', () => {
- const res: ResponseType<'READY_TO_REINDEX_SYNC'> = Either.right({
- type: 'synchronization_successful' as const,
- data: [],
- });
- const newState = model(state, res);
- expect(newState.controlState).toEqual('SET_TEMP_WRITE_BLOCK');
- });
-
- test('DONE_REINDEXING_SYNC -> FATAL if the synchronization between migrators fails', () => {
- const res: ResponseType<'DONE_REINDEXING_SYNC'> = Either.left({
- type: 'synchronization_failed',
- error: new Error('Other migrators failed to reach the synchronization point'),
- });
- const newState = model(state, res) as FatalState;
- expect(newState.controlState).toEqual('FATAL');
- expect(newState.reason).toMatchInlineSnapshot(
- `"An error occurred whilst waiting for other migrators to get to this step."`
- );
- });
- });
-
- describe('REINDEX_SOURCE_TO_TEMP_TRANSFORM', () => {
- const state: ReindexSourceToTempTransform = {
- ...postInitState,
- controlState: 'REINDEX_SOURCE_TO_TEMP_TRANSFORM',
- outdatedDocuments: [],
- sourceIndex: Option.some('.kibana') as Option.Some,
- sourceIndexMappings: Option.some({}) as Option.Some,
- sourceIndexPitId: 'pit_id',
- targetIndex: '.kibana_7.11.0_001',
- lastHitSortValue: undefined,
- corruptDocumentIds: [],
- transformErrors: [],
- progress: { processed: undefined, total: 1 },
- };
-
- it('REINDEX_SOURCE_TO_TEMP_TRANSFORM -> REINDEX_SOURCE_TO_TEMP_INDEX_BULK if action succeeded', () => {
- const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_TRANSFORM'> = Either.right({
- processedDocs,
- });
- const newState = model(state, res) as ReindexSourceToTempIndexBulk;
- expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_INDEX_BULK');
- expect(newState.currentBatch).toEqual(0);
- expect(newState.bulkOperationBatches).toEqual(bulkOperationBatches);
- expect(newState.progress.processed).toBe(0); // Result of `(undefined ?? 0) + corruptDocumentsId.length`
- });
-
- it('increments the progress.processed counter', () => {
- const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_TRANSFORM'> = Either.right({
- processedDocs,
- });
-
- const testState = {
- ...state,
- outdatedDocuments: [{ _id: '1', _source: { type: 'vis' } }],
- progress: {
- processed: 1,
- total: 1,
- },
- };
+ type: 'incompatible_cluster_routing_allocation',
+ }
+ );
+ const newState = model(compatibleUpdateCheckClusterRoutingAllocationState, res);
- const newState = model(testState, res) as ReindexSourceToTempIndexBulk;
- expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_INDEX_BULK');
- expect(newState.progress.processed).toBe(2);
+ expect(newState.controlState).toBe('COMPATIBLE_UPDATE_CHECK_CLUSTER_ROUTING_ALLOCATION');
+ expect(newState.retryCount).toEqual(1);
+ expect(newState.retryDelay).toEqual(2000);
});
- it('REINDEX_SOURCE_TO_TEMP_TRANSFORM -> REINDEX_SOURCE_TO_TEMP_READ if action succeeded but we have carried through previous failures', () => {
- const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_TRANSFORM'> = Either.right({
- processedDocs,
- });
- const testState = {
- ...state,
- corruptDocumentIds: ['a:b'],
- transformErrors: [],
- };
- const newState = model(testState, res) as ReindexSourceToTempTransform;
- expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_READ');
- expect(newState.corruptDocumentIds.length).toEqual(1);
- expect(newState.transformErrors.length).toEqual(0);
- expect(newState.progress.processed).toBe(0);
- });
-
- it('REINDEX_SOURCE_TO_TEMP_TRANSFORM -> REINDEX_SOURCE_TO_TEMP_READ when response is left documents_transform_failed', () => {
- const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_TRANSFORM'> = Either.left({
- type: 'documents_transform_failed',
- corruptDocumentIds: ['a:b'],
- transformErrors: [],
- processedDocs: [],
- });
- const newState = model(state, res) as ReindexSourceToTempRead;
- expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_READ');
- expect(newState.corruptDocumentIds.length).toEqual(1);
- expect(newState.transformErrors.length).toEqual(0);
+ test('COMPATIBLE_UPDATE_CHECK_CLUSTER_ROUTING_ALLOCATION -> CLEANUP_UNKNOWN_AND_EXCLUDED when cluster allocation is compatible', () => {
+ const res: ResponseType<'COMPATIBLE_UPDATE_CHECK_CLUSTER_ROUTING_ALLOCATION'> =
+ Either.right({});
+ const newState = model(compatibleUpdateCheckClusterRoutingAllocationState, res);
+
+ expect(newState.controlState).toEqual('CLEANUP_UNKNOWN_AND_EXCLUDED');
expect(newState.retryCount).toEqual(0);
expect(newState.retryDelay).toEqual(0);
});
});
- describe('REINDEX_SOURCE_TO_TEMP_INDEX_BULK', () => {
- const reindexSourceToTempIndexBulkState: ReindexSourceToTempIndexBulk = {
+ describe('CLEANUP_UNKNOWN_AND_EXCLUDED', () => {
+ const cleanupUnknownAndExcluded: CleanupUnknownAndExcluded = {
...postInitState,
- controlState: 'REINDEX_SOURCE_TO_TEMP_INDEX_BULK',
- bulkOperationBatches,
- currentBatch: 0,
- sourceIndex: Option.some('.kibana') as Option.Some,
- sourceIndexMappings: Option.some({}) as Option.Some,
- sourceIndexPitId: 'pit_id',
- lastHitSortValue: undefined,
- transformErrors: [],
- corruptDocumentIds: [],
- progress: createInitialProgress(),
+ controlState: 'CLEANUP_UNKNOWN_AND_EXCLUDED',
+ sourceIndex: Option.some('.kibana_7.11.0_001') as Option.Some,
+ sourceIndexMappings: Option.some(
+ baseState.targetIndexMappings
+ ) as Option.Some,
+ targetIndex: baseState.versionIndex,
+ kibanaVersion: '7.12.0', // new version!
+ currentAlias: '.kibana',
+ versionAlias: '.kibana_7.12.0',
};
- test('REINDEX_SOURCE_TO_TEMP_INDEX_BULK -> REINDEX_SOURCE_TO_TEMP_READ if action succeeded', () => {
- const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX_BULK'> =
- Either.right('bulk_index_succeeded');
- const newState = model(reindexSourceToTempIndexBulkState, res);
- expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_READ');
- expect(newState.retryCount).toEqual(0);
- expect(newState.retryDelay).toEqual(0);
- });
+ describe('if action succeeds', () => {
+ test('CLEANUP_UNKNOWN_AND_EXCLUDED -> CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK', () => {
+ const res: ResponseType<'CLEANUP_UNKNOWN_AND_EXCLUDED'> = Either.right({
+ type: 'cleanup_started' as const,
+ taskId: '1234',
+ unknownDocs: [],
+ errorsByType: {},
+ });
+ const newState = model(cleanupUnknownAndExcluded, res);
- test('REINDEX_SOURCE_TO_TEMP_INDEX_BULK -> REINDEX_SOURCE_TO_TEMP_CLOSE_PIT if response is left target_index_had_write_block', () => {
- const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX_BULK'> = Either.left({
- type: 'target_index_had_write_block',
+ expect(newState.controlState).toEqual('CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK');
});
- const newState = model(reindexSourceToTempIndexBulkState, res);
- expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_CLOSE_PIT');
- expect(newState.retryCount).toEqual(0);
- expect(newState.retryDelay).toEqual(0);
- });
- test('REINDEX_SOURCE_TO_TEMP_INDEX_BULK -> REINDEX_SOURCE_TO_TEMP_CLOSE_PIT if response is left index_not_found_exception', () => {
- const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX_BULK'> = Either.left({
- type: 'index_not_found_exception',
- index: 'the_temp_index',
- });
- const newState = model(reindexSourceToTempIndexBulkState, res);
- expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_CLOSE_PIT');
- expect(newState.retryCount).toEqual(0);
- expect(newState.retryDelay).toEqual(0);
- });
+ test('CLEANUP_UNKNOWN_AND_EXCLUDED -> PREPARE_COMPATIBLE_MIGRATION', () => {
+ const res: ResponseType<'CLEANUP_UNKNOWN_AND_EXCLUDED'> = Either.right({
+ type: 'cleanup_not_needed' as const,
+ });
+ const newState = model(cleanupUnknownAndExcluded, res);
- test('REINDEX_SOURCE_TO_TEMP_INDEX_BULK -> FATAL if action returns left request_entity_too_large_exception', () => {
- const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX_BULK'> = Either.left({
- type: 'request_entity_too_large_exception',
+ expect(newState.controlState).toEqual('PREPARE_COMPATIBLE_MIGRATION');
});
- const newState = model(reindexSourceToTempIndexBulkState, res) as FatalState;
- expect(newState.controlState).toEqual('FATAL');
- expect(newState.reason).toMatchInlineSnapshot(
- `"While indexing a batch of saved objects, Elasticsearch returned a 413 Request Entity Too Large exception. Ensure that the Kibana configuration option 'migrations.maxBatchSizeBytes' is set to a value that is lower than or equal to the Elasticsearch 'http.max_content_length' configuration option."`
- );
});
- test('REINDEX_SOURCE_TO_TEMP_INDEX_BULK should throw a throwBadResponse error if action failed', () => {
- const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX_BULK'> = Either.left({
- type: 'retryable_es_client_error',
- message: 'random documents bulk index error',
+ test('CLEANUP_UNKNOWN_AND_EXCLUDED -> FATAL if discardUnknownObjects=false', () => {
+ const res: ResponseType<'CLEANUP_UNKNOWN_AND_EXCLUDED'> = Either.left({
+ type: 'unknown_docs_found' as const,
+ unknownDocs: [
+ { id: 'dashboard:12', type: 'dashboard' },
+ { id: 'foo:17', type: 'foo' },
+ ],
});
- const newState = model(reindexSourceToTempIndexBulkState, res);
- expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_INDEX_BULK');
- expect(newState.retryCount).toEqual(1);
- expect(newState.retryDelay).toEqual(2000);
- });
- test('REINDEX_SOURCE_TO_TEMP_INDEX_BULK retries with exponential backoff on unavailable_shards_exception', () => {
- const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX_BULK'> = Either.left({
- type: 'unavailable_shards_exception' as const,
- message: 'Not enough active copies to meet shard count of [ALL]',
- });
- const newState = model(reindexSourceToTempIndexBulkState, res);
- expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_INDEX_BULK');
- expect(newState.retryCount).toEqual(1);
- expect(newState.retryDelay).toEqual(2000);
- });
+ const newState = model(cleanupUnknownAndExcluded, res);
- test('REINDEX_SOURCE_TO_TEMP_INDEX_BULK recovers after unavailable_shards_exception retry', () => {
- // First call: unavailable_shards_exception triggers retry
- const errorRes: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX_BULK'> = Either.left({
- type: 'unavailable_shards_exception' as const,
- message: 'Not enough active copies to meet shard count of [ALL]',
+ expect(newState).toMatchObject({
+ controlState: 'FATAL',
+ reason: expect.stringContaining(
+ 'Migration failed because some documents were found which use unknown saved object types'
+ ),
});
- const retryState = model(reindexSourceToTempIndexBulkState, errorRes);
- expect(retryState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_INDEX_BULK');
- expect(retryState.retryCount).toEqual(1);
-
- // Second call: success after shards become available
- const successRes: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX_BULK'> =
- Either.right('bulk_index_succeeded');
- const recoveredState = model(retryState as State, successRes);
- expect(recoveredState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_READ');
- expect(recoveredState.retryCount).toEqual(0);
- expect(recoveredState.retryDelay).toEqual(0);
});
});
- describe('SET_TEMP_WRITE_BLOCK', () => {
- const state: SetTempWriteBlock = {
+ describe('CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK', () => {
+ const cleanupUnknownAndExcludedWaitForTask: CleanupUnknownAndExcludedWaitForTaskState = {
...postInitState,
- controlState: 'SET_TEMP_WRITE_BLOCK',
- sourceIndex: Option.some('.kibana') as Option.Some,
- sourceIndexMappings: Option.some({}) as Option.Some,
+ controlState: 'CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK',
+ deleteByQueryTaskId: '1234',
+ sourceIndex: Option.some('.kibana_7.11.0_001') as Option.Some,
+ sourceIndexMappings: Option.some(
+ baseState.targetIndexMappings
+ ) as Option.Some,
+ targetIndex: baseState.versionIndex,
+ kibanaVersion: '7.12.0', // new version!
+ currentAlias: '.kibana',
+ versionAlias: '.kibana_7.12.0',
+ aliases: {
+ '.kibana': '.kibana_7.11.0_001',
+ '.kibana_7.11.0': '.kibana_7.11.0_001',
+ },
};
- test('SET_TEMP_WRITE_BLOCK -> CLONE_TEMP_TO_TARGET when response is right', () => {
- const res: ResponseType<'SET_TEMP_WRITE_BLOCK'> = Either.right('set_write_block_succeeded');
- const newState = model(state, res);
- expect(newState.controlState).toEqual('CLONE_TEMP_TO_TARGET');
- expect(newState.retryCount).toEqual(0);
- expect(newState.retryDelay).toEqual(0);
- });
- });
-
- describe('CLONE_TEMP_TO_TARGET', () => {
- const state: CloneTempToTarget = {
- ...postInitState,
- controlState: 'CLONE_TEMP_TO_TARGET',
- sourceIndex: Option.some('.kibana') as Option.Some,
- };
- it('CLONE_TEMP_TO_TARGET -> REFRESH_TARGET if response is right', () => {
- const res: ResponseType<'CLONE_TEMP_TO_TARGET'> = Either.right({
- acknowledged: true,
- shardsAcknowledged: true,
+ test('CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK -> CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK when response is left wait_for_task_completion_timeout', () => {
+ const res: ResponseType<'UPDATE_TARGET_MAPPINGS_PROPERTIES_WAIT_FOR_TASK'> = Either.left({
+ message: '[timeout_exception] Timeout waiting for ...',
+ type: 'wait_for_task_completion_timeout',
});
- const newState = model(state, res);
- expect(newState.controlState).toBe('REFRESH_TARGET');
- expect(newState.retryCount).toBe(0);
- expect(newState.retryDelay).toBe(0);
+ const newState = model(cleanupUnknownAndExcludedWaitForTask, res);
+ expect(newState.controlState).toEqual('CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK');
+ expect(newState.retryCount).toEqual(1);
+ expect(newState.retryDelay).toEqual(2000);
});
- it('CLONE_TEMP_TO_TARGET -> REFRESH_TARGET if response is left index_not_found_exception', () => {
- const res: ResponseType<'CLONE_TEMP_TO_TARGET'> = Either.left({
- type: 'index_not_found_exception',
- index: 'temp_index',
+
+ test('CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK -> PREPARE_COMPATIBLE_MIGRATION if action succeeds', () => {
+ const res: ResponseType<'CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK'> = Either.right({
+ type: 'cleanup_successful' as const,
});
- const newState = model(state, res);
- expect(newState.controlState).toBe('REFRESH_TARGET');
- expect(newState.retryCount).toBe(0);
- expect(newState.retryDelay).toBe(0);
+ const newState = model(
+ cleanupUnknownAndExcludedWaitForTask,
+ res
+ ) as PrepareCompatibleMigration;
+
+ expect(newState.controlState).toEqual('PREPARE_COMPATIBLE_MIGRATION');
+ expect(newState.targetIndexMappings).toEqual(indexMapping);
+ expect(newState.preTransformDocsActions).toEqual([
+ {
+ add: {
+ alias: '.kibana_7.12.0',
+ index: '.kibana_7.11.0_001',
+ },
+ },
+ {
+ remove: {
+ alias: '.kibana_7.11.0',
+ index: '.kibana_7.11.0_001',
+ must_exist: true,
+ },
+ },
+ ]);
});
- it('CLONE_TEMP_TO_TARGET -> CLONE_TEMP_TO_TARGET if action fails with index_not_green_timeout', () => {
- const res: ResponseType<'CLONE_TEMP_TO_TARGET'> = Either.left({
- message: '[index_not_green_timeout] Timeout waiting for ...',
- type: 'index_not_green_timeout',
+
+ test('CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK -> CLEANUP_UNKNOWN_AND_EXCLUDED if the deleteQuery fails and we have some attempts left', () => {
+ const res: ResponseType<'CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK'> = Either.left({
+ type: 'cleanup_failed' as const,
+ failures: ['Failed to delete dashboard:12345', 'Failed to delete dashboard:67890'],
+ versionConflicts: 12,
});
- const newState = model(state, res);
- expect(newState.controlState).toEqual('CLONE_TEMP_TO_TARGET');
- expect(newState.retryCount).toEqual(1);
- expect(newState.retryDelay).toEqual(2000);
- expect(newState.logs[0]).toMatchInlineSnapshot(`
- Object {
- "level": "error",
- "message": "Action failed with '[index_not_green_timeout] Timeout waiting for ... Refer to repeatedTimeoutRequests for information on how to resolve the issue.'. Retrying attempt 1 in 2 seconds.",
- }
- `);
- });
- it('CLONE_TEMP_TO_TARGET -> MARK_VERSION_INDEX_READY resets the retry count and delay', () => {
- const res: ResponseType<'CLONE_TEMP_TO_TARGET'> = Either.right({
- acknowledged: true,
- shardsAcknowledged: true,
+
+ const newState = model(cleanupUnknownAndExcludedWaitForTask, res);
+
+ expect(newState).toMatchObject({
+ controlState: 'CLEANUP_UNKNOWN_AND_EXCLUDED',
+ logs: [
+ {
+ level: 'warning',
+ message:
+ 'Errors occurred whilst deleting unwanted documents. Another instance is probably updating or deleting documents in the same index. Retrying attempt 1.',
+ },
+ ],
});
- const testState = {
- ...state,
- retryCount: 1,
- retryDelay: 2000,
- };
- const newState = model(testState, res);
- expect(newState.controlState).toBe('REFRESH_TARGET');
- expect(newState.retryCount).toBe(0);
- expect(newState.retryDelay).toBe(0);
});
- test('CLONE_TEMP_TO_TARGET -> FATAL if action fails with cluster_shard_limit_exceeded', () => {
- const res: ResponseType<'CLONE_TEMP_TO_TARGET'> = Either.left({
- type: 'cluster_shard_limit_exceeded',
+ test('CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK -> FAIL if the deleteQuery fails after N retries', () => {
+ const res: ResponseType<'CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK'> = Either.left({
+ type: 'cleanup_failed' as const,
+ failures: ['Failed to delete dashboard:12345', 'Failed to delete dashboard:67890'],
});
- const newState = model(state, res) as FatalState;
- expect(newState.controlState).toEqual('FATAL');
- expect(newState.reason).toMatchInlineSnapshot(
- `"[cluster_shard_limit_exceeded] Upgrading Kibana requires adding a small number of new shards. Ensure that Kibana is able to add 10 more shards by increasing the cluster.max_shards_per_node setting, or removing indices to clear up resources. See clusterShardLimitExceeded"`
+
+ const newState = model(
+ {
+ ...cleanupUnknownAndExcludedWaitForTask,
+ retryCount: cleanupUnknownAndExcludedWaitForTask.retryAttempts,
+ },
+ res
);
+
+ expect(newState).toMatchObject({
+ controlState: 'FATAL',
+ reason: expect.stringContaining(
+ 'Migration failed because it was unable to delete unwanted documents from the .kibana_7.11.0_001 system index'
+ ),
+ });
});
});
- describe('PREPARE_COMPATIBLE_MIGRATIONS', () => {
+ describe('PREPARE_COMPATIBLE_MIGRATION', () => {
const someAliasAction: AliasAction = { add: { index: '.kibana', alias: '.kibana_8.7.0' } };
const state: PrepareCompatibleMigration = {
...postInitState,
@@ -2507,7 +1228,7 @@ describe('migrations v2 model', () => {
preTransformDocsActions: [someAliasAction],
};
- it('PREPARE_COMPATIBLE_MIGRATIONS -> REFRESH_SOURCE if action succeeds and we must refresh the index', () => {
+ it('PREPARE_COMPATIBLE_MIGRATION -> REFRESH_SOURCE if action succeeds and we must refresh the index', () => {
const res: ResponseType<'PREPARE_COMPATIBLE_MIGRATION'> = Either.right(
'update_aliases_succeeded'
);
@@ -2519,7 +1240,7 @@ describe('migrations v2 model', () => {
expect(newState.versionIndexReadyActions).toEqual(Option.none);
});
- it('PREPARE_COMPATIBLE_MIGRATIONS -> OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT if action succeeds', () => {
+ it('PREPARE_COMPATIBLE_MIGRATION -> OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT if action succeeds', () => {
const res: ResponseType<'PREPARE_COMPATIBLE_MIGRATION'> = Either.right(
'update_aliases_succeeded'
);
@@ -2528,7 +1249,7 @@ describe('migrations v2 model', () => {
expect(newState.versionIndexReadyActions).toEqual(Option.none);
});
- it('PREPARE_COMPATIBLE_MIGRATIONS -> REFRESH_SOURCE if action fails because the alias is not found', () => {
+ it('PREPARE_COMPATIBLE_MIGRATION -> REFRESH_SOURCE if action fails because the alias is not found', () => {
const res: ResponseType<'PREPARE_COMPATIBLE_MIGRATION'> = Either.left({
type: 'alias_not_found_exception',
});
@@ -2541,7 +1262,7 @@ describe('migrations v2 model', () => {
expect(newState.versionIndexReadyActions).toEqual(Option.none);
});
- it('PREPARE_COMPATIBLE_MIGRATIONS -> OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT if action fails because the alias is not found', () => {
+ it('PREPARE_COMPATIBLE_MIGRATION -> OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT if action fails because the alias is not found', () => {
const res: ResponseType<'PREPARE_COMPATIBLE_MIGRATION'> = Either.left({
type: 'alias_not_found_exception',
});
@@ -2668,13 +1389,13 @@ describe('migrations v2 model', () => {
totalHits: 1,
processedDocs: [],
});
- let newState = model({ ...state, batchSize: 500 }, res) as ReindexSourceToTempTransform;
+ let newState = model({ ...state, batchSize: 500 }, res) as OutdatedDocumentsTransform;
expect(newState.batchSize).toBe(600);
- newState = model({ ...state, batchSize: 600 }, res) as ReindexSourceToTempTransform;
+ newState = model({ ...state, batchSize: 600 }, res) as OutdatedDocumentsTransform;
expect(newState.batchSize).toBe(720);
- newState = model({ ...state, batchSize: 720 }, res) as ReindexSourceToTempTransform;
+ newState = model({ ...state, batchSize: 720 }, res) as OutdatedDocumentsTransform;
expect(newState.batchSize).toBe(864);
- newState = model({ ...state, batchSize: 864 }, res) as ReindexSourceToTempTransform;
+ newState = model({ ...state, batchSize: 864 }, res) as OutdatedDocumentsTransform;
expect(newState.batchSize).toBe(1000); // + 20% would have been 1036
expect(newState.controlState).toBe('OUTDATED_DOCUMENTS_TRANSFORM');
expect(newState.maxBatchSize).toBe(1000);
@@ -2685,7 +1406,7 @@ describe('migrations v2 model', () => {
type: 'es_response_too_large',
contentLength: 3456,
});
- const newState = model(state, res) as ReindexSourceToTempRead;
+ const newState = model(state, res) as OutdatedDocumentsSearchRead;
expect(newState.controlState).toBe('OUTDATED_DOCUMENTS_SEARCH_READ');
expect(newState.lastHitSortValue).toBe(undefined); // lastHitSortValue should not be set
expect(newState.progress.processed).toBe(undefined); // don't increment progress
@@ -2706,7 +1427,7 @@ describe('migrations v2 model', () => {
type: 'es_response_too_large',
contentLength: 2345,
});
- const newState = model({ ...state, batchSize: 1.5 }, res) as ReindexSourceToTempRead;
+ const newState = model({ ...state, batchSize: 1.5 }, res) as OutdatedDocumentsSearchRead;
expect(newState.controlState).toBe('OUTDATED_DOCUMENTS_SEARCH_READ');
expect(newState.lastHitSortValue).toBe(undefined); // lastHitSortValue should not be set
expect(newState.progress.processed).toBe(undefined); // don't increment progress
@@ -2809,8 +1530,8 @@ describe('migrations v2 model', () => {
targetIndex: '.kibana_7.11.0_001',
};
- describe('reindex migration', () => {
- it('CHECK_TARGET_MAPPINGS -> UPDATE_TARGET_MAPPINGS_PROPERTIES if origin mappings did not exist', () => {
+ describe('when index mappings are incomplete', () => {
+ it('CHECK_TARGET_MAPPINGS -> UPDATE_TARGET_MAPPINGS_PROPERTIES if index mappings are incomplete', () => {
const res: ResponseType<'CHECK_TARGET_MAPPINGS'> = Either.left({
type: 'index_mappings_incomplete' as const,
});
@@ -2880,22 +1601,6 @@ describe('migrations v2 model', () => {
});
});
- describe('REFRESH_TARGET', () => {
- const state: RefreshTarget = {
- ...postInitState,
- controlState: 'REFRESH_TARGET',
- versionIndexReadyActions: Option.none,
- sourceIndex: Option.some('.kibana') as Option.Some,
- targetIndex: '.kibana_7.11.0_001',
- };
-
- it('REFRESH_TARGET -> OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT if action succeeded', () => {
- const res: ResponseType<'REFRESH_TARGET'> = Either.right({ refreshed: true });
- const newState = model(state, res);
- expect(newState.controlState).toBe('OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT');
- });
- });
-
describe('OUTDATED_DOCUMENTS_TRANSFORM', () => {
const outdatedDocuments = [{ _id: '1', _source: { type: 'vis' } }];
const corruptDocumentIds = ['a:somethingelse'];
@@ -3382,24 +2087,6 @@ describe('migrations v2 model', () => {
expect(newState.retryDelay).toEqual(0);
});
- test('CHECK_VERSION_INDEX_READY_ACTIONS -> MARK_VERSION_INDEX_READY_SYNC if mustRelocateDocuments === true', () => {
- const versionIndexReadyActions = Option.some([
- { add: { index: 'kibana-index', alias: 'my-alias' } },
- ]);
-
- const newState = model(
- {
- ...сheckVersionIndexReadyActionsState,
- mustRelocateDocuments: true,
- versionIndexReadyActions,
- },
- res
- );
- expect(newState.controlState).toEqual('MARK_VERSION_INDEX_READY_SYNC');
- expect(newState.retryCount).toEqual(0);
- expect(newState.retryDelay).toEqual(0);
- });
-
test('CHECK_VERSION_INDEX_READY_ACTIONS -> DONE if none versionIndexReadyActions', () => {
const newState = model(сheckVersionIndexReadyActionsState, res);
expect(newState.controlState).toEqual('DONE');
@@ -3495,17 +2182,6 @@ describe('migrations v2 model', () => {
expect(newState.retryCount).toEqual(0);
expect(newState.retryDelay).toEqual(0);
});
-
- test('MARK_VERSION_INDEX_READY -> MARK_VERSION_INDEX_CONFLICT if another node removed the temporary index', () => {
- const res: ResponseType<'MARK_VERSION_INDEX_READY'> = Either.left({
- type: 'index_not_found_exception',
- index: '.kibana_7.11.0_reindex_temp',
- });
- const newState = model(markVersionIndexReadyState, res);
- expect(newState.controlState).toEqual('MARK_VERSION_INDEX_READY_CONFLICT');
- expect(newState.retryCount).toEqual(0);
- expect(newState.retryDelay).toEqual(0);
- });
});
describe('MARK_VERSION_INDEX_READY_CONFLICT', () => {
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/model/model.ts b/src/core/packages/saved-objects/migration-server-internal/src/model/model.ts
index 0cc011889cbc4..284a36f9aa20a 100644
--- a/src/core/packages/saved-objects/migration-server-internal/src/model/model.ts
+++ b/src/core/packages/saved-objects/migration-server-internal/src/model/model.ts
@@ -33,9 +33,6 @@ import {
} from './extract_errors';
import type { ExcludeRetryableEsError } from './types';
import {
- addExcludedTypesToBoolQuery,
- addMustClausesToBoolQuery,
- addMustNotClausesToBoolQuery,
getAliases,
getMigrationType,
indexBelongsToLaterVersion,
@@ -48,10 +45,9 @@ import {
increaseBatchSize,
hasLaterVersionAlias,
aliasVersion,
- REINDEX_TEMP_SUFFIX,
getPrepareCompatibleMigrationStateProperties,
} from './helpers';
-import { buildTempIndexMap, createBatches } from './create_batches';
+import { createBatches } from './create_batches';
import type { MigrationLog } from '../types';
import {
CLUSTER_SHARD_LIMIT_EXCEEDED_REASON,
@@ -69,7 +65,6 @@ export const model = (currentState: State, resW: ResponseType):
let stateP: State = currentState;
let logs: MigrationLog[] = stateP.logs;
- let excludeOnUpgradeQuery = stateP.excludeOnUpgradeQuery;
// Handle retryable_es_client_errors. Other left values need to be handled
// by the control state specific code below.
@@ -198,75 +193,6 @@ export const model = (currentState: State, resW: ResponseType):
sourceIndexMappings: postInitState.sourceIndexMappings as Option.Some,
targetIndex: postInitState.sourceIndex.value, // We preserve the same index, source == target (E.g: ".xx8.7.0_001")
};
- } else if (indices[stateP.legacyIndex] != null) {
- // Migrate from a legacy index
-
- // If the user used default index names we can narrow the version
- // number we use when creating a backup index. This is purely to help
- // users more easily identify how "old" and index is so that they can
- // decide if it's safe to delete these rollback backups. Because
- // backups are kept for rollback, a version number is more useful than
- // a date.
- let legacyVersion = '';
- if (stateP.indexPrefix === '.kibana') {
- legacyVersion = 'pre6.5.0';
- } else if (stateP.indexPrefix === '.kibana_task_manager') {
- legacyVersion = 'pre7.4.0';
- } else {
- legacyVersion = 'pre' + stateP.kibanaVersion;
- }
-
- const legacyReindexTarget = `${stateP.indexPrefix}_${legacyVersion}_001`;
-
- return {
- ...stateP,
- ...postInitState,
- controlState: 'LEGACY_CHECK_CLUSTER_ROUTING_ALLOCATION',
- sourceIndex: Option.some(legacyReindexTarget) as Option.Some,
- sourceIndexMappings: Option.some(
- indices[stateP.legacyIndex].mappings
- ) as Option.Some,
- targetIndex: newVersionTarget,
- legacyPreMigrationDoneActions: [
- { remove_index: { index: stateP.legacyIndex } },
- {
- add: {
- index: legacyReindexTarget,
- alias: stateP.currentAlias,
- },
- },
- ],
- versionIndexReadyActions: Option.some([
- {
- remove: {
- index: legacyReindexTarget,
- alias: stateP.currentAlias,
- must_exist: true,
- },
- },
- { add: { index: newVersionTarget, alias: stateP.currentAlias } },
- { add: { index: newVersionTarget, alias: stateP.versionAlias } },
- { remove_index: { index: stateP.tempIndex } },
- ]),
- };
- } else if (
- // if we must relocate documents to this migrator's index, but the index does NOT yet exist:
- // this migrator must create a temporary index and synchronize with other migrators
- // this is a similar flow to the reindex one, but this migrator will not reindexing anything
- stateP.mustRelocateDocuments
- ) {
- return {
- ...stateP,
- ...postInitState,
- controlState: 'RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION',
- sourceIndex: Option.none as Option.None,
- targetIndex: newVersionTarget,
- versionIndexReadyActions: Option.some([
- { add: { index: newVersionTarget, alias: stateP.currentAlias } },
- { add: { index: newVersionTarget, alias: stateP.versionAlias } },
- { remove_index: { index: stateP.tempIndex } },
- ]),
- };
} else {
// no need to copy anything over from other indices, we can start with a clean, empty index
return {
@@ -297,22 +223,6 @@ export const model = (currentState: State, resW: ResponseType):
throwBadResponse(stateP, left);
}
}
- } else if (stateP.controlState === 'RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION') {
- const res = resW as ExcludeRetryableEsError>;
- if (Either.isRight(res)) {
- return {
- ...stateP,
- controlState: 'CREATE_REINDEX_TEMP',
- };
- } else {
- const left = res.left;
- if (isTypeof(left, 'incompatible_cluster_routing_allocation')) {
- const retryErrorMessage = `[${left.type}] Incompatible Elasticsearch cluster settings detected. Remove the persistent and transient Elasticsearch cluster setting 'cluster.routing.allocation.enable' or set it to a value of 'all' to allow migrations to proceed. Refer to ${stateP.migrationDocLinks.routingAllocationDisabled} for more information on how to resolve the issue.`;
- return delayRetryState(stateP, retryErrorMessage, stateP.retryAttempts);
- } else {
- throwBadResponse(stateP, left);
- }
- }
} else if (stateP.controlState === 'WAIT_FOR_MIGRATION_COMPLETION') {
const res = resW as ExcludeRetryableEsError>;
const indices = res.right;
@@ -349,182 +259,13 @@ export const model = (currentState: State, resW: ResponseType):
],
};
}
- } else if (stateP.controlState === 'LEGACY_CHECK_CLUSTER_ROUTING_ALLOCATION') {
- const res = resW as ExcludeRetryableEsError>;
- if (Either.isRight(res)) {
- return {
- ...stateP,
- controlState: 'LEGACY_SET_WRITE_BLOCK',
- };
- } else {
- const left = res.left;
- if (isTypeof(left, 'incompatible_cluster_routing_allocation')) {
- const retryErrorMessage = `[${left.type}] Incompatible Elasticsearch cluster settings detected. Remove the persistent and transient Elasticsearch cluster setting 'cluster.routing.allocation.enable' or set it to a value of 'all' to allow migrations to proceed. Refer to ${stateP.migrationDocLinks.routingAllocationDisabled} for more information on how to resolve the issue.`;
- return delayRetryState(stateP, retryErrorMessage, stateP.retryAttempts);
- } else {
- throwBadResponse(stateP, left);
- }
- }
- } else if (stateP.controlState === 'LEGACY_SET_WRITE_BLOCK') {
- const res = resW as ExcludeRetryableEsError>;
- // If the write block is successfully in place
- if (Either.isRight(res)) {
- return { ...stateP, controlState: 'LEGACY_CREATE_REINDEX_TARGET' };
- } else if (Either.isLeft(res)) {
- // If the write block failed because the index doesn't exist, it means
- // another instance already completed the legacy pre-migration. Proceed
- // to the next step.
- const left = res.left;
- if (isTypeof(left, 'index_not_found_exception')) {
- return { ...stateP, controlState: 'LEGACY_CREATE_REINDEX_TARGET' };
- } else {
- throwBadResponse(stateP, left);
- }
- } else {
- throwBadResponse(stateP, res);
- }
- } else if (stateP.controlState === 'LEGACY_CREATE_REINDEX_TARGET') {
- const res = resW as ExcludeRetryableEsError>;
- if (Either.isLeft(res)) {
- const left = res.left;
- if (isTypeof(left, 'index_not_green_timeout')) {
- // `index_not_green_timeout` for the LEGACY_CREATE_REINDEX_TARGET source index:
- // A yellow status timeout could theoretically be temporary for a busy cluster
- // that takes a long time to allocate the primary and we retry the action to see if
- // we get a response.
- // If the cluster hit the low watermark for disk usage the LEGACY_CREATE_REINDEX_TARGET action will
- // continue to timeout and eventually lead to a failed migration.
- const retryErrorMessage = `${left.message} Refer to ${stateP.migrationDocLinks.repeatedTimeoutRequests} for information on how to resolve the issue.`;
- return delayRetryState(stateP, retryErrorMessage, stateP.retryAttempts);
- } else if (isTypeof(left, 'cluster_shard_limit_exceeded')) {
- return {
- ...stateP,
- controlState: 'FATAL',
- reason: `${CLUSTER_SHARD_LIMIT_EXCEEDED_REASON} See ${stateP.migrationDocLinks.clusterShardLimitExceeded}`,
- };
- } else {
- throwBadResponse(stateP, left);
- }
- } else if (Either.isRight(res)) {
- return {
- ...stateP,
- controlState: 'LEGACY_REINDEX',
- };
- } else {
- // If the createIndex action receives an 'resource_already_exists_exception'
- // it will wait until the index status turns green so we don't have any
- // left responses to handle here.
- throwBadResponse(stateP, res);
- }
- } else if (stateP.controlState === 'LEGACY_REINDEX') {
- const res = resW as ExcludeRetryableEsError>;
- if (Either.isRight(res)) {
- return {
- ...stateP,
- controlState: 'LEGACY_REINDEX_WAIT_FOR_TASK',
- legacyReindexTaskId: res.right.taskId,
- };
- } else {
- throwBadResponse(stateP, res);
- }
- } else if (stateP.controlState === 'LEGACY_REINDEX_WAIT_FOR_TASK') {
+ } else if (stateP.controlState === 'WAIT_FOR_YELLOW_SOURCE') {
const res = resW as ExcludeRetryableEsError>;
if (Either.isRight(res)) {
return {
...stateP,
- controlState: 'LEGACY_DELETE',
+ controlState: 'UPDATE_SOURCE_MAPPINGS_PROPERTIES',
};
- } else {
- const left = res.left;
- if (
- (isTypeof(left, 'index_not_found_exception') && left.index === stateP.legacyIndex) ||
- isTypeof(left, 'target_index_had_write_block')
- ) {
- // index_not_found_exception for the LEGACY_REINDEX source index:
- // another instance already complete the LEGACY_DELETE step.
- //
- // target_index_had_write_block: another instance already completed the
- // SET_SOURCE_WRITE_BLOCK step.
- //
- // If we detect that another instance has already completed a step, we
- // can technically skip ahead in the process until after the completed
- // step. However, by not skipping ahead we limit branches in the
- // control state progression and simplify the implementation.
- return { ...stateP, controlState: 'LEGACY_DELETE' };
- } else if (isTypeof(left, 'wait_for_task_completion_timeout')) {
- // After waiting for the specified timeout, the task has not yet
- // completed. Retry this step to see if the task has completed after an
- // exponential delay. We will basically keep polling forever until the
- // Elasticsearch task succeeds or fails.
- return delayRetryState(stateP, left.message, Number.MAX_SAFE_INTEGER);
- } else if (
- isTypeof(left, 'index_not_found_exception') ||
- isTypeof(left, 'incompatible_mapping_exception')
- ) {
- // We don't handle the following errors as the algorithm will never
- // run into these during the LEGACY_REINDEX_WAIT_FOR_TASK step:
- // - index_not_found_exception for the LEGACY_REINDEX target index
- // - incompatible_mapping_exception
- throwBadResponse(stateP, left as never);
- } else {
- throwBadResponse(stateP, left);
- }
- }
- } else if (stateP.controlState === 'LEGACY_DELETE') {
- const res = resW as ExcludeRetryableEsError>;
- if (Either.isRight(res)) {
- return { ...stateP, controlState: 'SET_SOURCE_WRITE_BLOCK' };
- } else if (Either.isLeft(res)) {
- const left = res.left;
- if (
- isTypeof(left, 'remove_index_not_a_concrete_index') ||
- (isTypeof(left, 'index_not_found_exception') && left.index === stateP.legacyIndex)
- ) {
- // index_not_found_exception, another Kibana instance already
- // deleted the legacy index
- //
- // remove_index_not_a_concrete_index, another Kibana instance already
- // deleted the legacy index and created a .kibana alias
- //
- // If we detect that another instance has already completed a step, we
- // can technically skip ahead in the process until after the completed
- // step. However, by not skipping ahead we limit branches in the
- // control state progression and simplify the implementation.
- return { ...stateP, controlState: 'SET_SOURCE_WRITE_BLOCK' };
- } else if (
- isTypeof(left, 'index_not_found_exception') ||
- isTypeof(left, 'alias_not_found_exception')
- ) {
- // We don't handle the following errors as the migration algorithm
- // will never cause them to occur:
- // - alias_not_found_exception we're not using must_exist
- // - index_not_found_exception for source index into which we reindex
- // the legacy index
- throwBadResponse(stateP, left as never);
- } else {
- throwBadResponse(stateP, left);
- }
- } else {
- throwBadResponse(stateP, res);
- }
- } else if (stateP.controlState === 'WAIT_FOR_YELLOW_SOURCE') {
- const res = resW as ExcludeRetryableEsError>;
- if (Either.isRight(res)) {
- if (stateP.mustRelocateDocuments) {
- // this migrator's index must dispatch documents to other indices,
- // and/or it must receive documents from other indices
- // we must reindex and synchronize with other migrators
- return {
- ...stateP,
- controlState: 'REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION',
- };
- } else {
- // this migrator is not involved in a relocation, we can proceed with the standard flow
- return {
- ...stateP,
- controlState: 'UPDATE_SOURCE_MAPPINGS_PROPERTIES',
- };
- }
} else if (Either.isLeft(res)) {
const left = res.left;
if (isTypeof(left, 'index_not_yellow_timeout')) {
@@ -536,756 +277,218 @@ export const model = (currentState: State, resW: ResponseType):
const retryErrorMessage = `${left.message} Refer to ${stateP.migrationDocLinks.repeatedTimeoutRequests} for information on how to resolve the issue.`;
return delayRetryState(stateP, retryErrorMessage, stateP.retryAttempts);
} else {
- throwBadResponse(stateP, left);
- }
- } else {
- throwBadResponse(stateP, res);
- }
- } else if (stateP.controlState === 'UPDATE_SOURCE_MAPPINGS_PROPERTIES') {
- const res = resW as ExcludeRetryableEsError>;
- const migrationType = getMigrationType({
- isMappingsCompatible: Either.isRight(res),
- isVersionMigrationCompleted: versionMigrationCompleted(
- stateP.currentAlias,
- stateP.versionAlias,
- stateP.aliases
- ),
- });
-
- switch (migrationType) {
- case MigrationType.Compatible:
- return {
- ...stateP,
- controlState: 'CLEANUP_UNKNOWN_AND_EXCLUDED',
- };
- case MigrationType.Incompatible:
- return {
- ...stateP,
- controlState: 'REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION',
- };
- case MigrationType.Unnecessary:
- return {
- ...stateP,
- // Skip to 'OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT' so that if a new plugin was
- // installed / enabled we can transform any old documents and update
- // the mappings for this plugin's types.
- controlState: 'OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT',
- // Source is a none because we didn't do any migration from a source index
- sourceIndex: Option.none,
- targetIndex: stateP.sourceIndex.value,
- // in this scenario, a .kibana_X.Y.Z_001 index exists that matches the current kibana version
- // aka we are NOT upgrading to a newer version
- // we inject the source index's current mappings in the state, to check them later
- targetIndexMappings: mergeMappingMeta(
- stateP.targetIndexMappings,
- stateP.sourceIndexMappings.value
- ),
- };
- case MigrationType.Invalid:
- return {
- ...stateP,
- controlState: 'FATAL',
- reason: 'Incompatible mappings change on already migrated Kibana instance.',
- };
- }
- } else if (stateP.controlState === 'CLEANUP_UNKNOWN_AND_EXCLUDED') {
- const res = resW as ExcludeRetryableEsError>;
- if (Either.isRight(res)) {
- if (res.right.type === 'cleanup_started') {
- if (res.right.unknownDocs.length) {
- logs = [
- ...stateP.logs,
- { level: 'warning', message: extractDiscardedUnknownDocs(res.right.unknownDocs) },
- ];
- }
-
- logs = [
- ...logs,
- ...Object.entries(res.right.errorsByType).map(([soType, error]) => ({
- level: 'warning' as const,
- message: `Ignored excludeOnUpgrade hook on type [${soType}] that failed with error: "${error.toString()}"`,
- })),
- ];
-
- return {
- ...stateP,
- logs,
- controlState: 'CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK',
- deleteByQueryTaskId: res.right.taskId,
- };
- } else if (res.right.type === 'cleanup_not_needed') {
- // let's move to the step after CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK
- return {
- ...stateP,
- logs,
- controlState: 'PREPARE_COMPATIBLE_MIGRATION',
- ...getPrepareCompatibleMigrationStateProperties(stateP),
- };
- } else {
- throwBadResponse(stateP, res.right);
- }
- } else {
- const reason = extractUnknownDocFailureReason(
- stateP.migrationDocLinks.resolveMigrationFailures,
- res.left.unknownDocs
- );
- return {
- ...stateP,
- controlState: 'FATAL',
- reason,
- logs: [
- ...logs,
- {
- level: 'error',
- message: reason,
- },
- ],
- };
- }
- } else if (stateP.controlState === 'CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK') {
- const res = resW as ExcludeRetryableEsError>;
- if (Either.isRight(res)) {
- return {
- ...stateP,
- logs,
- controlState: 'PREPARE_COMPATIBLE_MIGRATION',
- mustRefresh:
- stateP.mustRefresh || typeof res.right.deleted === 'undefined' || res.right.deleted > 0,
- ...getPrepareCompatibleMigrationStateProperties(stateP),
- };
- } else {
- if (isTypeof(res.left, 'wait_for_task_completion_timeout')) {
- // After waiting for the specified timeout, the task has not yet
- // completed. Retry this step to see if the task has completed after an
- // exponential delay. We will basically keep polling forever until the
- // Elasticsearch task succeeds or fails.
- return delayRetryState(stateP, res.left.message, Number.MAX_SAFE_INTEGER);
- } else {
- if (stateP.retryCount < stateP.retryAttempts) {
- const retryCount = stateP.retryCount + 1;
- const retryDelay = 1500 + 1000 * Math.random();
- return {
- ...stateP,
- controlState: 'CLEANUP_UNKNOWN_AND_EXCLUDED',
- mustRefresh: true,
- retryCount,
- retryDelay,
- logs: [
- ...stateP.logs,
- {
- level: 'warning',
- message: `Errors occurred whilst deleting unwanted documents. Another instance is probably updating or deleting documents in the same index. Retrying attempt ${retryCount}.`,
- },
- ],
- };
- } else {
- const failures = res.left.failures.length;
- const versionConflicts = res.left.versionConflicts ?? 0;
-
- let reason = `Migration failed because it was unable to delete unwanted documents from the ${stateP.sourceIndex.value} system index (${failures} failures and ${versionConflicts} conflicts)`;
- if (failures) {
- reason += `:\n` + res.left.failures.map((failure: string) => `- ${failure}\n`).join('');
- }
- return {
- ...stateP,
- controlState: 'FATAL',
- reason,
- };
- }
- }
- }
- } else if (stateP.controlState === 'PREPARE_COMPATIBLE_MIGRATION') {
- const res = resW as ExcludeRetryableEsError>;
- if (Either.isRight(res)) {
- return {
- ...stateP,
- controlState: stateP.mustRefresh ? 'REFRESH_SOURCE' : 'OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT',
- };
- } else {
- const left = res.left;
- // Note: if multiple newer Kibana versions are competing with each other to perform a migration,
- // it might happen that another Kibana instance has deleted this instance's version index.
- // NIT to handle this in properly, we'd have to add a PREPARE_COMPATIBLE_MIGRATION_CONFLICT step,
- // similar to MARK_VERSION_INDEX_READY_CONFLICT.
- if (isTypeof(left, 'alias_not_found_exception')) {
- // We assume that the alias was already deleted by another Kibana instance
- return {
- ...stateP,
- controlState: stateP.mustRefresh
- ? 'REFRESH_SOURCE'
- : 'OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT',
- };
- } else if (isTypeof(left, 'index_not_found_exception')) {
- // We don't handle the following errors as the migration algorithm
- // will never cause them to occur:
- // - index_not_found_exception
- throwBadResponse(stateP, left as never);
- } else if (isTypeof(left, 'remove_index_not_a_concrete_index')) {
- // We don't handle this error as the migration algorithm will never
- // cause it to occur (this error is only relevant to the LEGACY_DELETE
- // step).
- throwBadResponse(stateP, left as never);
- } else {
- throwBadResponse(stateP, left);
- }
- }
- } else if (stateP.controlState === 'REFRESH_SOURCE') {
- const res = resW as ExcludeRetryableEsError>;
- if (Either.isRight(res)) {
- return {
- ...stateP,
- controlState: 'OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT',
- };
- } else {
- throwBadResponse(stateP, res);
- }
- } else if (stateP.controlState === 'REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION') {
- const res = resW as ExcludeRetryableEsError>;
- if (Either.isRight(res)) {
- return {
- ...stateP,
- controlState: 'CHECK_UNKNOWN_DOCUMENTS',
- };
- } else {
- const left = res.left;
- if (isTypeof(left, 'incompatible_cluster_routing_allocation')) {
- const retryErrorMessage = `[${left.type}] Incompatible Elasticsearch cluster settings detected. Remove the persistent and transient Elasticsearch cluster setting 'cluster.routing.allocation.enable' or set it to a value of 'all' to allow migrations to proceed. Refer to ${stateP.migrationDocLinks.routingAllocationDisabled} for more information on how to resolve the issue.`;
- return delayRetryState(stateP, retryErrorMessage, stateP.retryAttempts);
- } else {
- throwBadResponse(stateP, left);
- }
- }
- } else if (stateP.controlState === 'CHECK_UNKNOWN_DOCUMENTS') {
- const res = resW as ExcludeRetryableEsError>;
-
- if (isTypeof(res.right, 'unknown_docs_found')) {
- if (!stateP.discardUnknownObjects) {
- const reason = extractUnknownDocFailureReason(
- stateP.migrationDocLinks.resolveMigrationFailures,
- res.right.unknownDocs
- );
-
- return {
- ...stateP,
- controlState: 'FATAL',
- reason,
- logs: [
- ...logs,
- {
- level: 'error',
- message: reason,
- },
- ],
- };
- }
-
- // at this point, users have configured kibana to discard unknown objects
- // thus, we can ignore unknown documents and proceed with the migration
- logs = [
- ...stateP.logs,
- { level: 'warning', message: extractDiscardedUnknownDocs(res.right.unknownDocs) },
- ];
-
- const unknownTypes = [...new Set(res.right.unknownDocs.map(({ type }) => type))];
-
- excludeOnUpgradeQuery = addExcludedTypesToBoolQuery(
- unknownTypes,
- stateP.excludeOnUpgradeQuery?.bool
- );
-
- excludeOnUpgradeQuery = addMustClausesToBoolQuery(
- [{ exists: { field: 'type' } }],
- excludeOnUpgradeQuery?.bool
- );
- }
-
- const source = stateP.sourceIndex;
- const target = stateP.versionIndex;
- return {
- ...stateP,
- controlState: 'SET_SOURCE_WRITE_BLOCK',
- logs,
- excludeOnUpgradeQuery,
- sourceIndex: source,
- targetIndex: target,
- versionIndexReadyActions: Option.some([
- { remove: { index: source.value, alias: stateP.currentAlias, must_exist: true } },
- { add: { index: target, alias: stateP.currentAlias } },
- { add: { index: target, alias: stateP.versionAlias } },
- { remove_index: { index: stateP.tempIndex } },
- ]),
- };
- } else if (stateP.controlState === 'SET_SOURCE_WRITE_BLOCK') {
- const res = resW as ExcludeRetryableEsError>;
- if (Either.isRight(res)) {
- // If the write block is successfully in place, proceed to the next step.
- return {
- ...stateP,
- controlState: 'CALCULATE_EXCLUDE_FILTERS',
- };
- } else if (isTypeof(res.left, 'source_equals_target')) {
- // As part of a reindex-migration, we wanted to block the source index to prevent updates
- // However, this migrator's source index matches the target index.
- // Thus, another instance's migrator is ahead of us. We skip the clone steps and continue the flow
- return {
- ...stateP,
- controlState: 'REFRESH_TARGET',
- };
- } else if (isTypeof(res.left, 'index_not_found_exception')) {
- // We don't handle the following errors as the migration algorithm
- // will never cause them to occur:
- // - index_not_found_exception
- throwBadResponse(stateP, res.left as never);
- } else {
- throwBadResponse(stateP, res.left);
- }
- } else if (stateP.controlState === 'CALCULATE_EXCLUDE_FILTERS') {
- const res = resW as ExcludeRetryableEsError>;
-
- if (Either.isRight(res)) {
- excludeOnUpgradeQuery = addMustNotClausesToBoolQuery(
- res.right.filterClauses,
- stateP.excludeOnUpgradeQuery?.bool
- );
-
- return {
- ...stateP,
- controlState: 'CREATE_REINDEX_TEMP',
- excludeOnUpgradeQuery,
- logs: [
- ...stateP.logs,
- ...Object.entries(res.right.errorsByType).map(([soType, error]) => ({
- level: 'warning' as const,
- message: `Ignoring excludeOnUpgrade hook on type [${soType}] that failed with error: "${error.toString()}"`,
- })),
- ],
- };
- } else {
- throwBadResponse(stateP, res);
- }
- } else if (stateP.controlState === 'CREATE_REINDEX_TEMP') {
- const res = resW as ExcludeRetryableEsError>;
- if (Either.isRight(res)) {
- if (stateP.mustRelocateDocuments) {
- // we are reindexing, and this migrator's index is involved in document relocations
- return { ...stateP, controlState: 'READY_TO_REINDEX_SYNC' };
- } else {
- // we are reindexing but this migrator's index is not involved in any document relocation
- return {
- ...stateP,
- controlState: 'REINDEX_SOURCE_TO_TEMP_OPEN_PIT',
- sourceIndex: stateP.sourceIndex as Option.Some,
- sourceIndexMappings: stateP.sourceIndexMappings as Option.Some,
- };
- }
- } else if (Either.isLeft(res)) {
- const left = res.left;
- if (isTypeof(left, 'index_not_green_timeout')) {
- // `index_not_green_timeout` for the CREATE_REINDEX_TEMP target temp index:
- // The index status did not go green within the specified timeout period.
- // A green status timeout could theoretically be temporary for a busy cluster.
- //
- // If there is a problem CREATE_REINDEX_TEMP action will
- // continue to timeout and eventually lead to a failed migration.
- const retryErrorMessage = `${left.message} Refer to ${stateP.migrationDocLinks.repeatedTimeoutRequests} for information on how to resolve the issue.`;
- return delayRetryState(stateP, retryErrorMessage, stateP.retryAttempts);
- } else if (isTypeof(left, 'cluster_shard_limit_exceeded')) {
- return {
- ...stateP,
- controlState: 'FATAL',
- reason: `${CLUSTER_SHARD_LIMIT_EXCEEDED_REASON} See ${stateP.migrationDocLinks.clusterShardLimitExceeded}`,
- };
- } else {
- throwBadResponse(stateP, left);
- }
- } else {
- // If the createIndex action receives an 'resource_already_exists_exception'
- // it will wait until the index status turns green so we don't have any
- // left responses to handle here.
- throwBadResponse(stateP, res);
- }
- } else if (stateP.controlState === 'READY_TO_REINDEX_SYNC') {
- const res = resW as ExcludeRetryableEsError>;
- if (Either.isRight(res)) {
- if (Option.isSome(stateP.sourceIndex) && Option.isSome(stateP.sourceIndexMappings)) {
- // this migrator's source index exist, reindex its entries
- return {
- ...stateP,
- controlState: 'REINDEX_SOURCE_TO_TEMP_OPEN_PIT',
- sourceIndex: stateP.sourceIndex as Option.Some,
- sourceIndexMappings: stateP.sourceIndexMappings as Option.Some,
- };
- } else {
- // this migrator's source index did NOT exist
- // this migrator does not need to reindex anything (others might need to)
- return { ...stateP, controlState: 'DONE_REINDEXING_SYNC' };
- }
- } else if (Either.isLeft(res)) {
- const left = res.left;
-
- if (isTypeof(left, 'synchronization_failed')) {
- return {
- ...stateP,
- controlState: 'FATAL',
- reason: 'An error occurred whilst waiting for other migrators to get to this step.',
- throwDelayMillis: 1000, // another migrator has failed for a reason, let it take Kibana down and log its problem
- };
- } else {
- throwBadResponse(stateP, left);
- }
- } else {
- throwBadResponse(stateP, res);
- }
- } else if (stateP.controlState === 'REINDEX_SOURCE_TO_TEMP_OPEN_PIT') {
- const res = resW as ExcludeRetryableEsError>;
- if (Either.isRight(res)) {
- return {
- ...stateP,
- controlState: 'REINDEX_SOURCE_TO_TEMP_READ',
- sourceIndexPitId: res.right.pitId,
- lastHitSortValue: undefined,
- // placeholders to collect document transform problems
- corruptDocumentIds: [],
- transformErrors: [],
- progress: createInitialProgress(),
- logs: [
- ...logs,
- {
- level: 'info',
- message: `REINDEX_SOURCE_TO_TEMP_OPEN_PIT PitId:${res.right.pitId}`,
- },
- ],
- };
- } else {
- throwBadResponse(stateP, res);
- }
- } else if (stateP.controlState === 'REINDEX_SOURCE_TO_TEMP_READ') {
- // we carry through any failures we've seen with transforming documents on state
- const res = resW as ExcludeRetryableEsError>;
- if (Either.isRight(res)) {
- const sourceIndexPitId = res.right.pitId;
- const progress = setProgressTotal(stateP.progress, res.right.totalHits);
- logs = logProgress(stateP.logs, progress);
- if (res.right.outdatedDocuments.length > 0) {
- return {
- ...stateP,
- sourceIndexPitId,
- controlState: 'REINDEX_SOURCE_TO_TEMP_TRANSFORM',
- outdatedDocuments: res.right.outdatedDocuments,
- lastHitSortValue: res.right.lastHitSortValue,
- progress,
- logs,
- // We succeeded in reading this batch, so increase the batch size for the next request.
- batchSize: increaseBatchSize(stateP),
- };
- } else {
- // we don't have any more outdated documents and need to either fail or move on to updating the target mappings.
- if (stateP.corruptDocumentIds.length > 0 || stateP.transformErrors.length > 0) {
- if (!stateP.discardCorruptObjects) {
- const transformFailureReason = extractTransformFailuresReason(
- stateP.migrationDocLinks.resolveMigrationFailures,
- stateP.corruptDocumentIds,
- stateP.transformErrors
- );
- return {
- ...stateP,
- controlState: 'FATAL',
- reason: transformFailureReason,
- };
- }
-
- // at this point, users have configured kibana to discard corrupt objects
- // thus, we can ignore corrupt documents and transform errors and proceed with the migration
- logs = [
- ...stateP.logs,
- {
- level: 'warning',
- message: extractDiscardedCorruptDocs(
- stateP.corruptDocumentIds,
- stateP.transformErrors
- ),
- },
- ];
- }
-
- // we don't have any more outdated documents and either
- // we haven't encountered any document transformation issues.
- // or the user chose to ignore them
- // Close the PIT search and carry on with the happy path.
- return {
- ...stateP,
- sourceIndexPitId,
- controlState: 'REINDEX_SOURCE_TO_TEMP_CLOSE_PIT',
- logs,
- };
- }
- } else {
- const left = res.left;
- if (isTypeof(left, 'es_response_too_large')) {
- if (stateP.batchSize === 1) {
- return {
- ...stateP,
- controlState: 'FATAL',
- reason: `After reducing the read batch size to a single document, the Elasticsearch response content length was ${left.contentLength}bytes which still exceeded migrations.maxReadBatchSizeBytes. Increase migrations.maxReadBatchSizeBytes and try again.`,
- };
- } else {
- const batchSize = Math.max(Math.floor(stateP.batchSize / 2), 1);
- return {
- ...stateP,
- batchSize,
- controlState: 'REINDEX_SOURCE_TO_TEMP_READ',
- logs: [
- ...stateP.logs,
- {
- level: 'warning',
- message: `Read a batch with a response content length of ${left.contentLength} bytes which exceeds migrations.maxReadBatchSizeBytes, retrying by reducing the batch size in half to ${batchSize}.`,
- },
- ],
- };
- }
- } else {
- throwBadResponse(stateP, left);
- }
- }
- } else if (stateP.controlState === 'REINDEX_SOURCE_TO_TEMP_CLOSE_PIT') {
- const res = resW as ExcludeRetryableEsError>;
- if (Either.isRight(res)) {
- const { sourceIndexPitId, ...state } = stateP;
-
- if (stateP.mustRelocateDocuments) {
- return {
- ...state,
- controlState: 'DONE_REINDEXING_SYNC',
- };
- } else {
- return {
- ...stateP,
- controlState: 'SET_TEMP_WRITE_BLOCK',
- sourceIndex: stateP.sourceIndex as Option.Some,
- sourceIndexMappings: Option.none,
- };
+ throwBadResponse(stateP, left);
}
} else {
throwBadResponse(stateP, res);
}
- } else if (stateP.controlState === 'DONE_REINDEXING_SYNC') {
+ } else if (stateP.controlState === 'UPDATE_SOURCE_MAPPINGS_PROPERTIES') {
const res = resW as ExcludeRetryableEsError>;
- if (Either.isRight(res)) {
- return {
- ...stateP,
- controlState: 'SET_TEMP_WRITE_BLOCK',
- sourceIndex: stateP.sourceIndex as Option.Some,
- sourceIndexMappings: Option.none,
- };
- } else if (Either.isLeft(res)) {
- const left = res.left;
+ const migrationType = getMigrationType({
+ isMappingsCompatible: Either.isRight(res),
+ isVersionMigrationCompleted: versionMigrationCompleted(
+ stateP.currentAlias,
+ stateP.versionAlias,
+ stateP.aliases
+ ),
+ });
- if (isTypeof(left, 'synchronization_failed')) {
+ switch (migrationType) {
+ case MigrationType.Compatible:
+ return {
+ ...stateP,
+ controlState: 'COMPATIBLE_UPDATE_CHECK_CLUSTER_ROUTING_ALLOCATION',
+ };
+ case MigrationType.Incompatible:
return {
...stateP,
controlState: 'FATAL',
- reason: 'An error occurred whilst waiting for other migrators to get to this step.',
- throwDelayMillis: 1000, // another migrator has failed for a reason, let it take Kibana down and log its problem
+ reason:
+ 'Incompatible mappings detected. This code path should be unreachable in a supported upgrade path. Please contact Elastic Support.',
};
- } else {
- throwBadResponse(stateP, left);
- }
- } else {
- throwBadResponse(stateP, res);
- }
- } else if (stateP.controlState === 'REINDEX_SOURCE_TO_TEMP_TRANSFORM') {
- // We follow a similar control flow as for
- // outdated document search -> outdated document transform -> transform documents bulk index
- // collecting issues along the way rather than failing
- // REINDEX_SOURCE_TO_TEMP_TRANSFORM handles the document transforms
- const res = resW as ExcludeRetryableEsError>;
-
- // Increment the processed documents, no matter what the results are.
- // Otherwise the progress might look off when there are errors.
- const progress = incrementProcessedProgress(stateP.progress, stateP.outdatedDocuments.length);
-
- if (
- Either.isRight(res) ||
- (isTypeof(res.left, 'documents_transform_failed') && stateP.discardCorruptObjects)
- ) {
- if (
- (stateP.corruptDocumentIds.length === 0 && stateP.transformErrors.length === 0) ||
- stateP.discardCorruptObjects
- ) {
- const documents = Either.isRight(res) ? res.right.processedDocs : res.left.processedDocs;
- const batches = createBatches({
- documents,
- maxBatchSizeBytes: stateP.maxBatchSizeBytes,
- typeIndexMap: buildTempIndexMap(stateP.indexTypesMap, stateP.kibanaVersion),
- });
- if (Either.isRight(batches)) {
- let corruptDocumentIds = stateP.corruptDocumentIds;
- let transformErrors = stateP.transformErrors;
-
- if (Either.isLeft(res)) {
- corruptDocumentIds = [...stateP.corruptDocumentIds, ...res.left.corruptDocumentIds];
- transformErrors = [...stateP.transformErrors, ...res.left.transformErrors];
- }
-
- return {
- ...stateP,
- corruptDocumentIds,
- transformErrors,
- controlState: 'REINDEX_SOURCE_TO_TEMP_INDEX_BULK', // handles the actual bulk indexing into temp index
- bulkOperationBatches: batches.right,
- currentBatch: 0,
- progress,
- };
- } else {
- return {
- ...stateP,
- controlState: 'FATAL',
- reason: fatalReasonDocumentExceedsMaxBatchSizeBytes({
- _id: batches.left.documentId,
- docSizeBytes: batches.left.docSizeBytes,
- maxBatchSizeBytes: batches.left.maxBatchSizeBytes,
- }),
- };
- }
- } else {
- // we don't have any transform issues with the current batch of outdated docs but
- // we have carried through previous transformation issues.
- // The migration will ultimately fail but before we do that, continue to
- // search through remaining docs for more issues and pass the previous failures along on state
+ case MigrationType.Unnecessary:
return {
...stateP,
- controlState: 'REINDEX_SOURCE_TO_TEMP_READ',
- progress,
+ // Skip to 'OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT' so that if a new plugin was
+ // installed / enabled we can transform any old documents and update
+ // the mappings for this plugin's types.
+ controlState: 'OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT',
+ // Source is a none because we didn't do any migration from a source index
+ sourceIndex: Option.none,
+ targetIndex: stateP.sourceIndex.value,
+ // in this scenario, a .kibana_X.Y.Z_001 index exists that matches the current kibana version
+ // aka we are NOT upgrading to a newer version
+ // we inject the source index's current mappings in the state, to check them later
+ targetIndexMappings: mergeMappingMeta(
+ stateP.targetIndexMappings,
+ stateP.sourceIndexMappings.value
+ ),
};
- }
- } else {
- // we have failures from the current batch of documents and add them to the lists
- const left = res.left;
- if (isTypeof(left, 'documents_transform_failed')) {
+ case MigrationType.Invalid:
return {
...stateP,
- controlState: 'REINDEX_SOURCE_TO_TEMP_READ',
- corruptDocumentIds: [...stateP.corruptDocumentIds, ...left.corruptDocumentIds],
- transformErrors: [...stateP.transformErrors, ...left.transformErrors],
- progress,
+ controlState: 'FATAL',
+ reason: 'Incompatible mappings change on already migrated Kibana instance.',
};
+ }
+ } else if (stateP.controlState === 'COMPATIBLE_UPDATE_CHECK_CLUSTER_ROUTING_ALLOCATION') {
+ const res = resW as ExcludeRetryableEsError>;
+ if (Either.isRight(res)) {
+ return {
+ ...stateP,
+ controlState: 'CLEANUP_UNKNOWN_AND_EXCLUDED',
+ };
+ } else {
+ const left = res.left;
+ if (isTypeof(left, 'incompatible_cluster_routing_allocation')) {
+ const retryErrorMessage = `[${left.type}] Incompatible Elasticsearch cluster settings detected. Remove the persistent and transient Elasticsearch cluster setting 'cluster.routing.allocation.enable' or set it to a value of 'all' to allow migrations to proceed. Refer to ${stateP.migrationDocLinks.routingAllocationDisabled} for more information on how to resolve the issue.`;
+ return delayRetryState(stateP, retryErrorMessage, stateP.retryAttempts);
} else {
- // should never happen
throwBadResponse(stateP, left);
}
}
- } else if (stateP.controlState === 'REINDEX_SOURCE_TO_TEMP_INDEX_BULK') {
+ } else if (stateP.controlState === 'CLEANUP_UNKNOWN_AND_EXCLUDED') {
const res = resW as ExcludeRetryableEsError>;
if (Either.isRight(res)) {
- if (stateP.currentBatch + 1 < stateP.bulkOperationBatches.length) {
+ if (res.right.type === 'cleanup_started') {
+ if (res.right.unknownDocs.length) {
+ logs = [
+ ...stateP.logs,
+ { level: 'warning', message: extractDiscardedUnknownDocs(res.right.unknownDocs) },
+ ];
+ }
+
+ logs = [
+ ...logs,
+ ...Object.entries(res.right.errorsByType).map(([soType, error]) => ({
+ level: 'warning' as const,
+ message: `Ignored excludeOnUpgrade hook on type [${soType}] that failed with error: "${error.toString()}"`,
+ })),
+ ];
+
return {
...stateP,
- controlState: 'REINDEX_SOURCE_TO_TEMP_INDEX_BULK',
- currentBatch: stateP.currentBatch + 1,
+ logs,
+ controlState: 'CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK',
+ deleteByQueryTaskId: res.right.taskId,
};
- } else {
+ } else if (res.right.type === 'cleanup_not_needed') {
+ // let's move to the step after CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK
return {
...stateP,
- controlState: 'REINDEX_SOURCE_TO_TEMP_READ',
+ logs,
+ controlState: 'PREPARE_COMPATIBLE_MIGRATION',
+ ...getPrepareCompatibleMigrationStateProperties(stateP),
};
+ } else {
+ throwBadResponse(stateP, res.right);
}
} else {
- if (
- isTypeof(res.left, 'target_index_had_write_block') ||
- isTypeof(res.left, 'index_not_found_exception')
- ) {
- // When the temp index has a write block or has been deleted another
- // instance already completed this step. Close the PIT search and carry
- // on with the happy path.
- return {
- ...stateP,
- controlState: 'REINDEX_SOURCE_TO_TEMP_CLOSE_PIT',
- };
- } else if (isTypeof(res.left, 'request_entity_too_large_exception')) {
- return {
- ...stateP,
- controlState: 'FATAL',
- reason: FATAL_REASON_REQUEST_ENTITY_TOO_LARGE,
- };
- } else if (isTypeof(res.left, 'unavailable_shards_exception')) {
- // Not all shard copies are active. Retry indefinitely with exponential
- // backoff until shards become available, matching wait_for_task_completion_timeout.
- return delayRetryState(stateP, res.left.message, Number.MAX_SAFE_INTEGER);
- }
- throwBadResponse(stateP, res.left);
+ const reason = extractUnknownDocFailureReason(
+ stateP.migrationDocLinks.resolveMigrationFailures,
+ res.left.unknownDocs
+ );
+ return {
+ ...stateP,
+ controlState: 'FATAL',
+ reason,
+ logs: [
+ ...logs,
+ {
+ level: 'error',
+ message: reason,
+ },
+ ],
+ };
}
- } else if (stateP.controlState === 'SET_TEMP_WRITE_BLOCK') {
+ } else if (stateP.controlState === 'CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK') {
const res = resW as ExcludeRetryableEsError>;
if (Either.isRight(res)) {
return {
...stateP,
- controlState: 'CLONE_TEMP_TO_TARGET',
+ logs,
+ controlState: 'PREPARE_COMPATIBLE_MIGRATION',
+ mustRefresh:
+ stateP.mustRefresh || typeof res.right.deleted === 'undefined' || res.right.deleted > 0,
+ ...getPrepareCompatibleMigrationStateProperties(stateP),
};
} else {
- const left = res.left;
- if (isTypeof(left, 'index_not_found_exception')) {
- // index_not_found_exception:
- // another instance completed the MARK_VERSION_INDEX_READY and
- // removed the temp index.
- //
- // For simplicity we continue linearly through the next steps even if
- // we know another instance already completed these.
- return {
- ...stateP,
- controlState: 'CLONE_TEMP_TO_TARGET',
- };
+ if (isTypeof(res.left, 'wait_for_task_completion_timeout')) {
+ // After waiting for the specified timeout, the task has not yet
+ // completed. Retry this step to see if the task has completed after an
+ // exponential delay. We will basically keep polling forever until the
+ // Elasticsearch task succeeds or fails.
+ return delayRetryState(stateP, res.left.message, Number.MAX_SAFE_INTEGER);
} else {
- throwBadResponse(stateP, left);
+ if (stateP.retryCount < stateP.retryAttempts) {
+ const retryCount = stateP.retryCount + 1;
+ const retryDelay = 1500 + 1000 * Math.random();
+ return {
+ ...stateP,
+ controlState: 'CLEANUP_UNKNOWN_AND_EXCLUDED',
+ mustRefresh: true,
+ retryCount,
+ retryDelay,
+ logs: [
+ ...stateP.logs,
+ {
+ level: 'warning',
+ message: `Errors occurred whilst deleting unwanted documents. Another instance is probably updating or deleting documents in the same index. Retrying attempt ${retryCount}.`,
+ },
+ ],
+ };
+ } else {
+ const failures = res.left.failures.length;
+ const versionConflicts = res.left.versionConflicts ?? 0;
+
+ let reason = `Migration failed because it was unable to delete unwanted documents from the ${stateP.sourceIndex.value} system index (${failures} failures and ${versionConflicts} conflicts)`;
+ if (failures) {
+ reason += `:\n` + res.left.failures.map((failure: string) => `- ${failure}\n`).join('');
+ }
+ return {
+ ...stateP,
+ controlState: 'FATAL',
+ reason,
+ };
+ }
}
}
- } else if (stateP.controlState === 'CLONE_TEMP_TO_TARGET') {
+ } else if (stateP.controlState === 'PREPARE_COMPATIBLE_MIGRATION') {
const res = resW as ExcludeRetryableEsError>;
if (Either.isRight(res)) {
return {
...stateP,
- controlState: 'REFRESH_TARGET',
+ controlState: stateP.mustRefresh ? 'REFRESH_SOURCE' : 'OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT',
};
} else {
const left = res.left;
- if (isTypeof(left, 'index_not_found_exception')) {
- // index_not_found_exception means another instance already completed
- // the MARK_VERSION_INDEX_READY step and removed the temp index
- // We still perform the REFRESH_TARGET, OUTDATED_DOCUMENTS_* and
- // UPDATE_TARGET_MAPPINGS_PROPERTIES steps since we might have plugins enabled
- // which the other instances don't.
- return {
- ...stateP,
- controlState: 'REFRESH_TARGET',
- };
- } else if (isTypeof(left, 'index_not_green_timeout')) {
- // `index_not_green_timeout` for the CLONE_TEMP_TO_TARGET source -> target index:
- // The target index status did not go green within the specified timeout period.
- // The cluster could just be busy and we retry the action.
-
- // Once we run out of retries, the migration fails.
- // Identifying the cause requires inspecting the ouput of the
- // `_cluster/allocation/explain?index=${targetIndex}` API.
- // Unless the root cause is identified and addressed, the request will
- // continue to timeout and eventually lead to a failed migration.
- const retryErrorMessage = `${left.message} Refer to ${stateP.migrationDocLinks.repeatedTimeoutRequests} for information on how to resolve the issue.`;
- return delayRetryState(stateP, retryErrorMessage, stateP.retryAttempts);
- } else if (isTypeof(left, 'cluster_shard_limit_exceeded')) {
- return {
- ...stateP,
- controlState: 'FATAL',
- reason: `${CLUSTER_SHARD_LIMIT_EXCEEDED_REASON} See ${stateP.migrationDocLinks.clusterShardLimitExceeded}`,
- };
- } else if (isTypeof(left, 'operation_not_supported')) {
+ // Note: if multiple newer Kibana versions are competing with each other to perform a migration,
+ // it might happen that another Kibana instance has deleted this instance's version index.
+ // NIT to handle this in properly, we'd have to add a PREPARE_COMPATIBLE_MIGRATION_CONFLICT step,
+ // similar to MARK_VERSION_INDEX_READY_CONFLICT.
+ if (isTypeof(left, 'alias_not_found_exception')) {
+ // We assume that the alias was already deleted by another Kibana instance
return {
...stateP,
- controlState: 'FATAL',
- reason: `Action failed due to unsupported operation: ${left.operationName}`,
+ controlState: stateP.mustRefresh
+ ? 'REFRESH_SOURCE'
+ : 'OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT',
};
+ } else if (isTypeof(left, 'index_not_found_exception')) {
+ // We don't handle the following errors as the migration algorithm
+ // will never cause them to occur:
+ // - index_not_found_exception
+ throwBadResponse(stateP, left as never);
+ } else if (isTypeof(left, 'remove_index_not_a_concrete_index')) {
+ // We don't handle this error as the migration algorithm will never
+ // cause it to occur (this error is only relevant to the LEGACY_DELETE
+ // step).
+ throwBadResponse(stateP, left as never);
} else {
throwBadResponse(stateP, left);
}
}
- } else if (stateP.controlState === 'REFRESH_TARGET') {
+ } else if (stateP.controlState === 'REFRESH_SOURCE') {
const res = resW as ExcludeRetryableEsError>;
if (Either.isRight(res)) {
return {
@@ -1365,6 +568,7 @@ export const model = (currentState: State, resW: ResponseType):
// and can proceed to the next step
return {
...stateP,
+ logs,
pitId: res.right.pitId,
controlState: 'OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT',
};
@@ -1655,9 +859,7 @@ export const model = (currentState: State, resW: ResponseType):
// index.
return {
...stateP,
- controlState: stateP.mustRelocateDocuments
- ? 'MARK_VERSION_INDEX_READY_SYNC'
- : 'MARK_VERSION_INDEX_READY',
+ controlState: 'MARK_VERSION_INDEX_READY',
versionIndexReadyActions: stateP.versionIndexReadyActions,
};
} else {
@@ -1712,10 +914,7 @@ export const model = (currentState: State, resW: ResponseType):
// left responses to handle here.
throwBadResponse(stateP, res);
}
- } else if (
- stateP.controlState === 'MARK_VERSION_INDEX_READY' ||
- stateP.controlState === 'MARK_VERSION_INDEX_READY_SYNC'
- ) {
+ } else if (stateP.controlState === 'MARK_VERSION_INDEX_READY') {
const res = resW as ExcludeRetryableEsError>;
if (Either.isRight(res)) {
return { ...stateP, controlState: 'DONE' };
@@ -1728,30 +927,13 @@ export const model = (currentState: State, resW: ResponseType):
// migration from the same source.
return { ...stateP, controlState: 'MARK_VERSION_INDEX_READY_CONFLICT' };
} else if (isTypeof(left, 'index_not_found_exception')) {
- if (left.index.endsWith(REINDEX_TEMP_SUFFIX)) {
- // another instance has already completed the migration and deleted
- // the temporary index
- return { ...stateP, controlState: 'MARK_VERSION_INDEX_READY_CONFLICT' };
- } else if (isTypeof(left, 'index_not_found_exception')) {
- // The migration algorithm will never cause a
- // index_not_found_exception for an index other than the temporary
- // index handled above.
- throwBadResponse(stateP, left as never);
- } else {
- throwBadResponse(stateP, left);
- }
+ // The migration algorithm will never cause an index_not_found_exception here.
+ throwBadResponse(stateP, left as never);
} else if (isTypeof(left, 'remove_index_not_a_concrete_index')) {
// We don't handle this error as the migration algorithm will never
// cause it to occur (this error is only relevant to the LEGACY_DELETE
// step).
throwBadResponse(stateP, left as never);
- } else if (isTypeof(left, 'synchronization_failed')) {
- return {
- ...stateP,
- controlState: 'FATAL',
- reason: 'An error occurred whilst waiting for other migrators to get to this step.',
- throwDelayMillis: 1000, // another migrator has failed for a reason, let it take Kibana down and log its problem
- };
} else {
throwBadResponse(stateP, left);
}
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/next.test.ts b/src/core/packages/saved-objects/migration-server-internal/src/next.test.ts
index cd0c52f441a86..c5b314aa6fc2d 100644
--- a/src/core/packages/saved-objects/migration-server-internal/src/next.test.ts
+++ b/src/core/packages/saved-objects/migration-server-internal/src/next.test.ts
@@ -8,7 +8,6 @@
*/
import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
-import { waitGroup } from './kibana_migrator_utils';
import { next } from './next';
import type { State } from './state';
@@ -16,26 +15,12 @@ describe('migrations v2 next', () => {
it.todo('when state.retryDelay > 0 delays execution of the next action');
it('DONE returns null', () => {
const state = { controlState: 'DONE' } as State;
- const action = next(
- {} as ElasticsearchClient,
- (() => {}) as any,
- waitGroup(),
- waitGroup(),
- waitGroup(),
- []
- )(state);
+ const action = next({} as ElasticsearchClient, (() => {}) as any, [])(state);
expect(action).toEqual(null);
});
it('FATAL returns null', () => {
const state = { controlState: 'FATAL', reason: '' } as State;
- const action = next(
- {} as ElasticsearchClient,
- (() => {}) as any,
- waitGroup(),
- waitGroup(),
- waitGroup(),
- []
- )(state);
+ const action = next({} as ElasticsearchClient, (() => {}) as any, [])(state);
expect(action).toEqual(null);
});
});
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/next.ts b/src/core/packages/saved-objects/migration-server-internal/src/next.ts
index 889efcb9150d3..dc52ae094dbc5 100644
--- a/src/core/packages/saved-objects/migration-server-internal/src/next.ts
+++ b/src/core/packages/saved-objects/migration-server-internal/src/next.ts
@@ -7,28 +7,16 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import { pipe } from 'fp-ts/pipeable';
import * as Option from 'fp-ts/Option';
-import * as TaskEither from 'fp-ts/TaskEither';
import { omit } from 'lodash';
import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
-import type { WaitGroup } from './kibana_migrator_utils';
import type {
AllActionStates,
- CalculateExcludeFiltersState,
CheckTargetTypesMappingsState,
- CheckUnknownDocumentsState,
CleanupUnknownAndExcluded,
CleanupUnknownAndExcludedWaitForTaskState,
- CloneTempToTarget,
CreateNewTargetState,
- CreateReindexTempState,
InitState,
- LegacyCreateReindexTargetState,
- LegacyDeleteState,
- LegacyReindexState,
- LegacyReindexWaitForTaskState,
- LegacySetWriteBlockState,
MarkVersionIndexReady,
MarkVersionIndexReadyConflict,
OutdatedDocumentsRefresh,
@@ -38,14 +26,6 @@ import type {
OutdatedDocumentsTransform,
PrepareCompatibleMigration,
RefreshSource,
- RefreshTarget,
- ReindexSourceToTempClosePit,
- ReindexSourceToTempIndexBulk,
- ReindexSourceToTempOpenPit,
- ReindexSourceToTempRead,
- ReindexSourceToTempTransform,
- SetSourceWriteBlockState,
- SetTempWriteBlock,
State,
TransformedDocumentsBulkIndex,
UpdateSourceMappingsPropertiesState,
@@ -74,9 +54,6 @@ export type ResponseType = Awaited<
export const nextActionMap = (
client: ElasticsearchClient,
transformRawDocs: TransformRawDocs,
- readyToReindex: WaitGroup,
- doneReindexing: WaitGroup,
- updateRelocationAliases: WaitGroup,
removedTypes: string[]
) => {
return {
@@ -96,6 +73,8 @@ export const nextActionMap = (
latestMappingsVersions: state.latestMappingsVersions,
hashToVersionMap: state.hashToVersionMap,
}),
+ COMPATIBLE_UPDATE_CHECK_CLUSTER_ROUTING_ALLOCATION: () =>
+ Actions.checkClusterRoutingAllocationEnabled(client),
CLEANUP_UNKNOWN_AND_EXCLUDED: (state: CleanupUnknownAndExcluded) =>
Actions.cleanupUnknownAndExcluded({
client,
@@ -118,30 +97,8 @@ export const nextActionMap = (
Actions.updateAliases({ client, aliasActions: state.preTransformDocsActions }),
REFRESH_SOURCE: (state: RefreshSource) =>
Actions.refreshIndex({ client, index: state.sourceIndex.value }),
- REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION: () =>
- Actions.checkClusterRoutingAllocationEnabled(client),
CREATE_INDEX_CHECK_CLUSTER_ROUTING_ALLOCATION: () =>
Actions.checkClusterRoutingAllocationEnabled(client),
- RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION: () =>
- Actions.checkClusterRoutingAllocationEnabled(client),
- CHECK_UNKNOWN_DOCUMENTS: (state: CheckUnknownDocumentsState) =>
- Actions.checkForUnknownDocs({
- client,
- indexName: state.sourceIndex.value,
- excludeOnUpgradeQuery: state.excludeOnUpgradeQuery,
- knownTypes: state.knownTypes,
- }),
- SET_SOURCE_WRITE_BLOCK: (state: SetSourceWriteBlockState) =>
- Actions.safeWriteBlock({
- client,
- sourceIndex: state.sourceIndex.value,
- targetIndex: state.targetIndex,
- }),
- CALCULATE_EXCLUDE_FILTERS: (state: CalculateExcludeFiltersState) =>
- Actions.calculateExcludeFilters({
- client,
- excludeFromUpgradeFilterHooks: state.excludeFromUpgradeFilterHooks,
- }),
CREATE_NEW_TARGET: (state: CreateNewTargetState) =>
Actions.createIndex({
client,
@@ -149,71 +106,6 @@ export const nextActionMap = (
mappings: state.targetIndexMappings,
esCapabilities: state.esCapabilities,
}),
- CREATE_REINDEX_TEMP: (state: CreateReindexTempState) =>
- Actions.createIndex({
- client,
- indexName: state.tempIndex,
- aliases: [state.tempIndexAlias],
- mappings: state.tempIndexMappings,
- esCapabilities: state.esCapabilities,
- }),
- READY_TO_REINDEX_SYNC: () =>
- Actions.synchronizeMigrators({
- waitGroup: readyToReindex,
- }),
- REINDEX_SOURCE_TO_TEMP_OPEN_PIT: (state: ReindexSourceToTempOpenPit) =>
- Actions.openPit({ client, index: state.sourceIndex.value }),
- REINDEX_SOURCE_TO_TEMP_READ: (state: ReindexSourceToTempRead) =>
- Actions.readWithPit({
- client,
- pitId: state.sourceIndexPitId,
- /* When reading we use a source query to exclude saved objects types which
- * are no longer used. These saved objects will still be kept in the outdated
- * index for backup purposes, but won't be available in the upgraded index.
- */
- query: state.excludeOnUpgradeQuery,
- batchSize: state.batchSize,
- searchAfter: state.lastHitSortValue,
- }),
- REINDEX_SOURCE_TO_TEMP_CLOSE_PIT: (state: ReindexSourceToTempClosePit) =>
- Actions.closePit({ client, pitId: state.sourceIndexPitId }),
- REINDEX_SOURCE_TO_TEMP_TRANSFORM: (state: ReindexSourceToTempTransform) =>
- Actions.transformDocs({ transformRawDocs, outdatedDocuments: state.outdatedDocuments }),
- REINDEX_SOURCE_TO_TEMP_INDEX_BULK: (state: ReindexSourceToTempIndexBulk) =>
- Actions.bulkOverwriteTransformedDocuments({
- client,
- /*
- * Since other nodes can delete the temp index while we're busy writing
- * to it, we use the alias to prevent the auto-creation of the index if
- * it doesn't exist.
- */
- index: state.tempIndexAlias,
- useAliasToPreventAutoCreate: true,
- operations: state.bulkOperationBatches[state.currentBatch],
- /**
- * Since we don't run a search against the target index, we disable "refresh" to speed up
- * the migration process.
- * Although any further step must run "refresh" for the target index
- * before we reach out to the OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT step.
- * Right now, it's performed during REFRESH_TARGET step.
- */
- refresh: false,
- }),
- DONE_REINDEXING_SYNC: () =>
- Actions.synchronizeMigrators({
- waitGroup: doneReindexing,
- }),
- SET_TEMP_WRITE_BLOCK: (state: SetTempWriteBlock) =>
- Actions.setWriteBlock({ client, index: state.tempIndex }),
- CLONE_TEMP_TO_TARGET: (state: CloneTempToTarget) =>
- Actions.cloneIndex({
- client,
- source: state.tempIndex,
- target: state.targetIndex,
- esCapabilities: state.esCapabilities,
- }),
- REFRESH_TARGET: (state: RefreshTarget) =>
- Actions.refreshIndex({ client, index: state.targetIndex }),
CHECK_TARGET_MAPPINGS: (state: CheckTargetTypesMappingsState) =>
Actions.checkTargetTypesMappings({
indexTypes: state.indexTypes,
@@ -280,65 +172,17 @@ export const nextActionMap = (
}),
MARK_VERSION_INDEX_READY: (state: MarkVersionIndexReady) =>
Actions.updateAliases({ client, aliasActions: state.versionIndexReadyActions.value }),
- MARK_VERSION_INDEX_READY_SYNC: (state: MarkVersionIndexReady) =>
- pipe(
- // First, we wait for all the migrators involved in a relocation to reach this point.
- Actions.synchronizeMigrators({
- waitGroup: updateRelocationAliases,
- payload: state.versionIndexReadyActions.value,
- }),
- // Then, all migrators will try to update all aliases (from all indices). Only the first one will succeed.
- // The others will receive alias_not_found_exception and cause MARK_VERSION_INDEX_READY_CONFLICT (that's acceptable).
- TaskEither.chainW(({ data }) =>
- Actions.updateAliases({ client, aliasActions: data.flat() })
- )
- ),
MARK_VERSION_INDEX_READY_CONFLICT: (state: MarkVersionIndexReadyConflict) =>
Actions.fetchIndices({ client, indices: [state.currentAlias, state.versionAlias] }),
- LEGACY_CHECK_CLUSTER_ROUTING_ALLOCATION: () =>
- Actions.checkClusterRoutingAllocationEnabled(client),
- LEGACY_SET_WRITE_BLOCK: (state: LegacySetWriteBlockState) =>
- Actions.setWriteBlock({ client, index: state.legacyIndex }),
- LEGACY_CREATE_REINDEX_TARGET: (state: LegacyCreateReindexTargetState) =>
- Actions.createIndex({
- client,
- indexName: state.sourceIndex.value,
- mappings: state.sourceIndexMappings.value,
- esCapabilities: state.esCapabilities,
- }),
- LEGACY_REINDEX: (state: LegacyReindexState) =>
- Actions.reindex({
- client,
- sourceIndex: state.legacyIndex,
- targetIndex: state.sourceIndex.value,
- reindexScript: state.preMigrationScript,
- requireAlias: false,
- excludeOnUpgradeQuery: state.excludeOnUpgradeQuery,
- batchSize: state.batchSize,
- }),
- LEGACY_REINDEX_WAIT_FOR_TASK: (state: LegacyReindexWaitForTaskState) =>
- Actions.waitForReindexTask({ client, taskId: state.legacyReindexTaskId, timeout: '60s' }),
- LEGACY_DELETE: (state: LegacyDeleteState) =>
- Actions.updateAliases({ client, aliasActions: state.legacyPreMigrationDoneActions }),
};
};
export const next = (
client: ElasticsearchClient,
transformRawDocs: TransformRawDocs,
- readyToReindex: WaitGroup,
- doneReindexing: WaitGroup,
- updateRelocationAliases: WaitGroup,
removedTypes: string[]
) => {
- const map = nextActionMap(
- client,
- transformRawDocs,
- readyToReindex,
- doneReindexing,
- updateRelocationAliases,
- removedTypes
- );
+ const map = nextActionMap(client, transformRawDocs, removedTypes);
return (state: State) => {
const delay = createDelayFn(state);
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/run_resilient_migrator.fixtures.ts b/src/core/packages/saved-objects/migration-server-internal/src/run_resilient_migrator.fixtures.ts
index b38c86c659a2b..c599fe47ce047 100644
--- a/src/core/packages/saved-objects/migration-server-internal/src/run_resilient_migrator.fixtures.ts
+++ b/src/core/packages/saved-objects/migration-server-internal/src/run_resilient_migrator.fixtures.ts
@@ -29,12 +29,6 @@ export const createRegistry = (types: Array>) => {
return registry;
};
-export const indexTypesMapMock = {
- '.my_index': ['testtype', 'testtype2'],
- '.task_index': ['testtasktype'],
- '.complementary_index': ['testtype3'],
-};
-
export const hashToVersionMapMock = {
'testtype|someHash': '10.1.0',
'testtype2|anotherHash': '10.2.0',
@@ -43,17 +37,12 @@ export const hashToVersionMapMock = {
};
export const savedObjectTypeRegistryMock = createRegistry([
- // typeRegistry depicts an updated index map:
- // .my_index: ['testtype', 'testtype3'],
- // .other_index: ['testtype2'],
- // .task_index': ['testtasktype'],
{
name: 'testtype',
migrations: { '8.2.3': jest.fn().mockImplementation((doc) => doc) },
},
{
name: 'testtype2',
- // We are moving 'testtype2' from '.my_index' to '.other_index'
indexPattern: '.other_index',
},
{
@@ -61,7 +50,6 @@ export const savedObjectTypeRegistryMock = createRegistry([
indexPattern: '.task_index',
},
{
- // We are moving 'testtype3' from '.complementary_index' to '.my_index'
name: 'testtype3',
},
]);
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/run_resilient_migrator.test.ts b/src/core/packages/saved-objects/migration-server-internal/src/run_resilient_migrator.test.ts
index 3526f536a4ca9..8f1cf53d77259 100644
--- a/src/core/packages/saved-objects/migration-server-internal/src/run_resilient_migrator.test.ts
+++ b/src/core/packages/saved-objects/migration-server-internal/src/run_resilient_migrator.test.ts
@@ -15,13 +15,11 @@ import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks';
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
import type { MigrationResult } from '@kbn/core-saved-objects-base-server-internal';
import { createInitialState } from './initial_state';
-import { waitGroup } from './kibana_migrator_utils';
import { migrationStateActionMachine } from './migrations_state_action_machine';
import { next } from './next';
import { runResilientMigrator, type RunResilientMigratorParams } from './run_resilient_migrator';
import {
hashToVersionMapMock,
- indexTypesMapMock,
savedObjectTypeRegistryMock,
} from './run_resilient_migrator.fixtures';
import type { InitState, State } from './state';
@@ -74,12 +72,9 @@ describe('runResilientMigrator', () => {
expect(createInitialState).toHaveBeenCalledWith({
kibanaVersion: options.kibanaVersion,
waitForMigrationCompletion: options.waitForMigrationCompletion,
- mustRelocateDocuments: options.mustRelocateDocuments,
indexTypes: options.indexTypes,
- indexTypesMap: options.indexTypesMap,
hashToVersionMap: options.hashToVersionMap,
targetIndexMappings: options.targetIndexMappings,
- preMigrationScript: options.preMigrationScript,
migrationVersionPerType: options.migrationVersionPerType,
coreMigrationVersionPerType: options.coreMigrationVersionPerType,
indexPrefix: options.indexPrefix,
@@ -123,9 +118,7 @@ const mockOptions = (): RunResilientMigratorParams => {
client: mockedClient,
kibanaVersion: '8.8.0',
waitForMigrationCompletion: false,
- mustRelocateDocuments: true,
indexTypes: ['a', 'c'],
- indexTypesMap: indexTypesMapMock,
hashToVersionMap: hashToVersionMapMock,
targetIndexMappings: {
properties: {
@@ -139,12 +132,8 @@ const mockOptions = (): RunResilientMigratorParams => {
},
},
},
- readyToReindex: waitGroup(),
- doneReindexing: waitGroup(),
- updateRelocationAliases: waitGroup(),
logger,
transformRawDocs: jest.fn(),
- preMigrationScript: "ctx._id = ctx._source.type + ':' + ctx._id",
migrationVersionPerType: { my_dashboard: '7.10.1', my_viz: '8.0.0' },
coreMigrationVersionPerType: {},
indexPrefix: '.my_index',
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/run_resilient_migrator.ts b/src/core/packages/saved-objects/migration-server-internal/src/run_resilient_migrator.ts
index 3daba731e05b5..4830a4b8d39d6 100644
--- a/src/core/packages/saved-objects/migration-server-internal/src/run_resilient_migrator.ts
+++ b/src/core/packages/saved-objects/migration-server-internal/src/run_resilient_migrator.ts
@@ -19,9 +19,7 @@ import type {
IndexMapping,
SavedObjectsMigrationConfigType,
MigrationResult,
- IndexTypesMap,
} from '@kbn/core-saved-objects-base-server-internal';
-import type { WaitGroup } from './kibana_migrator_utils';
import type { TransformRawDocs } from './types';
import { next } from './next';
import { model } from './model';
@@ -29,7 +27,6 @@ import { createInitialState } from './initial_state';
import { migrationStateActionMachine } from './migrations_state_action_machine';
import { cleanup } from './migrations_state_machine_cleanup';
import type { State } from './state';
-import type { AliasAction } from './actions';
/**
* To avoid the Elasticsearch-js client aborting our requests before we
@@ -48,15 +45,9 @@ export interface RunResilientMigratorParams {
client: ElasticsearchClient;
kibanaVersion: string;
waitForMigrationCompletion: boolean;
- mustRelocateDocuments: boolean;
indexTypes: string[];
- indexTypesMap: IndexTypesMap;
targetIndexMappings: IndexMapping;
hashToVersionMap: Record;
- preMigrationScript?: string;
- readyToReindex: WaitGroup;
- doneReindexing: WaitGroup;
- updateRelocationAliases: WaitGroup;
logger: Logger;
transformRawDocs: TransformRawDocs;
coreMigrationVersionPerType: SavedObjectsMigrationVersion;
@@ -77,16 +68,10 @@ export async function runResilientMigrator({
client,
kibanaVersion,
waitForMigrationCompletion,
- mustRelocateDocuments,
indexTypes,
- indexTypesMap,
targetIndexMappings,
hashToVersionMap,
logger,
- preMigrationScript,
- readyToReindex,
- doneReindexing,
- updateRelocationAliases,
transformRawDocs,
coreMigrationVersionPerType,
migrationVersionPerType,
@@ -99,12 +84,9 @@ export async function runResilientMigrator({
const initialState = createInitialState({
kibanaVersion,
waitForMigrationCompletion,
- mustRelocateDocuments,
indexTypes,
- indexTypesMap,
hashToVersionMap,
targetIndexMappings,
- preMigrationScript,
coreMigrationVersionPerType,
migrationVersionPerType,
indexPrefix,
@@ -118,18 +100,9 @@ export async function runResilientMigrator({
return migrationStateActionMachine({
initialState,
logger,
- next: next(
- client,
- transformRawDocs,
- readyToReindex,
- doneReindexing,
- updateRelocationAliases,
- typeRegistry.getLegacyTypes()
- ),
+ next: next(client, transformRawDocs, typeRegistry.getLegacyTypes()),
model,
abort: async (state?: State) => {
- // At this point, we could reject this migrator's defers and unblock other migrators
- // but we are going to throw and shutdown Kibana anyway, so there's no real point in it
await cleanup(client, state);
},
});
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/run_v2_migration.test.ts b/src/core/packages/saved-objects/migration-server-internal/src/run_v2_migration.test.ts
index bbdad58a18599..1deded15bc21a 100644
--- a/src/core/packages/saved-objects/migration-server-internal/src/run_v2_migration.test.ts
+++ b/src/core/packages/saved-objects/migration-server-internal/src/run_v2_migration.test.ts
@@ -22,16 +22,9 @@ import { runV2Migration } from './run_v2_migration';
import { DocumentMigrator } from './document_migrator';
import { ALLOWED_CONVERT_VERSION } from './kibana_migrator_constants';
import { buildTypesMappings, createIndexMap } from './core';
-import {
- getIndicesInvolvedInRelocation,
- indexMapToIndexTypesMap,
- createWaitGroupMap,
- waitGroup,
-} from './kibana_migrator_utils';
import { runResilientMigrator } from './run_resilient_migrator';
import {
hashToVersionMapMock,
- indexTypesMapMock,
savedObjectTypeRegistryMock,
} from './run_resilient_migrator.fixtures';
import { getIndexDetails } from './core/get_index_details';
@@ -50,30 +43,13 @@ jest.mock('./core/get_index_details', () => {
...actual,
getIndexDetails: jest.fn(() =>
Promise.resolve({
- mappings: {
- _meta: {
- indexTypesMap: {
- '.my_index': ['testtype', 'testtype2', 'testtype3'],
- '.task_index': ['testtasktype'],
- },
- },
- },
+ mappings: {},
aliases: ['.my_index', '.my_index_9.1.0'],
})
),
};
});
-jest.mock('./kibana_migrator_utils', () => {
- const actual = jest.requireActual('./kibana_migrator_utils');
- return {
- ...actual,
- indexMapToIndexTypesMap: jest.fn(actual.indexMapToIndexTypesMap),
- createWaitGroupMap: jest.fn(actual.createWaitGroupMap),
- getIndicesInvolvedInRelocation: jest.fn(actual.getIndicesInvolvedInRelocation),
- };
-});
-
const V2_SUCCESSFUL_MIGRATION_RESULT: MigrationResult[] = [
{
sourceIndex: '.my_index_pre8.2.3_001',
@@ -104,13 +80,6 @@ jest.mock('./run_resilient_migrator', () => {
const nextTick = () => new Promise((resolve) => setImmediate(resolve));
const mockCreateIndexMap = createIndexMap as jest.MockedFunction;
-const mockIndexMapToIndexTypesMap = indexMapToIndexTypesMap as jest.MockedFunction<
- typeof indexMapToIndexTypesMap
->;
-const mockCreateWaitGroupMap = createWaitGroupMap as jest.MockedFunction;
-const mockGetIndicesInvolvedInRelocation = getIndicesInvolvedInRelocation as jest.MockedFunction<
- typeof getIndicesInvolvedInRelocation
->;
const mockRunResilientMigrator = runResilientMigrator as jest.MockedFunction<
typeof runResilientMigrator
>;
@@ -120,9 +89,6 @@ const mockGetIndexDetails = getIndexDetails as jest.MockedFunction {
beforeEach(() => {
mockCreateIndexMap.mockClear();
- mockIndexMapToIndexTypesMap.mockClear();
- mockCreateWaitGroupMap.mockClear();
- mockGetIndicesInvolvedInRelocation.mockClear();
mockRunResilientMigrator.mockClear();
});
@@ -170,39 +136,6 @@ describe('runV2Migration', () => {
});
});
- it('calls indexMapToIndexTypesMap with the result from createIndexMap', async () => {
- const options = mockOptions();
- options.documentMigrator.prepareMigrations();
- await runV2Migration(options);
- expect(indexMapToIndexTypesMap).toBeCalledTimes(1);
- expect(indexMapToIndexTypesMap).toBeCalledWith(mockCreateIndexMap.mock.results[0].value);
- });
-
- it('calls getIndicesInvolvedInRelocation with the right params', async () => {
- const options = mockOptions();
- options.documentMigrator.prepareMigrations();
- await runV2Migration(options);
- expect(getIndicesInvolvedInRelocation).toBeCalledTimes(1);
- expect(getIndicesInvolvedInRelocation).toBeCalledWith(
- { '.my_index': ['testtype', 'testtype2', 'testtype3'], '.task_index': ['testtasktype'] },
- {
- '.my_index': ['testtype', 'testtype3'],
- '.other_index': ['testtype2'],
- '.task_index': ['testtasktype'],
- }
- );
- });
-
- it('calls createMultiPromiseDefer, with the list of moving indices', async () => {
- const options = mockOptions();
- options.documentMigrator.prepareMigrations();
- await runV2Migration(options);
- expect(mockCreateWaitGroupMap).toBeCalledTimes(3);
- expect(mockCreateWaitGroupMap).toHaveBeenNthCalledWith(1, ['.my_index', '.other_index']);
- expect(mockCreateWaitGroupMap).toHaveBeenNthCalledWith(2, ['.my_index', '.other_index']);
- expect(mockCreateWaitGroupMap).toHaveBeenNthCalledWith(3, ['.my_index', '.other_index']);
- });
-
it('calls runResilientMigrator for each migrator it must spawn', async () => {
const options = mockOptions();
options.documentMigrator.prepareMigrations();
@@ -220,10 +153,6 @@ describe('runV2Migration', () => {
expect.objectContaining({
...runResilientMigratorCommonParams,
indexPrefix: '.my_index',
- mustRelocateDocuments: true,
- readyToReindex: expect.any(Object),
- doneReindexing: expect.any(Object),
- updateRelocationAliases: expect.any(Object),
})
);
expect(runResilientMigrator).toHaveBeenNthCalledWith(
@@ -231,10 +160,6 @@ describe('runV2Migration', () => {
expect.objectContaining({
...runResilientMigratorCommonParams,
indexPrefix: '.other_index',
- mustRelocateDocuments: true,
- readyToReindex: expect.any(Object),
- doneReindexing: expect.any(Object),
- updateRelocationAliases: expect.any(Object),
})
);
expect(runResilientMigrator).toHaveBeenNthCalledWith(
@@ -242,34 +167,41 @@ describe('runV2Migration', () => {
expect.objectContaining({
...runResilientMigratorCommonParams,
indexPrefix: '.task_index',
- mustRelocateDocuments: false,
- readyToReindex: undefined,
- doneReindexing: undefined,
- updateRelocationAliases: undefined,
})
);
});
it('awaits on all runResilientMigrator promises, and resolves with the results of each of them', async () => {
- const myIndexMigratorDefer = waitGroup();
- const otherIndexMigratorDefer = waitGroup();
- const taskIndexMigratorDefer = waitGroup();
+ let resolveMyIndex: (value: MigrationResult) => void;
+ let resolveOtherIndex: (value: MigrationResult) => void;
+ let resolveTaskIndex: (value: MigrationResult) => void;
+
+ const myIndexPromise = new Promise((resolve) => {
+ resolveMyIndex = resolve;
+ });
+ const otherIndexPromise = new Promise((resolve) => {
+ resolveOtherIndex = resolve;
+ });
+ const taskIndexPromise = new Promise((resolve) => {
+ resolveTaskIndex = resolve;
+ });
+
let migrationResults: MigrationResult[] | undefined;
- mockRunResilientMigrator.mockReturnValueOnce(myIndexMigratorDefer.promise);
- mockRunResilientMigrator.mockReturnValueOnce(otherIndexMigratorDefer.promise);
- mockRunResilientMigrator.mockReturnValueOnce(taskIndexMigratorDefer.promise);
+ mockRunResilientMigrator.mockReturnValueOnce(myIndexPromise);
+ mockRunResilientMigrator.mockReturnValueOnce(otherIndexPromise);
+ mockRunResilientMigrator.mockReturnValueOnce(taskIndexPromise);
const options = mockOptions();
options.documentMigrator.prepareMigrations();
runV2Migration(options).then((results) => (migrationResults = results));
await nextTick();
expect(migrationResults).toBeUndefined();
- myIndexMigratorDefer.resolve(V2_SUCCESSFUL_MIGRATION_RESULT[0]);
- otherIndexMigratorDefer.resolve(V2_SUCCESSFUL_MIGRATION_RESULT[1]);
+ resolveMyIndex!(V2_SUCCESSFUL_MIGRATION_RESULT[0]);
+ resolveOtherIndex!(V2_SUCCESSFUL_MIGRATION_RESULT[1]);
await nextTick();
expect(migrationResults).toBeUndefined();
- taskIndexMigratorDefer.resolve(V2_SUCCESSFUL_MIGRATION_RESULT[2]);
+ resolveTaskIndex!(V2_SUCCESSFUL_MIGRATION_RESULT[2]);
await nextTick();
expect(migrationResults).toEqual(V2_SUCCESSFUL_MIGRATION_RESULT);
});
@@ -302,7 +234,6 @@ const mockOptions = (kibanaVersion = '8.2.3'): RunV2MigrationOpts => {
waitForMigrationCompletion: false,
typeRegistry,
kibanaIndexPrefix: '.my_index',
- defaultIndexTypesMap: indexTypesMapMock,
hashToVersionMap: hashToVersionMapMock,
migrationConfig: {
algorithm: 'v2' as const,
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/run_v2_migration.ts b/src/core/packages/saved-objects/migration-server-internal/src/run_v2_migration.ts
index 7ae60b0669a0c..54c30165b4886 100644
--- a/src/core/packages/saved-objects/migration-server-internal/src/run_v2_migration.ts
+++ b/src/core/packages/saved-objects/migration-server-internal/src/run_v2_migration.ts
@@ -21,7 +21,6 @@ import type {
import {
getVirtualVersionMap,
type IndexMappingMeta,
- type IndexTypesMap,
type MigrationResult,
type SavedObjectsMigrationConfigType,
type SavedObjectsTypeMappingDefinitions,
@@ -31,11 +30,6 @@ import { pick } from 'lodash';
import type { Histogram } from '@opentelemetry/api';
import type { DocumentMigrator } from './document_migrator';
import { buildActiveMappings, createIndexMap } from './core';
-import {
- createWaitGroupMap,
- getIndicesInvolvedInRelocation,
- indexMapToIndexTypesMap,
-} from './kibana_migrator_utils';
import { runResilientMigrator } from './run_resilient_migrator';
import { migrateRawDocsSafely } from './core/migrate_raw_docs';
import type { IndexDetails } from './core/get_index_details';
@@ -48,8 +42,6 @@ export interface RunV2MigrationOpts {
kibanaIndexPrefix: string;
/** The SO type registry to use for the migration */
typeRegistry: ISavedObjectTypeRegistry;
- /** The map of indices => types to use as a default / baseline state */
- defaultIndexTypesMap: IndexTypesMap;
/** A map that holds [last md5 used => modelVersion] for each of the SO types */
hashToVersionMap: Record;
/** Logger to use for migration output */
@@ -128,56 +120,15 @@ export const runV2Migration = async (options: RunV2MigrationOpts): Promise migratorIndices.add(index));
-
// we will store model versions instead of hashes (to be FIPS compliant)
const appVersions = getVirtualVersionMap({
types: options.typeRegistry.getAllTypes(),
useModelVersionsOnly: true,
});
- const migrators = Array.from(migratorIndices).map((indexName, i) => {
+ const migrators = Array.from(new Set(Object.keys(indexMap))).map((indexName, i) => {
return {
migrate: (): Promise => {
- const readyToReindex = readyToReindexWaitGroupMap[indexName];
- const doneReindexing = doneReindexingWaitGroupMap[indexName];
- const updateRelocationAliases = updateAliasesWaitGroupMap[indexName];
- // check if this migrator's index is involved in some document redistribution
- const mustRelocateDocuments = indicesWithRelocatingTypes.includes(indexName);
-
// a migrator's index might no longer have any associated types to it
const typeDefinitions = indexMap[indexName]?.typeMappings ?? {};
@@ -186,24 +137,17 @@ export const runV2Migration = async (options: RunV2MigrationOpts): Promise
migrateRawDocsSafely({
serializer: options.serializer,
diff --git a/src/core/packages/saved-objects/migration-server-internal/src/state.ts b/src/core/packages/saved-objects/migration-server-internal/src/state.ts
index f11dcbf087afc..ac7f6e5a66fef 100644
--- a/src/core/packages/saved-objects/migration-server-internal/src/state.ts
+++ b/src/core/packages/saved-objects/migration-server-internal/src/state.ts
@@ -14,11 +14,7 @@ import type {
SavedObjectsRawDoc,
SavedObjectTypeExcludeFromUpgradeFilterHook,
} from '@kbn/core-saved-objects-server';
-import type {
- IndexMapping,
- IndexTypesMap,
- VirtualVersionMap,
-} from '@kbn/core-saved-objects-base-server-internal';
+import type { IndexMapping, VirtualVersionMap } from '@kbn/core-saved-objects-base-server-internal';
import type { ElasticsearchCapabilities } from '@kbn/core-elasticsearch-server';
import type { ControlState } from './state_action_machine';
import type { AliasAction } from './actions';
@@ -30,25 +26,10 @@ import type { Aliases } from './model/helpers';
export interface BaseState extends ControlState {
/** The first part of the index name such as `.kibana` or `.kibana_task_manager` */
readonly indexPrefix: string;
- /**
- * The name of the concrete legacy index (if it exists) e.g. `.kibana` for <
- * 6.5 or `.kibana_task_manager` for < 7.4
- */
- readonly legacyIndex: string;
/** Kibana version number */
readonly kibanaVersion: string;
/** The mappings to apply to the target index */
readonly targetIndexMappings: IndexMapping;
- /**
- * Special mappings set when creating the temp index into which we reindex.
- * These mappings have `dynamic: false` to allow for any kind of outdated
- * document to be written to the index, but still define mappings for the
- * `migrationVersion` and `type` fields so that we can search for and
- * transform outdated documents.
- */
- readonly tempIndexMappings: IndexMapping;
- /** Script to apply to a legacy index before it can be used as a migration source */
- readonly preMigrationScript: Option.Option;
readonly outdatedDocumentsQuery: QueryDslQueryContainer;
readonly retryCount: number;
readonly skipRetryReset: boolean;
@@ -138,17 +119,6 @@ export interface BaseState extends ControlState {
* The index used by this version of Kibana e.g. `.kibana_7.11.0_001`
*/
readonly versionIndex: string;
- /**
- * A temporary index used as part of an "reindex block" that
- * prevents lost deletes e.g. `.kibana_7.11.0_reindex`.
- */
- readonly tempIndex: string;
- /**
- * An alias to the tempIndex used to prevent ES from auto-creating the temp
- * index if one node deletes it while another writes to it
- * e.g. `.kibana_7.11.0_reindex_temp_alias`.
- */
- readonly tempIndexAlias: string;
/**
* When upgrading to a more recent kibana version, some saved object types
* might be conflicting or no longer used.
@@ -188,21 +158,6 @@ export interface BaseState extends ControlState {
*/
readonly migrationDocLinks: DocLinks['kibanaUpgradeSavedObjects'];
readonly waitForMigrationCompletion: boolean;
- /**
- * This flag tells the migrator that SO documents must be redistributed,
- * i.e. stored in different system indices, compared to where they are currently stored.
- * This requires reindexing documents.
- */
- readonly mustRelocateDocuments: boolean;
- /**
- * This object holds a relation of all the types that are stored in each index, e.g.:
- * {
- * '.kibana': [ 'type_1', 'type_2', ... 'type_N' ],
- * '.kibana_cases': [ 'type_N+1', 'type_N+2', ... 'type_N+M' ],
- * ...
- * }
- */
- readonly indexTypesMap: IndexTypesMap;
/** Capabilities of the ES cluster we're using */
readonly esCapabilities: ElasticsearchCapabilities;
}
@@ -297,18 +252,8 @@ export interface UpdateSourceMappingsPropertiesState extends SourceExistsState {
readonly controlState: 'UPDATE_SOURCE_MAPPINGS_PROPERTIES';
}
-export interface CheckUnknownDocumentsState extends SourceExistsState {
- /** Check if any unknown document is present in the source index */
- readonly controlState: 'CHECK_UNKNOWN_DOCUMENTS';
-}
-
-export interface SetSourceWriteBlockState extends SourceExistsState {
- /** Set a write block on the source index to prevent any further writes */
- readonly controlState: 'SET_SOURCE_WRITE_BLOCK';
-}
-
-export interface CalculateExcludeFiltersState extends SourceExistsState {
- readonly controlState: 'CALCULATE_EXCLUDE_FILTERS';
+export interface CompatibleUpdateCheckClusterRoutingAllocationState extends SourceExistsState {
+ readonly controlState: 'COMPATIBLE_UPDATE_CHECK_CLUSTER_ROUTING_ALLOCATION';
}
export interface CreateIndexCheckClusterRoutingAllocationState extends PostInitState {
@@ -324,82 +269,6 @@ export interface CreateNewTargetState extends PostInitState {
readonly versionIndexReadyActions: Option.Some;
}
-export interface RelocateCheckClusterRoutingAllocationState extends PostInitState {
- readonly controlState: 'RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION';
-}
-
-export interface CreateReindexTempState extends PostInitState {
- /**
- * Create a target index with mappings from the source index and registered
- * plugins
- */
- readonly controlState: 'CREATE_REINDEX_TEMP';
-}
-
-export interface ReadyToReindexSyncState extends PostInitState {
- /** Open PIT to the source index */
- readonly controlState: 'READY_TO_REINDEX_SYNC';
-}
-
-export interface ReindexSourceToTempOpenPit extends SourceExistsState {
- /** Open PIT to the source index */
- readonly controlState: 'REINDEX_SOURCE_TO_TEMP_OPEN_PIT';
-}
-
-interface ReindexSourceToTempBatch extends SourceExistsState {
- readonly sourceIndexPitId: string;
- readonly lastHitSortValue: number[] | undefined;
- readonly corruptDocumentIds: string[];
- readonly transformErrors: TransformErrorObjects[];
- readonly progress: Progress;
-}
-
-export interface ReindexSourceToTempRead extends ReindexSourceToTempBatch {
- readonly controlState: 'REINDEX_SOURCE_TO_TEMP_READ';
-}
-
-export interface ReindexSourceToTempClosePit extends SourceExistsState {
- readonly controlState: 'REINDEX_SOURCE_TO_TEMP_CLOSE_PIT';
- readonly sourceIndexPitId: string;
-}
-
-export interface ReindexSourceToTempTransform extends ReindexSourceToTempBatch {
- readonly controlState: 'REINDEX_SOURCE_TO_TEMP_TRANSFORM';
- readonly outdatedDocuments: SavedObjectsRawDoc[];
-}
-
-export interface ReindexSourceToTempIndexBulk extends ReindexSourceToTempBatch {
- readonly controlState: 'REINDEX_SOURCE_TO_TEMP_INDEX_BULK';
- readonly bulkOperationBatches: BulkOperation[][];
- readonly currentBatch: number;
-}
-
-export interface DoneReindexingSyncState extends PostInitState {
- /** Open PIT to the source index */
- readonly controlState: 'DONE_REINDEXING_SYNC';
-}
-
-export interface SetTempWriteBlock extends PostInitState {
- readonly controlState: 'SET_TEMP_WRITE_BLOCK';
-}
-
-export interface CloneTempToTarget extends PostInitState {
- /**
- * Clone the temporary reindex index into
- */
- readonly controlState: 'CLONE_TEMP_TO_TARGET';
-}
-
-export interface RefreshTarget extends PostInitState {
- /** Refresh temp index before searching for outdated docs */
- readonly controlState: 'REFRESH_TARGET';
- readonly targetIndex: string;
-}
-
-export interface ReindexCheckClusterRoutingAllocationState extends SourceExistsState {
- readonly controlState: 'REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION';
-}
-
export interface CheckTargetTypesMappingsState extends PostInitState {
readonly controlState: 'CHECK_TARGET_MAPPINGS';
}
@@ -494,14 +363,6 @@ export interface MarkVersionIndexReady extends PostInitState {
readonly versionIndexReadyActions: Option.Some;
}
-export interface MarkVersionIndexReadySync extends PostInitState {
- /** Single "client.indices.updateAliases" operation
- * to update multiple indices' aliases simultaneously
- * */
- readonly controlState: 'MARK_VERSION_INDEX_READY_SYNC';
- readonly versionIndexReadyActions: Option.Some;
-}
-
export interface MarkVersionIndexReadyConflict extends PostInitState {
/**
* If the MARK_VERSION_INDEX_READY step fails another instance was
@@ -519,78 +380,18 @@ export interface MarkVersionIndexReadyConflict extends PostInitState {
readonly controlState: 'MARK_VERSION_INDEX_READY_CONFLICT';
}
-/**
- * If we're migrating from a legacy index we need to perform some additional
- * steps to prepare this index so that it can be used as a migration 'source'.
- */
-export interface LegacyBaseState extends SourceExistsState {
- readonly legacyPreMigrationDoneActions: AliasAction[];
-}
-
-export interface LegacyCheckClusterRoutingAllocationState extends LegacyBaseState {
- readonly controlState: 'LEGACY_CHECK_CLUSTER_ROUTING_ALLOCATION';
-}
-
-export interface LegacySetWriteBlockState extends LegacyBaseState {
- /** Set a write block on the legacy index to prevent any further writes */
- readonly controlState: 'LEGACY_SET_WRITE_BLOCK';
-}
-
-export interface LegacyCreateReindexTargetState extends LegacyBaseState {
- /**
- * Create a new index into which we can reindex the legacy index. This
- * index will have the same mappings as the legacy index. Once the legacy
- * pre-migration is complete, this index will be used a migration 'source'.
- */
- readonly controlState: 'LEGACY_CREATE_REINDEX_TARGET';
-}
-
-export interface LegacyReindexState extends LegacyBaseState {
- /**
- * Reindex the legacy index into the new index created in the
- * LEGACY_CREATE_REINDEX_TARGET step (and apply the preMigration script).
- */
- readonly controlState: 'LEGACY_REINDEX';
-}
-
-export interface LegacyReindexWaitForTaskState extends LegacyBaseState {
- /** Wait for the reindex operation to complete */
- readonly controlState: 'LEGACY_REINDEX_WAIT_FOR_TASK';
- readonly legacyReindexTaskId: string;
-}
-
-export interface LegacyDeleteState extends LegacyBaseState {
- /**
- * After reindexed has completed, delete the legacy index so that it won't
- * conflict with the `currentAlias` that we want to create in a later step
- * e.g. `.kibana`.
- */
- readonly controlState: 'LEGACY_DELETE';
-}
-
export type State = Readonly<
- | CalculateExcludeFiltersState
| CheckTargetTypesMappingsState
- | CheckUnknownDocumentsState
| CheckVersionIndexReadyActions
| CleanupUnknownAndExcluded
| CleanupUnknownAndExcludedWaitForTaskState
- | CloneTempToTarget
+ | CompatibleUpdateCheckClusterRoutingAllocationState
| CreateIndexCheckClusterRoutingAllocationState
| CreateNewTargetState
- | CreateReindexTempState
- | DoneReindexingSyncState
| DoneState
| FatalState
| InitState
- | LegacyCheckClusterRoutingAllocationState
- | LegacyCreateReindexTargetState
- | LegacyDeleteState
- | LegacyReindexState
- | LegacyReindexWaitForTaskState
- | LegacySetWriteBlockState
| MarkVersionIndexReady
- | MarkVersionIndexReadySync
| MarkVersionIndexReadyConflict
| OutdatedDocumentsRefresh
| OutdatedDocumentsSearchClosePit
@@ -598,18 +399,7 @@ export type State = Readonly<
| OutdatedDocumentsSearchRead
| OutdatedDocumentsTransform
| PrepareCompatibleMigration
- | ReadyToReindexSyncState
| RefreshSource
- | RefreshTarget
- | ReindexCheckClusterRoutingAllocationState
- | ReindexSourceToTempClosePit
- | ReindexSourceToTempIndexBulk
- | ReindexSourceToTempOpenPit
- | ReindexSourceToTempRead
- | ReindexSourceToTempTransform
- | RelocateCheckClusterRoutingAllocationState
- | SetSourceWriteBlockState
- | SetTempWriteBlock
| TransformedDocumentsBulkIndex
| UpdateSourceMappingsPropertiesState
| UpdateTargetMappingsMeta
diff --git a/src/core/packages/saved-objects/server-internal/src/saved_objects_service.ts b/src/core/packages/saved-objects/server-internal/src/saved_objects_service.ts
index 1b01d68902290..95e91699dac76 100644
--- a/src/core/packages/saved-objects/server-internal/src/saved_objects_service.ts
+++ b/src/core/packages/saved-objects/server-internal/src/saved_objects_service.ts
@@ -46,7 +46,6 @@ import {
type SavedObjectsConfigType,
type SavedObjectsMigrationConfigType,
type IKibanaMigrator,
- DEFAULT_INDEX_TYPES_MAP,
HASH_TO_VERSION_MAP,
} from '@kbn/core-saved-objects-base-server-internal';
import {
@@ -523,7 +522,6 @@ export class SavedObjectsService
kibanaVersion: this.kibanaVersion,
soMigrationsConfig,
kibanaIndex: MAIN_SAVED_OBJECT_INDEX,
- defaultIndexTypesMap: DEFAULT_INDEX_TYPES_MAP,
hashToVersionMap: HASH_TO_VERSION_MAP,
client,
docLinks,
diff --git a/src/core/packages/ui-settings/common/src/ui_settings.ts b/src/core/packages/ui-settings/common/src/ui_settings.ts
index 41ba9d6654bae..9b93ce61cd180 100644
--- a/src/core/packages/ui-settings/common/src/ui_settings.ts
+++ b/src/core/packages/ui-settings/common/src/ui_settings.ts
@@ -67,10 +67,10 @@ export interface GetUiSettingsContext {
export type UiSettingsSolutions = Array;
/**
- * UiSettings parameters defined by the plugins.
+ * Base UiSettings parameters shared by all settings.
* @public
* */
-export interface UiSettingsParams {
+interface UiSettingsParamsBase {
/** title in the UI */
name?: string;
/** default value to fall back to if a user doesn't provide any */
@@ -100,8 +100,6 @@ export interface UiSettingsParams {
type?: UiSettingsType;
/** optional deprecation information. Used to generate a deprecation warning. */
deprecation?: DeprecationSettings;
- /** A flag indicating that this setting is a technical preview. If true, the setting will display a tech preview badge after the title. */
- technicalPreview?: TechnicalPreviewSettings;
/**
* index of the settings within its category (ascending order, smallest will be displayed first).
* Used for ordering in the UI.
@@ -142,6 +140,18 @@ export interface UiSettingsParams {
solutionViews?: UiSettingsSolutions;
}
+/**
+ * UiSettings parameters defined by the plugins.
+ * A setting should carry at most one maturity badge — avoid setting both `technicalPreview` and `experimental`.
+ * @public
+ * */
+export interface UiSettingsParams extends UiSettingsParamsBase {
+ /** A flag indicating that this setting is a technical preview. If true, the setting will display a tech preview badge after the title. */
+ technicalPreview?: TechnicalPreviewSettings;
+ /** A flag indicating that this setting is experimental. Displays an experimental badge after the title. Supports the same options as {@link TechnicalPreviewSettings}. */
+ experimental?: TechnicalPreviewSettings;
+}
+
/**
* Describes the values explicitly set by user.
* @public
diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts
index d0d7c8e0dab1a..633238ff1788a 100644
--- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts
+++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts
@@ -110,7 +110,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"fleet-cloud-onboarding-deployment": "bad508764b7eaada2556e13153679953736c68e190110e281b9a7d52c7d10bc2",
"fleet-fleet-server-host": "edbc06c4a73586e7820549ab481244989af89ba9191b002cce97d0843a01008e",
"fleet-message-signing-keys": "67aecd34e081183b2a99cc1451583977e4ad918074dc5b1579cc4b23750d3829",
- "fleet-package-policies": "5c5d0debdefd5322af7015fd582b5141742e36f6b2a00be58155e25c8f8241b6",
+ "fleet-package-policies": "14130d3b3b0ae171699e42c77de311936ac967e0ca47b314a873e53954255eb6",
"fleet-preconfiguration-deletion-record": "1154f80d0ef53014ea52c7642131e31365f86909e93b265e7f38c2c317c645cf",
"fleet-proxy": "b38a96aa9da6664ff35cd67c4470e0280dbd4b07e8d063a71d6e97dc077d9be4",
"fleet-setup-lock": "df3c142ba8907c8ccf004d2240c79d476a70946db092ab4c485d3eb1a3f5bb82",
@@ -126,7 +126,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"ingest-agent-policies": "1966acba3d49b5057979b1c8518e359be28e7f21450f75a6ad9246dc334f5f95",
"ingest-download-sources": "c87e062ef293585e85fccec0c865d7cef48e0ff9a919d7781d5f7627d275484b",
"ingest-outputs": "b377c664edc65976f10f339f4b26271b2d238df90f7c5dd126b0c825926486b9",
- "ingest-package-policies": "958b60978741bf0f2755dbacd44b4aa9a31d3e5b483872fa1f500722b79b30d5",
+ "ingest-package-policies": "8fabe42af04b2429606259653194b63516c6cf02e96d41479da4aead4c89f928",
"ingest_manager_settings": "d7f88bef81425b890d9d277acd01423556e804269c9e405aeced2629b55695b4",
"integration-config": "8fecaf29e55097075e6d8927bf8353ca3cfa8bc9e352389411da05b31ae704e0",
"intercept_interaction_record": "d7cb1aad5a2e5f459aa1fea81337ab206987845814dc14f151645d3be13cb293",
@@ -768,8 +768,9 @@ describe('checking migration metadata changes on all registered SO types', () =>
"fleet-package-policies|global: b8c5158782fe91d5a5636274dee693a6fef2e457",
"fleet-package-policies|mappings: fb3acda96f9119aa483b39736c9a07da565b8489",
"fleet-package-policies|schemas: da39a3ee5e6b4b0d3255bfef95601890afd80709",
- "fleet-package-policies|10.9.0: 00464256f3d400ef3382cc3696c29a16f5df01bdc41c01f0440f6a1ad8f5097b",
- "fleet-package-policies|10.8.0: 5f4fbabdb466e88079735c9284de829636999e37e125f808d43eb4807a9cefa1",
+ "fleet-package-policies|10.10.0: a132223c07bdc210a1e94e3ddba748b4b5b8cce3d646ec009a0367db92fcc3fe",
+ "fleet-package-policies|10.9.0: 29744bf9dcc8f60b42a490c570ea937289cf04398602a33909caf38fc95f450d",
+ "fleet-package-policies|10.8.0: 1cd8c1c33b652cd1ce02abae1db87d052351853123c465ac06788f951cc1107c",
"fleet-package-policies|10.7.0: 175fe637899f2c70d1c5e2b2dbe459962d4b7048367b9930d393f280222093cf",
"fleet-package-policies|10.6.0: ef0c3e9699868aa625f197708fda2114eac175a8d3c0f2984634102adf61cb15",
"fleet-package-policies|10.5.0: d60de40b75a31ee199487f5a53329033afbfc78767c42d16d987e95173df9516",
@@ -887,8 +888,9 @@ describe('checking migration metadata changes on all registered SO types', () =>
"ingest-package-policies|global: a89e06415e12609fa3575379d06ab1b542da6f04",
"ingest-package-policies|mappings: fb3acda96f9119aa483b39736c9a07da565b8489",
"ingest-package-policies|schemas: da39a3ee5e6b4b0d3255bfef95601890afd80709",
- "ingest-package-policies|10.23.0: 2566b4db65ac68fd79020b74783d69695448b680de2d22546ae04ad6aba16b94",
- "ingest-package-policies|10.22.0: 5f4fbabdb466e88079735c9284de829636999e37e125f808d43eb4807a9cefa1",
+ "ingest-package-policies|10.24.0: a132223c07bdc210a1e94e3ddba748b4b5b8cce3d646ec009a0367db92fcc3fe",
+ "ingest-package-policies|10.23.0: c3d05e8b3df3009e5ab135be8f6f9b719e24f34957054e7e24ac9d333ce2ff33",
+ "ingest-package-policies|10.22.0: 1cd8c1c33b652cd1ce02abae1db87d052351853123c465ac06788f951cc1107c",
"ingest-package-policies|10.21.0: 175fe637899f2c70d1c5e2b2dbe459962d4b7048367b9930d393f280222093cf",
"ingest-package-policies|10.20.0: 522700650b5a10db91d2337e8b82582841a3884049e40c20525aed0a1e1f475e",
"ingest-package-policies|10.19.0: d60de40b75a31ee199487f5a53329033afbfc78767c42d16d987e95173df9516",
@@ -1530,7 +1532,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"fleet-cloud-onboarding-deployment": "10.1.0",
"fleet-fleet-server-host": "10.2.0",
"fleet-message-signing-keys": "10.0.0",
- "fleet-package-policies": "10.9.0",
+ "fleet-package-policies": "10.10.0",
"fleet-preconfiguration-deletion-record": "10.0.0",
"fleet-proxy": "10.0.0",
"fleet-setup-lock": "10.0.0",
@@ -1546,7 +1548,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"ingest-agent-policies": "10.11.0",
"ingest-download-sources": "10.1.0",
"ingest-outputs": "10.10.0",
- "ingest-package-policies": "10.23.0",
+ "ingest-package-policies": "10.24.0",
"ingest_manager_settings": "10.8.0",
"integration-config": "10.3.0",
"intercept_interaction_record": "10.1.0",
@@ -1703,7 +1705,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"fleet-cloud-onboarding-deployment": "10.1.0",
"fleet-fleet-server-host": "10.2.0",
"fleet-message-signing-keys": "0.0.0",
- "fleet-package-policies": "10.9.0",
+ "fleet-package-policies": "10.10.0",
"fleet-preconfiguration-deletion-record": "0.0.0",
"fleet-proxy": "0.0.0",
"fleet-setup-lock": "0.0.0",
@@ -1719,7 +1721,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"ingest-agent-policies": "10.11.0",
"ingest-download-sources": "10.1.0",
"ingest-outputs": "10.10.0",
- "ingest-package-policies": "10.23.0",
+ "ingest-package-policies": "10.24.0",
"ingest_manager_settings": "10.8.0",
"integration-config": "10.3.0",
"intercept_interaction_record": "10.1.0",
diff --git a/src/core/server/integration_tests/saved_objects/migrations/group1/__snapshots__/v2_migration.test.ts.snap b/src/core/server/integration_tests/saved_objects/migrations/group1/__snapshots__/v2_migration.test.ts.snap
index 731cccc454e9e..a1ddfca365a23 100644
--- a/src/core/server/integration_tests/saved_objects/migrations/group1/__snapshots__/v2_migration.test.ts.snap
+++ b/src/core/server/integration_tests/saved_objects/migrations/group1/__snapshots__/v2_migration.test.ts.snap
@@ -1,504 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`v2 migration to a newer stack version with transform errors collects corrupt saved object documents across batches 1`] = `
-"Error: Cannot convert 'complex' objects with values that are multiple of 100 aab076d1-25e4-4d80-be70-35c4599abd84
-Error: Cannot convert 'complex' objects with values that are multiple of 100 a2bc01df-f81a-410b-97c0-c6ba25f6c678
-Error: Cannot convert 'complex' objects with values that are multiple of 100 c874c14d-01ba-4488-8cf5-16c9f13ffaeb
-Error: Cannot convert 'complex' objects with values that are multiple of 100 f05814ca-e1f6-482d-8e59-47eb0aa30de7
-Error: Cannot convert 'complex' objects with values that are multiple of 100 2d5f23b6-11fa-40fa-9253-cd80b92de617
-Error: Cannot convert 'complex' objects with values that are multiple of 100 741dda5c-2808-4948-83ad-c327be056759
-Error: Cannot convert 'complex' objects with values that are multiple of 100 d5913339-37e5-4894-9d53-9ca35249504f
-Error: Cannot convert 'complex' objects with values that are multiple of 100 565fdc24-b3a6-4a45-8a7c-0d55a8f46501
-Error: Cannot convert 'complex' objects with values that are multiple of 100 b6358617-8590-45d8-9ef1-33a3c43fbe33
-Error: Cannot convert 'complex' objects with values that are multiple of 100 a7add3d9-7c00-46d5-b604-aca03c8ac300
-Error: Cannot convert 'complex' objects with values that are multiple of 100 8ad6a825-3a41-4769-b7cf-3dce661e4869
-Error: Cannot convert 'complex' objects with values that are multiple of 100 5cb5b593-b876-486a-8c90-e7278e7183d1
-Error: Cannot convert 'complex' objects with values that are multiple of 100 d2e93d5f-1545-44d0-a347-3f8409fb37cf
-Error: Cannot convert 'complex' objects with values that are multiple of 100 be3c0b3a-bba3-4740-8172-a2755229cc82
-Error: Cannot convert 'complex' objects with values that are multiple of 100 58f5872c-6ffe-4e1f-bb7c-f05d327e978f
-Error: Cannot convert 'complex' objects with values that are multiple of 100 611e502c-7ab0-4058-bd46-c9a82e690390
-Error: Cannot convert 'complex' objects with values that are multiple of 100 aaf15a2a-5eea-4823-a7e1-fbb9482e3b72
-Error: Cannot convert 'complex' objects with values that are multiple of 100 368ae555-2d7b-464d-ab34-5b369ad07818
-Error: Cannot convert 'complex' objects with values that are multiple of 100 06c12c27-7312-456a-9247-116abf9cdf1a
-Error: Cannot convert 'complex' objects with values that are multiple of 100 a57361f7-f5f4-4770-bd28-f8c5547bf1d7
-Error: Cannot convert 'complex' objects with values that are multiple of 100 636e8130-842e-46fc-8b93-8d2b560f8ab6
-Error: Cannot convert 'complex' objects with values that are multiple of 100 15825f78-4e47-4a83-8228-43406ee1f879
-Error: Cannot convert 'complex' objects with values that are multiple of 100 7234fb70-9a4d-49e6-bd8d-2cfe80d0bbc8
-Error: Cannot convert 'complex' objects with values that are multiple of 100 429fc003-1dc7-4882-9792-5fab6a2a0e2e
-Error: Cannot convert 'complex' objects with values that are multiple of 100 8b67fef9-449c-406f-8651-cff064ffeabf
-Error: Cannot convert 'complex' objects with values that are multiple of 100 ba62bd7a-d4b6-4f74-8b8d-68219d272ffe
-Error: Cannot convert 'complex' objects with values that are multiple of 100 3767a782-0d53-4a03-aa5f-bef98e5c86ad
-Error: Cannot convert 'complex' objects with values that are multiple of 100 0a3f9c56-8549-4b73-bc76-5db66cbd0950
-Error: Cannot convert 'complex' objects with values that are multiple of 100 b912b1b4-52be-4392-8672-21949baebe6c
-Error: Cannot convert 'complex' objects with values that are multiple of 100 2004d06a-9a70-43c7-86d6-fe8fa36b24e5
-Error: Cannot convert 'complex' objects with values that are multiple of 100 3cf92202-95b2-494b-8321-8b7d9e781c6d
-Error: Cannot convert 'complex' objects with values that are multiple of 100 6f1d8d76-c6ef-4aca-8b8e-41e3009acaa8
-Error: Cannot convert 'complex' objects with values that are multiple of 100 7bb72e51-830e-4c98-b293-9269d3608279
-Error: Cannot convert 'complex' objects with values that are multiple of 100 08fc52f9-1977-4309-8103-a6f0a58578df
-Error: Cannot convert 'complex' objects with values that are multiple of 100 67e9e24c-c220-41b0-80ba-2ab7a40b9c8e
-Error: Cannot convert 'complex' objects with values that are multiple of 100 b003602b-1b28-4b20-94f3-0402f0250388
-Error: Cannot convert 'complex' objects with values that are multiple of 100 02b30d93-5a82-4c1c-ad9b-c2c1278f0db9
-Error: Cannot convert 'complex' objects with values that are multiple of 100 738f6daf-d212-42a7-ab8c-c60999bb0591
-Error: Cannot convert 'complex' objects with values that are multiple of 100 cad11df4-6236-4c95-bfef-3481f8994276
-Error: Cannot convert 'complex' objects with values that are multiple of 100 394bea27-9749-45d1-8602-2f8fe28a3a7e
-Error: Cannot convert 'complex' objects with values that are multiple of 100 b9e43818-d1b3-464c-bba7-dccc722577ca
-Error: Cannot convert 'complex' objects with values that are multiple of 100 bd9503b8-fe1a-45ad-b9bf-3e1f4bc42e87
-Error: Cannot convert 'complex' objects with values that are multiple of 100 6a42ecff-4987-4ed5-a3cc-fb095a8469ce
-Error: Cannot convert 'complex' objects with values that are multiple of 100 965ff5e1-3e9e-4ad5-aa64-dd8e27fe51cc
-Error: Cannot convert 'complex' objects with values that are multiple of 100 b5b2b876-bb22-41c1-9bb0-83652a9b1c67
-Error: Cannot convert 'complex' objects with values that are multiple of 100 280ac251-fa90-49d6-8b67-73e5f55cab9f
-Error: Cannot convert 'complex' objects with values that are multiple of 100 1a5fec1a-b0d4-41ab-a2b6-4280621a5079
-Error: Cannot convert 'complex' objects with values that are multiple of 100 02909989-ee2b-4167-b677-1e690a836913
-Error: Cannot convert 'complex' objects with values that are multiple of 100 56db91ba-7ea6-47af-9ace-167ba6a936ca
-Error: Cannot convert 'complex' objects with values that are multiple of 100 60725a82-d00c-4276-9526-f39eec24b9dc
-Error: Cannot convert 'complex' objects with values that are multiple of 100 f44406ec-adaa-4b2a-8230-fe21a2aebb4a
-Error: Cannot convert 'complex' objects with values that are multiple of 100 a9c7c7b3-66f1-423f-9af3-17c36905de60
-Error: Cannot convert 'complex' objects with values that are multiple of 100 cbbdcabb-295c-4895-a38a-a42eb2496ee4
-Error: Cannot convert 'complex' objects with values that are multiple of 100 6ed7296b-83ab-4afb-8b1f-f38cf6585d4d
-Error: Cannot convert 'complex' objects with values that are multiple of 100 b054ae90-af45-440f-b7cb-2d116d2ae663
-Error: Cannot convert 'complex' objects with values that are multiple of 100 bf9573d8-c4f1-4469-ba7a-513ea1fb8d58
-Error: Cannot convert 'complex' objects with values that are multiple of 100 d62c5bb8-f6b9-449f-a94e-dd75975b5db2
-Error: Cannot convert 'complex' objects with values that are multiple of 100 cb32ecd4-8e24-4f0f-ba46-fd7b3915e46c
-Error: Cannot convert 'complex' objects with values that are multiple of 100 8dd85e7a-30a9-44ba-b5c6-f014a2b89f3f
-Error: Cannot convert 'complex' objects with values that are multiple of 100 087730f7-b570-489c-bff5-3d1e41aeff4c
-Error: Cannot convert 'complex' objects with values that are multiple of 100 04f417d4-48df-47a4-b68d-a5f48a6d9bc0
-Error: Cannot convert 'complex' objects with values that are multiple of 100 31532d57-08b3-4541-9c2f-4b4e21c5158f
-Error: Cannot convert 'complex' objects with values that are multiple of 100 36563db4-83a1-4595-9072-96584c4580c4
-Error: Cannot convert 'complex' objects with values that are multiple of 100 c0992c3b-cd16-4bbe-b12e-87c656dfa991
-Error: Cannot convert 'complex' objects with values that are multiple of 100 cb83dfdd-9b56-4eb7-b851-c6951760eb97
-Error: Cannot convert 'complex' objects with values that are multiple of 100 66005b76-943c-4661-9768-efb658aa7cde
-Error: Cannot convert 'complex' objects with values that are multiple of 100 b3c2e84d-1bfd-4039-82d2-25e14a15e829
-Error: Cannot convert 'complex' objects with values that are multiple of 100 e681fefc-4dc6-4704-a154-047b7481e7bb
-Error: Cannot convert 'complex' objects with values that are multiple of 100 ce769d2d-c257-4af6-863a-642ad31ce2ae
-Error: Cannot convert 'complex' objects with values that are multiple of 100 ad0b2238-942d-42fd-bf97-6d90976dc6c1
-Error: Cannot convert 'complex' objects with values that are multiple of 100 733a4715-eeb3-45f8-849a-ec580af86ed4
-Error: Cannot convert 'complex' objects with values that are multiple of 100 3c2a390a-e404-4a80-bcb8-bd39ab1ac234
-Error: Cannot convert 'complex' objects with values that are multiple of 100 2e944371-8e2a-4ff3-b730-5017e3b47d07
-Error: Cannot convert 'complex' objects with values that are multiple of 100 46863b0c-13b7-4753-bd87-cbf1b8677ab4
-Error: Cannot convert 'complex' objects with values that are multiple of 100 75ada9c3-ada7-4049-a3b8-0f17291aec10
-Error: Cannot convert 'complex' objects with values that are multiple of 100 01639e07-745f-458b-9741-76d61b4f893c
-Error: Cannot convert 'complex' objects with values that are multiple of 100 2c7dfaf3-043e-46f5-8d70-f84c3a83c4b1
-Error: Cannot convert 'complex' objects with values that are multiple of 100 d6b19cf8-f7e8-4b09-beab-465e6f6941da
-Error: Cannot convert 'complex' objects with values that are multiple of 100 11da6714-0c93-4533-ba32-57edf42bbbbd
-Error: Cannot convert 'complex' objects with values that are multiple of 100 578b6f19-34ce-4a70-bd18-a77e56da91a6
-Error: Cannot convert 'complex' objects with values that are multiple of 100 908e2375-7f82-492b-ad1c-922c0e7d648f
-Error: Cannot convert 'complex' objects with values that are multiple of 100 10b7db73-23cc-4009-b242-d382d5ac02c9
-Error: Cannot convert 'complex' objects with values that are multiple of 100 95d7d3d6-c916-4999-a1b2-22d67f75316a
-Error: Cannot convert 'complex' objects with values that are multiple of 100 b3dcffef-408a-40e4-ba6a-eb2fde79ebb4
-Error: Cannot convert 'complex' objects with values that are multiple of 100 0fbb9bb0-7c86-436e-a2bf-c7526a352d4d
-Error: Cannot convert 'complex' objects with values that are multiple of 100 6def16c0-0650-4cef-8a70-e524636cb69c
-Error: Cannot convert 'complex' objects with values that are multiple of 100 1a52c9f8-0f26-4acb-9ef3-ea4001227f72
-Error: Cannot convert 'complex' objects with values that are multiple of 100 9d8c8f11-cc09-45c5-9a9b-5064508abb94
-Error: Cannot convert 'complex' objects with values that are multiple of 100 a0c70dd1-9a7e-4e0b-ac2c-f4e142558b92
-Error: Cannot convert 'complex' objects with values that are multiple of 100 e3bca80b-6547-4d4b-8c9f-be412f3477bd
-Error: Cannot convert 'complex' objects with values that are multiple of 100 f39d6eb5-82dc-4f53-9e41-f393a47252a8
-Error: Cannot convert 'complex' objects with values that are multiple of 100 720fffb8-8016-4671-ab5e-f39ea9216227
-Error: Cannot convert 'complex' objects with values that are multiple of 100 b11ddff4-09fd-4299-8a22-841da822edc9
-Error: Cannot convert 'complex' objects with values that are multiple of 100 601bfd3f-6367-48b7-8bf2-cdb9d1f80529
-Error: Cannot convert 'complex' objects with values that are multiple of 100 37255d78-90ce-46c2-ac9a-728703ce1fef
-Error: Cannot convert 'complex' objects with values that are multiple of 100 0aa5e719-4670-44db-ae9d-c04e76cb9659
-Error: Cannot convert 'complex' objects with values that are multiple of 100 a031b416-0321-408d-a09e-56c10ff3f052
-Error: Cannot convert 'complex' objects with values that are multiple of 100 bbeef659-4c46-4e61-a44c-8de529544a4e
-Error: Cannot convert 'complex' objects with values that are multiple of 100 3fb37f02-82d8-455d-90f9-ec10e8205ee9
-Error: Cannot convert 'complex' objects with values that are multiple of 100 4a692582-b980-4bb1-a29d-ed4f389eba0a
-Error: Cannot convert 'complex' objects with values that are multiple of 100 54d546e7-fc1c-4294-90c8-506d9452c449
-Error: Cannot convert 'complex' objects with values that are multiple of 100 accdc496-dd99-4b6c-b1ac-da9ab54aa942
-Error: Cannot convert 'complex' objects with values that are multiple of 100 07646030-4826-4944-bd90-60885631a270
-Error: Cannot convert 'complex' objects with values that are multiple of 100 c2c2af17-846c-41aa-828e-e7e2b33ce997
-Error: Cannot convert 'complex' objects with values that are multiple of 100 39bf0f76-68de-408f-bc38-9954815c7c27
-Error: Cannot convert 'complex' objects with values that are multiple of 100 ce4d11bd-e89b-43bf-93f5-8332d898675e
-Error: Cannot convert 'complex' objects with values that are multiple of 100 6d9f146d-f129-4b04-8fcc-b759d12d621e
-Error: Cannot convert 'complex' objects with values that are multiple of 100 3d26c633-4a19-4f68-9a8c-2d393f8c40c1
-Error: Cannot convert 'complex' objects with values that are multiple of 100 059af9c6-eaf2-4fad-b40c-601a4f9660ee
-Error: Cannot convert 'complex' objects with values that are multiple of 100 31af1be1-9695-4a86-b29f-4aa0ad2c1c2e
-Error: Cannot convert 'complex' objects with values that are multiple of 100 27a80378-ec6a-473a-b8b8-12df5516ebca
-Error: Cannot convert 'complex' objects with values that are multiple of 100 ea9d3c08-8941-4252-8b81-02c2ad9769ee
-Error: Cannot convert 'complex' objects with values that are multiple of 100 d1000f0a-edeb-4186-b23a-83db265697a0
-Error: Cannot convert 'complex' objects with values that are multiple of 100 546401f6-e5df-48ad-8da0-f19bc49355d8
-Error: Cannot convert 'complex' objects with values that are multiple of 100 1703e5fa-5bbb-4afa-991e-3aefcc841850
-Error: Cannot convert 'complex' objects with values that are multiple of 100 2d449780-d155-42e5-a4fb-7351ad6e4ff7
-Error: Cannot convert 'complex' objects with values that are multiple of 100 f789aa39-2863-4e25-9d02-e062a8f76986
-Error: Cannot convert 'complex' objects with values that are multiple of 100 ad69e225-5fc5-44ac-a302-4fdfd8bbb3ea
-Error: Cannot convert 'complex' objects with values that are multiple of 100 1fe81b31-da30-4528-af44-16babc910f0f
-Error: Cannot convert 'complex' objects with values that are multiple of 100 fcd7971b-9dd1-4203-86f7-ff0e1397e58a
-Error: Cannot convert 'complex' objects with values that are multiple of 100 c548cf0c-13be-4ab8-8945-eec1c83b4b0d
-Error: Cannot convert 'complex' objects with values that are multiple of 100 6a66102a-b300-4ba8-a94b-fc3aa3365ef1
-Error: Cannot convert 'complex' objects with values that are multiple of 100 90bae210-e307-4a8f-839f-ef881d3fa6e6
-Error: Cannot convert 'complex' objects with values that are multiple of 100 4131d22e-f363-48b0-b797-f79fab338f9d
-Error: Cannot convert 'complex' objects with values that are multiple of 100 65cc63b9-b6ea-436a-bac0-4a3d90a6f4bf
-Error: Cannot convert 'complex' objects with values that are multiple of 100 b7197838-473b-4cab-ab12-229ad3ac65c7
-Error: Cannot convert 'complex' objects with values that are multiple of 100 dbe1e20b-4d8c-4455-a4d6-8e797c12550b
-Error: Cannot convert 'complex' objects with values that are multiple of 100 f725eb7e-5947-4c63-8723-5b1a048fd936
-Error: Cannot convert 'complex' objects with values that are multiple of 100 a36c28fb-7ed4-447e-89d0-be643068c990
-Error: Cannot convert 'complex' objects with values that are multiple of 100 759f23ee-7275-4e7e-a955-c67f978e186e
-Error: Cannot convert 'complex' objects with values that are multiple of 100 20907e64-ed5c-4c88-8078-1c5e1b0b2cfa
-Error: Cannot convert 'complex' objects with values that are multiple of 100 d3e1d7f4-971a-4254-9f55-c5389c334df4
-Error: Cannot convert 'complex' objects with values that are multiple of 100 7d4450a7-cdd5-4337-94c5-6b1d0b7b9169
-Error: Cannot convert 'complex' objects with values that are multiple of 100 f805fb2a-bf43-4abd-a1e5-ae4cd1808433
-Error: Cannot convert 'complex' objects with values that are multiple of 100 faaf387e-2ee5-4814-b9fd-c384d7064d23
-Error: Cannot convert 'complex' objects with values that are multiple of 100 9f7de8b7-0669-4fe5-b1c0-52ef6efc5d79
-Error: Cannot convert 'complex' objects with values that are multiple of 100 95f0f374-b81f-4547-89b4-e35ac8fbb056
-Error: Cannot convert 'complex' objects with values that are multiple of 100 7b2e69f0-b845-4c49-bc04-ef3b86c67029
-Error: Cannot convert 'complex' objects with values that are multiple of 100 e8b9ea8f-4872-4a9d-a058-826774c10439
-Error: Cannot convert 'complex' objects with values that are multiple of 100 204398da-4dcb-452e-8a02-c54d2e222ae1
-Error: Cannot convert 'complex' objects with values that are multiple of 100 93feed88-dbd9-4660-9057-141084f7334b
-Error: Cannot convert 'complex' objects with values that are multiple of 100 3e566a51-e8b2-4b04-9671-0f5fe20ed3f2
-Error: Cannot convert 'complex' objects with values that are multiple of 100 f97d6116-da8f-4bc0-9790-97d4830e08e2
-Error: Cannot convert 'complex' objects with values that are multiple of 100 2889d19f-113b-4da3-915d-22efaaa655cc
-Error: Cannot convert 'complex' objects with values that are multiple of 100 56192ec6-2943-4143-a414-9cf3a4ca48fc
-Error: Cannot convert 'complex' objects with values that are multiple of 100 b17621c1-26a5-4983-b9d1-92a193e23f6a
-Error: Cannot convert 'complex' objects with values that are multiple of 100 4d023d1e-44d7-4e31-a35d-39e468efc6f8
-Error: Cannot convert 'complex' objects with values that are multiple of 100 91e272b2-ee39-4669-bc6f-799f55fd1efd
-Error: Cannot convert 'complex' objects with values that are multiple of 100 5a1511b0-867d-4f5a-ab84-3e291d0a1914
-Error: Cannot convert 'complex' objects with values that are multiple of 100 30a4127c-1231-4ca5-87f5-bd829c05a343
-Error: Cannot convert 'complex' objects with values that are multiple of 100 3a066498-2539-46e6-a54b-d4a851bdf2ac
-Error: Cannot convert 'complex' objects with values that are multiple of 100 2f8dc8ac-5f8c-4594-abfc-81a1d07d0500
-Error: Cannot convert 'complex' objects with values that are multiple of 100 e904a535-a96e-49fa-afab-7b89a1f704e6
-Error: Cannot convert 'complex' objects with values that are multiple of 100 c1043a76-52b1-4235-9b9e-4d4693bc6db3
-Error: Cannot convert 'complex' objects with values that are multiple of 100 9b05d820-6385-46f8-9528-a2b434f2086b
-Error: Cannot convert 'complex' objects with values that are multiple of 100 5005451f-b3cf-4cdc-b821-98308997c22a
-Error: Cannot convert 'complex' objects with values that are multiple of 100 ba8882b5-3699-4f4f-a66c-c661441d44c1
-Error: Cannot convert 'complex' objects with values that are multiple of 100 ef33b1c4-d967-4656-b2d4-a5b25a8c15f3
-Error: Cannot convert 'complex' objects with values that are multiple of 100 a0f1eb8e-46e7-47be-9ecd-640edf1b5833
-Error: Cannot convert 'complex' objects with values that are multiple of 100 af214aeb-5744-4e44-8c33-59ac5dbcf5bb
-Error: Cannot convert 'complex' objects with values that are multiple of 100 43770205-98c5-4a2d-919c-8da320b9ff72
-Error: Cannot convert 'complex' objects with values that are multiple of 100 68cfbac1-9844-4d5c-b405-1a230d48165a
-Error: Cannot convert 'complex' objects with values that are multiple of 100 f7781ccd-94fb-4373-bdfc-36f573d71d15
-Error: Cannot convert 'complex' objects with values that are multiple of 100 1052e71e-050b-4057-8307-c411303b2b4b
-Error: Cannot convert 'complex' objects with values that are multiple of 100 59eeafb0-8cf7-432a-8823-849a9314bf14
-Error: Cannot convert 'complex' objects with values that are multiple of 100 2a670c73-b4e8-4c5e-afb3-bc3e30d68a27
-Error: Cannot convert 'complex' objects with values that are multiple of 100 79c9864f-c60e-492c-8e16-999302dc067f
-Error: Cannot convert 'complex' objects with values that are multiple of 100 76a5fb90-ef1b-4f32-9c3a-0684cebbafcc
-Error: Cannot convert 'complex' objects with values that are multiple of 100 89e1276f-d9e1-4afd-a576-c0358654eff2
-Error: Cannot convert 'complex' objects with values that are multiple of 100 ef908c5d-39fa-4dd3-aae8-9bed232ada08
-Error: Cannot convert 'complex' objects with values that are multiple of 100 6a330950-5b21-49a9-b9d7-6094807e2afb
-Error: Cannot convert 'complex' objects with values that are multiple of 100 f1ec37ea-0b01-4f09-9576-ea1093691c78
-Error: Cannot convert 'complex' objects with values that are multiple of 100 edaa7061-5ba7-4fa9-ac7a-f88da67e6287
-Error: Cannot convert 'complex' objects with values that are multiple of 100 7cc95728-a281-47f9-ab51-767204574ac8
-Error: Cannot convert 'complex' objects with values that are multiple of 100 08922966-f777-42bf-a21c-480938899316
-Error: Cannot convert 'complex' objects with values that are multiple of 100 cc75b4c5-f572-4015-944e-9b340703298e
-Error: Cannot convert 'complex' objects with values that are multiple of 100 9eac39d5-572f-4a97-a3db-85013583e199
-Error: Cannot convert 'complex' objects with values that are multiple of 100 9ea63ec6-5a02-43be-9f34-ab0c60eca88c
-Error: Cannot convert 'complex' objects with values that are multiple of 100 c938415f-eb53-4bdc-8ae6-f305f6b2a093
-Error: Cannot convert 'complex' objects with values that are multiple of 100 607a094b-df98-4987-ac1d-3d0727265356
-Error: Cannot convert 'complex' objects with values that are multiple of 100 cae2906b-0d8a-4632-9f43-02755adfb6bb
-Error: Cannot convert 'complex' objects with values that are multiple of 100 778aa358-8872-4792-ba3d-af11ed5f09d3
-Error: Cannot convert 'complex' objects with values that are multiple of 100 b57db793-287d-4173-9541-be38e1dbe5b4
-Error: Cannot convert 'complex' objects with values that are multiple of 100 b91461eb-fd87-435a-90f5-f0b072001cdf
-Error: Cannot convert 'complex' objects with values that are multiple of 100 6efde1b8-1b19-40f6-bcf6-85d2c8c29f54
-Error: Cannot convert 'complex' objects with values that are multiple of 100 49cd0c17-00e1-491d-aa60-9fa33e12c562
-Error: Cannot convert 'complex' objects with values that are multiple of 100 56ff6c82-8ccd-44f8-aa71-1c79a2a59ebb
-Error: Cannot convert 'complex' objects with values that are multiple of 100 9931023b-5b4d-437a-834f-da5abe92dcbb
-Error: Cannot convert 'complex' objects with values that are multiple of 100 4995acca-50f9-4220-b5e7-832ce3af89a1
-Error: Cannot convert 'complex' objects with values that are multiple of 100 6092a681-a969-43d5-833c-3f55313be0a6
-Error: Cannot convert 'complex' objects with values that are multiple of 100 46c8ee4a-97be-47c1-bee8-f70e6f41ddd9
-Error: Cannot convert 'complex' objects with values that are multiple of 100 22b11300-5660-4cf4-9f5c-f4429028e7cd
-Error: Cannot convert 'complex' objects with values that are multiple of 100 8be3ea7e-6cf7-4ff8-a253-7d68899f7c27
-Error: Cannot convert 'complex' objects with values that are multiple of 100 3ff0f54a-c320-4a6a-841c-4d1806597bce
-Error: Cannot convert 'complex' objects with values that are multiple of 100 de75ccf9-c10c-4d17-8b64-f7d6f094c680
-Error: Cannot convert 'complex' objects with values that are multiple of 100 80db02d5-383e-47b5-90a4-528a8fcc3546
-Error: Cannot convert 'complex' objects with values that are multiple of 100 94491dbc-2686-4d8c-82ef-da0b3abb337d
-Error: Cannot convert 'complex' objects with values that are multiple of 100 9a563ac2-6ed8-4187-a50c-20ea8f7ae8bc
-Error: Cannot convert 'complex' objects with values that are multiple of 100 52806b8e-7364-4be8-adff-c624ea5270ae
-Error: Cannot convert 'complex' objects with values that are multiple of 100 f245cbf2-3e30-4011-974e-c8b5fcf45749
-Error: Cannot convert 'complex' objects with values that are multiple of 100 16f55dcb-df6d-49d5-862b-ef65ccb0473b
-Error: Cannot convert 'complex' objects with values that are multiple of 100 ab57fbb2-ba55-4dca-9fb6-45f497e7990a
-Error: Cannot convert 'complex' objects with values that are multiple of 100 fbee80c7-8ee6-41fd-bb84-f28d4e561bcc
-Error: Cannot convert 'complex' objects with values that are multiple of 100 b89c4033-4774-462b-abb6-fcb38943cfb7
-Error: Cannot convert 'complex' objects with values that are multiple of 100 17afcdcd-a0b1-4f45-a52a-558b2b68479f
-Error: Cannot convert 'complex' objects with values that are multiple of 100 811d692a-8f80-4359-92df-1c009fc18462
-Error: Cannot convert 'complex' objects with values that are multiple of 100 10596a69-faa4-4b83-8438-26d93024e686
-Error: Cannot convert 'complex' objects with values that are multiple of 100 2d2f9a30-c845-49b0-b6b5-2b23883daa8b
-Error: Cannot convert 'complex' objects with values that are multiple of 100 12898fde-d034-41fb-ad88-8f6dc65b32bb
-Error: Cannot convert 'complex' objects with values that are multiple of 100 7a0928ea-66bd-497c-8061-c37ea99eba90
-Error: Cannot convert 'complex' objects with values that are multiple of 100 284edbee-2471-4711-ac22-9cd8ddfbd87d
-Error: Cannot convert 'complex' objects with values that are multiple of 100 5763b837-9976-4ac5-be63-3334c38794fa
-Error: Cannot convert 'complex' objects with values that are multiple of 100 580c7e29-e05f-4cdc-9bda-13a96ba85cf4
-Error: Cannot convert 'complex' objects with values that are multiple of 100 77e27456-5eaf-4d13-8986-72cadd7a45a3
-Error: Cannot convert 'complex' objects with values that are multiple of 100 4630188e-1c96-4ee6-8593-a6a58c285803
-Error: Cannot convert 'complex' objects with values that are multiple of 100 6f0d069f-9859-4f42-b9ec-ea012e6a7ced
-Error: Cannot convert 'complex' objects with values that are multiple of 100 55b2439f-c3fd-4f54-a756-3df1aeb2e188
-Error: Cannot convert 'complex' objects with values that are multiple of 100 ab62a501-78e0-4066-9a1e-f354efd03cda
-Error: Cannot convert 'complex' objects with values that are multiple of 100 a991df05-92ed-41ad-a036-95090e638ea5
-Error: Cannot convert 'complex' objects with values that are multiple of 100 c5a6ea77-dc1f-4fb1-b493-047656279f96
-Error: Cannot convert 'complex' objects with values that are multiple of 100 bfc2178f-28bc-4eb1-9c96-04ef4d1e6fec
-Error: Cannot convert 'complex' objects with values that are multiple of 100 e426f4f1-98ff-43fd-a3e0-2fbe39a558ac
-Error: Cannot convert 'complex' objects with values that are multiple of 100 55270c95-186a-4cfc-8947-15ac35df8d03
-Error: Cannot convert 'complex' objects with values that are multiple of 100 98cfa325-16ee-43f5-91e1-68a8b85c179b
-Error: Cannot convert 'complex' objects with values that are multiple of 100 f9f050a0-494e-4202-9794-ae44238e2d27
-Error: Cannot convert 'complex' objects with values that are multiple of 100 7ff603f6-aacc-4edf-932b-97024affc819
-Error: Cannot convert 'complex' objects with values that are multiple of 100 b67be7ba-b2e7-4a8d-aa4c-5a06bb72b4d0
-Error: Cannot convert 'complex' objects with values that are multiple of 100 cb11135f-e1ee-41a5-9081-b2104bfc2647
-Error: Cannot convert 'complex' objects with values that are multiple of 100 13b0f5c6-5e91-40d1-95f6-b375cb3d68dd
-Error: Cannot convert 'complex' objects with values that are multiple of 100 10568609-99a6-48b4-91b8-3b4d21606203
-Error: Cannot convert 'complex' objects with values that are multiple of 100 d3a4bee2-9f8f-4f21-9c9d-494b99ae5d9e
-Error: Cannot convert 'complex' objects with values that are multiple of 100 b97d0197-1236-4653-b160-1c1e830cb066
-Error: Cannot convert 'complex' objects with values that are multiple of 100 1429b840-d3d2-45b5-802b-f3b20835992e
-Error: Cannot convert 'complex' objects with values that are multiple of 100 c1b82a9a-0a52-41eb-b9fe-191a690b4a47
-Error: Cannot convert 'complex' objects with values that are multiple of 100 5d2339de-a12c-4a1d-86f3-9729edeb5f0b
-Error: Cannot convert 'complex' objects with values that are multiple of 100 813d676a-4c74-44bc-99b3-c80e35a965a4
-Error: Cannot convert 'complex' objects with values that are multiple of 100 8827a0d0-85cd-4b02-8a8f-5b8958fb9576
-Error: Cannot convert 'complex' objects with values that are multiple of 100 6d571a4a-2de0-4615-9ec5-c22090f7ed1d
-Error: Cannot convert 'complex' objects with values that are multiple of 100 1bc30778-5e0d-4784-aa08-182092095196
-Error: Cannot convert 'complex' objects with values that are multiple of 100 ecf2f5f7-0099-4117-9954-d9c603f6cd83
-Error: Cannot convert 'complex' objects with values that are multiple of 100 a2897479-d02d-4945-8386-1e3f290a27b0
-Error: Cannot convert 'complex' objects with values that are multiple of 100 1652b0b7-4a49-4d85-a25c-4fbb65583cf0
-Error: Cannot convert 'complex' objects with values that are multiple of 100 43abb324-24c9-4331-8be2-eb5445078f85
-Error: Cannot convert 'complex' objects with values that are multiple of 100 4fd96471-1396-43f7-97d6-c9e770b2d3da
-Error: Cannot convert 'complex' objects with values that are multiple of 100 fca1a3ea-7394-40db-a725-d8ba7fad3aa8
-Error: Cannot convert 'complex' objects with values that are multiple of 100 54522831-7fd1-45b9-bea7-9792f3e904d7
-Error: Cannot convert 'complex' objects with values that are multiple of 100 0313c8d7-849e-416d-ad07-ce5ed3e796ac
-Error: Cannot convert 'complex' objects with values that are multiple of 100 8e3750ca-fe2e-4ac9-9fc5-81896d17cec9
-Error: Cannot convert 'complex' objects with values that are multiple of 100 e37afc41-9878-47b2-921f-b80496ca4521
-Error: Cannot convert 'complex' objects with values that are multiple of 100 46a74942-893a-4ee4-8175-e29ba1ad4361
-Error: Cannot convert 'complex' objects with values that are multiple of 100 c207cba6-1cbd-4152-b3fe-1e598e8521fa
-Error: Cannot convert 'complex' objects with values that are multiple of 100 582e5065-d6db-4f0a-b70b-d9c0a090abbd
-Error: Cannot convert 'complex' objects with values that are multiple of 100 567a4fef-dc1a-4c15-a792-ce57e62d41ed
-Error: Cannot convert 'complex' objects with values that are multiple of 100 c21b3fd4-3ddf-49b2-a09f-db5d205e0030
-Error: Cannot convert 'complex' objects with values that are multiple of 100 61fda36f-423b-48ca-a76b-ed2f7145eec2
-Error: Cannot convert 'complex' objects with values that are multiple of 100 a73b2371-43cb-4ae2-a342-0a57bdd7b9f8
-Error: Cannot convert 'complex' objects with values that are multiple of 100 88a5ad19-5056-433c-956f-d5ac98fb6da6
-Error: Cannot convert 'complex' objects with values that are multiple of 100 7ad19cb7-7ec1-4317-a834-2b4300f797f2
-Error: Cannot convert 'complex' objects with values that are multiple of 100 2fc4288e-d783-4221-8234-9de18989dc48
-Error: Cannot convert 'complex' objects with values that are multiple of 100 a286ff00-9c0a-4495-9e21-b12d37368023
-Error: Cannot convert 'complex' objects with values that are multiple of 100 bc5055e5-49c4-4cbb-b6e5-c2dc1427e1a1
-Error: Cannot convert 'complex' objects with values that are multiple of 100 b42d9f87-ee3a-47be-aa7b-9b1d2b53acd5
-Error: Cannot convert 'complex' objects with values that are multiple of 100 77ad2283-6a1e-42d5-a7c8-27e46e9fa19b
-Error: Cannot convert 'complex' objects with values that are multiple of 100 debca476-2e93-498e-8f39-7fb586f19646
-Error: Cannot convert 'complex' objects with values that are multiple of 100 848caf5f-3486-467c-8466-0c038d900487
-Error: Cannot convert 'complex' objects with values that are multiple of 100 753edad9-0522-4437-9b4c-652c7ecd1c80
-Error: Cannot convert 'complex' objects with values that are multiple of 100 af0977af-5a77-4604-9478-bf7d588820c0
-Error: Cannot convert 'complex' objects with values that are multiple of 100 fdcf6c6e-c62f-47c9-a815-57a059cf789b
-Error: Cannot convert 'complex' objects with values that are multiple of 100 789b87f3-7978-4599-9888-f93441b8070d
-Error: Cannot convert 'complex' objects with values that are multiple of 100 3f9e77df-e9f0-4665-b87b-88a5ffcb16c2
-Error: Cannot convert 'complex' objects with values that are multiple of 100 2933f68f-a478-4f40-bd77-9d2317b5c1bc
-Error: Cannot convert 'complex' objects with values that are multiple of 100 cca0369b-51e2-4187-af74-88a7fbdc91c8
-Error: Cannot convert 'complex' objects with values that are multiple of 100 4b47afc4-dd93-4acc-962a-5863a28ef6de
-Error: Cannot convert 'complex' objects with values that are multiple of 100 aa0b6e32-5590-4be1-8bc9-e0e6a2353289
-Error: Cannot convert 'complex' objects with values that are multiple of 100 f021cd06-cece-423b-a8a1-6d6603649511
-Error: Cannot convert 'complex' objects with values that are multiple of 100 3f9b4a22-f01f-434e-83e3-a21c023e165b
-Error: Cannot convert 'complex' objects with values that are multiple of 100 0f8aabd7-8006-4187-9cd9-57eebe6ff3ba
-Error: Cannot convert 'complex' objects with values that are multiple of 100 c834a6e9-0214-44c4-b89a-07f9ea656df0
-Error: Cannot convert 'complex' objects with values that are multiple of 100 ba42a255-306f-4ebe-82be-425f0ec8ad10
-Error: Cannot convert 'complex' objects with values that are multiple of 100 e0ee2301-7753-4cc5-bcc9-dcbf0a12af8e
-Error: Cannot convert 'complex' objects with values that are multiple of 100 5b1a54a2-aff0-4ef3-9318-6106e66ae7ea
-Error: Cannot convert 'complex' objects with values that are multiple of 100 b026cd6c-49d7-42f7-9c67-7fce59581985
-Error: Cannot convert 'complex' objects with values that are multiple of 100 d049580d-4d27-4099-81b8-852841125092
-Error: Cannot convert 'complex' objects with values that are multiple of 100 5aca730a-f97c-4efc-a1a4-d76e07d74c21
-Error: Cannot convert 'complex' objects with values that are multiple of 100 e6748ee5-dfb1-4c93-b5ff-725539c846ef
-Error: Cannot convert 'complex' objects with values that are multiple of 100 cf0f8c2c-c76f-48de-9a0c-a08db26ddee4
-Error: Cannot convert 'complex' objects with values that are multiple of 100 07e3bd27-6147-4522-9dd2-0d63edc0cf1b
-Error: Cannot convert 'complex' objects with values that are multiple of 100 9e6b6223-caf3-4eec-acb3-8872a42c84fc
-Error: Cannot convert 'complex' objects with values that are multiple of 100 b07e3a4b-3e20-4428-b1af-678314379dad
-Error: Cannot convert 'complex' objects with values that are multiple of 100 09b7a123-82c3-4d6c-ae1e-01824d74e175
-Error: Cannot convert 'complex' objects with values that are multiple of 100 c0d1e838-a5e4-4dba-b6dc-2bbfeea1d58c
-Error: Cannot convert 'complex' objects with values that are multiple of 100 70061015-0bc6-4e98-b843-fcf542f11a72
-Error: Cannot convert 'complex' objects with values that are multiple of 100 5ffbde64-7453-4df9-a0ba-2340baa9ba14
-Error: Cannot convert 'complex' objects with values that are multiple of 100 eb4eba9a-0d99-49cd-85c7-c888958a1b6e
-Error: Cannot convert 'complex' objects with values that are multiple of 100 569b908b-08e6-4c86-a122-0951b12a771e
-Error: Cannot convert 'complex' objects with values that are multiple of 100 ce48ccad-69cb-4346-a4d3-60cb45df36d0
-Error: Cannot convert 'complex' objects with values that are multiple of 100 c67b2149-9310-4e5e-a567-589c47319843
-Error: Cannot convert 'complex' objects with values that are multiple of 100 a3ee94cf-9c39-487f-baeb-cdac062861e8
-Error: Cannot convert 'complex' objects with values that are multiple of 100 82f4e2cf-83b0-4e70-a451-2b4963278ce5
-Error: Cannot convert 'complex' objects with values that are multiple of 100 51cd2487-29dd-45c6-a32c-ade4276ce418
-Error: Cannot convert 'complex' objects with values that are multiple of 100 ea242ac9-265a-4c8b-8b23-c7b10ab78954
-Error: Cannot convert 'complex' objects with values that are multiple of 100 f5a1ebfa-a232-405c-9e34-9dda11d62c7c
-Error: Cannot convert 'complex' objects with values that are multiple of 100 81178cd7-2fe3-4dad-96ea-eaca777d6e8f
-Error: Cannot convert 'complex' objects with values that are multiple of 100 ee9e3e0e-3f89-4566-a579-e80e38cb39bc
-Error: Cannot convert 'complex' objects with values that are multiple of 100 f9778a12-82db-436e-9a0e-a74533625f61
-Error: Cannot convert 'complex' objects with values that are multiple of 100 ce0ca2e0-f430-4cfa-8298-b55afadb41cf
-Error: Cannot convert 'complex' objects with values that are multiple of 100 26cd8362-db32-4adc-af89-252ec0ddf6c7
-Error: Cannot convert 'complex' objects with values that are multiple of 100 45fd8f42-f7b5-4f5d-9788-c06255d00852
-Error: Cannot convert 'complex' objects with values that are multiple of 100 39658b97-26d5-4924-afa0-da5897bc3978
-Error: Cannot convert 'complex' objects with values that are multiple of 100 29547df0-bfad-46a0-a069-a715e78237cd
-Error: Cannot convert 'complex' objects with values that are multiple of 100 d323df45-c829-4c8c-862f-a0eb63dc26a6
-Error: Cannot convert 'complex' objects with values that are multiple of 100 1fdafb81-7f94-4b3a-ad17-5c7071a1518a
-Error: Cannot convert 'complex' objects with values that are multiple of 100 e42d0415-4cc0-440d-bab1-107436f6a8d2
-Error: Cannot convert 'complex' objects with values that are multiple of 100 d3e25b1f-39a3-433f-95ff-a0cbf85f4d2d
-Error: Cannot convert 'complex' objects with values that are multiple of 100 7c52e74c-b159-42eb-8f4e-c6a8ba592fa6
-Error: Cannot convert 'complex' objects with values that are multiple of 100 5874aac4-258c-4e9a-bd6e-1862ee484157
-Error: Cannot convert 'complex' objects with values that are multiple of 100 20c77ddc-a228-4c6b-8336-c201c1f547dd
-Error: Cannot convert 'complex' objects with values that are multiple of 100 14debcf9-b093-4dab-9a24-888c26d6acc2
-Error: Cannot convert 'complex' objects with values that are multiple of 100 c53fa7ec-7a92-4c3f-95b7-3107b050f45f
-Error: Cannot convert 'complex' objects with values that are multiple of 100 8120c087-4935-4848-8a54-1e0728fc69af
-Error: Cannot convert 'complex' objects with values that are multiple of 100 89acc355-e3ff-4594-8f04-657e2ae690bb
-Error: Cannot convert 'complex' objects with values that are multiple of 100 5336a4e4-05f1-4e98-a260-a4fce489823e
-Error: Cannot convert 'complex' objects with values that are multiple of 100 7b2b15d8-fdad-4ca5-b0da-6b44093194f1
-Error: Cannot convert 'complex' objects with values that are multiple of 100 af7ffa1d-f8fb-4b13-a39d-c07965c5f130
-Error: Cannot convert 'complex' objects with values that are multiple of 100 15626af4-76e5-41a4-ae85-dae82965f0d7
-Error: Cannot convert 'complex' objects with values that are multiple of 100 ca3184ce-291f-47bc-ab61-616eb0fb3fce
-Error: Cannot convert 'complex' objects with values that are multiple of 100 d7e4e6e0-44c9-4bf9-b09e-5df3f47291e9
-Error: Cannot convert 'complex' objects with values that are multiple of 100 ad72c63d-3307-413b-8555-f0b6873e5135
-Error: Cannot convert 'complex' objects with values that are multiple of 100 a69e76c1-032d-4672-8a9e-4112b0573f33
-Error: Cannot convert 'complex' objects with values that are multiple of 100 d499d88a-b113-4567-8aab-5d47bb2e4ac2
-Error: Cannot convert 'complex' objects with values that are multiple of 100 4339fcd3-0bc5-48ec-8804-dc68ec10e48e
-Error: Cannot convert 'complex' objects with values that are multiple of 100 de47855a-2448-4085-83a6-5617cb8a6048
-Error: Cannot convert 'complex' objects with values that are multiple of 100 f71c72e1-3efb-4629-8afd-1edd1d496822
-Error: Cannot convert 'complex' objects with values that are multiple of 100 c5f65fbc-fd54-4c9d-9531-783dae926546
-Error: Cannot convert 'complex' objects with values that are multiple of 100 00dc9ab2-ce3d-46b9-a696-39815941eb57
-Error: Cannot convert 'complex' objects with values that are multiple of 100 12a76491-f0e0-4d7f-94bc-f999a2872595
-Error: Cannot convert 'complex' objects with values that are multiple of 100 6c291e99-1151-4cff-a758-26a6eb66b471
-Error: Cannot convert 'complex' objects with values that are multiple of 100 931de8c4-a876-45bf-8f81-f2db16b078bc
-Error: Cannot convert 'complex' objects with values that are multiple of 100 13f1b402-f08d-4f3d-b838-18ad85a4b0bb
-Error: Cannot convert 'complex' objects with values that are multiple of 100 df848efb-1a6d-400d-b5d7-2253251cc56c
-Error: Cannot convert 'complex' objects with values that are multiple of 100 47e26eaa-baf5-47a5-9ade-c6a5c013bcf6
-Error: Cannot convert 'complex' objects with values that are multiple of 100 a54d271e-4449-4985-9558-1fbb58dccc2b
-Error: Cannot convert 'complex' objects with values that are multiple of 100 25da4f82-b672-477e-901d-d7dc8c3c6d0f
-Error: Cannot convert 'complex' objects with values that are multiple of 100 1cf1f304-3859-40af-bcc3-24b103c0a565
-Error: Cannot convert 'complex' objects with values that are multiple of 100 1dc22633-b838-4502-acbe-dba6cdacc20d
-Error: Cannot convert 'complex' objects with values that are multiple of 100 e5b4d5a4-51d6-4056-8b3c-04290b7006f9
-Error: Cannot convert 'complex' objects with values that are multiple of 100 32529472-2f4d-4db2-b590-9809e9d7e28a
-Error: Cannot convert 'complex' objects with values that are multiple of 100 9120e535-3132-4548-bfe4-f8bbf72ee2a2
-Error: Cannot convert 'complex' objects with values that are multiple of 100 e0d5dec3-df62-46d4-b2bf-b5821169fce9
-Error: Cannot convert 'complex' objects with values that are multiple of 100 57c04798-dfa0-429d-b09d-92416cd3e2f6
-Error: Cannot convert 'complex' objects with values that are multiple of 100 6837ec9a-eaa9-4961-b8ff-4a903551b9a6
-Error: Cannot convert 'complex' objects with values that are multiple of 100 b32066fb-35e9-478a-9f0f-64f427efa07d
-Error: Cannot convert 'complex' objects with values that are multiple of 100 be2e1a0a-18bf-4669-813b-ff0cedec4527
-Error: Cannot convert 'complex' objects with values that are multiple of 100 c3b40409-5590-42f3-81b9-d446487e9cdd
-Error: Cannot convert 'complex' objects with values that are multiple of 100 2c3dcf04-4937-4f9f-9d5f-ba4c2cfc3d91
-Error: Cannot convert 'complex' objects with values that are multiple of 100 707851e6-46a3-49ce-b78b-de18eaebc7e8
-Error: Cannot convert 'complex' objects with values that are multiple of 100 d017a465-34a9-4ffc-b636-6aa56db68dad
-Error: Cannot convert 'complex' objects with values that are multiple of 100 c819cc14-c35d-4917-abb6-4f4af3135281
-Error: Cannot convert 'complex' objects with values that are multiple of 100 38ee257a-e50b-433c-95fd-55446b345df7
-Error: Cannot convert 'complex' objects with values that are multiple of 100 da9f5bff-dd22-450d-b668-a030c02a2a8d
-Error: Cannot convert 'complex' objects with values that are multiple of 100 738219df-b917-4676-a52f-93e1d2e7970e
-Error: Cannot convert 'complex' objects with values that are multiple of 100 2abdb34e-4306-4628-a33a-e4950ac8842c
-Error: Cannot convert 'complex' objects with values that are multiple of 100 fff135cf-547c-4312-ab7a-09bba3a9cf79
-Error: Cannot convert 'complex' objects with values that are multiple of 100 827e362b-a2a2-4b20-883f-60b873023f4e
-Error: Cannot convert 'complex' objects with values that are multiple of 100 0be7ebac-a53e-476f-9950-438549383acc
-Error: Cannot convert 'complex' objects with values that are multiple of 100 bb77aaa7-233b-484f-8497-031ccdc9bcdc
-Error: Cannot convert 'complex' objects with values that are multiple of 100 df680115-3d98-4963-8bfb-93f9b80f2d45
-Error: Cannot convert 'complex' objects with values that are multiple of 100 b59f4df7-29c2-4575-a72b-b5b24bfe1472
-Error: Cannot convert 'complex' objects with values that are multiple of 100 ce40cc6f-7281-4dc2-9b4e-e5e440dc2294
-Error: Cannot convert 'complex' objects with values that are multiple of 100 da9c3a74-0e70-4a77-b951-c894e171d4fe
-Error: Cannot convert 'complex' objects with values that are multiple of 100 f18e7ec2-e302-435a-b105-b8c812c8aa54
-Error: Cannot convert 'complex' objects with values that are multiple of 100 c2ee3e7e-18f8-4420-9bd6-465289dc82ad
-Error: Cannot convert 'complex' objects with values that are multiple of 100 8f7cb266-402f-4a5f-81e2-f0c7c39279c6
-Error: Cannot convert 'complex' objects with values that are multiple of 100 41d04cb1-f8ba-40a3-95e4-f333e7bc6376
-Error: Cannot convert 'complex' objects with values that are multiple of 100 9a33f522-8523-43e6-955f-263bfe7f78dd
-Error: Cannot convert 'complex' objects with values that are multiple of 100 b38bab13-b560-44fd-90d0-6af2f361cb92
-Error: Cannot convert 'complex' objects with values that are multiple of 100 983589d1-6edf-45f1-90bd-5ed1925a99fa
-Error: Cannot convert 'complex' objects with values that are multiple of 100 b323dae3-42cd-4d04-9030-931cf2e5c8ce
-Error: Cannot convert 'complex' objects with values that are multiple of 100 fec168d1-6a74-44e1-b48c-3332ace4b8c7
-Error: Cannot convert 'complex' objects with values that are multiple of 100 7d886d03-f2b8-4245-9218-2a42558e1a13
-Error: Cannot convert 'complex' objects with values that are multiple of 100 e3666a8c-db6d-4f1b-bcd0-64df738cfa79
-Error: Cannot convert 'complex' objects with values that are multiple of 100 1ab4098f-01e0-4643-b6c7-005e1fd31c7c
-Error: Cannot convert 'complex' objects with values that are multiple of 100 ecfa9706-d521-4f8d-8162-b262453feb0d
-Error: Cannot convert 'complex' objects with values that are multiple of 100 f4ae37ad-1395-4d49-ab84-141345655bb2
-Error: Cannot convert 'complex' objects with values that are multiple of 100 207a76be-338a-4041-a353-da194bbb4844
-Error: Cannot convert 'complex' objects with values that are multiple of 100 edd78f7f-8f8e-4b43-9a43-b80a9344fd06
-Error: Cannot convert 'complex' objects with values that are multiple of 100 562ae9d4-ff5d-4ea3-9b94-3f817c50a1c4
-Error: Cannot convert 'complex' objects with values that are multiple of 100 91a2897e-d109-4e74-909c-5b855e721afe
-Error: Cannot convert 'complex' objects with values that are multiple of 100 ed7fc3b2-15ba-4ad2-ae5b-d49bcb403b22
-Error: Cannot convert 'complex' objects with values that are multiple of 100 8590c1a6-ad67-4271-95f7-03b995768332
-Error: Cannot convert 'complex' objects with values that are multiple of 100 b8504f3e-15ec-4ae1-9610-efed0061f8d1
-Error: Cannot convert 'complex' objects with values that are multiple of 100 712c586c-d3e0-4df5-aaf0-60258ecf35df
-Error: Cannot convert 'complex' objects with values that are multiple of 100 704d8dc5-1ce5-4cb4-af81-096df71ebb41
-Error: Cannot convert 'complex' objects with values that are multiple of 100 800997f8-220f-4215-9e4a-d9b22d4e329c
-Error: Cannot convert 'complex' objects with values that are multiple of 100 853ed350-f20b-466b-968d-89a32df25a37
-Error: Cannot convert 'complex' objects with values that are multiple of 100 c6beebdf-dd99-4399-9d67-4751e626be5a
-Error: Cannot convert 'complex' objects with values that are multiple of 100 890053b3-71a4-4c6a-8ea8-fe5e302598ab
-Error: Cannot convert 'complex' objects with values that are multiple of 100 ee5385bc-1a51-41e4-9a3e-b3de8b13c9ce
-Error: Cannot convert 'complex' objects with values that are multiple of 100 29baa95a-ab53-4ca9-9e07-0bb23892fe5d
-Error: Cannot convert 'complex' objects with values that are multiple of 100 fa0f15f0-c687-44cb-970a-5b433b93da5d
-Error: Cannot convert 'complex' objects with values that are multiple of 100 2229e477-12d3-4f6f-9106-fcd2bf32889b
-Error: Cannot convert 'complex' objects with values that are multiple of 100 68c97562-78af-4323-9601-e1a29368e641
-Error: Cannot convert 'complex' objects with values that are multiple of 100 9005e741-1bdd-436a-b064-5e604c71a403
-Error: Cannot convert 'complex' objects with values that are multiple of 100 b9e62822-45c7-4eae-8539-24bdc6ba45a6
-Error: Cannot convert 'complex' objects with values that are multiple of 100 a1dd8da2-674a-4e62-92aa-c9c9a137c0ab
-Error: Cannot convert 'complex' objects with values that are multiple of 100 feadf5fe-e790-402a-acce-8e4657d755ca
-Error: Cannot convert 'complex' objects with values that are multiple of 100 27aaf709-234d-412c-b127-9bd8c1d70637
-Error: Cannot convert 'complex' objects with values that are multiple of 100 215e44ca-3507-467a-a63a-07ebbeca410b
-Error: Cannot convert 'complex' objects with values that are multiple of 100 1480af78-323a-4d1e-b6ac-9cf8eca608c6
-Error: Cannot convert 'complex' objects with values that are multiple of 100 6e7c1122-5d6f-4536-8e80-63fd51734a05
-Error: Cannot convert 'complex' objects with values that are multiple of 100 b6188c6e-28ee-49b6-ad38-c4dae8580f11
-Error: Cannot convert 'complex' objects with values that are multiple of 100 56f2178f-c7df-44ee-9ce8-0ba4fa6a0faa
-Error: Cannot convert 'complex' objects with values that are multiple of 100 26aa5c26-20bb-4e34-ab45-841a2b2c311e
-Error: Cannot convert 'complex' objects with values that are multiple of 100 7e00ad7e-bbd9-4241-aceb-41be178c4b36
-Error: Cannot convert 'complex' objects with values that are multiple of 100 61e4cb11-a777-4dd6-bcc2-38a6aaf30000
-Error: Cannot convert 'complex' objects with values that are multiple of 100 e5c835dd-7d6a-4c28-be23-1c7232c87709
-Error: Cannot convert 'complex' objects with values that are multiple of 100 53991f50-050f-4921-bdd0-946a60f4234d
-Error: Cannot convert 'complex' objects with values that are multiple of 100 85be1b13-4762-4784-a89b-37d52abd1a75
-Error: Cannot convert 'complex' objects with values that are multiple of 100 4cb7d5c8-968b-448a-8e05-da34af26e797
-Error: Cannot convert 'complex' objects with values that are multiple of 100 2f7998e8-652c-4d0b-b60b-6590eaf736c8
-Error: Cannot convert 'complex' objects with values that are multiple of 100 f73212d1-4e90-48f1-b865-8226e026c8f5
-Error: Cannot convert 'complex' objects with values that are multiple of 100 b2cf064c-0d1a-4ae5-97af-50c5f848b27f
-Error: Cannot convert 'complex' objects with values that are multiple of 100 986a9a5b-cc27-4401-ad59-8e0cb0621220
-Error: Cannot convert 'complex' objects with values that are multiple of 100 3da5d0ec-a5be-42a6-ace5-722f5358b15a
-Error: Cannot convert 'complex' objects with values that are multiple of 100 bf447d75-cfd5-4d65-bb92-3cce18d92d90
-Error: Cannot convert 'complex' objects with values that are multiple of 100 7046e934-02eb-4bdf-8fa5-e1f7a0c32f95
-Error: Cannot convert 'complex' objects with values that are multiple of 100 a45f4bd2-e343-4605-886f-7012bca9d38f
-Error: Cannot convert 'complex' objects with values that are multiple of 100 26f8e96f-8b15-4401-9249-2c113f0a48b0
-Error: Cannot convert 'complex' objects with values that are multiple of 100 6f28e8fd-884b-4972-b053-d49fab9108ac
-Error: Cannot convert 'complex' objects with values that are multiple of 100 a45e9fc8-25de-4976-af55-94c6067ab363
-Error: Cannot convert 'complex' objects with values that are multiple of 100 1e2ddb16-28f2-40cf-b15a-4bf381df9596
-Error: Cannot convert 'complex' objects with values that are multiple of 100 a7f3191f-e46a-465d-b476-025483f466ec
-Error: Cannot convert 'complex' objects with values that are multiple of 100 c402e053-426e-4a01-ad65-e305183ee0e2
-Error: Cannot convert 'complex' objects with values that are multiple of 100 349b8cb5-ff5a-4e17-a3d7-7e76fdfce654
-Error: Cannot convert 'complex' objects with values that are multiple of 100 c857e17a-e79c-4a1b-a764-eee2907af558
-Error: Cannot convert 'complex' objects with values that are multiple of 100 27667702-7262-4927-8dfd-98d85135416a
-Error: Cannot convert 'complex' objects with values that are multiple of 100 45453fcc-7476-4d33-867c-e3d8b641bd68
-Error: Cannot convert 'complex' objects with values that are multiple of 100 8ebbeb26-6b85-40d2-8599-0a50d10629d2
-Error: Cannot convert 'complex' objects with values that are multiple of 100 6c6a6a0f-b73b-4809-97a5-f6cb2c69f0ea
-Error: Cannot convert 'complex' objects with values that are multiple of 100 cf668f8e-e91d-4191-8c38-0633b0b0a638
-Error: Cannot convert 'complex' objects with values that are multiple of 100 49e1b87f-9b85-4fbb-9c58-1656fe3611ce
-Error: Cannot convert 'complex' objects with values that are multiple of 100 15043c97-bf39-4131-b613-04ff0dc588a7
-Error: Cannot convert 'complex' objects with values that are multiple of 100 f468f6f1-23ea-48db-918d-8ad23cc1694d
-Error: Cannot convert 'complex' objects with values that are multiple of 100 c7b2b31c-9b45-43e9-bd00-ded79dab512f
-Error: Cannot convert 'complex' objects with values that are multiple of 100 1288956d-753c-411c-a07d-b96303adff77
-Error: Cannot convert 'complex' objects with values that are multiple of 100 8262754f-554f-4307-ad22-b9c5851814f5
-Error: Cannot convert 'complex' objects with values that are multiple of 100 c62995e2-30e9-4d61-b0e4-67b08ede057b
-Error: Cannot convert 'complex' objects with values that are multiple of 100 dbf64b3f-8618-40e8-a695-a376051340ae
-Error: Cannot convert 'complex' objects with values that are multiple of 100 1c28ff5a-68be-42ac-af23-110043e387d3
-Error: Cannot convert 'complex' objects with values that are multiple of 100 ba5ea364-d161-44ef-ac9b-5ff426250eb3
-Error: Cannot convert 'complex' objects with values that are multiple of 100 bb9f6d9d-2766-48f7-9309-6a723fecd2e8
-Error: Cannot convert 'complex' objects with values that are multiple of 100 bc6c455b-671c-4d3d-9dd0-9093acfc71a0
-Error: Cannot convert 'complex' objects with values that are multiple of 100 cc69746a-fe34-48f4-8bbb-a2c33e8141de
-Error: Cannot convert 'complex' objects with values that are multiple of 100 f337e855-523c-453d-a830-404e7c960444
-Error: Cannot convert 'complex' objects with values that are multiple of 100 dbf4964d-7758-4a3a-bee8-87942f293573
-Error: Cannot convert 'complex' objects with values that are multiple of 100 c03fd534-03b1-4d7e-961d-045cd1af91e3
-Error: Cannot convert 'complex' objects with values that are multiple of 100 defdc1e6-2092-4e15-94cd-e651abfa6d1d
-Error: Cannot convert 'complex' objects with values that are multiple of 100 990f20f9-6d7c-4dcb-9981-3022c468e56a
-Error: Cannot convert 'complex' objects with values that are multiple of 100 60f5f8df-daec-4784-a3fe-0a366cccb0c6
-Error: Cannot convert 'complex' objects with values that are multiple of 100 089e4076-435c-43a0-a05f-c522673949de
-Error: Cannot convert 'complex' objects with values that are multiple of 100 e30c0ff5-f513-4814-8dfc-0dabeda128d2
-Error: Cannot convert 'complex' objects with values that are multiple of 100 2659dc4b-78a4-4b9d-a8e5-2246a0ae6907
-Error: Cannot convert 'complex' objects with values that are multiple of 100 e854a036-5400-4170-a384-4886f443e66f
-Error: Cannot convert 'complex' objects with values that are multiple of 100 a25dd214-9cdb-499a-aea5-31390ac04d14
-Error: Cannot convert 'complex' objects with values that are multiple of 100 fbcf060d-ba09-473c-aa62-d4c24f74f426
-Error: Cannot convert 'complex' objects with values that are multiple of 100 5f8d34c3-d027-4721-929f-f278872d6898
-Error: Cannot convert 'complex' objects with values that are multiple of 100 cd7d3ed0-b062-4b63-bef5-424259d1ac21
-Error: Cannot convert 'complex' objects with values that are multiple of 100 fa060e36-ec05-4ccb-bcc1-74d49f0f786a
-Error: Cannot convert 'complex' objects with values that are multiple of 100 477c78ff-2ad4-415d-bd4c-41cb67c606ce
-Error: Cannot convert 'complex' objects with values that are multiple of 100 e61d773d-2dae-45cb-9eda-fe7985ebc89a
-Error: Cannot convert 'complex' objects with values that are multiple of 100 929969b5-db7e-4165-a8fb-9a637bcc8111
-Error: Cannot convert 'complex' objects with values that are multiple of 100 8eb40651-518e-4f69-8479-399b31eb29fd
-Error: Cannot convert 'complex' objects with values that are multiple of 100 e0f877ae-7fd4-40fc-9f62-42bf4c1236d5
-Error: Cannot convert 'complex' objects with values that are multiple of 100 accad06e-5005-41c7-9471-f7b5ea7e4e2d
-Error: Cannot convert 'complex' objects with values that are multiple of 100 56f8f9d8-8809-47be-9dea-bee265abff42
-Error: Cannot convert 'complex' objects with values that are multiple of 100 d295c1e3-b44f-4615-a373-81eddca8c6e4
-Error: Cannot convert 'complex' objects with values that are multiple of 100 dc4b6072-b5a2-4af4-91d4-731c28b8cb61
-Error: Cannot convert 'complex' objects with values that are multiple of 100 24d0d41c-8b73-4366-90f3-ccde8982de05
-Error: Cannot convert 'complex' objects with values that are multiple of 100 2fcd2746-1015-4aea-8138-8e6f12661e6f
-Error: Cannot convert 'complex' objects with values that are multiple of 100 f6f978ce-a901-4171-98e2-6a6a385d4dce
-Error: Cannot convert 'complex' objects with values that are multiple of 100 17eadbe4-1e01-4057-b9cf-9615a295fd61
-Error: Cannot convert 'complex' objects with values that are multiple of 100 54eca4bf-2331-44a2-98a6-0a174fe42de2
-Error: Cannot convert 'complex' objects with values that are multiple of 100 0c7cff06-3b8f-4a63-9c50-80876e16ce92
-Error: Cannot convert 'complex' objects with values that are multiple of 100 6af8f20d-4410-42e3-b569-178e9a51d402
-Error: Cannot convert 'complex' objects with values that are multiple of 100 d0c56638-b6b8-423a-a511-011386a2c896
-Error: Cannot convert 'complex' objects with values that are multiple of 100 82820e20-8575-4f7b-81c1-1a768a72862d
-Error: Cannot convert 'complex' objects with values that are multiple of 100 8443e759-0102-4dd5-88ef-fb34b5fa1588
-Error: Cannot convert 'complex' objects with values that are multiple of 100 a0c5c00e-ced5-453c-865f-1c6e1d0867bd
-Error: Cannot convert 'complex' objects with values that are multiple of 100 0ee9fc4c-e17c-48b6-a303-f367517ca53f
-Error: Cannot convert 'complex' objects with values that are multiple of 100 b5d88765-3ce4-4190-b72e-7a036afe61f5
-Error: Cannot convert 'complex' objects with values that are multiple of 100 a8a1896a-9a5f-46aa-b29d-f79805965c0f
-Error: Cannot convert 'complex' objects with values that are multiple of 100 68c7f873-d34a-4ede-98f7-03c4bf8c7d5a
-Error: Cannot convert 'complex' objects with values that are multiple of 100 bce262f4-1960-4b93-a195-2c38f27de857
-Error: Cannot convert 'complex' objects with values that are multiple of 100 81385491-3d88-4efb-ba0f-b1def870d08f
-Error: Cannot convert 'complex' objects with values that are multiple of 100 bda9fa96-4690-4832-9ead-09d7411d85a4
-Error: Cannot convert 'complex' objects with values that are multiple of 100 c2e38e6c-5b41-4f43-8a82-b0fd2208bbff
-Error: Cannot convert 'complex' objects with values that are multiple of 100 a08994b1-7fde-47b8-98cf-fcb6e259c57a
-Error: Cannot convert 'complex' objects with values that are multiple of 100 80ed51b6-ea54-49c1-82a9-c67702a482ed
-Error: Cannot convert 'complex' objects with values that are multiple of 100 1130814f-a38e-4b61-8bae-d5f5fb08dc24
-Error: Cannot convert 'complex' objects with values that are multiple of 100 eb552a07-8850-42ca-9eb0-d98cc7cc0ee2"
+"Error: Cannot convert 'complex' objects with values that are multiple of 100
+Error: Cannot convert 'complex' objects with values that are multiple of 100
+Error: Cannot convert 'complex' objects with values that are multiple of 100
+Error: Cannot convert 'complex' objects with values that are multiple of 100
+Error: Cannot convert 'complex' objects with values that are multiple of 100
+Error: Cannot convert 'complex' objects with values that are multiple of 100
+Error: Cannot convert 'complex' objects with values that are multiple of 100
+Error: Cannot convert 'complex' objects with values that are multiple of 100
+Error: Cannot convert 'complex' objects with values that are multiple of 100
+Error: Cannot convert 'complex' objects with values that are multiple of 100 "
`;
diff --git a/src/core/server/integration_tests/saved_objects/migrations/group1/v2_migration.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group1/v2_migration.test.ts
index e177310c7c7f8..c67b9a9f38f0c 100644
--- a/src/core/server/integration_tests/saved_objects/migrations/group1/v2_migration.test.ts
+++ b/src/core/server/integration_tests/saved_objects/migrations/group1/v2_migration.test.ts
@@ -9,7 +9,6 @@
import { join } from 'path';
import { omit } from 'lodash';
-import { parse } from 'hjson';
import type { TestElasticsearchUtils } from '@kbn/core-test-helpers-kbn-server';
import type { MigrationResult } from '@kbn/core-saved-objects-base-server-internal';
@@ -17,21 +16,17 @@ import {
defaultKibanaIndex,
defaultKibanaTaskIndex,
startElasticsearch,
- getAggregatedTypesCount,
type KibanaMigratorTestKit,
readLog,
clearLog,
currentVersion,
+ getKibanaMigratorTestKit,
nextMinor,
} from '@kbn/migrator-test-kit';
+import { BASELINE_TEST_ARCHIVE_LARGE } from '../kibana_migrator_archive_utils';
import {
- BASELINE_COMPLEX_DOCUMENTS_LARGE_AFTER,
- BASELINE_DOCUMENTS_PER_TYPE_LARGE,
- BASELINE_TEST_ARCHIVE_LARGE,
-} from '../kibana_migrator_archive_utils';
-import {
- getReindexingBaselineTypes,
- getReindexingMigratorTestKit,
+ getCompatibleBaselineTypes,
+ getTransformErrorBaselineTypes,
getUpToDateMigratorTestKit,
} from '@kbn/migrator-test-kit/fixtures';
import { expectDocumentsMigratedToHighestVersion } from '@kbn/migrator-test-kit/expect';
@@ -64,7 +59,6 @@ describe('v2 migration', () => {
const res = await upToDateKit.client.indices.getMapping({ index: defaultKibanaIndex });
const mappings = res[`${defaultKibanaIndex}_${currentVersion}_001`].mappings;
- expect(mappings._meta?.indexTypesMap[defaultKibanaIndex]).toContain('recent');
expect(mappings.properties?.recent).toEqual({
properties: {
name: {
@@ -119,14 +113,15 @@ describe('v2 migration', () => {
beforeAll(async () => {
await clearLog(logFilePath);
- unknownTypesKit = await getReindexingMigratorTestKit({
+ unknownTypesKit = await getKibanaMigratorTestKit({
logFilePath,
// we must exclude 'deprecated' from the list of registered types
// so that it is considered unknown
- types: getReindexingBaselineTypes(['server', 'task', 'deprecated']),
+ types: getCompatibleBaselineTypes(['server', 'task', 'deprecated']),
// however we don't want to flag 'deprecated' as a removed type
// because we want the migrator to consider it unknown
removedTypes: ['server', 'task'],
+ kibanaVersion: nextMinor,
settings: {
migrations: {
discardUnknownObjects: currentVersion, // instead of the actual target, 'nextMinor'
@@ -148,35 +143,43 @@ describe('v2 migration', () => {
expect(logs).toMatch(
`[${defaultKibanaIndex}] Migration failed because some documents were found which use unknown saved object types: deprecated`
);
- expect(logs).toMatch(`[${defaultKibanaIndex}] CHECK_UNKNOWN_DOCUMENTS -> FATAL.`);
+ expect(logs).toMatch(`[${defaultKibanaIndex}] CLEANUP_UNKNOWN_AND_EXCLUDED -> FATAL.`);
});
});
describe('with transform errors', () => {
let transformErrorsKit: KibanaMigratorTestKit;
let logs: string;
-
+ // filter out 'task' objects in order to not spawn that migrator for this test
+ const removedTypes = ['deprecated', 'server', 'task'];
beforeAll(async () => {
await clearLog(logFilePath);
- transformErrorsKit = await getReindexingMigratorTestKit({
+ transformErrorsKit = await getKibanaMigratorTestKit({
logFilePath,
- // filter out 'task' objects in order to not spawn that migrator for this test
- removedTypes: ['deprecated', 'server', 'task'],
- settings: {
- migrations: {
- discardCorruptObjects: currentVersion, // instead of the actual target, 'nextMinor'
- },
- },
+ removedTypes,
+ kibanaVersion: nextMinor,
+ types: getTransformErrorBaselineTypes(removedTypes),
});
});
it('collects corrupt saved object documents across batches', async () => {
+ expect.hasAssertions();
try {
await transformErrorsKit.runMigrations();
} catch (error) {
- const lines = error.message
+ const complexLines = (error.message as string)
.split('\n')
- .filter((line: string) => line.includes(`'complex'`))
+ .filter((line: string) => line.includes(`'complex'`));
+ // Verify that errors are collected across batches (more than 10 = more than one batch)
+ expect(error.message).toMatch(/showing the first 10 - check the logs for the full list/);
+ // Verify the format of the error lines, redacting UUIDs to keep the assertion stable
+ const lines = complexLines
+ .map((line) =>
+ line.replace(
+ /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi,
+ ''
+ )
+ )
.join('\n');
expect(lines).toMatchSnapshot();
}
@@ -187,42 +190,34 @@ describe('v2 migration', () => {
expect(logs).toMatch(
`Cannot convert 'complex' objects with values that are multiple of 100`
);
- expect(logs).toMatch(`[${defaultKibanaIndex}] REINDEX_SOURCE_TO_TEMP_READ -> FATAL.`);
- });
-
- it('closes reindex PIT upon failure', async () => {
- const lineWithPit = logs
- .split('\n')
- .find((line) =>
- line.includes(`[${defaultKibanaIndex}] REINDEX_SOURCE_TO_TEMP_OPEN_PIT PitId:`)
- );
-
- expect(lineWithPit).toBeTruthy();
-
- const id = parse(lineWithPit!).message.split(':')[1];
- expect(id).toBeTruthy();
-
- await expect(
- transformErrorsKit.client.search({
- pit: { id },
- })
- // throws an exception that cannot search with closed PIT
- ).rejects.toThrow(/search_phase_execution_exception/);
+ expect(logs).toMatch(`[${defaultKibanaIndex}] OUTDATED_DOCUMENTS_SEARCH_READ -> FATAL.`);
});
});
describe('configured to discard transform errors and unknown types', () => {
let kit: KibanaMigratorTestKit;
- let migrationResults: MigrationResult[];
let logs: string;
beforeAll(async () => {
await clearLog(logFilePath);
- kit = await getReindexingMigratorTestKit({
+ kit = await getKibanaMigratorTestKit({
logFilePath,
+ // we must exclude 'deprecated' from the list of registered types
+ // so that it is considered unknown
+ types: getTransformErrorBaselineTypes(['server', 'deprecated']),
+ // however we don't want to flag 'deprecated' as a removed type
+ // because we want the migrator to consider it unknown
+ removedTypes: ['server'],
+ kibanaVersion: nextMinor,
+ settings: {
+ migrations: {
+ discardUnknownObjects: nextMinor,
+ discardCorruptObjects: nextMinor,
+ },
+ },
});
- migrationResults = await kit.runMigrations();
+ await kit.runMigrations();
logs = await readLog(logFilePath);
});
@@ -263,108 +258,6 @@ describe('v2 migration', () => {
);
});
});
-
- describe('a migrator performing a reindexing migration', () => {
- describe('when an index contains SO types with incompatible mappings', () => {
- it('executes the reindexing migration steps', () => {
- expect(logs).toMatch(`[${defaultKibanaIndex}] INIT -> WAIT_FOR_YELLOW_SOURCE.`);
- expect(logs).toMatch(
- `[${defaultKibanaIndex}] WAIT_FOR_YELLOW_SOURCE -> UPDATE_SOURCE_MAPPINGS_PROPERTIES.`
- );
- expect(logs).toMatch(
- `[${defaultKibanaIndex}] UPDATE_SOURCE_MAPPINGS_PROPERTIES -> REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION.`
- );
- expect(logs).toMatch(
- `[${defaultKibanaIndex}] REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION -> CHECK_UNKNOWN_DOCUMENTS.`
- );
- expect(logs).toMatch(
- `[${defaultKibanaIndex}] CHECK_TARGET_MAPPINGS -> UPDATE_TARGET_MAPPINGS_PROPERTIES.`
- );
- expect(logs).toMatch(
- `[${defaultKibanaIndex}] UPDATE_TARGET_MAPPINGS_META -> CHECK_VERSION_INDEX_READY_ACTIONS.`
- );
- expect(logs).toMatch(
- `[${defaultKibanaIndex}] CHECK_VERSION_INDEX_READY_ACTIONS -> MARK_VERSION_INDEX_READY.`
- );
- expect(logs).toMatch(`[${defaultKibanaIndex}] MARK_VERSION_INDEX_READY -> DONE.`);
-
- expect(logs).not.toMatch(`[${defaultKibanaIndex}] CREATE_NEW_TARGET`);
- expect(logs).not.toMatch(`[${defaultKibanaIndex}] CLEANUP_UNKNOWN_AND_EXCLUDED`);
- expect(logs).not.toMatch(`[${defaultKibanaIndex}] PREPARE_COMPATIBLE_MIGRATION`);
- });
- });
-
- it('updates mappings meta properties with the correct modelVersions (>=10.0.0)', async () => {
- const res = await kit.client.indices.getMapping({ index: defaultKibanaIndex });
- const indexMeta = Object.values(res)[0].mappings._meta!;
- expect(indexMeta.mappingVersions.basic).toEqual('10.1.0');
- expect(indexMeta.mappingVersions.complex).toEqual('10.2.0');
- expect(indexMeta.mappingVersions.old).toEqual('10.0.0');
- expect(indexMeta.mappingVersions.recent).toEqual('10.1.0');
- });
-
- describe('copies the right documents over to the target indices', () => {
- let primaryIndexCounts: Record;
- let taskIndexCounts: Record;
-
- beforeAll(async () => {
- primaryIndexCounts = await getAggregatedTypesCount(kit.client, defaultKibanaIndex);
- taskIndexCounts = await getAggregatedTypesCount(kit.client, defaultKibanaTaskIndex);
- });
-
- it('copies documents to the right indices depending on their types', () => {
- expect(primaryIndexCounts.basic).toBeDefined();
- expect(primaryIndexCounts.complex).toBeDefined();
- expect(primaryIndexCounts.task).not.toBeDefined();
-
- expect(taskIndexCounts.basic).not.toBeDefined();
- expect(taskIndexCounts.complex).not.toBeDefined();
- expect(taskIndexCounts.task).toBeDefined();
- });
-
- it('discards REMOVED_TYPES', () => {
- expect(primaryIndexCounts.server).not.toBeDefined();
- expect(taskIndexCounts.server).not.toBeDefined();
- });
-
- it('discards unknown types', () => {
- expect(primaryIndexCounts.deprecated).not.toBeDefined();
- expect(taskIndexCounts.deprecated).not.toBeDefined();
- });
-
- it('copies all of the documents', () => {
- expect(primaryIndexCounts.basic).toEqual(BASELINE_DOCUMENTS_PER_TYPE_LARGE);
- expect(taskIndexCounts.task).toEqual(BASELINE_DOCUMENTS_PER_TYPE_LARGE);
- });
-
- it('executes the excludeOnUpgrade hook', () => {
- expect(primaryIndexCounts.complex).toEqual(BASELINE_COMPLEX_DOCUMENTS_LARGE_AFTER);
- });
- });
-
- it('returns a migrated status for each SO index', () => {
- // omit elapsedMs as it varies in each execution
- expect(migrationResults.map((result) => omit(result, 'elapsedMs'))).toEqual([
- {
- destIndex: `${defaultKibanaIndex}_${nextMinor}_001`,
- sourceIndex: `${defaultKibanaIndex}_${currentVersion}_001`,
- status: 'migrated',
- },
- {
- destIndex: `${defaultKibanaTaskIndex}_${currentVersion}_001`,
- sourceIndex: `${defaultKibanaTaskIndex}_${currentVersion}_001`,
- status: 'migrated',
- },
- ]);
- });
-
- it('each migrator takes less than 60 seconds', () => {
- const painfulMigrator = (migrationResults as Array<{ elapsedMs?: number }>).find(
- ({ elapsedMs }) => elapsedMs && elapsedMs > 60_000
- );
- expect(painfulMigrator).toBeUndefined();
- });
- });
});
});
});
diff --git a/src/core/server/integration_tests/saved_objects/migrations/group2/multiple_kb_nodes.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group2/multiple_kb_nodes.test.ts
deleted file mode 100644
index b34a37026d9e0..0000000000000
--- a/src/core/server/integration_tests/saved_objects/migrations/group2/multiple_kb_nodes.test.ts
+++ /dev/null
@@ -1,290 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
- * Public License v 1"; you may not use this file except in compliance with, at
- * your election, the "Elastic License 2.0", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-import { setTimeout as timer } from 'timers/promises';
-import { join } from 'path';
-import { omit, sortBy } from 'lodash';
-import type { TestElasticsearchUtils } from '@kbn/core-test-helpers-kbn-server';
-import type { MigrationResult } from '@kbn/core-saved-objects-base-server-internal';
-import type { Client } from '@elastic/elasticsearch';
-import {
- clearLog,
- defaultKibanaIndex,
- defaultKibanaTaskIndex,
- getAggregatedTypesCount,
- getEsClient,
- nextMinor,
- startElasticsearch,
-} from '@kbn/migrator-test-kit';
-import {
- BASELINE_COMPLEX_DOCUMENTS_LARGE_AFTER,
- BASELINE_DOCUMENTS_PER_TYPE_LARGE,
- BASELINE_TEST_ARCHIVE_LARGE,
-} from '../kibana_migrator_archive_utils';
-import { getRelocatingMigratorTestKit, kibanaSplitIndex } from '@kbn/migrator-test-kit/fixtures';
-import { parseLogFile } from '../test_utils';
-import '../jest_matchers';
-import { expectDocumentsMigratedToHighestVersion } from '@kbn/migrator-test-kit/expect';
-
-const PARALLEL_MIGRATORS = 3;
-type Job = () => Promise;
-
-const getLogFile = (node: number) => join(__dirname, `multiple_kb_nodes_${node}.log`);
-const logFileSecondRun = join(__dirname, `multiple_kb_nodes_second_run.log`);
-
-describe('multiple Kibana nodes performing a reindexing migration', () => {
- jest.setTimeout(1200000); // costly test
- let esServer: TestElasticsearchUtils['es'];
- let client: Client;
- let results: MigrationResult[][];
-
- beforeEach(async () => {
- for (let i = 0; i < PARALLEL_MIGRATORS; ++i) {
- await clearLog(getLogFile(i));
- }
- await clearLog(logFileSecondRun);
-
- esServer = await startElasticsearch({ dataArchive: BASELINE_TEST_ARCHIVE_LARGE });
- client = await getEsClient();
- await checkBeforeState();
- });
-
- it.each([
- {
- case: 'migrate saved objects normally when started at the same time',
- delaySeconds: 0,
- },
- {
- case: 'migrate saved objects normally when started with a small interval',
- delaySeconds: 1,
- },
- {
- case: 'migrate saved objects normally when started with an average interval',
- delaySeconds: 5,
- },
- {
- case: 'migrate saved objects normally when started with a bigger interval',
- delaySeconds: 20,
- },
- ])('$case', async ({ delaySeconds }) => {
- const jobs = await createMigratorJobs(PARALLEL_MIGRATORS);
- results = await startWithDelay(jobs, delaySeconds);
- checkMigratorsResults();
- await checkIndicesInfo();
- await checkSavedObjectDocuments();
- await checkFirstNodeSteps();
- await checkUpToDateOnRestart();
- });
-
- afterEach(async () => {
- await esServer?.stop();
- await timer(5_000); // give it a few seconds... cause we always do ¯\_(ツ)_/¯
- });
-
- async function checkBeforeState() {
- await expect(getAggregatedTypesCount(client, [defaultKibanaIndex])).resolves.toEqual({
- basic: BASELINE_DOCUMENTS_PER_TYPE_LARGE,
- complex: BASELINE_DOCUMENTS_PER_TYPE_LARGE,
- deprecated: BASELINE_DOCUMENTS_PER_TYPE_LARGE,
- old: BASELINE_DOCUMENTS_PER_TYPE_LARGE,
- server: BASELINE_DOCUMENTS_PER_TYPE_LARGE,
- });
- await expect(getAggregatedTypesCount(client, [defaultKibanaTaskIndex])).resolves.toEqual({
- task: BASELINE_DOCUMENTS_PER_TYPE_LARGE,
- });
- await expect(getAggregatedTypesCount(client, [kibanaSplitIndex])).resolves.toEqual({});
- }
-
- function checkMigratorsResults() {
- const flatResults = results.flat(); // multiple nodes, multiple migrators each
-
- // each migrator should take less than 120 seconds
- const painfulMigrator = (flatResults as Array<{ elapsedMs?: number }>).find(
- ({ elapsedMs }) => elapsedMs && elapsedMs > 120_000
- );
- expect(painfulMigrator).toBeUndefined();
-
- // each migrator has either migrated or patched
- const failedMigrator = flatResults.find(
- ({ status }) => status !== 'migrated' && status !== 'patched'
- );
- expect(failedMigrator).toBeUndefined();
- }
-
- async function checkIndicesInfo() {
- const indicesInfo = await client.indices.get({ index: '.kibana*' });
- [defaultKibanaIndex, kibanaSplitIndex].forEach((index) =>
- expect(indicesInfo[`${index}_${nextMinor}_001`]).toEqual(
- expect.objectContaining({
- aliases: expect.objectContaining({ [index]: expect.any(Object) }),
- mappings: {
- dynamic: 'strict',
- _meta: {
- mappingVersions: expect.any(Object),
- indexTypesMap: expect.any(Object),
- },
- properties: expect.any(Object),
- },
- settings: { index: expect.any(Object) },
- })
- )
- );
-
- const typesMap =
- indicesInfo[`${defaultKibanaIndex}_${nextMinor}_001`].mappings?._meta?.indexTypesMap;
- expect(typesMap[defaultKibanaIndex]).toEqual(['complex', 'old', 'recent']); // 'deprecated' and 'server' no longer present
- expect(typesMap[kibanaSplitIndex]).toEqual(['basic', 'task']);
- }
-
- async function checkSavedObjectDocuments() {
- // check documents have been migrated
- await expect(getAggregatedTypesCount(client, [defaultKibanaIndex])).resolves.toEqual({
- complex: BASELINE_COMPLEX_DOCUMENTS_LARGE_AFTER,
- old: BASELINE_DOCUMENTS_PER_TYPE_LARGE,
- });
- await expect(getAggregatedTypesCount(client, [defaultKibanaTaskIndex])).resolves.toEqual({});
- await expect(getAggregatedTypesCount(client, [kibanaSplitIndex])).resolves.toEqual({
- basic: BASELINE_DOCUMENTS_PER_TYPE_LARGE,
- task: BASELINE_DOCUMENTS_PER_TYPE_LARGE,
- });
- await expectDocumentsMigratedToHighestVersion(client, [defaultKibanaIndex, kibanaSplitIndex]);
- }
-
- async function checkFirstNodeSteps() {
- const logs = await parseLogFile(getLogFile(0));
- // '.kibana_migrator_split' is a new index, all nodes' migrators must attempt to create it
- expect(logs).toContainLogEntries(
- [
- `[${kibanaSplitIndex}] INIT -> RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION.`,
- `[${kibanaSplitIndex}] RELOCATE_CHECK_CLUSTER_ROUTING_ALLOCATION -> CREATE_REINDEX_TEMP.`,
- `[${kibanaSplitIndex}] CREATE_REINDEX_TEMP -> READY_TO_REINDEX_SYNC.`,
- // no docs to reindex, as source index did NOT exist
- `[${kibanaSplitIndex}] READY_TO_REINDEX_SYNC -> DONE_REINDEXING_SYNC.`,
- ],
- { ordered: true }
- );
-
- // '.kibana_migrator' and '.kibana_migrator_tasks' are involved in a relocation
- [defaultKibanaIndex, defaultKibanaTaskIndex].forEach((index) => {
- expect(logs).toContainLogEntries(
- [
- `[${index}] INIT -> WAIT_FOR_YELLOW_SOURCE.`,
- `[${index}] WAIT_FOR_YELLOW_SOURCE -> REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION.`,
- `[${index}] REINDEX_CHECK_CLUSTER_ROUTING_ALLOCATION -> CHECK_UNKNOWN_DOCUMENTS.`,
- `[${index}] CHECK_UNKNOWN_DOCUMENTS -> SET_SOURCE_WRITE_BLOCK.`,
- `[${index}] SET_SOURCE_WRITE_BLOCK -> CALCULATE_EXCLUDE_FILTERS.`,
- `[${index}] CALCULATE_EXCLUDE_FILTERS -> CREATE_REINDEX_TEMP.`,
- `[${index}] CREATE_REINDEX_TEMP -> READY_TO_REINDEX_SYNC.`,
- `[${index}] READY_TO_REINDEX_SYNC -> REINDEX_SOURCE_TO_TEMP_OPEN_PIT.`,
- `[${index}] REINDEX_SOURCE_TO_TEMP_OPEN_PIT -> REINDEX_SOURCE_TO_TEMP_READ.`,
- `[${index}] REINDEX_SOURCE_TO_TEMP_READ -> REINDEX_SOURCE_TO_TEMP_TRANSFORM.`,
- `[${index}] REINDEX_SOURCE_TO_TEMP_TRANSFORM -> REINDEX_SOURCE_TO_TEMP_INDEX_BULK.`,
- `[${index}] REINDEX_SOURCE_TO_TEMP_INDEX_BULK`,
- // if the index is closed by another node, we will have instead: REINDEX_SOURCE_TO_TEMP_TRANSFORM => REINDEX_SOURCE_TO_TEMP_CLOSE_PIT.
- // `[${index}] REINDEX_SOURCE_TO_TEMP_READ -> REINDEX_SOURCE_TO_TEMP_CLOSE_PIT.`,
- `[${index}] REINDEX_SOURCE_TO_TEMP_CLOSE_PIT -> DONE_REINDEXING_SYNC.`,
- ],
- { ordered: true }
- );
- });
-
- // after the relocation, all migrators share the final part of the flow
- [defaultKibanaIndex, defaultKibanaTaskIndex, kibanaSplitIndex].forEach((index) => {
- expect(logs).toContainLogEntries(
- [
- `[${index}] DONE_REINDEXING_SYNC -> SET_TEMP_WRITE_BLOCK.`,
- `[${index}] SET_TEMP_WRITE_BLOCK -> CLONE_TEMP_TO_TARGET.`,
- `[${index}] CLONE_TEMP_TO_TARGET -> REFRESH_TARGET.`,
- `[${index}] REFRESH_TARGET -> OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT.`,
- `[${index}] OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT -> OUTDATED_DOCUMENTS_SEARCH_READ.`,
- `[${index}] OUTDATED_DOCUMENTS_SEARCH_READ -> OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT.`,
- `[${index}] OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT -> CHECK_TARGET_MAPPINGS.`,
- `[${index}] CHECK_TARGET_MAPPINGS -> UPDATE_TARGET_MAPPINGS_PROPERTIES.`,
- `[${index}] UPDATE_TARGET_MAPPINGS_PROPERTIES -> UPDATE_TARGET_MAPPINGS_PROPERTIES_WAIT_FOR_TASK.`,
- `[${index}] UPDATE_TARGET_MAPPINGS_PROPERTIES_WAIT_FOR_TASK -> UPDATE_TARGET_MAPPINGS_META.`,
- `[${index}] UPDATE_TARGET_MAPPINGS_META -> CHECK_VERSION_INDEX_READY_ACTIONS.`,
- `[${index}] CHECK_VERSION_INDEX_READY_ACTIONS -> MARK_VERSION_INDEX_READY_SYNC.`,
- `[${index}] MARK_VERSION_INDEX_READY_SYNC`, // all migrators try to update all aliases, all but one will have conclicts
- `[${index}] Migration completed after`,
- ],
- { ordered: true }
- );
- });
-
- // should NOT retransform anything (we reindexed, thus we transformed already)
- [defaultKibanaIndex, defaultKibanaTaskIndex, kibanaSplitIndex].forEach((index) => {
- expect(logs).not.toContainLogEntry(`[${index}] OUTDATED_DOCUMENTS_TRANSFORM`);
- expect(logs).not.toContainLogEntry(
- `[${index}] Kibana is performing a compatible update and it will update the following SO types so that ES can pickup the updated mappings`
- );
- });
- }
-
- async function checkUpToDateOnRestart() {
- // run a new migrator to ensure everything is up to date
- const { runMigrations } = await getRelocatingMigratorTestKit({
- logFilePath: logFileSecondRun,
- // no need to filter deprecated this time, they should not be there anymore
- });
- const secondRunResults = await runMigrations();
- expect(
- sortBy(
- secondRunResults.map((result) => omit(result, 'elapsedMs')),
- 'destIndex'
- )
- ).toEqual([
- {
- destIndex: `${defaultKibanaIndex}_${nextMinor}_001`,
- status: 'patched',
- },
- {
- destIndex: `${kibanaSplitIndex}_${nextMinor}_001`,
- status: 'patched',
- },
- ]);
- const logs = await parseLogFile(logFileSecondRun);
- expect(logs).not.toContainLogEntries(['REINDEX', 'CREATE', 'UPDATE_TARGET_MAPPINGS']);
- }
-});
-
-async function createMigratorJobs(nodes: number): Promise>> {
- const jobs: Array> = [];
-
- for (let i = 0; i < nodes; ++i) {
- const kit = await getRelocatingMigratorTestKit({
- logFilePath: getLogFile(i),
- });
- jobs.push(kit.runMigrations);
- }
-
- return jobs;
-}
-
-async function startWithDelay(runnables: Array>, delayInSec: number) {
- const promises: Array> = [];
- const errors: string[] = [];
- for (let i = 0; i < runnables.length; i++) {
- promises.push(
- runnables[i]().catch((reason) => {
- errors.push(reason.message ?? reason);
- return reason;
- })
- );
- if (i < runnables.length - 2) {
- // We wait between instances, but not after the last one
- await timer(delayInSec * 1000);
- }
- }
- const results = await Promise.all(promises);
- if (errors.length) {
- throw new Error(`Failed to run all parallel jobs: ${errors.join(',')}`);
- } else {
- return results;
- }
-}
diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/actions/actions.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/actions/actions.test.ts
index d9584ae45cae0..e932be339fd53 100644
--- a/src/core/server/integration_tests/saved_objects/migrations/group3/actions/actions.test.ts
+++ b/src/core/server/integration_tests/saved_objects/migrations/group3/actions/actions.test.ts
@@ -8,7 +8,6 @@
*/
import * as Either from 'fp-ts/Either';
-import * as Option from 'fp-ts/Option';
import { errors } from '@elastic/elasticsearch';
import type { TaskEither } from 'fp-ts/TaskEither';
import type { SavedObjectsRawDoc } from '@kbn/core-saved-objects-server';
@@ -22,25 +21,19 @@ import {
createIndex,
openPit,
type OpenPitResponse,
- reindex,
readWithPit,
type EsResponseTooLargeError,
type ReadWithPit,
- setWriteBlock,
updateAliases,
- waitForReindexTask,
- type ReindexResponse,
waitForPickupUpdatedMappingsTask,
pickupUpdatedMappings,
type UpdateByQueryResponse,
updateAndPickupMappings,
type UpdateAndPickupMappingsResponse,
updateMappings,
- removeWriteBlock,
transformDocs,
waitForIndexStatus,
fetchIndices,
- cloneIndex,
type DocumentsTransformFailed,
type DocumentsTransformSuccess,
createBulkIndexOperationTuple,
@@ -131,7 +124,7 @@ describe('migration actions', () => {
operations: docs.map((doc) => createBulkIndexOperationTuple(doc)),
refresh: 'wait_for',
})();
- await setWriteBlock({ client, index: 'existing_index_with_write_block' })();
+ await client.indices.addBlock({ index: 'existing_index_with_write_block', block: 'write' });
await updateAliases({
client,
aliasActions: [{ add: { index: 'existing_index_2', alias: 'existing_index_2_alias' } }],
@@ -270,115 +263,6 @@ describe('migration actions', () => {
});
});
- describe('setWriteBlock', () => {
- beforeAll(async () => {
- await createIndex({
- client,
- indexName: 'new_index_without_write_block',
- mappings: { properties: {} },
- esCapabilities,
- })();
- });
- it('resolves right when setting the write block succeeds', async () => {
- expect.assertions(1);
- const task = setWriteBlock({ client, index: 'new_index_without_write_block' });
- await expect(task()).resolves.toMatchInlineSnapshot(`
- Object {
- "_tag": "Right",
- "right": "set_write_block_succeeded",
- }
- `);
- });
- it('resolves right when setting a write block on an index that already has one', async () => {
- expect.assertions(1);
- const task = setWriteBlock({ client, index: 'existing_index_with_write_block' });
- await expect(task()).resolves.toMatchInlineSnapshot(`
- Object {
- "_tag": "Right",
- "right": "set_write_block_succeeded",
- }
- `);
- });
- it('once resolved, prevents further writes to the index', async () => {
- expect.assertions(1);
- const task = setWriteBlock({ client, index: 'new_index_without_write_block' });
- await task();
- const sourceDocs = [
- { _source: { title: 'doc 1' } },
- { _source: { title: 'doc 2' } },
- { _source: { title: 'doc 3' } },
- { _source: { title: 'doc 4' } },
- ] as unknown as SavedObjectsRawDoc[];
-
- const res = (await bulkOverwriteTransformedDocuments({
- client,
- index: 'new_index_without_write_block',
- operations: sourceDocs.map((doc) => createBulkIndexOperationTuple(doc)),
- refresh: 'wait_for',
- })()) as Either.Left;
-
- expect(res.left).toEqual({
- type: 'target_index_had_write_block',
- });
- });
- it('resolves left index_not_found_exception when the index does not exist', async () => {
- expect.assertions(1);
- const task = setWriteBlock({ client, index: 'no_such_index' });
- await expect(task()).resolves.toMatchInlineSnapshot(`
- Object {
- "_tag": "Left",
- "left": Object {
- "index": "no_such_index",
- "type": "index_not_found_exception",
- },
- }
- `);
- });
- });
-
- describe('removeWriteBlock', () => {
- beforeAll(async () => {
- await createIndex({
- client,
- indexName: 'existing_index_without_write_block_2',
- mappings: { properties: {} },
- esCapabilities,
- })();
- await createIndex({
- client,
- indexName: 'existing_index_with_write_block_2',
- mappings: { properties: {} },
- esCapabilities,
- })();
- await setWriteBlock({ client, index: 'existing_index_with_write_block_2' })();
- });
- it('resolves right if successful when an index already has a write block', async () => {
- expect.assertions(1);
- const task = removeWriteBlock({ client, index: 'existing_index_with_write_block_2' });
- await expect(task()).resolves.toMatchInlineSnapshot(`
- Object {
- "_tag": "Right",
- "right": "remove_write_block_succeeded",
- }
- `);
- });
- it('resolves right if successful when an index does not have a write block', async () => {
- expect.assertions(1);
- const task = removeWriteBlock({ client, index: 'existing_index_without_write_block_2' });
- await expect(task()).resolves.toMatchInlineSnapshot(`
- Object {
- "_tag": "Right",
- "right": "remove_write_block_succeeded",
- }
- `);
- });
- it('rejects if there is a non-retryable error', async () => {
- expect.assertions(1);
- const task = removeWriteBlock({ client, index: 'no_such_index' });
- await expect(task()).rejects.toThrow('index_not_found_exception');
- });
- });
-
describe('waitForIndexStatus', () => {
afterEach(async () => {
try {
@@ -493,628 +377,6 @@ describe('migration actions', () => {
});
});
- describe('cloneIndex', () => {
- afterAll(async () => {
- try {
- // Restore the default setting of 1000 shards per node
- await client.cluster.putSettings({
- persistent: { cluster: { max_shards_per_node: null } },
- });
- await client.indices.delete({ index: 'clone_*' });
- } catch (e) {
- /** ignore */
- }
- });
- it('resolves right if cloning into a new target index', async () => {
- const task = cloneIndex({
- client,
- source: 'existing_index_with_write_block',
- target: 'clone_target_1',
- esCapabilities,
- });
- expect.assertions(3);
- await expect(task()).resolves.toMatchInlineSnapshot(`
- Object {
- "_tag": "Right",
- "right": Object {
- "acknowledged": true,
- "shardsAcknowledged": true,
- },
- }
- `);
- const { clone_target_1: cloneTarget1 } = await client.indices.getSettings({
- index: 'clone_target_1',
- });
- // @ts-expect-error https://github.com/elastic/elasticsearch/issues/89381
- expect(cloneTarget1.settings?.index.mapping?.total_fields.limit).toBe('1500');
- expect(cloneTarget1.settings?.blocks?.write).toBeUndefined();
- });
- it('resolves right if clone target already existed after waiting for index status to be green ', async () => {
- expect.assertions(2);
-
- // Create a red index that we later turn into green
- await client.indices
- .create({
- index: 'clone_red_then_green_index',
- timeout: '5s',
- mappings: { properties: {} },
- settings: {
- // Allocate 1 replica so that this index can go to green
- number_of_replicas: '0',
- // Disable all shard allocation so that the index status is red
- index: { routing: { allocation: { enable: 'none' } } },
- },
- })
- .catch((e) => {});
-
- // Call clone even though the index already exists
- const cloneIndexPromise = cloneIndex({
- client,
- source: 'existing_index_with_write_block',
- target: 'clone_red_then_green_index',
- esCapabilities,
- })();
-
- let indexGreen = false;
- setTimeout(() => {
- void client.indices.putSettings({
- index: 'clone_red_then_green_index',
- settings: {
- // Enable all shard allocation so that the index status goes green
- routing: { allocation: { enable: 'all' } },
- },
- });
- indexGreen = true;
- }, 10);
-
- await cloneIndexPromise.then((res) => {
- // Assert that the promise didn't resolve before the index became green
- expect(indexGreen).toBe(true);
- expect(res).toMatchInlineSnapshot(`
- Object {
- "_tag": "Right",
- "right": Object {
- "acknowledged": true,
- "shardsAcknowledged": true,
- },
- }
- `);
- });
- });
- it('resolves left with a index_not_green_timeout if clone target already exists but takes longer than the specified timeout before turning green', async () => {
- // Create a red index
- await client.indices
- .create({
- index: 'clone_red_index',
- timeout: '5s',
- mappings: { properties: {} },
- settings: {
- // Allocate 1 replica so that this index stays yellow
- number_of_replicas: '1',
- // Disable all shard allocation so that the index status is red
- index: { routing: { allocation: { enable: 'none' } } },
- },
- })
- .catch((e) => {});
-
- // Call clone even though the index already exists
- let cloneIndexPromise = cloneIndex({
- client,
- source: 'existing_index_with_write_block',
- target: 'clone_red_index',
- timeout: '1s',
- esCapabilities,
- })();
-
- await expect(cloneIndexPromise).resolves.toMatchInlineSnapshot(`
- Object {
- "_tag": "Left",
- "left": Object {
- "message": "[index_not_green_timeout] Timeout waiting for the status of the [clone_red_index] index to become 'green'",
- "type": "index_not_green_timeout",
- },
- }
- `);
-
- // Now make the index yellow and repeat
-
- await client.indices.putSettings({
- index: 'clone_red_index',
- settings: {
- // Enable all shard allocation so that the index status goes yellow
- routing: { allocation: { enable: 'all' } },
- },
- });
-
- // Call clone even though the index already exists
- cloneIndexPromise = cloneIndex({
- client,
- source: 'existing_index_with_write_block',
- target: 'clone_red_index',
- timeout: '1s',
- esCapabilities,
- })();
-
- await expect(cloneIndexPromise).resolves.toMatchInlineSnapshot(`
- Object {
- "_tag": "Left",
- "left": Object {
- "message": "[index_not_green_timeout] Timeout waiting for the status of the [clone_red_index] index to become 'green'",
- "type": "index_not_green_timeout",
- },
- }
- `);
-
- // Now make the index green and it should succeed
-
- await client.indices.putSettings({
- index: 'clone_red_index',
- settings: {
- // Set zero replicas so status goes green
- number_of_replicas: 0,
- },
- });
-
- // Call clone even though the index already exists
- cloneIndexPromise = cloneIndex({
- client,
- source: 'existing_index_with_write_block',
- target: 'clone_red_index',
- timeout: '30s',
- esCapabilities,
- })();
-
- await expect(cloneIndexPromise).resolves.toMatchInlineSnapshot(`
- Object {
- "_tag": "Right",
- "right": Object {
- "acknowledged": true,
- "shardsAcknowledged": true,
- },
- }
- `);
- });
- it('resolves left index_not_found_exception if the source index does not exist', async () => {
- expect.assertions(1);
- const task = cloneIndex({
- client,
- source: 'no_such_index',
- target: 'clone_target_3',
- esCapabilities,
- });
- await expect(task()).resolves.toMatchInlineSnapshot(`
- Object {
- "_tag": "Left",
- "left": Object {
- "index": "no_such_index",
- "type": "index_not_found_exception",
- },
- }
- `);
- });
- it('resolves left cluster_shard_limit_exceeded when the action would exceed the maximum normal open shards', async () => {
- // Set the max shards per node really low so that any new index that's created would exceed the maximum open shards for this cluster
- await client.cluster.putSettings({ persistent: { cluster: { max_shards_per_node: 1 } } });
- const cloneIndexPromise = cloneIndex({
- client,
- source: 'existing_index_with_write_block',
- target: 'clone_target_4',
- esCapabilities,
- })();
- await expect(cloneIndexPromise).resolves.toMatchInlineSnapshot(`
- Object {
- "_tag": "Left",
- "left": Object {
- "type": "cluster_shard_limit_exceeded",
- },
- }
- `);
- });
- });
-
- // Reindex doesn't return any errors on it's own, so we have to test
- // together with waitForReindexTask
- describe('reindex & waitForReindexTask', () => {
- it('resolves right when reindex succeeds without reindex script', async () => {
- const res = (await reindex({
- client,
- sourceIndex: 'existing_index_with_docs',
- targetIndex: 'reindex_target',
- reindexScript: Option.none,
- requireAlias: false,
- excludeOnUpgradeQuery: { match_all: {} },
- batchSize: 1000,
- })()) as Either.Right;
- const task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' });
- await expect(task()).resolves.toMatchInlineSnapshot(`
- Object {
- "_tag": "Right",
- "right": "reindex_succeeded",
- }
- `);
-
- const results = await client.search({ index: 'reindex_target', size: 1000 });
- expect((results.hits?.hits as SavedObjectsRawDoc[]).map((doc) => doc._source.title).sort())
- .toMatchInlineSnapshot(`
- Array [
- "a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a",
- "doc 1",
- "doc 2",
- "doc 3",
- "f-agent-event 5",
- "saved object 4",
- ]
- `);
- });
- it('resolves right and excludes all documents not matching the excludeOnUpgradeQuery', async () => {
- const res = (await reindex({
- client,
- sourceIndex: 'existing_index_with_docs',
- targetIndex: 'reindex_target_excluded_docs',
- reindexScript: Option.none,
- requireAlias: false,
- excludeOnUpgradeQuery: {
- bool: {
- must_not: ['f_agent_event', 'another_unused_type'].map((type) => ({
- term: { type },
- })),
- },
- },
- batchSize: 1000,
- })()) as Either.Right;
- const task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' });
- await expect(task()).resolves.toMatchInlineSnapshot(`
- Object {
- "_tag": "Right",
- "right": "reindex_succeeded",
- }
- `);
-
- const results = await client.search({ index: 'reindex_target_excluded_docs', size: 1000 });
- expect((results.hits?.hits as SavedObjectsRawDoc[]).map((doc) => doc._source.title).sort())
- .toMatchInlineSnapshot(`
- Array [
- "a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a",
- "doc 1",
- "doc 2",
- "doc 3",
- ]
- `);
- });
- it('resolves right when reindex succeeds with reindex script', async () => {
- expect.assertions(2);
- const res = (await reindex({
- client,
- sourceIndex: 'existing_index_with_docs',
- targetIndex: 'reindex_target_2',
- reindexScript: Option.some(`ctx._source.title = ctx._source.title + '_updated'`),
- requireAlias: false,
- excludeOnUpgradeQuery: { match_all: {} },
- batchSize: 1000,
- })()) as Either.Right