mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Fleet] Implement state machine behavior for package install (#178657)
Closes https://github.com/elastic/kibana/issues/175592 ## Summary Implement state machine behavior for package install. It keeps track of the current step and save it in the SO , then exposes it in the`installationInfo` property. - Implemented a generic state machine function that can automatically handle state transitions based on a simple data structure: https://github.com/elastic/kibana/pull/178657/files#diff-f350d9630cd1f22cd1b3e70c9e95388d72dc877190bbeb33c739cb0433949e95R1-R88. In theory, this state machine could be reused for something else, since is generic enough and it's decoupled from the transition functions that we pass to it. - The state transitions passed to the state machine are defined in [services/epm/packages/install_steps.ts](5f09e58ae7/x-pack/plugins/fleet/server/services/epm/packages/install_steps.ts
) and are based off the existing steps in10d5167fa7/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts (L61)
I simply divided that long function in smaller steps and wrapped them to accept a common parameter, based off [InstallContext](https://github.com/elastic/kibana/pull/178657/files#diff-39d1f59e77a329eb06241c220156e5cf2d350649bb548707b0b0f54365ea91bfR49-R72) - Defined a **feature flag** `enablePackagesStateMachine` and called the new [installPackageWitStateMachine](https://github.com/elastic/kibana/pull/178657/files#diff-cf9cec44de2ad0a6a3b74cca05e5308231d57d5c3e180ae4bc5114c2bf9af4ebR466-R483) only when it's enabled. - For now this new function is only applied to `InstallPackageFromRegistry`, so `upload` and `bundled` case don't use it yet. ### Testing - Enable `enablePackagesStateMachine` in kibana.dev.yml - Try to install an integration from registry, either from API or UI. For instance ``` POST kbn:api/fleet/epm/packages/nginx/1.20.0 ``` The installation process should succeed and the installationInfo property will expose the `latest_executed_state` along with the error. <details> <summary>Screenshots</summary> ### Logging With `logger.debug` enabled:   ### InstallationInfo object Content of `installationInfo` property when install process was successful:  ### Errors during install process I manually triggered an error inside `stepInstallIndexTemplatePipelines` and it's reported in the `installationInfo` property along with the latest executed step (latest successful state) and error message:  </details> ### Checklist - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
9f8433e564
commit
3a31ee0872
51 changed files with 5698 additions and 242 deletions
|
@ -159,7 +159,7 @@ export const HASH_TO_VERSION_MAP = {
|
|||
'endpoint:user-artifact-manifest|7502b5c5bc923abe8aa5ccfd636e8c3d': '10.0.0',
|
||||
'enterprise_search_telemetry|3d1b76c39bfb2cc8296b024d73854724': '10.0.0',
|
||||
'epm-packages-assets|44621b2f6052ef966da47b7c3a00f33b': '10.0.0',
|
||||
'epm-packages|c1e2020399dbebba2448096ca007c668': '10.1.0',
|
||||
'epm-packages|8ce219acd0f6f3529237d52193866afb': '10.2.0',
|
||||
'event_loop_delays_daily|5df7e292ddd5028e07c1482e130e6654': '10.0.0',
|
||||
'event-annotation-group|df07b1a361c32daf4e6842c1d5521dbe': '10.0.0',
|
||||
'exception-list-agnostic|8a1defe5981db16792cb9a772e84bb9a': '10.0.0',
|
||||
|
|
|
@ -290,6 +290,7 @@
|
|||
"installed_kibana_space_id",
|
||||
"internal",
|
||||
"keep_policies_up_to_date",
|
||||
"latest_executed_state",
|
||||
"latest_install_failed_attempts",
|
||||
"name",
|
||||
"package_assets",
|
||||
|
|
|
@ -1003,6 +1003,10 @@
|
|||
"index": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"latest_executed_state": {
|
||||
"enabled": false,
|
||||
"type": "object"
|
||||
},
|
||||
"latest_install_failed_attempts": {
|
||||
"enabled": false,
|
||||
"type": "object"
|
||||
|
|
|
@ -85,7 +85,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
|
|||
"dashboard": "211e9ca30f5a95d5f3c27b1bf2b58e6cfa0c9ae9",
|
||||
"endpoint:user-artifact-manifest": "1c3533161811a58772e30cdc77bac4631da3ef2b",
|
||||
"enterprise_search_telemetry": "9ac912e1417fc8681e0cd383775382117c9e3d3d",
|
||||
"epm-packages": "c23d3d00c051a08817335dba26f542b64b18a56a",
|
||||
"epm-packages": "f8ee125b57df31fd035dc04ad81aef475fd2f5bd",
|
||||
"epm-packages-assets": "7a3e58efd9a14191d0d1a00b8aaed30a145fd0b1",
|
||||
"event-annotation-group": "715ba867d8c68f3c9438052210ea1c30a9362582",
|
||||
"event_loop_delays_daily": "01b967e8e043801357503de09199dfa3853bab88",
|
||||
|
|
|
@ -28,6 +28,7 @@ export const allowedExperimentalValues = Object.freeze<Record<string, boolean>>(
|
|||
agentless: false,
|
||||
enableStrictKQLValidation: false,
|
||||
subfeaturePrivileges: false,
|
||||
enablePackagesStateMachine: true,
|
||||
});
|
||||
|
||||
type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
|
||||
|
|
|
@ -6302,6 +6302,34 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"latest_executed_state": {
|
||||
"description": "Latest successfully executed state in package install state machine",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"create_restart_installation",
|
||||
"install_kibana_assets",
|
||||
"install_ilm_policies",
|
||||
"install_ml_model",
|
||||
"install_index_template_pipelines",
|
||||
"remove_legacy_templates",
|
||||
"update_current_write_indices",
|
||||
"install_transforms",
|
||||
"delete_previous_pipelines",
|
||||
"save_archive_entries_from_assets_map",
|
||||
"update_so"
|
||||
]
|
||||
},
|
||||
"started_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"error": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"verification_status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
|
|
@ -3970,6 +3970,28 @@ components:
|
|||
type: string
|
||||
stack:
|
||||
type: string
|
||||
latest_executed_state:
|
||||
description: Latest successfully executed state in package install state machine
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
enum:
|
||||
- create_restart_installation
|
||||
- install_kibana_assets
|
||||
- install_ilm_policies
|
||||
- install_ml_model
|
||||
- install_index_template_pipelines
|
||||
- remove_legacy_templates
|
||||
- update_current_write_indices
|
||||
- install_transforms
|
||||
- delete_previous_pipelines
|
||||
- save_archive_entries_from_assets_map
|
||||
- update_so
|
||||
started_at:
|
||||
type: string
|
||||
error:
|
||||
type: string
|
||||
verification_status:
|
||||
type: string
|
||||
enum:
|
||||
|
|
|
@ -66,6 +66,28 @@ properties:
|
|||
type: string
|
||||
stack:
|
||||
type: string
|
||||
latest_executed_state:
|
||||
description: Latest successfully executed state in package install state machine
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
enum:
|
||||
- create_restart_installation
|
||||
- install_kibana_assets
|
||||
- install_ilm_policies
|
||||
- install_ml_model
|
||||
- install_index_template_pipelines
|
||||
- remove_legacy_templates
|
||||
- update_current_write_indices
|
||||
- install_transforms
|
||||
- delete_previous_pipelines
|
||||
- save_archive_entries_from_assets_map
|
||||
- update_so
|
||||
started_at:
|
||||
type: string
|
||||
error:
|
||||
type: string
|
||||
verification_status:
|
||||
type: string
|
||||
enum:
|
||||
|
|
|
@ -37,6 +37,7 @@ export type InstallType = 'reinstall' | 'reupdate' | 'rollback' | 'update' | 'in
|
|||
export type InstallSource = 'registry' | 'upload' | 'bundled' | 'custom';
|
||||
|
||||
export type EpmPackageInstallStatus = 'installed' | 'installing' | 'install_failed';
|
||||
export type InstallResultStatus = 'installed' | 'already_installed';
|
||||
|
||||
export type ServiceName = 'kibana' | 'elasticsearch';
|
||||
export type AgentAssetType = typeof agentAssetTypes;
|
||||
|
@ -548,6 +549,36 @@ export interface InstallFailedAttempt {
|
|||
};
|
||||
}
|
||||
|
||||
export enum INSTALL_STATES {
|
||||
CREATE_RESTART_INSTALLATION = 'create_restart_installation',
|
||||
INSTALL_KIBANA_ASSETS = 'install_kibana_assets',
|
||||
INSTALL_ILM_POLICIES = 'install_ilm_policies',
|
||||
INSTALL_ML_MODEL = 'install_ml_model',
|
||||
INSTALL_INDEX_TEMPLATE_PIPELINES = 'install_index_template_pipelines',
|
||||
REMOVE_LEGACY_TEMPLATES = 'remove_legacy_templates',
|
||||
UPDATE_CURRENT_WRITE_INDICES = 'update_current_write_indices',
|
||||
INSTALL_TRANSFORMS = 'install_transforms',
|
||||
DELETE_PREVIOUS_PIPELINES = 'delete_previous_pipelines',
|
||||
SAVE_ARCHIVE_ENTRIES = 'save_archive_entries_from_assets_map',
|
||||
RESOLVE_KIBANA_PROMISE = 'resolve_kibana_promise',
|
||||
UPDATE_SO = 'update_so',
|
||||
}
|
||||
type StatesKeys = keyof typeof INSTALL_STATES;
|
||||
export type StateNames = typeof INSTALL_STATES[StatesKeys];
|
||||
|
||||
export interface LatestExecutedState<T> {
|
||||
name: T;
|
||||
started_at: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export type InstallLatestExecutedState = LatestExecutedState<StateNames>;
|
||||
|
||||
export interface StateContext<T> {
|
||||
[key: string]: any;
|
||||
latestExecutedState?: LatestExecutedState<T>;
|
||||
}
|
||||
|
||||
export interface Installation {
|
||||
installed_kibana: KibanaAssetReference[];
|
||||
installed_es: EsAssetReference[];
|
||||
|
@ -568,6 +599,7 @@ export interface Installation {
|
|||
internal?: boolean;
|
||||
removable?: boolean;
|
||||
latest_install_failed_attempts?: InstallFailedAttempt[];
|
||||
latest_executed_state?: InstallLatestExecutedState;
|
||||
}
|
||||
|
||||
export interface PackageUsageStats {
|
||||
|
|
|
@ -18,6 +18,7 @@ import type {
|
|||
EpmPackageInstallStatus,
|
||||
SimpleSOAssetType,
|
||||
AssetSOObject,
|
||||
InstallResultStatus,
|
||||
} from '../models/epm';
|
||||
|
||||
export interface GetCategoriesRequest {
|
||||
|
@ -154,7 +155,7 @@ export interface IBulkInstallPackageHTTPError {
|
|||
|
||||
export interface InstallResult {
|
||||
assets?: AssetReference[];
|
||||
status?: 'installed' | 'already_installed';
|
||||
status?: InstallResultStatus;
|
||||
error?: Error;
|
||||
installType: InstallType;
|
||||
installSource: InstallSource;
|
||||
|
|
|
@ -612,6 +612,7 @@ const soToInstallationInfo = (pkg: PackageListItem | PackageInfo) => {
|
|||
verification_key_id: attributes.verification_key_id,
|
||||
experimental_data_stream_features: attributes.experimental_data_stream_features,
|
||||
latest_install_failed_attempts: attributes.latest_install_failed_attempts,
|
||||
latest_executed_state: attributes.latest_executed_state,
|
||||
};
|
||||
|
||||
return {
|
||||
|
|
|
@ -530,6 +530,7 @@ export const getSavedObjectTypes = (): { [key: string]: SavedObjectsType } => ({
|
|||
},
|
||||
},
|
||||
latest_install_failed_attempts: { type: 'object', enabled: false },
|
||||
latest_executed_state: { type: 'object', enabled: false },
|
||||
installed_kibana: {
|
||||
dynamic: false,
|
||||
properties: {},
|
||||
|
@ -571,6 +572,16 @@ export const getSavedObjectTypes = (): { [key: string]: SavedObjectsType } => ({
|
|||
},
|
||||
],
|
||||
},
|
||||
'2': {
|
||||
changes: [
|
||||
{
|
||||
type: 'mappings_addition',
|
||||
addedMappings: {
|
||||
latest_executed_state: { type: 'object', enabled: false },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
migrations: {
|
||||
'7.14.0': migrateInstallationToV7140,
|
||||
|
|
|
@ -172,7 +172,6 @@ export async function installKibanaAssetsAndReferences({
|
|||
pkgName,
|
||||
pkgTitle,
|
||||
packageInstallContext,
|
||||
paths,
|
||||
installedPkg,
|
||||
spaceId,
|
||||
assetTags,
|
||||
|
@ -185,7 +184,6 @@ export async function installKibanaAssetsAndReferences({
|
|||
pkgName: string;
|
||||
pkgTitle: string;
|
||||
packageInstallContext: PackageInstallContext;
|
||||
paths: string[];
|
||||
installedPkg?: SavedObject<Installation>;
|
||||
spaceId: string;
|
||||
assetTags?: PackageSpecTags[];
|
||||
|
|
|
@ -161,7 +161,6 @@ export async function _installPackage({
|
|||
pkgName,
|
||||
pkgTitle,
|
||||
packageInstallContext,
|
||||
paths,
|
||||
installedPkg,
|
||||
logger,
|
||||
spaceId,
|
||||
|
|
|
@ -29,6 +29,7 @@ import {
|
|||
isPackageVersionOrLaterInstalled,
|
||||
} from './install';
|
||||
import * as install from './_install_package';
|
||||
import * as installStateMachine from './install_state_machine/_state_machine_package_install';
|
||||
import { getBundledPackageByPkgKey } from './bundled_packages';
|
||||
|
||||
import { getInstalledPackageWithAssets, getInstallationObject } from './get';
|
||||
|
@ -57,6 +58,7 @@ jest.mock('../../app_context', () => {
|
|||
getConfig: jest.fn(() => ({})),
|
||||
getSavedObjectsTagging: jest.fn(() => mockedSavedObjectTagging),
|
||||
getInternalUserSOClientForSpaceId: jest.fn(),
|
||||
getExperimentalFeatures: jest.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
@ -79,6 +81,11 @@ jest.mock('./_install_package', () => {
|
|||
_installPackage: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
});
|
||||
jest.mock('./install_state_machine/_state_machine_package_install', () => {
|
||||
return {
|
||||
_stateMachineInstallPackage: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
});
|
||||
jest.mock('../kibana/index_pattern/install', () => {
|
||||
return {
|
||||
installIndexPatterns: jest.fn(() => Promise.resolve()),
|
||||
|
@ -161,246 +168,504 @@ describe('install', () => {
|
|||
jest.mocked(Registry.getPackage).mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
packageInfo: { license: 'basic', conditions: { elastic: { subscription: 'basic' } } },
|
||||
paths: [],
|
||||
} as any)
|
||||
);
|
||||
|
||||
mockGetBundledPackageByPkgKey.mockReset();
|
||||
(install._installPackage as jest.Mock).mockClear();
|
||||
(installStateMachine._stateMachineInstallPackage as jest.Mock).mockClear();
|
||||
jest.mocked(appContextService.getInternalUserSOClientForSpaceId).mockReset();
|
||||
});
|
||||
|
||||
describe('registry', () => {
|
||||
beforeEach(() => {
|
||||
mockGetBundledPackageByPkgKey.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('should send telemetry on install failure, out of date', async () => {
|
||||
await installPackage({
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
installSource: 'registry',
|
||||
pkgkey: 'apache-1.1.0',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
describe('with enablePackagesStateMachine = false', () => {
|
||||
beforeEach(() => {
|
||||
mockGetBundledPackageByPkgKey.mockResolvedValue(undefined);
|
||||
jest.mocked(appContextService.getExperimentalFeatures).mockReturnValue({
|
||||
enablePackagesStateMachine: false,
|
||||
} as any);
|
||||
});
|
||||
|
||||
expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, {
|
||||
currentVersion: 'not_installed',
|
||||
dryRun: false,
|
||||
errorMessage: 'apache-1.1.0 is out-of-date and cannot be installed or updated',
|
||||
eventType: 'package-install',
|
||||
installType: 'install',
|
||||
newVersion: '1.1.0',
|
||||
packageName: 'apache',
|
||||
status: 'failure',
|
||||
});
|
||||
});
|
||||
it('should send telemetry on install failure, out of date', async () => {
|
||||
await installPackage({
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
installSource: 'registry',
|
||||
pkgkey: 'apache-1.1.0',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
});
|
||||
|
||||
it('should send telemetry on install failure, license error', async () => {
|
||||
jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(false);
|
||||
await installPackage({
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
installSource: 'registry',
|
||||
pkgkey: 'apache-1.3.0',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, {
|
||||
currentVersion: 'not_installed',
|
||||
dryRun: false,
|
||||
errorMessage: 'apache-1.1.0 is out-of-date and cannot be installed or updated',
|
||||
eventType: 'package-install',
|
||||
installType: 'install',
|
||||
newVersion: '1.1.0',
|
||||
packageName: 'apache',
|
||||
status: 'failure',
|
||||
});
|
||||
});
|
||||
|
||||
expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, {
|
||||
currentVersion: 'not_installed',
|
||||
dryRun: false,
|
||||
errorMessage: 'Installation requires basic license',
|
||||
eventType: 'package-install',
|
||||
installType: 'install',
|
||||
newVersion: '1.3.0',
|
||||
packageName: 'apache',
|
||||
status: 'failure',
|
||||
});
|
||||
});
|
||||
it('should send telemetry on install failure, license error', async () => {
|
||||
jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(false);
|
||||
await installPackage({
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
installSource: 'registry',
|
||||
pkgkey: 'apache-1.3.0',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
});
|
||||
|
||||
it('should send telemetry on install success', async () => {
|
||||
jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true);
|
||||
await installPackage({
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
installSource: 'registry',
|
||||
pkgkey: 'apache-1.3.0',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
});
|
||||
|
||||
expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, {
|
||||
currentVersion: 'not_installed',
|
||||
dryRun: false,
|
||||
eventType: 'package-install',
|
||||
installType: 'install',
|
||||
newVersion: '1.3.0',
|
||||
packageName: 'apache',
|
||||
status: 'success',
|
||||
});
|
||||
});
|
||||
|
||||
it('should send telemetry on update success', async () => {
|
||||
jest
|
||||
.mocked(getInstallationObject)
|
||||
.mockResolvedValueOnce({ attributes: { version: '1.2.0' } } as any);
|
||||
|
||||
jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true);
|
||||
await installPackage({
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
installSource: 'registry',
|
||||
pkgkey: 'apache-1.3.0',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
});
|
||||
|
||||
expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, {
|
||||
currentVersion: '1.2.0',
|
||||
dryRun: false,
|
||||
eventType: 'package-install',
|
||||
installType: 'update',
|
||||
newVersion: '1.3.0',
|
||||
packageName: 'apache',
|
||||
status: 'success',
|
||||
});
|
||||
});
|
||||
|
||||
it('should send telemetry on install failure, async error', async () => {
|
||||
jest.mocked(install._installPackage).mockRejectedValue(new Error('error'));
|
||||
jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true);
|
||||
|
||||
await installPackage({
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
installSource: 'registry',
|
||||
pkgkey: 'apache-1.3.0',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
});
|
||||
|
||||
expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, {
|
||||
currentVersion: 'not_installed',
|
||||
dryRun: false,
|
||||
errorMessage: 'error',
|
||||
eventType: 'package-install',
|
||||
installType: 'install',
|
||||
newVersion: '1.3.0',
|
||||
packageName: 'apache',
|
||||
status: 'failure',
|
||||
});
|
||||
});
|
||||
|
||||
it('should install from bundled package if one exists', async () => {
|
||||
(install._installPackage as jest.Mock).mockResolvedValue({});
|
||||
mockGetBundledPackageByPkgKey.mockResolvedValue({
|
||||
name: 'test_package',
|
||||
version: '1.0.0',
|
||||
getBuffer: async () => Buffer.from('test_package'),
|
||||
});
|
||||
|
||||
const response = await installPackage({
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
installSource: 'registry',
|
||||
pkgkey: 'test_package-1.0.0',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
});
|
||||
|
||||
expect(response.error).toBeUndefined();
|
||||
|
||||
expect(install._installPackage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ installSource: 'bundled' })
|
||||
);
|
||||
});
|
||||
|
||||
it('should fetch latest version if version not provided', async () => {
|
||||
jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true);
|
||||
const response = await installPackage({
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
installSource: 'registry',
|
||||
pkgkey: 'test_package',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
});
|
||||
|
||||
expect(response.status).toEqual('installed');
|
||||
|
||||
expect(sendTelemetryEvents).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
undefined,
|
||||
expect.objectContaining({
|
||||
expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, {
|
||||
currentVersion: 'not_installed',
|
||||
dryRun: false,
|
||||
errorMessage: 'Installation requires basic license',
|
||||
eventType: 'package-install',
|
||||
installType: 'install',
|
||||
newVersion: '1.3.0',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should do nothing if same version is installed', async () => {
|
||||
jest.mocked(getInstallationObject).mockResolvedValueOnce({
|
||||
attributes: {
|
||||
version: '1.2.0',
|
||||
install_status: 'installed',
|
||||
installed_es: [],
|
||||
installed_kibana: [],
|
||||
},
|
||||
} as any);
|
||||
jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true);
|
||||
const response = await installPackage({
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
installSource: 'registry',
|
||||
pkgkey: 'apache-1.2.0',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
packageName: 'apache',
|
||||
status: 'failure',
|
||||
});
|
||||
});
|
||||
|
||||
expect(response.status).toEqual('already_installed');
|
||||
});
|
||||
it('should send telemetry on install success', async () => {
|
||||
jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true);
|
||||
await installPackage({
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
installSource: 'registry',
|
||||
pkgkey: 'apache-1.3.0',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
});
|
||||
|
||||
it('should allow to install fleet_server if internal.fleetServerStandalone is configured', async () => {
|
||||
jest.mocked(appContextService.getConfig).mockReturnValueOnce({
|
||||
internal: {
|
||||
fleetServerStandalone: true,
|
||||
},
|
||||
} as any);
|
||||
|
||||
const response = await installPackage({
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
installSource: 'registry',
|
||||
pkgkey: 'fleet_server-2.0.0',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, {
|
||||
currentVersion: 'not_installed',
|
||||
dryRun: false,
|
||||
eventType: 'package-install',
|
||||
installType: 'install',
|
||||
newVersion: '1.3.0',
|
||||
packageName: 'apache',
|
||||
status: 'success',
|
||||
});
|
||||
});
|
||||
|
||||
expect(response.status).toEqual('installed');
|
||||
});
|
||||
it('should send telemetry on update success', async () => {
|
||||
jest
|
||||
.mocked(getInstallationObject)
|
||||
.mockResolvedValueOnce({ attributes: { version: '1.2.0' } } as any);
|
||||
|
||||
it('should use a scopped to package space soClient for tagging', async () => {
|
||||
const mockedTaggingSo = savedObjectsClientMock.create();
|
||||
jest
|
||||
.mocked(appContextService.getInternalUserSOClientForSpaceId)
|
||||
.mockReturnValue(mockedTaggingSo);
|
||||
jest
|
||||
.mocked(getInstallationObject)
|
||||
.mockResolvedValueOnce({ attributes: { version: '1.2.0' } } as any);
|
||||
jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true);
|
||||
await installPackage({
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
installSource: 'registry',
|
||||
pkgkey: 'apache-1.3.0',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
});
|
||||
|
||||
jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true);
|
||||
await installPackage({
|
||||
spaceId: 'test',
|
||||
installSource: 'registry',
|
||||
pkgkey: 'apache-1.3.0',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, {
|
||||
currentVersion: '1.2.0',
|
||||
dryRun: false,
|
||||
eventType: 'package-install',
|
||||
installType: 'update',
|
||||
newVersion: '1.3.0',
|
||||
packageName: 'apache',
|
||||
status: 'success',
|
||||
});
|
||||
});
|
||||
|
||||
expect(appContextService.getInternalUserSOClientForSpaceId).toBeCalledWith('test');
|
||||
expect(appContextService.getSavedObjectsTagging().createTagClient).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
client: mockedTaggingSo,
|
||||
})
|
||||
);
|
||||
expect(
|
||||
appContextService.getSavedObjectsTagging().createInternalAssignmentService
|
||||
).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
client: mockedTaggingSo,
|
||||
})
|
||||
);
|
||||
it('should send telemetry on install failure, async error', async () => {
|
||||
jest.mocked(install._installPackage).mockRejectedValue(new Error('error'));
|
||||
jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true);
|
||||
|
||||
await installPackage({
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
installSource: 'registry',
|
||||
pkgkey: 'apache-1.3.0',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
});
|
||||
|
||||
expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, {
|
||||
currentVersion: 'not_installed',
|
||||
dryRun: false,
|
||||
errorMessage: 'error',
|
||||
eventType: 'package-install',
|
||||
installType: 'install',
|
||||
newVersion: '1.3.0',
|
||||
packageName: 'apache',
|
||||
status: 'failure',
|
||||
});
|
||||
});
|
||||
|
||||
it('should install from bundled package if one exists', async () => {
|
||||
(install._installPackage as jest.Mock).mockResolvedValue({});
|
||||
mockGetBundledPackageByPkgKey.mockResolvedValue({
|
||||
name: 'test_package',
|
||||
version: '1.0.0',
|
||||
getBuffer: async () => Buffer.from('test_package'),
|
||||
});
|
||||
|
||||
const response = await installPackage({
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
installSource: 'registry',
|
||||
pkgkey: 'test_package-1.0.0',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
});
|
||||
|
||||
expect(response.error).toBeUndefined();
|
||||
|
||||
expect(install._installPackage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ installSource: 'bundled' })
|
||||
);
|
||||
});
|
||||
|
||||
it('should fetch latest version if version not provided', async () => {
|
||||
jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true);
|
||||
const response = await installPackage({
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
installSource: 'registry',
|
||||
pkgkey: 'test_package',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
});
|
||||
|
||||
expect(response.status).toEqual('installed');
|
||||
|
||||
expect(sendTelemetryEvents).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
undefined,
|
||||
expect.objectContaining({
|
||||
newVersion: '1.3.0',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should do nothing if same version is installed', async () => {
|
||||
jest.mocked(getInstallationObject).mockResolvedValueOnce({
|
||||
attributes: {
|
||||
version: '1.2.0',
|
||||
install_status: 'installed',
|
||||
installed_es: [],
|
||||
installed_kibana: [],
|
||||
},
|
||||
} as any);
|
||||
jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true);
|
||||
const response = await installPackage({
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
installSource: 'registry',
|
||||
pkgkey: 'apache-1.2.0',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
});
|
||||
|
||||
expect(response.status).toEqual('already_installed');
|
||||
});
|
||||
|
||||
it('should allow to install fleet_server if internal.fleetServerStandalone is configured', async () => {
|
||||
jest.mocked(appContextService.getConfig).mockReturnValueOnce({
|
||||
internal: {
|
||||
fleetServerStandalone: true,
|
||||
},
|
||||
} as any);
|
||||
|
||||
const response = await installPackage({
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
installSource: 'registry',
|
||||
pkgkey: 'fleet_server-2.0.0',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
});
|
||||
|
||||
expect(response.status).toEqual('installed');
|
||||
});
|
||||
|
||||
it('should use a scoped to package space soClient for tagging', async () => {
|
||||
const mockedTaggingSo = savedObjectsClientMock.create();
|
||||
jest
|
||||
.mocked(appContextService.getInternalUserSOClientForSpaceId)
|
||||
.mockReturnValue(mockedTaggingSo);
|
||||
jest
|
||||
.mocked(getInstallationObject)
|
||||
.mockResolvedValueOnce({ attributes: { version: '1.2.0' } } as any);
|
||||
|
||||
jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true);
|
||||
await installPackage({
|
||||
spaceId: 'test',
|
||||
installSource: 'registry',
|
||||
pkgkey: 'apache-1.3.0',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
});
|
||||
|
||||
expect(appContextService.getInternalUserSOClientForSpaceId).toBeCalledWith('test');
|
||||
expect(appContextService.getSavedObjectsTagging().createTagClient).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
client: mockedTaggingSo,
|
||||
})
|
||||
);
|
||||
expect(
|
||||
appContextService.getSavedObjectsTagging().createInternalAssignmentService
|
||||
).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
client: mockedTaggingSo,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with enablePackagesStateMachine = true', () => {
|
||||
beforeEach(() => {
|
||||
mockGetBundledPackageByPkgKey.mockResolvedValue(undefined);
|
||||
jest.mocked(appContextService.getExperimentalFeatures).mockReturnValue({
|
||||
enablePackagesStateMachine: true,
|
||||
} as any);
|
||||
});
|
||||
afterEach(() => {
|
||||
(install._installPackage as jest.Mock).mockClear();
|
||||
// jest.resetAllMocks();
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.mocked(appContextService.getExperimentalFeatures).mockReturnValue({
|
||||
enablePackagesStateMachine: false,
|
||||
} as any);
|
||||
});
|
||||
|
||||
it('should send telemetry on install failure, out of date', async () => {
|
||||
await installPackage({
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
installSource: 'registry',
|
||||
pkgkey: 'apache-1.1.0',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
});
|
||||
|
||||
expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, {
|
||||
currentVersion: 'not_installed',
|
||||
dryRun: false,
|
||||
errorMessage: 'apache-1.1.0 is out-of-date and cannot be installed or updated',
|
||||
eventType: 'package-install',
|
||||
installType: 'install',
|
||||
newVersion: '1.1.0',
|
||||
packageName: 'apache',
|
||||
status: 'failure',
|
||||
});
|
||||
});
|
||||
|
||||
it('should send telemetry on install failure, license error', async () => {
|
||||
jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(false);
|
||||
await installPackage({
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
installSource: 'registry',
|
||||
pkgkey: 'apache-1.3.0',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
});
|
||||
|
||||
expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, {
|
||||
currentVersion: 'not_installed',
|
||||
dryRun: false,
|
||||
errorMessage: 'Installation requires basic license',
|
||||
eventType: 'package-install',
|
||||
installType: 'install',
|
||||
newVersion: '1.3.0',
|
||||
packageName: 'apache',
|
||||
status: 'failure',
|
||||
});
|
||||
});
|
||||
|
||||
it('should send telemetry on install success', async () => {
|
||||
jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true);
|
||||
await installPackage({
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
installSource: 'registry',
|
||||
pkgkey: 'apache-1.3.0',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
});
|
||||
|
||||
expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, {
|
||||
currentVersion: 'not_installed',
|
||||
dryRun: false,
|
||||
eventType: 'package-install',
|
||||
installType: 'install',
|
||||
newVersion: '1.3.0',
|
||||
packageName: 'apache',
|
||||
status: 'success',
|
||||
});
|
||||
});
|
||||
|
||||
it('should send telemetry on update success', async () => {
|
||||
jest
|
||||
.mocked(getInstallationObject)
|
||||
.mockResolvedValueOnce({ attributes: { version: '1.2.0', installed_kibana: [] } } as any);
|
||||
|
||||
jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true);
|
||||
await installPackage({
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
installSource: 'registry',
|
||||
pkgkey: 'apache-1.3.0',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
});
|
||||
|
||||
expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, {
|
||||
currentVersion: '1.2.0',
|
||||
dryRun: false,
|
||||
eventType: 'package-install',
|
||||
installType: 'update',
|
||||
newVersion: '1.3.0',
|
||||
packageName: 'apache',
|
||||
status: 'success',
|
||||
});
|
||||
});
|
||||
|
||||
it('should send telemetry on install failure, async error', async () => {
|
||||
jest
|
||||
.mocked(installStateMachine._stateMachineInstallPackage)
|
||||
.mockRejectedValue(new Error('error'));
|
||||
jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true);
|
||||
|
||||
await installPackage({
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
installSource: 'registry',
|
||||
pkgkey: 'apache-1.3.0',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
});
|
||||
|
||||
expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, {
|
||||
currentVersion: 'not_installed',
|
||||
dryRun: false,
|
||||
errorMessage: 'error',
|
||||
eventType: 'package-install',
|
||||
installType: 'install',
|
||||
newVersion: '1.3.0',
|
||||
packageName: 'apache',
|
||||
status: 'failure',
|
||||
});
|
||||
});
|
||||
|
||||
it('should install from bundled package if one exists', async () => {
|
||||
(installStateMachine._stateMachineInstallPackage as jest.Mock).mockResolvedValue({});
|
||||
jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true);
|
||||
mockGetBundledPackageByPkgKey.mockResolvedValue({
|
||||
name: 'test_package',
|
||||
version: '1.0.0',
|
||||
getBuffer: async () => Buffer.from('test_package'),
|
||||
});
|
||||
|
||||
const response = await installPackage({
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
installSource: 'registry',
|
||||
pkgkey: 'test_package-1.0.0',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
});
|
||||
|
||||
expect(response.error).toBeUndefined();
|
||||
|
||||
expect(install._installPackage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ installSource: 'bundled' })
|
||||
);
|
||||
});
|
||||
|
||||
it('should fetch latest version if version not provided', async () => {
|
||||
jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true);
|
||||
const response = await installPackage({
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
installSource: 'registry',
|
||||
pkgkey: 'test_package',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
});
|
||||
|
||||
expect(response.status).toEqual('installed');
|
||||
|
||||
expect(sendTelemetryEvents).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
undefined,
|
||||
expect.objectContaining({
|
||||
newVersion: '1.3.0',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should do nothing if same version is installed', async () => {
|
||||
jest.mocked(getInstallationObject).mockResolvedValueOnce({
|
||||
attributes: {
|
||||
version: '1.2.0',
|
||||
install_status: 'installed',
|
||||
installed_es: [],
|
||||
installed_kibana: [],
|
||||
},
|
||||
} as any);
|
||||
jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true);
|
||||
const response = await installPackage({
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
installSource: 'registry',
|
||||
pkgkey: 'apache-1.2.0',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
});
|
||||
|
||||
expect(response.status).toEqual('already_installed');
|
||||
});
|
||||
|
||||
// failing
|
||||
it('should allow to install fleet_server if internal.fleetServerStandalone is configured', async () => {
|
||||
jest.mocked(appContextService.getConfig).mockReturnValueOnce({
|
||||
internal: {
|
||||
fleetServerStandalone: true,
|
||||
},
|
||||
} as any);
|
||||
|
||||
const response = await installPackage({
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
installSource: 'registry',
|
||||
pkgkey: 'fleet_server-2.0.0',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
});
|
||||
|
||||
expect(response.status).toEqual('installed');
|
||||
});
|
||||
|
||||
it('should use a scoped to package space soClient for tagging', async () => {
|
||||
const mockedTaggingSo = savedObjectsClientMock.create();
|
||||
jest
|
||||
.mocked(appContextService.getInternalUserSOClientForSpaceId)
|
||||
.mockReturnValue(mockedTaggingSo);
|
||||
jest
|
||||
.mocked(getInstallationObject)
|
||||
.mockResolvedValueOnce({ attributes: { version: '1.2.0', installed_kibana: [] } } as any);
|
||||
|
||||
jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true);
|
||||
await installPackage({
|
||||
spaceId: 'test',
|
||||
installSource: 'registry',
|
||||
pkgkey: 'apache-1.3.0',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
esClient: {} as ElasticsearchClient,
|
||||
});
|
||||
|
||||
expect(appContextService.getInternalUserSOClientForSpaceId).toBeCalledWith('test');
|
||||
expect(appContextService.getSavedObjectsTagging().createTagClient).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
client: mockedTaggingSo,
|
||||
})
|
||||
);
|
||||
expect(
|
||||
appContextService.getSavedObjectsTagging().createInternalAssignmentService
|
||||
).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
client: mockedTaggingSo,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -453,6 +718,7 @@ describe('install', () => {
|
|||
|
||||
it('should send telemetry on install failure, async error', async () => {
|
||||
jest.mocked(install._installPackage).mockRejectedValue(new Error('error'));
|
||||
jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true);
|
||||
await installPackage({
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
installSource: 'upload',
|
||||
|
|
|
@ -38,6 +38,7 @@ import type {
|
|||
NewPackagePolicy,
|
||||
PackageInfo,
|
||||
PackageVerificationResult,
|
||||
InstallResultStatus,
|
||||
} from '../../../types';
|
||||
import {
|
||||
AUTO_UPGRADE_POLICIES_PACKAGES,
|
||||
|
@ -70,6 +71,8 @@ import { sendTelemetryEvents, UpdateEventType } from '../../upgrade_sender';
|
|||
import { auditLoggingService } from '../../audit_logging';
|
||||
import { getFilteredInstallPackages } from '../filtered_packages';
|
||||
|
||||
import { _stateMachineInstallPackage } from './install_state_machine/_state_machine_package_install';
|
||||
|
||||
import { formatVerificationResultForSO } from './package_verification';
|
||||
import { getInstallation, getInstallationObject } from './get';
|
||||
import { removeInstallation } from './remove';
|
||||
|
@ -458,24 +461,44 @@ async function installPackageFromRegistry({
|
|||
}`
|
||||
);
|
||||
}
|
||||
|
||||
return await installPackageCommon({
|
||||
pkgName,
|
||||
pkgVersion,
|
||||
installSource,
|
||||
installedPkg,
|
||||
installType,
|
||||
savedObjectsClient,
|
||||
esClient,
|
||||
spaceId,
|
||||
force,
|
||||
packageInstallContext,
|
||||
paths,
|
||||
verificationResult,
|
||||
authorizationHeader,
|
||||
ignoreMappingUpdateErrors,
|
||||
skipDataStreamRollover,
|
||||
});
|
||||
const { enablePackagesStateMachine } = appContextService.getExperimentalFeatures();
|
||||
if (enablePackagesStateMachine) {
|
||||
return await installPackageWitStateMachine({
|
||||
pkgName,
|
||||
pkgVersion,
|
||||
installSource,
|
||||
installedPkg,
|
||||
installType,
|
||||
savedObjectsClient,
|
||||
esClient,
|
||||
spaceId,
|
||||
force,
|
||||
packageInstallContext,
|
||||
paths,
|
||||
verificationResult,
|
||||
authorizationHeader,
|
||||
ignoreMappingUpdateErrors,
|
||||
skipDataStreamRollover,
|
||||
});
|
||||
} else {
|
||||
return await installPackageCommon({
|
||||
pkgName,
|
||||
pkgVersion,
|
||||
installSource,
|
||||
installedPkg,
|
||||
installType,
|
||||
savedObjectsClient,
|
||||
esClient,
|
||||
spaceId,
|
||||
force,
|
||||
packageInstallContext,
|
||||
paths,
|
||||
verificationResult,
|
||||
authorizationHeader,
|
||||
ignoreMappingUpdateErrors,
|
||||
skipDataStreamRollover,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
sendEvent({
|
||||
...telemetryEvent,
|
||||
|
@ -607,7 +630,6 @@ async function installPackageCommon(options: {
|
|||
.createTagClient({ client: savedObjectClientWithSpace });
|
||||
|
||||
// try installing the package, if there was an error, call error handler and rethrow
|
||||
// @ts-expect-error status is string instead of InstallResult.status 'installed' | 'already_installed'
|
||||
return await _installPackage({
|
||||
savedObjectsClient,
|
||||
savedObjectsImporter,
|
||||
|
@ -637,7 +659,187 @@ async function installPackageCommon(options: {
|
|||
...telemetryEvent!,
|
||||
status: 'success',
|
||||
});
|
||||
return { assets, status: 'installed', installType, installSource };
|
||||
return { assets, status: 'installed' as InstallResultStatus, installType, installSource };
|
||||
})
|
||||
.catch(async (err: Error) => {
|
||||
logger.warn(`Failure to install package [${pkgName}]: [${err.toString()}]`, {
|
||||
error: { stack_trace: err.stack },
|
||||
});
|
||||
await handleInstallPackageFailure({
|
||||
savedObjectsClient,
|
||||
error: err,
|
||||
pkgName,
|
||||
pkgVersion,
|
||||
installedPkg,
|
||||
spaceId,
|
||||
esClient,
|
||||
authorizationHeader,
|
||||
});
|
||||
sendEvent({
|
||||
...telemetryEvent!,
|
||||
errorMessage: err.message,
|
||||
});
|
||||
return { error: err, installType, installSource };
|
||||
});
|
||||
} catch (e) {
|
||||
sendEvent({
|
||||
...telemetryEvent,
|
||||
errorMessage: e.message,
|
||||
});
|
||||
return {
|
||||
error: e,
|
||||
installType,
|
||||
installSource,
|
||||
};
|
||||
} finally {
|
||||
span?.end();
|
||||
}
|
||||
}
|
||||
|
||||
async function installPackageWitStateMachine(options: {
|
||||
pkgName: string;
|
||||
pkgVersion: string;
|
||||
installSource: InstallSource;
|
||||
installedPkg?: SavedObject<Installation>;
|
||||
installType: InstallType;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
esClient: ElasticsearchClient;
|
||||
spaceId: string;
|
||||
force?: boolean;
|
||||
packageInstallContext: PackageInstallContext;
|
||||
paths: string[];
|
||||
verificationResult?: PackageVerificationResult;
|
||||
telemetryEvent?: PackageUpdateEvent;
|
||||
authorizationHeader?: HTTPAuthorizationHeader | null;
|
||||
ignoreMappingUpdateErrors?: boolean;
|
||||
skipDataStreamRollover?: boolean;
|
||||
}): Promise<InstallResult> {
|
||||
const packageInfo = options.packageInstallContext.packageInfo;
|
||||
|
||||
const {
|
||||
pkgName,
|
||||
pkgVersion,
|
||||
installSource,
|
||||
installedPkg,
|
||||
installType,
|
||||
savedObjectsClient,
|
||||
force,
|
||||
esClient,
|
||||
spaceId,
|
||||
verificationResult,
|
||||
authorizationHeader,
|
||||
ignoreMappingUpdateErrors,
|
||||
skipDataStreamRollover,
|
||||
packageInstallContext,
|
||||
} = options;
|
||||
let { telemetryEvent } = options;
|
||||
const logger = appContextService.getLogger();
|
||||
logger.info(
|
||||
`Install with enablePackagesStateMachine - Starting installation of ${pkgName}@${pkgVersion} from ${installSource} `
|
||||
);
|
||||
|
||||
// Workaround apm issue with async spans: https://github.com/elastic/apm-agent-nodejs/issues/2611
|
||||
await Promise.resolve();
|
||||
const span = apm.startSpan(
|
||||
`Install package from ${installSource} ${pkgName}@${pkgVersion}`,
|
||||
'package'
|
||||
);
|
||||
|
||||
if (!telemetryEvent) {
|
||||
telemetryEvent = getTelemetryEvent(pkgName, pkgVersion);
|
||||
telemetryEvent.installType = installType;
|
||||
telemetryEvent.currentVersion = installedPkg?.attributes.version || 'not_installed';
|
||||
}
|
||||
|
||||
try {
|
||||
span?.addLabels({
|
||||
packageName: pkgName,
|
||||
packageVersion: pkgVersion,
|
||||
installType,
|
||||
});
|
||||
|
||||
const filteredPackages = getFilteredInstallPackages();
|
||||
if (filteredPackages.includes(pkgName)) {
|
||||
throw new FleetUnauthorizedError(`${pkgName} installation is not authorized`);
|
||||
}
|
||||
|
||||
// if the requested version is the same as installed version, check if we allow it based on
|
||||
// current installed package status and force flag, if we don't allow it,
|
||||
// just return the asset references from the existing installation
|
||||
if (
|
||||
installedPkg?.attributes.version === pkgVersion &&
|
||||
installedPkg?.attributes.install_status === 'installed'
|
||||
) {
|
||||
if (!force) {
|
||||
logger.debug(`${pkgName}-${pkgVersion} is already installed, skipping installation`);
|
||||
return {
|
||||
assets: [
|
||||
...installedPkg.attributes.installed_es,
|
||||
...installedPkg.attributes.installed_kibana,
|
||||
],
|
||||
status: 'already_installed',
|
||||
installType,
|
||||
installSource,
|
||||
};
|
||||
}
|
||||
}
|
||||
const elasticSubscription = getElasticSubscription(packageInfo);
|
||||
if (!licenseService.hasAtLeast(elasticSubscription)) {
|
||||
logger.error(`Installation requires ${elasticSubscription} license`);
|
||||
const err = new FleetError(`Installation requires ${elasticSubscription} license`);
|
||||
sendEvent({
|
||||
...telemetryEvent,
|
||||
errorMessage: err.message,
|
||||
});
|
||||
return { error: err, installType, installSource };
|
||||
}
|
||||
|
||||
// Saved object client need to be scopped with the package space for saved object tagging
|
||||
const savedObjectClientWithSpace = appContextService.getInternalUserSOClientForSpaceId(spaceId);
|
||||
|
||||
const savedObjectsImporter = appContextService
|
||||
.getSavedObjects()
|
||||
.createImporter(savedObjectClientWithSpace, { importSizeLimit: 15_000 });
|
||||
|
||||
const savedObjectTagAssignmentService = appContextService
|
||||
.getSavedObjectsTagging()
|
||||
.createInternalAssignmentService({ client: savedObjectClientWithSpace });
|
||||
|
||||
const savedObjectTagClient = appContextService
|
||||
.getSavedObjectsTagging()
|
||||
.createTagClient({ client: savedObjectClientWithSpace });
|
||||
|
||||
// try installing the package, if there was an error, call error handler and rethrow
|
||||
return await _stateMachineInstallPackage({
|
||||
savedObjectsClient,
|
||||
savedObjectsImporter,
|
||||
savedObjectTagAssignmentService,
|
||||
savedObjectTagClient,
|
||||
esClient,
|
||||
logger,
|
||||
installedPkg,
|
||||
packageInstallContext,
|
||||
installType,
|
||||
spaceId,
|
||||
verificationResult,
|
||||
installSource,
|
||||
authorizationHeader,
|
||||
force,
|
||||
ignoreMappingUpdateErrors,
|
||||
skipDataStreamRollover,
|
||||
})
|
||||
.then(async (assets) => {
|
||||
logger.debug(`Removing old assets from previous versions of ${pkgName}`);
|
||||
await removeOldAssets({
|
||||
soClient: savedObjectsClient,
|
||||
pkgName: packageInfo.name,
|
||||
currentVersion: packageInfo.version,
|
||||
});
|
||||
sendEvent({
|
||||
...telemetryEvent!,
|
||||
status: 'success',
|
||||
});
|
||||
return { assets, status: 'installed' as InstallResultStatus, installType, installSource };
|
||||
})
|
||||
.catch(async (err: Error) => {
|
||||
logger.warn(`Failure to install package [${pkgName}]: [${err.toString()}]`, {
|
||||
|
|
|
@ -0,0 +1,438 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type {
|
||||
SavedObjectsClientContract,
|
||||
ElasticsearchClient,
|
||||
SavedObject,
|
||||
} from '@kbn/core/server';
|
||||
import { savedObjectsClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants';
|
||||
|
||||
import {
|
||||
PackageSavedObjectConflictError,
|
||||
ConcurrentInstallOperationError,
|
||||
} from '../../../../errors';
|
||||
|
||||
import type { Installation } from '../../../../../common';
|
||||
|
||||
import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common';
|
||||
|
||||
import { appContextService } from '../../../app_context';
|
||||
import { createAppContextStartContractMock } from '../../../../mocks';
|
||||
import { saveArchiveEntriesFromAssetsMap } from '../../archive/storage';
|
||||
import { installILMPolicy } from '../../elasticsearch/ilm/install';
|
||||
import { installIlmForDataStream } from '../../elasticsearch/datastream_ilm/install';
|
||||
|
||||
jest.mock('../../elasticsearch/template/template');
|
||||
jest.mock('../../kibana/assets/install');
|
||||
jest.mock('../../kibana/index_pattern/install');
|
||||
jest.mock('../install');
|
||||
jest.mock('../get');
|
||||
jest.mock('../install_index_template_pipeline');
|
||||
|
||||
jest.mock('../../archive/storage');
|
||||
jest.mock('../../elasticsearch/ilm/install');
|
||||
jest.mock('../../elasticsearch/datastream_ilm/install');
|
||||
|
||||
import { updateCurrentWriteIndices } from '../../elasticsearch/template/template';
|
||||
import { installKibanaAssetsAndReferences } from '../../kibana/assets/install';
|
||||
|
||||
import { MAX_TIME_COMPLETE_INSTALL } from '../../../../../common/constants';
|
||||
|
||||
import { restartInstallation } from '../install';
|
||||
import { installIndexTemplatesAndPipelines } from '../install_index_template_pipeline';
|
||||
|
||||
import { _stateMachineInstallPackage } from './_state_machine_package_install';
|
||||
|
||||
const mockedInstallIndexTemplatesAndPipelines =
|
||||
installIndexTemplatesAndPipelines as jest.MockedFunction<
|
||||
typeof installIndexTemplatesAndPipelines
|
||||
>;
|
||||
const mockedUpdateCurrentWriteIndices = updateCurrentWriteIndices as jest.MockedFunction<
|
||||
typeof updateCurrentWriteIndices
|
||||
>;
|
||||
const mockedInstallKibanaAssetsAndReferences =
|
||||
installKibanaAssetsAndReferences as jest.MockedFunction<typeof installKibanaAssetsAndReferences>;
|
||||
|
||||
function sleep(millis: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, millis));
|
||||
}
|
||||
|
||||
describe('_stateMachineInstallPackage', () => {
|
||||
let soClient: jest.Mocked<SavedObjectsClientContract>;
|
||||
let esClient: jest.Mocked<ElasticsearchClient>;
|
||||
|
||||
beforeEach(async () => {
|
||||
soClient = savedObjectsClientMock.create();
|
||||
|
||||
soClient.update.mockImplementation(async (type, id, attributes) => {
|
||||
return { id, attributes } as any;
|
||||
});
|
||||
soClient.get.mockImplementation(async (type, id) => {
|
||||
return { id, attributes: {} } as any;
|
||||
});
|
||||
|
||||
esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
appContextService.start(createAppContextStartContractMock());
|
||||
jest.mocked(installILMPolicy).mockReset();
|
||||
jest.mocked(installIlmForDataStream).mockReset();
|
||||
jest.mocked(installIlmForDataStream).mockResolvedValue({
|
||||
esReferences: [],
|
||||
installedIlms: [],
|
||||
});
|
||||
jest.mocked(saveArchiveEntriesFromAssetsMap).mockResolvedValue({
|
||||
saved_objects: [],
|
||||
});
|
||||
jest.mocked(restartInstallation).mockReset();
|
||||
});
|
||||
|
||||
it('Handles errors from installKibanaAssets', async () => {
|
||||
// force errors from this function
|
||||
mockedInstallKibanaAssetsAndReferences.mockImplementation(async () => {
|
||||
throw new Error('mocked async error A: should be caught');
|
||||
});
|
||||
|
||||
// pick any function between when those are called and when await Promise.all is defined later
|
||||
// and force it to take long enough for the errors to occur
|
||||
// @ts-expect-error about call signature
|
||||
mockedUpdateCurrentWriteIndices.mockImplementation(async () => await sleep(1000));
|
||||
mockedInstallIndexTemplatesAndPipelines.mockResolvedValue({
|
||||
installedTemplates: [],
|
||||
esReferences: [],
|
||||
});
|
||||
|
||||
const installationPromise = _stateMachineInstallPackage({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext: {
|
||||
assetsMap: new Map(),
|
||||
paths: [],
|
||||
packageInfo: {
|
||||
title: 'title',
|
||||
name: 'xyz',
|
||||
version: '4.5.6',
|
||||
description: 'test',
|
||||
type: 'integration',
|
||||
categories: ['cloud', 'custom'],
|
||||
format_version: 'string',
|
||||
release: 'experimental',
|
||||
conditions: { kibana: { version: 'x.y.z' } },
|
||||
owner: { github: 'elastic/fleet' },
|
||||
},
|
||||
},
|
||||
installType: 'install',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
});
|
||||
// if we have a .catch this will fail nicely (test pass)
|
||||
// otherwise the test will fail with either of the mocked errors
|
||||
await expect(installationPromise).rejects.toThrow('mocked');
|
||||
await expect(installationPromise).rejects.toThrow('should be caught');
|
||||
});
|
||||
|
||||
it('Do not install ILM policies if disabled in config', async () => {
|
||||
appContextService.start(
|
||||
createAppContextStartContractMock({
|
||||
internal: {
|
||||
disableILMPolicies: true,
|
||||
fleetServerStandalone: false,
|
||||
onlyAllowAgentUpgradeToKnownVersions: false,
|
||||
retrySetupOnBoot: false,
|
||||
registry: {
|
||||
kibanaVersionCheckEnabled: true,
|
||||
capabilities: [],
|
||||
excludePackages: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
// force errors from this function
|
||||
mockedInstallKibanaAssetsAndReferences.mockResolvedValue([]);
|
||||
// pick any function between when those are called and when await Promise.all is defined later
|
||||
// and force it to take long enough for the errors to occur
|
||||
// @ts-expect-error about call signature
|
||||
mockedUpdateCurrentWriteIndices.mockImplementation(async () => await sleep(1000));
|
||||
mockedInstallIndexTemplatesAndPipelines.mockResolvedValue({
|
||||
installedTemplates: [],
|
||||
esReferences: [],
|
||||
});
|
||||
await _stateMachineInstallPackage({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext: {
|
||||
assetsMap: new Map(),
|
||||
paths: [],
|
||||
packageInfo: {
|
||||
title: 'title',
|
||||
name: 'xyz',
|
||||
version: '4.5.6',
|
||||
description: 'test',
|
||||
type: 'integration',
|
||||
categories: ['cloud', 'custom'],
|
||||
format_version: 'string',
|
||||
release: 'experimental',
|
||||
conditions: { kibana: { version: 'x.y.z' } },
|
||||
owner: { github: 'elastic/fleet' },
|
||||
},
|
||||
},
|
||||
installType: 'install',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
});
|
||||
|
||||
expect(installILMPolicy).not.toBeCalled();
|
||||
expect(installIlmForDataStream).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('Installs ILM policies if not disabled in config', async () => {
|
||||
appContextService.start(
|
||||
createAppContextStartContractMock({
|
||||
internal: {
|
||||
disableILMPolicies: false,
|
||||
fleetServerStandalone: false,
|
||||
onlyAllowAgentUpgradeToKnownVersions: false,
|
||||
retrySetupOnBoot: false,
|
||||
registry: {
|
||||
kibanaVersionCheckEnabled: true,
|
||||
capabilities: [],
|
||||
excludePackages: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
// force errors from this function
|
||||
mockedInstallKibanaAssetsAndReferences.mockResolvedValue([]);
|
||||
// pick any function between when those are called and when await Promise.all is defined later
|
||||
// and force it to take long enough for the errors to occur
|
||||
// @ts-expect-error about call signature
|
||||
mockedUpdateCurrentWriteIndices.mockImplementation(async () => await sleep(1000));
|
||||
mockedInstallIndexTemplatesAndPipelines.mockResolvedValue({
|
||||
installedTemplates: [],
|
||||
esReferences: [],
|
||||
});
|
||||
await _stateMachineInstallPackage({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext: {
|
||||
packageInfo: {
|
||||
title: 'title',
|
||||
name: 'xyz',
|
||||
version: '4.5.6',
|
||||
description: 'test',
|
||||
type: 'integration',
|
||||
categories: ['cloud', 'custom'],
|
||||
format_version: 'string',
|
||||
release: 'experimental',
|
||||
conditions: { kibana: { version: 'x.y.z' } },
|
||||
owner: { github: 'elastic/fleet' },
|
||||
} as any,
|
||||
assetsMap: new Map(),
|
||||
paths: [],
|
||||
},
|
||||
installType: 'install',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
});
|
||||
|
||||
expect(installILMPolicy).toBeCalled();
|
||||
expect(installIlmForDataStream).toBeCalled();
|
||||
});
|
||||
|
||||
describe('When package is stuck in `installing`', () => {
|
||||
const mockInstalledPackageSo: SavedObject<Installation> = {
|
||||
id: 'mocked-package',
|
||||
attributes: {
|
||||
name: 'test-package',
|
||||
version: '1.0.0',
|
||||
install_status: 'installing',
|
||||
install_version: '1.0.0',
|
||||
install_started_at: new Date().toISOString(),
|
||||
install_source: 'registry',
|
||||
verification_status: 'verified',
|
||||
installed_kibana: [] as any,
|
||||
installed_es: [] as any,
|
||||
es_index_patterns: {},
|
||||
},
|
||||
type: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
references: [],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
appContextService.start(
|
||||
createAppContextStartContractMock({
|
||||
internal: {
|
||||
disableILMPolicies: true,
|
||||
fleetServerStandalone: false,
|
||||
onlyAllowAgentUpgradeToKnownVersions: false,
|
||||
retrySetupOnBoot: false,
|
||||
registry: {
|
||||
kibanaVersionCheckEnabled: true,
|
||||
capabilities: [],
|
||||
excludePackages: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe('When timeout is reached', () => {
|
||||
it('restarts installation', async () => {
|
||||
await _stateMachineInstallPackage({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext: {
|
||||
paths: [],
|
||||
assetsMap: new Map(),
|
||||
packageInfo: {
|
||||
name: mockInstalledPackageSo.attributes.name,
|
||||
version: mockInstalledPackageSo.attributes.version,
|
||||
title: mockInstalledPackageSo.attributes.name,
|
||||
} as any,
|
||||
},
|
||||
installedPkg: {
|
||||
...mockInstalledPackageSo,
|
||||
attributes: {
|
||||
...mockInstalledPackageSo.attributes,
|
||||
install_started_at: new Date(
|
||||
Date.now() - MAX_TIME_COMPLETE_INSTALL * 2
|
||||
).toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(restartInstallation).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('When timeout is not reached', () => {
|
||||
describe('With no force flag', () => {
|
||||
it('throws concurrent installation error', async () => {
|
||||
const installPromise = _stateMachineInstallPackage({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext: {
|
||||
paths: [],
|
||||
assetsMap: new Map(),
|
||||
packageInfo: {
|
||||
name: mockInstalledPackageSo.attributes.name,
|
||||
version: mockInstalledPackageSo.attributes.version,
|
||||
title: mockInstalledPackageSo.attributes.name,
|
||||
} as any,
|
||||
},
|
||||
installedPkg: {
|
||||
...mockInstalledPackageSo,
|
||||
attributes: {
|
||||
...mockInstalledPackageSo.attributes,
|
||||
install_started_at: new Date(Date.now() - 1000).toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(installPromise).rejects.toThrowError(ConcurrentInstallOperationError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('With force flag provided', () => {
|
||||
it('restarts installation', async () => {
|
||||
await _stateMachineInstallPackage({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext: {
|
||||
paths: [],
|
||||
assetsMap: new Map(),
|
||||
packageInfo: {
|
||||
name: mockInstalledPackageSo.attributes.name,
|
||||
version: mockInstalledPackageSo.attributes.version,
|
||||
title: mockInstalledPackageSo.attributes.name,
|
||||
} as any,
|
||||
},
|
||||
installedPkg: {
|
||||
...mockInstalledPackageSo,
|
||||
attributes: {
|
||||
...mockInstalledPackageSo.attributes,
|
||||
install_started_at: new Date(Date.now() - 1000).toISOString(),
|
||||
},
|
||||
},
|
||||
force: true,
|
||||
});
|
||||
|
||||
expect(restartInstallation).toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Surfaces saved object conflicts error', async () => {
|
||||
appContextService.start(
|
||||
createAppContextStartContractMock({
|
||||
internal: {
|
||||
disableILMPolicies: false,
|
||||
fleetServerStandalone: false,
|
||||
onlyAllowAgentUpgradeToKnownVersions: false,
|
||||
retrySetupOnBoot: false,
|
||||
registry: {
|
||||
kibanaVersionCheckEnabled: true,
|
||||
capabilities: [],
|
||||
excludePackages: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
mockedInstallKibanaAssetsAndReferences.mockRejectedValueOnce(
|
||||
new PackageSavedObjectConflictError('test')
|
||||
);
|
||||
|
||||
const installPromise = _stateMachineInstallPackage({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext: {
|
||||
packageInfo: {
|
||||
title: 'title',
|
||||
name: 'xyz',
|
||||
version: '4.5.6',
|
||||
description: 'test',
|
||||
type: 'integration',
|
||||
categories: ['cloud', 'custom'],
|
||||
format_version: 'string',
|
||||
release: 'experimental',
|
||||
conditions: { kibana: { version: 'x.y.z' } },
|
||||
owner: { github: 'elastic/fleet' },
|
||||
} as any,
|
||||
assetsMap: new Map(),
|
||||
paths: [],
|
||||
},
|
||||
installType: 'install',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
});
|
||||
await expect(installPromise).rejects.toThrowError(PackageSavedObjectConflictError);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type {
|
||||
ElasticsearchClient,
|
||||
Logger,
|
||||
SavedObject,
|
||||
SavedObjectsClientContract,
|
||||
ISavedObjectsImporter,
|
||||
} from '@kbn/core/server';
|
||||
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
|
||||
|
||||
import type { IAssignmentService, ITagsClient } from '@kbn/saved-objects-tagging-plugin/server';
|
||||
|
||||
import { PackageSavedObjectConflictError } from '../../../../errors';
|
||||
|
||||
import type { HTTPAuthorizationHeader } from '../../../../../common/http_authorization_header';
|
||||
import { INSTALL_STATES } from '../../../../../common/types';
|
||||
import type { PackageInstallContext, StateNames, StateContext } from '../../../../../common/types';
|
||||
import type { PackageAssetReference } from '../../../../types';
|
||||
|
||||
import type {
|
||||
Installation,
|
||||
InstallType,
|
||||
InstallSource,
|
||||
PackageVerificationResult,
|
||||
EsAssetReference,
|
||||
KibanaAssetReference,
|
||||
IndexTemplateEntry,
|
||||
AssetReference,
|
||||
} from '../../../../types';
|
||||
|
||||
import {
|
||||
stepCreateRestartInstallation,
|
||||
stepInstallKibanaAssets,
|
||||
stepInstallILMPolicies,
|
||||
stepInstallMlModel,
|
||||
stepInstallIndexTemplatePipelines,
|
||||
stepRemoveLegacyTemplates,
|
||||
stepUpdateCurrentWriteIndices,
|
||||
stepInstallTransforms,
|
||||
stepDeletePreviousPipelines,
|
||||
stepSaveArchiveEntries,
|
||||
stepResolveKibanaPromise,
|
||||
stepSaveSystemObject,
|
||||
updateLatestExecutedState,
|
||||
} from './steps';
|
||||
import type { StateMachineDefinition } from './state_machine';
|
||||
import { handleState } from './state_machine';
|
||||
|
||||
export interface InstallContext extends StateContext<StateNames> {
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
savedObjectsImporter: Pick<ISavedObjectsImporter, 'import' | 'resolveImportErrors'>;
|
||||
savedObjectTagAssignmentService: IAssignmentService;
|
||||
savedObjectTagClient: ITagsClient;
|
||||
esClient: ElasticsearchClient;
|
||||
logger: Logger;
|
||||
installedPkg?: SavedObject<Installation>;
|
||||
packageInstallContext: PackageInstallContext;
|
||||
installType: InstallType;
|
||||
installSource: InstallSource;
|
||||
spaceId: string;
|
||||
force?: boolean;
|
||||
verificationResult?: PackageVerificationResult;
|
||||
authorizationHeader?: HTTPAuthorizationHeader | null;
|
||||
ignoreMappingUpdateErrors?: boolean;
|
||||
skipDataStreamRollover?: boolean;
|
||||
|
||||
indexTemplates?: IndexTemplateEntry[];
|
||||
packageAssetRefs?: PackageAssetReference[];
|
||||
// output values
|
||||
esReferences?: EsAssetReference[];
|
||||
kibanaAssetPromise?: Promise<KibanaAssetReference[]>;
|
||||
}
|
||||
/*
|
||||
* _stateMachineInstallPackage installs packages using the generic state machine in ./state_machine
|
||||
* installStates is the data structure providing the state machine definition
|
||||
* Usually the install process starts with `create_restart_installation` and continues based on nextState parameter in the definition
|
||||
* The `onTransition` functions are the steps executed to go from one state to another, and all accept an `InstallContext` object as input parameter
|
||||
* After each transition `updateLatestExecutedState` is executed, it updates the executed state in the SO
|
||||
*/
|
||||
export async function _stateMachineInstallPackage(
|
||||
context: InstallContext
|
||||
): Promise<AssetReference[]> {
|
||||
const installStates: StateMachineDefinition<StateNames> = {
|
||||
context,
|
||||
states: {
|
||||
create_restart_installation: {
|
||||
nextState: 'install_kibana_assets',
|
||||
onTransition: stepCreateRestartInstallation,
|
||||
onPostTransition: updateLatestExecutedState,
|
||||
},
|
||||
install_kibana_assets: {
|
||||
onTransition: stepInstallKibanaAssets,
|
||||
nextState: 'install_ilm_policies',
|
||||
onPostTransition: updateLatestExecutedState,
|
||||
},
|
||||
install_ilm_policies: {
|
||||
onTransition: stepInstallILMPolicies,
|
||||
nextState: 'install_ml_model',
|
||||
onPostTransition: updateLatestExecutedState,
|
||||
},
|
||||
install_ml_model: {
|
||||
onTransition: stepInstallMlModel,
|
||||
nextState: 'install_index_template_pipelines',
|
||||
onPostTransition: updateLatestExecutedState,
|
||||
},
|
||||
install_index_template_pipelines: {
|
||||
onTransition: stepInstallIndexTemplatePipelines,
|
||||
nextState: 'remove_legacy_templates',
|
||||
onPostTransition: updateLatestExecutedState,
|
||||
},
|
||||
remove_legacy_templates: {
|
||||
onTransition: stepRemoveLegacyTemplates,
|
||||
nextState: 'update_current_write_indices',
|
||||
onPostTransition: updateLatestExecutedState,
|
||||
},
|
||||
update_current_write_indices: {
|
||||
onTransition: stepUpdateCurrentWriteIndices,
|
||||
nextState: 'install_transforms',
|
||||
onPostTransition: updateLatestExecutedState,
|
||||
},
|
||||
install_transforms: {
|
||||
onTransition: stepInstallTransforms,
|
||||
nextState: 'delete_previous_pipelines',
|
||||
onPostTransition: updateLatestExecutedState,
|
||||
},
|
||||
delete_previous_pipelines: {
|
||||
onTransition: stepDeletePreviousPipelines,
|
||||
nextState: 'save_archive_entries_from_assets_map',
|
||||
onPostTransition: updateLatestExecutedState,
|
||||
},
|
||||
save_archive_entries_from_assets_map: {
|
||||
onTransition: stepSaveArchiveEntries,
|
||||
nextState: 'resolve_kibana_promise',
|
||||
onPostTransition: updateLatestExecutedState,
|
||||
},
|
||||
resolve_kibana_promise: {
|
||||
onTransition: stepResolveKibanaPromise,
|
||||
nextState: 'update_so',
|
||||
onPostTransition: updateLatestExecutedState,
|
||||
},
|
||||
update_so: {
|
||||
onTransition: stepSaveSystemObject,
|
||||
nextState: 'end',
|
||||
onPostTransition: updateLatestExecutedState,
|
||||
},
|
||||
},
|
||||
};
|
||||
try {
|
||||
const { installedKibanaAssetsRefs, esReferences } = await handleState(
|
||||
INSTALL_STATES.CREATE_RESTART_INSTALLATION,
|
||||
installStates,
|
||||
installStates.context
|
||||
);
|
||||
return [
|
||||
...(installedKibanaAssetsRefs as KibanaAssetReference[]),
|
||||
...(esReferences as EsAssetReference[]),
|
||||
];
|
||||
} catch (err) {
|
||||
const { packageInfo } = installStates.context.packageInstallContext;
|
||||
const { name: pkgName, version: pkgVersion } = packageInfo;
|
||||
|
||||
if (SavedObjectsErrorHelpers.isConflictError(err)) {
|
||||
throw new PackageSavedObjectConflictError(
|
||||
`Saved Object conflict encountered while installing ${pkgName || 'unknown'}-${
|
||||
pkgVersion || 'unknown'
|
||||
}. There may be a conflicting Saved Object saved to another Space. Original error: ${
|
||||
err.message
|
||||
}`
|
||||
);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,542 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createAppContextStartContractMock } from '../../../../mocks';
|
||||
import { appContextService } from '../../..';
|
||||
|
||||
import { handleState } from './state_machine';
|
||||
|
||||
const getTestDefinition = (
|
||||
mockOnTransition1: any,
|
||||
mockOnTransition2: any,
|
||||
mockOnTransition3: any,
|
||||
context?: any,
|
||||
onPostTransition?: any
|
||||
) => {
|
||||
return {
|
||||
context,
|
||||
states: {
|
||||
state1: {
|
||||
onTransition: mockOnTransition1,
|
||||
onPostTransition,
|
||||
nextState: 'state2',
|
||||
},
|
||||
state2: {
|
||||
onTransition: mockOnTransition2,
|
||||
onPostTransition,
|
||||
nextState: 'state3',
|
||||
},
|
||||
state3: {
|
||||
onTransition: mockOnTransition3,
|
||||
onPostTransition,
|
||||
nextState: 'end',
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe('handleState', () => {
|
||||
let mockContract: ReturnType<typeof createAppContextStartContractMock>;
|
||||
beforeEach(async () => {
|
||||
// prevents `Logger not set.` and other appContext errors
|
||||
mockContract = createAppContextStartContractMock();
|
||||
appContextService.start(mockContract);
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
appContextService.stop();
|
||||
});
|
||||
|
||||
it('should execute all the state machine transitions based on the provided data structure', async () => {
|
||||
const mockOnTransitionState1 = jest.fn();
|
||||
const mockOnTransitionState2 = jest.fn();
|
||||
const mockOnTransitionState3 = jest.fn();
|
||||
const testDefinition = getTestDefinition(
|
||||
mockOnTransitionState1,
|
||||
mockOnTransitionState2,
|
||||
mockOnTransitionState3
|
||||
);
|
||||
|
||||
await handleState('state1', testDefinition, testDefinition.context);
|
||||
expect(mockOnTransitionState1).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnTransitionState2).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnTransitionState3).toHaveBeenCalledTimes(1);
|
||||
expect(mockContract.logger?.debug).toHaveBeenCalledWith(
|
||||
'Executed state: state1 with status: success - nextState: state2'
|
||||
);
|
||||
expect(mockContract.logger?.debug).toHaveBeenCalledWith(
|
||||
'Executed state: state2 with status: success - nextState: state3'
|
||||
);
|
||||
expect(mockContract.logger?.debug).toHaveBeenCalledWith(
|
||||
'Executed state: state3 with status: success - nextState: end'
|
||||
);
|
||||
});
|
||||
|
||||
it('should call the onTransition function with context data and the return value is saved for the next iteration', async () => {
|
||||
const mockOnTransitionState1 = jest.fn().mockReturnValue({ arrayData: ['test1', 'test2'] });
|
||||
const mockOnTransitionState2 = jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve({ promiseData: {} }));
|
||||
const mockOnTransitionState3 = jest.fn().mockReturnValue({ lastData: ['test3'] });
|
||||
const contextData = { testData: 'test' };
|
||||
const testDefinition = getTestDefinition(
|
||||
mockOnTransitionState1,
|
||||
mockOnTransitionState2,
|
||||
mockOnTransitionState3,
|
||||
contextData
|
||||
);
|
||||
|
||||
await handleState('state1', testDefinition, testDefinition.context);
|
||||
expect(mockOnTransitionState1).toHaveBeenCalledWith({ testData: 'test' });
|
||||
expect(mockOnTransitionState2).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
testData: 'test',
|
||||
arrayData: ['test1', 'test2'],
|
||||
latestExecutedState: {
|
||||
name: 'state1',
|
||||
started_at: expect.anything(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(mockOnTransitionState3).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
testData: 'test',
|
||||
arrayData: ['test1', 'test2'],
|
||||
promiseData: {},
|
||||
latestExecutedState: {
|
||||
name: 'state2',
|
||||
started_at: expect.anything(),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(mockContract.logger?.debug).toHaveBeenCalledWith(
|
||||
'Executed state: state1 with status: success - nextState: state2'
|
||||
);
|
||||
expect(mockContract.logger?.debug).toHaveBeenCalledWith(
|
||||
'Executed state: state2 with status: success - nextState: state3'
|
||||
);
|
||||
expect(mockContract.logger?.debug).toHaveBeenCalledWith(
|
||||
'Executed state: state3 with status: success - nextState: end'
|
||||
);
|
||||
});
|
||||
|
||||
it('should save the return data from transitions also when return type is function', async () => {
|
||||
const mockOnTransitionState1 = jest.fn().mockReturnValue({ arrayData: ['test1', 'test2'] });
|
||||
const state2Result = () => {
|
||||
return {
|
||||
result: 'test',
|
||||
};
|
||||
};
|
||||
const mockOnTransitionState2 = jest.fn().mockImplementation(() => {
|
||||
return state2Result;
|
||||
});
|
||||
const mockOnTransitionState3 = jest.fn();
|
||||
const contextData = { testData: 'test' };
|
||||
const testDefinition = getTestDefinition(
|
||||
mockOnTransitionState1,
|
||||
mockOnTransitionState2,
|
||||
mockOnTransitionState3,
|
||||
contextData
|
||||
);
|
||||
|
||||
await handleState('state1', testDefinition, testDefinition.context);
|
||||
expect(mockOnTransitionState1).toHaveBeenCalledWith({ testData: 'test' });
|
||||
expect(mockOnTransitionState2).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
testData: 'test',
|
||||
arrayData: ['test1', 'test2'],
|
||||
state2Result,
|
||||
latestExecutedState: {
|
||||
name: 'state1',
|
||||
started_at: expect.anything(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(mockOnTransitionState3).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
testData: 'test',
|
||||
arrayData: ['test1', 'test2'],
|
||||
state2Result,
|
||||
latestExecutedState: {
|
||||
name: 'state2',
|
||||
started_at: expect.anything(),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(mockContract.logger?.debug).toHaveBeenCalledWith(
|
||||
'Executed state: state1 with status: success - nextState: state2'
|
||||
);
|
||||
expect(mockContract.logger?.debug).toHaveBeenCalledWith(
|
||||
'Executed state: state2 with status: success - nextState: state3'
|
||||
);
|
||||
expect(mockContract.logger?.debug).toHaveBeenCalledWith(
|
||||
'Executed state: state3 with status: success - nextState: end'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return updated context data', async () => {
|
||||
const mockOnTransitionState1 = jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve({ promiseData: {} }));
|
||||
const state2Result = () => {
|
||||
return {
|
||||
result: 'test',
|
||||
};
|
||||
};
|
||||
const mockOnTransitionState2 = jest.fn().mockImplementation(() => {
|
||||
return state2Result;
|
||||
});
|
||||
const mockOnTransitionState3 = jest.fn().mockReturnValue({ lastData: ['test3'] });
|
||||
const contextData = { testData: 'test' };
|
||||
const testDefinition = getTestDefinition(
|
||||
mockOnTransitionState1,
|
||||
mockOnTransitionState2,
|
||||
mockOnTransitionState3,
|
||||
contextData
|
||||
);
|
||||
|
||||
const updatedContext = await handleState('state1', testDefinition, testDefinition.context);
|
||||
expect(mockOnTransitionState1).toHaveBeenCalledWith({ testData: 'test' });
|
||||
expect(mockOnTransitionState2).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
testData: 'test',
|
||||
promiseData: {},
|
||||
state2Result,
|
||||
latestExecutedState: {
|
||||
name: 'state1',
|
||||
started_at: expect.anything(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(mockOnTransitionState3).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
testData: 'test',
|
||||
promiseData: {},
|
||||
state2Result,
|
||||
latestExecutedState: {
|
||||
name: 'state2',
|
||||
started_at: expect.anything(),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(updatedContext).toEqual(
|
||||
expect.objectContaining({
|
||||
testData: 'test',
|
||||
promiseData: {},
|
||||
state2Result,
|
||||
lastData: ['test3'],
|
||||
latestExecutedState: {
|
||||
name: 'state3',
|
||||
started_at: expect.anything(),
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should update a variable in the context at every call and return the updated value', async () => {
|
||||
const mockOnTransitionState1 = jest.fn().mockReturnValue({ runningVal: 'test1' });
|
||||
const mockOnTransitionState2 = jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve({ runningVal: 'test2' }));
|
||||
const mockOnTransitionState3 = jest.fn().mockReturnValue({ runningVal: 'test3' });
|
||||
const contextData = { runningVal: [], fixedVal: 'something' };
|
||||
const testDefinition = getTestDefinition(
|
||||
mockOnTransitionState1,
|
||||
mockOnTransitionState2,
|
||||
mockOnTransitionState3,
|
||||
contextData
|
||||
);
|
||||
|
||||
const updatedContext = await handleState('state1', testDefinition, testDefinition.context);
|
||||
expect(mockOnTransitionState1).toHaveBeenCalledWith({ runningVal: [], fixedVal: 'something' });
|
||||
expect(mockOnTransitionState2).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
runningVal: 'test1',
|
||||
fixedVal: 'something',
|
||||
latestExecutedState: {
|
||||
name: 'state1',
|
||||
started_at: expect.anything(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(mockOnTransitionState3).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
runningVal: 'test2',
|
||||
fixedVal: 'something',
|
||||
latestExecutedState: {
|
||||
name: 'state2',
|
||||
started_at: expect.anything(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(updatedContext).toEqual(
|
||||
expect.objectContaining({
|
||||
fixedVal: 'something',
|
||||
runningVal: 'test3',
|
||||
latestExecutedState: {
|
||||
name: 'state3',
|
||||
started_at: expect.anything(),
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should execute the transition starting from the provided state', async () => {
|
||||
const mockOnTransitionState1 = jest.fn().mockReturnValue({ runningVal: 'test1' });
|
||||
const mockOnTransitionState2 = jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve({ runningVal: 'test2' }));
|
||||
const mockOnTransitionState3 = jest.fn().mockReturnValue({ runningVal: 'test3' });
|
||||
const contextData = { runningVal: [], fixedVal: 'something' };
|
||||
const testDefinition = getTestDefinition(
|
||||
mockOnTransitionState1,
|
||||
mockOnTransitionState2,
|
||||
mockOnTransitionState3,
|
||||
contextData
|
||||
);
|
||||
|
||||
const updatedContext = await handleState('state2', testDefinition, testDefinition.context);
|
||||
|
||||
expect(mockOnTransitionState1).toHaveBeenCalledTimes(0);
|
||||
expect(mockOnTransitionState2).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
runningVal: [],
|
||||
fixedVal: 'something',
|
||||
})
|
||||
);
|
||||
expect(mockOnTransitionState3).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
runningVal: 'test2',
|
||||
fixedVal: 'something',
|
||||
latestExecutedState: {
|
||||
name: 'state2',
|
||||
started_at: expect.anything(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(updatedContext).toEqual(
|
||||
expect.objectContaining({
|
||||
fixedVal: 'something',
|
||||
runningVal: 'test3',
|
||||
latestExecutedState: {
|
||||
name: 'state3',
|
||||
started_at: expect.anything(),
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw and return updated context with latest error when a state returns error', async () => {
|
||||
const error = new Error('Installation failed');
|
||||
const mockOnTransitionState1 = jest.fn().mockRejectedValue(error);
|
||||
const mockOnTransitionState2 = jest.fn();
|
||||
const mockOnTransitionState3 = jest.fn();
|
||||
const contextData = { fixedVal: 'something' };
|
||||
const testDefinition = getTestDefinition(
|
||||
mockOnTransitionState1,
|
||||
mockOnTransitionState2,
|
||||
mockOnTransitionState3,
|
||||
contextData
|
||||
);
|
||||
const promise = handleState('state1', testDefinition, testDefinition.context);
|
||||
await expect(promise).rejects.toThrowError('Installation failed');
|
||||
|
||||
expect(mockOnTransitionState1).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnTransitionState2).toHaveBeenCalledTimes(0);
|
||||
expect(mockOnTransitionState3).toHaveBeenCalledTimes(0);
|
||||
expect(mockContract.logger?.warn).toHaveBeenCalledWith(
|
||||
'Error during execution of state "state1" with status "failed": Installation failed'
|
||||
);
|
||||
});
|
||||
|
||||
it('should execute postTransition function after the transition is complete', async () => {
|
||||
const mockOnTransitionState1 = jest.fn();
|
||||
const mockOnTransitionState2 = jest.fn();
|
||||
const mockOnTransitionState3 = jest.fn();
|
||||
const mockPostTransition = jest.fn();
|
||||
const testDefinition = getTestDefinition(
|
||||
mockOnTransitionState1,
|
||||
mockOnTransitionState2,
|
||||
mockOnTransitionState3,
|
||||
undefined,
|
||||
mockPostTransition
|
||||
);
|
||||
await handleState('state1', testDefinition, testDefinition.context);
|
||||
|
||||
expect(mockOnTransitionState1).toHaveBeenCalled();
|
||||
expect(mockPostTransition).toHaveBeenCalled();
|
||||
expect(mockOnTransitionState2).toHaveBeenCalled();
|
||||
expect(mockPostTransition).toHaveBeenCalled();
|
||||
expect(mockOnTransitionState3).toHaveBeenCalled();
|
||||
expect(mockPostTransition).toHaveBeenCalled();
|
||||
expect(mockContract.logger?.debug).toHaveBeenCalledWith(
|
||||
'Executing post transition function: mockConstructor'
|
||||
);
|
||||
});
|
||||
|
||||
it('should execute postTransition function after the transition passing the updated context', async () => {
|
||||
const mockOnTransitionState1 = jest.fn().mockReturnValue({ runningVal: 'test1' });
|
||||
const mockOnTransitionState2 = jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve({ runningVal: 'test2' }));
|
||||
const mockOnTransitionState3 = jest.fn().mockReturnValue({ runningVal: 'test3' });
|
||||
const mockPostTransition = jest.fn();
|
||||
const contextData = { fixedVal: 'something' };
|
||||
const testDefinition = getTestDefinition(
|
||||
mockOnTransitionState1,
|
||||
mockOnTransitionState2,
|
||||
mockOnTransitionState3,
|
||||
contextData,
|
||||
mockPostTransition
|
||||
);
|
||||
const updatedContext = await handleState('state1', testDefinition, testDefinition.context);
|
||||
|
||||
expect(mockOnTransitionState1).toHaveBeenCalled();
|
||||
expect(mockPostTransition).toHaveBeenCalled();
|
||||
expect(mockOnTransitionState2).toHaveBeenCalled();
|
||||
expect(mockPostTransition).toHaveBeenCalled();
|
||||
expect(mockOnTransitionState3).toHaveBeenCalled();
|
||||
expect(updatedContext).toEqual(
|
||||
expect.objectContaining({
|
||||
fixedVal: 'something',
|
||||
runningVal: 'test3',
|
||||
latestExecutedState: {
|
||||
name: 'state3',
|
||||
started_at: expect.anything(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(mockPostTransition).toHaveBeenCalledWith(updatedContext);
|
||||
expect(mockContract.logger?.debug).toHaveBeenCalledWith(
|
||||
'Executing post transition function: mockConstructor'
|
||||
);
|
||||
});
|
||||
|
||||
it('should execute postTransition correctly also when a transition throws', async () => {
|
||||
const error = new Error('Installation failed');
|
||||
const mockOnTransitionState1 = jest.fn().mockReturnValue({ result1: 'test' });
|
||||
const mockOnTransitionState2 = jest.fn().mockRejectedValue(error);
|
||||
const mockOnTransitionState3 = jest.fn();
|
||||
const mockPostTransition = jest.fn();
|
||||
const contextData = { testData: 'test' };
|
||||
const testDefinition = getTestDefinition(
|
||||
mockOnTransitionState1,
|
||||
mockOnTransitionState2,
|
||||
mockOnTransitionState3,
|
||||
contextData,
|
||||
mockPostTransition
|
||||
);
|
||||
const promise = handleState('state1', testDefinition, testDefinition.context);
|
||||
await expect(promise).rejects.toThrowError('Installation failed');
|
||||
|
||||
expect(mockOnTransitionState1).toHaveBeenCalledTimes(1);
|
||||
expect(mockPostTransition).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
result1: 'test',
|
||||
testData: 'test',
|
||||
latestExecutedState: {
|
||||
name: 'state1',
|
||||
started_at: expect.anything(),
|
||||
error:
|
||||
'Error during execution of state "state2" with status "failed": Installation failed',
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(mockOnTransitionState2).toHaveBeenCalledTimes(1);
|
||||
expect(mockPostTransition).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
result1: 'test',
|
||||
testData: 'test',
|
||||
latestExecutedState: {
|
||||
name: 'state1',
|
||||
started_at: expect.anything(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(mockOnTransitionState3).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should log a warning when postTransition exits with errors and continue executing the states', async () => {
|
||||
const error = new Error('Installation failed');
|
||||
const mockOnTransitionState1 = jest.fn().mockReturnValue({ result1: 'test' });
|
||||
const mockOnTransitionState2 = jest.fn();
|
||||
const mockOnTransitionState3 = jest.fn();
|
||||
const mockPostTransition = jest.fn().mockRejectedValue(error);
|
||||
const contextData = { testData: 'test' };
|
||||
const testDefinition = getTestDefinition(
|
||||
mockOnTransitionState1,
|
||||
mockOnTransitionState2,
|
||||
mockOnTransitionState3,
|
||||
contextData,
|
||||
mockPostTransition
|
||||
);
|
||||
const updatedContext = await handleState('state1', testDefinition, testDefinition.context);
|
||||
|
||||
expect(mockOnTransitionState1).toHaveBeenCalledTimes(1);
|
||||
expect(mockPostTransition).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
result1: 'test',
|
||||
testData: 'test',
|
||||
latestExecutedState: {
|
||||
name: 'state1',
|
||||
started_at: expect.anything(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(mockOnTransitionState2).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnTransitionState3).toHaveBeenCalledTimes(1);
|
||||
expect(mockContract.logger?.warn).toHaveBeenCalledWith(
|
||||
'Error during execution of post transition function: Installation failed'
|
||||
);
|
||||
|
||||
expect(updatedContext).toEqual(
|
||||
expect.objectContaining({
|
||||
testData: 'test',
|
||||
result1: 'test',
|
||||
latestExecutedState: {
|
||||
name: 'state3',
|
||||
started_at: expect.anything(),
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should exit and log a warning when the provided OnTransition is not a function', async () => {
|
||||
const mockOnTransitionState1 = jest.fn().mockReturnValue({ result1: 'test' });
|
||||
const mockOnTransitionState2 = undefined;
|
||||
const mockOnTransitionState3 = jest.fn();
|
||||
|
||||
const contextData = { testData: 'test' };
|
||||
const testDefinition = getTestDefinition(
|
||||
mockOnTransitionState1,
|
||||
mockOnTransitionState2,
|
||||
mockOnTransitionState3,
|
||||
contextData
|
||||
);
|
||||
const updatedContext = await handleState('state1', testDefinition, testDefinition.context);
|
||||
|
||||
expect(mockOnTransitionState1).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnTransitionState3).toHaveBeenCalledTimes(0);
|
||||
expect(mockContract.logger?.warn).toHaveBeenCalledWith(
|
||||
'Execution of state "state2" with status "failed": provided onTransition is not a valid function'
|
||||
);
|
||||
|
||||
expect(updatedContext).toEqual(
|
||||
expect.objectContaining({
|
||||
testData: 'test',
|
||||
result1: 'test',
|
||||
latestExecutedState: {
|
||||
name: 'state1',
|
||||
started_at: expect.anything(),
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Logger } from '@kbn/core/server';
|
||||
|
||||
import { appContextService } from '../../../app_context';
|
||||
import type { StateContext, LatestExecutedState } from '../../../../../common/types';
|
||||
export interface State {
|
||||
onTransition: any;
|
||||
nextState?: string;
|
||||
currentStatus?: string;
|
||||
onPostTransition?: any;
|
||||
}
|
||||
|
||||
export type StatusName = 'success' | 'failed' | 'pending';
|
||||
export type StateMachineStates<T extends string> = Record<T, State>;
|
||||
/*
|
||||
* Data structure defining the state machine
|
||||
* {
|
||||
* context: {},
|
||||
* states: {
|
||||
* state1: {
|
||||
* onTransition: onState1Transition,
|
||||
* onPostTransition: onPostTransition,
|
||||
* nextState: 'state2',
|
||||
* },
|
||||
* state2: {
|
||||
* onTransition: onState2Transition,
|
||||
* onPostTransition: onPostTransition,,
|
||||
* nextState: 'state3',
|
||||
* },
|
||||
* state3: {
|
||||
* onTransition: onState3Transition,
|
||||
* onPostTransition: onPostTransition,
|
||||
* nextState: 'end',
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export interface StateMachineDefinition<T extends string> {
|
||||
context: StateContext<string>;
|
||||
states: StateMachineStates<T>;
|
||||
}
|
||||
/*
|
||||
* Generic state machine implemented to handle state transitions, based on a provided data structure
|
||||
* currentStateName: iniital state
|
||||
* definition: data structure defined as a StateMachineDefinition
|
||||
* context: object keeping the state between transitions. All the transition functions accept it as input parameter and write to it
|
||||
*
|
||||
* It recursively traverses all the states until it finds the last state.
|
||||
*/
|
||||
export async function handleState(
|
||||
currentStateName: string,
|
||||
definition: StateMachineDefinition<string>,
|
||||
context: StateContext<string>
|
||||
): Promise<StateContext<string>> {
|
||||
const logger = appContextService.getLogger();
|
||||
const { states } = definition;
|
||||
const currentState = states[currentStateName];
|
||||
let currentStatus = 'pending';
|
||||
let stateResult;
|
||||
let updatedContext = { ...context };
|
||||
if (typeof currentState.onTransition === 'function') {
|
||||
logger.debug(
|
||||
`Current state ${currentStateName}: running transition ${currentState.onTransition.name}`
|
||||
);
|
||||
try {
|
||||
// inject information about the state into context
|
||||
const startedAt = new Date(Date.now()).toISOString();
|
||||
const latestExecutedState: LatestExecutedState<string> = {
|
||||
name: currentStateName,
|
||||
started_at: startedAt,
|
||||
};
|
||||
stateResult = await currentState.onTransition.call(undefined, updatedContext);
|
||||
// check if is a function/promise
|
||||
if (typeof stateResult === 'function') {
|
||||
const promiseName = `${currentStateName}Result`;
|
||||
updatedContext[promiseName] = stateResult;
|
||||
updatedContext = { ...updatedContext, latestExecutedState };
|
||||
} else {
|
||||
updatedContext = {
|
||||
...updatedContext,
|
||||
...stateResult,
|
||||
latestExecutedState,
|
||||
};
|
||||
}
|
||||
currentStatus = 'success';
|
||||
logger.debug(
|
||||
`Executed state: ${currentStateName} with status: ${currentStatus} - nextState: ${currentState.nextState}`
|
||||
);
|
||||
} catch (error) {
|
||||
currentStatus = 'failed';
|
||||
const errorMessage = `Error during execution of state "${currentStateName}" with status "${currentStatus}": ${error.message}`;
|
||||
const latestStateWithError = {
|
||||
...updatedContext.latestExecutedState,
|
||||
error: errorMessage,
|
||||
} as LatestExecutedState<string>;
|
||||
updatedContext = { ...updatedContext, latestExecutedState: latestStateWithError };
|
||||
logger.warn(errorMessage);
|
||||
|
||||
// execute post transition function when transition failed too
|
||||
await executePostTransition(logger, updatedContext, currentState);
|
||||
|
||||
// bubble up the error
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
currentStatus = 'failed';
|
||||
logger.warn(
|
||||
`Execution of state "${currentStateName}" with status "${currentStatus}": provided onTransition is not a valid function`
|
||||
);
|
||||
}
|
||||
// execute post transition function
|
||||
await executePostTransition(logger, updatedContext, currentState);
|
||||
|
||||
if (currentStatus === 'success' && currentState?.nextState && currentState?.nextState !== 'end') {
|
||||
return await handleState(currentState.nextState, definition, updatedContext);
|
||||
} else {
|
||||
return updatedContext;
|
||||
}
|
||||
}
|
||||
|
||||
async function executePostTransition(
|
||||
logger: Logger,
|
||||
updatedContext: StateContext<string>,
|
||||
currentState: State
|
||||
) {
|
||||
if (typeof currentState.onPostTransition === 'function') {
|
||||
try {
|
||||
await currentState.onPostTransition.call(undefined, updatedContext);
|
||||
logger.debug(`Executing post transition function: ${currentState.onPostTransition.name}`);
|
||||
} catch (error) {
|
||||
logger.warn(`Error during execution of post transition function: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './step_create_restart_installation';
|
||||
export * from './step_install_kibana_assets';
|
||||
export * from './step_install_mlmodel';
|
||||
export * from './step_install_ilm_policies';
|
||||
export * from './step_install_index_template_pipelines';
|
||||
export * from './step_remove_legacy_templates';
|
||||
export * from './step_update_current_write_indices';
|
||||
export * from './step_install_transforms';
|
||||
export * from './step_delete_previous_pipelines';
|
||||
export * from './step_save_archive_entries';
|
||||
export * from './step_save_system_object';
|
||||
export * from './step_resolve_kibana_promise';
|
||||
export * from './update_latest_executed_state';
|
|
@ -0,0 +1,198 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type {
|
||||
SavedObjectsClientContract,
|
||||
ElasticsearchClient,
|
||||
SavedObject,
|
||||
} from '@kbn/core/server';
|
||||
import {
|
||||
savedObjectsClientMock,
|
||||
elasticsearchServiceMock,
|
||||
loggingSystemMock,
|
||||
} from '@kbn/core/server/mocks';
|
||||
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants';
|
||||
|
||||
import {
|
||||
MAX_TIME_COMPLETE_INSTALL,
|
||||
PACKAGES_SAVED_OBJECT_TYPE,
|
||||
} from '../../../../../../common/constants';
|
||||
|
||||
import { appContextService } from '../../../../app_context';
|
||||
import { createAppContextStartContractMock } from '../../../../../mocks';
|
||||
|
||||
import { INSTALL_STATES } from '../../../../../../common/types';
|
||||
|
||||
import { auditLoggingService } from '../../../../audit_logging';
|
||||
import { restartInstallation, createInstallation } from '../../install';
|
||||
import type { Installation } from '../../../../../../common';
|
||||
|
||||
import { stepCreateRestartInstallation } from './step_create_restart_installation';
|
||||
|
||||
jest.mock('../../../../audit_logging');
|
||||
jest.mock('../../install');
|
||||
|
||||
const mockedRestartInstallation = jest.mocked(restartInstallation);
|
||||
const mockedCreateInstallation = createInstallation as jest.Mocked<typeof createInstallation>;
|
||||
|
||||
const mockedAuditLoggingService = auditLoggingService as jest.Mocked<typeof auditLoggingService>;
|
||||
|
||||
describe('stepCreateRestartInstallation', () => {
|
||||
let soClient: jest.Mocked<SavedObjectsClientContract>;
|
||||
let esClient: jest.Mocked<ElasticsearchClient>;
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
const mockInstalledPackageSo: SavedObject<Installation> = {
|
||||
id: 'mocked-package',
|
||||
attributes: {
|
||||
name: 'test-package',
|
||||
version: '1.0.0',
|
||||
install_status: 'installing',
|
||||
install_version: '1.0.0',
|
||||
install_started_at: new Date().toISOString(),
|
||||
install_source: 'registry',
|
||||
verification_status: 'verified',
|
||||
installed_kibana: [] as any,
|
||||
installed_es: [] as any,
|
||||
es_index_patterns: {},
|
||||
},
|
||||
type: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
references: [],
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
soClient = savedObjectsClientMock.create();
|
||||
esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
appContextService.start(createAppContextStartContractMock());
|
||||
});
|
||||
afterEach(() => {
|
||||
mockedAuditLoggingService.writeCustomSoAuditLog.mockReset();
|
||||
soClient.update.mockReset();
|
||||
// mockedCreateInstallation.mockReset();
|
||||
});
|
||||
|
||||
it('Should call createInstallation if no installedPkg is available', async () => {
|
||||
await stepCreateRestartInstallation({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger,
|
||||
packageInstallContext: {
|
||||
assetsMap: new Map(),
|
||||
paths: [],
|
||||
packageInfo: {
|
||||
title: 'title',
|
||||
name: 'xyz',
|
||||
version: '4.5.6',
|
||||
description: 'test',
|
||||
type: 'integration',
|
||||
categories: ['cloud', 'custom'],
|
||||
format_version: 'string',
|
||||
release: 'experimental',
|
||||
conditions: { kibana: { version: 'x.y.z' } },
|
||||
owner: { github: 'elastic/fleet' },
|
||||
},
|
||||
},
|
||||
latestExecutedState: {
|
||||
name: INSTALL_STATES.SAVE_ARCHIVE_ENTRIES,
|
||||
started_at: new Date(Date.now() - MAX_TIME_COMPLETE_INSTALL * 2).toISOString(),
|
||||
},
|
||||
installType: 'install',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
});
|
||||
expect(logger.debug).toHaveBeenCalledWith(`Package install - Create installation`);
|
||||
expect(mockedCreateInstallation).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Should call restartInstallation if installedPkg is available and force = true', async () => {
|
||||
await stepCreateRestartInstallation({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger,
|
||||
packageInstallContext: {
|
||||
assetsMap: new Map(),
|
||||
paths: [],
|
||||
packageInfo: {
|
||||
title: 'title',
|
||||
name: 'xyz',
|
||||
version: '4.5.6',
|
||||
description: 'test',
|
||||
type: 'integration',
|
||||
categories: ['cloud', 'custom'],
|
||||
format_version: 'string',
|
||||
release: 'experimental',
|
||||
conditions: { kibana: { version: 'x.y.z' } },
|
||||
owner: { github: 'elastic/fleet' },
|
||||
},
|
||||
},
|
||||
installedPkg: {
|
||||
...mockInstalledPackageSo,
|
||||
attributes: {
|
||||
...mockInstalledPackageSo.attributes,
|
||||
install_started_at: new Date(Date.now() - 1000).toISOString(),
|
||||
},
|
||||
},
|
||||
force: true,
|
||||
latestExecutedState: {
|
||||
name: INSTALL_STATES.SAVE_ARCHIVE_ENTRIES,
|
||||
started_at: new Date(Date.now() - MAX_TIME_COMPLETE_INSTALL * 2).toISOString(),
|
||||
},
|
||||
installType: 'install',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
});
|
||||
expect(mockedRestartInstallation).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Should call restartInstallation and throw if installedPkg is available and force is not provided', async () => {
|
||||
const promise = stepCreateRestartInstallation({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger,
|
||||
packageInstallContext: {
|
||||
assetsMap: new Map(),
|
||||
paths: [],
|
||||
packageInfo: {
|
||||
title: 'title',
|
||||
name: 'xyz',
|
||||
version: '4.5.6',
|
||||
description: 'test',
|
||||
type: 'integration',
|
||||
categories: ['cloud', 'custom'],
|
||||
format_version: 'string',
|
||||
release: 'experimental',
|
||||
conditions: { kibana: { version: 'x.y.z' } },
|
||||
owner: { github: 'elastic/fleet' },
|
||||
},
|
||||
},
|
||||
installedPkg: {
|
||||
...mockInstalledPackageSo,
|
||||
attributes: {
|
||||
...mockInstalledPackageSo.attributes,
|
||||
install_started_at: new Date(Date.now() - 1000).toISOString(),
|
||||
},
|
||||
},
|
||||
latestExecutedState: {
|
||||
name: INSTALL_STATES.SAVE_ARCHIVE_ENTRIES,
|
||||
started_at: new Date(Date.now() - MAX_TIME_COMPLETE_INSTALL * 2).toISOString(),
|
||||
},
|
||||
installType: 'install',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
});
|
||||
|
||||
await expect(promise).rejects.toThrowError(
|
||||
'Concurrent installation or upgrade of xyz-4.5.6 detected, aborting.'
|
||||
);
|
||||
});
|
||||
expect(mockedRestartInstallation).toHaveBeenCalledTimes(0);
|
||||
});
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ConcurrentInstallOperationError } from '../../../../../errors';
|
||||
import { MAX_TIME_COMPLETE_INSTALL } from '../../../../../constants';
|
||||
|
||||
import { restartInstallation, createInstallation } from '../../install';
|
||||
|
||||
import type { InstallContext } from '../_state_machine_package_install';
|
||||
|
||||
export async function stepCreateRestartInstallation(context: InstallContext) {
|
||||
const {
|
||||
savedObjectsClient,
|
||||
logger,
|
||||
installSource,
|
||||
packageInstallContext,
|
||||
spaceId,
|
||||
force,
|
||||
verificationResult,
|
||||
installedPkg,
|
||||
} = context;
|
||||
const { packageInfo } = packageInstallContext;
|
||||
const { name: pkgName, version: pkgVersion } = packageInfo;
|
||||
|
||||
// if some installation already exists
|
||||
if (installedPkg) {
|
||||
const isStatusInstalling = installedPkg.attributes.install_status === 'installing';
|
||||
const hasExceededTimeout =
|
||||
Date.now() - Date.parse(installedPkg.attributes.install_started_at) <
|
||||
MAX_TIME_COMPLETE_INSTALL;
|
||||
logger.debug(`Package install - Install status ${installedPkg.attributes.install_status}`);
|
||||
|
||||
// if the installation is currently running, don't try to install
|
||||
// instead, only return already installed assets
|
||||
if (isStatusInstalling && hasExceededTimeout) {
|
||||
// If this is a forced installation, ignore the timeout and restart the installation anyway
|
||||
logger.debug(`Package install - Installation is running and has exceeded timeout`);
|
||||
|
||||
if (force) {
|
||||
logger.debug(`Package install - Forced installation, restarting`);
|
||||
await restartInstallation({
|
||||
savedObjectsClient,
|
||||
pkgName,
|
||||
pkgVersion,
|
||||
installSource,
|
||||
verificationResult,
|
||||
});
|
||||
} else {
|
||||
throw new ConcurrentInstallOperationError(
|
||||
`Concurrent installation or upgrade of ${pkgName || 'unknown'}-${
|
||||
pkgVersion || 'unknown'
|
||||
} detected, aborting.`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// if no installation is running, or the installation has been running longer than MAX_TIME_COMPLETE_INSTALL
|
||||
// (it might be stuck) update the saved object and proceed
|
||||
logger.debug(
|
||||
`Package install - no installation running or the installation has been running longer than ${MAX_TIME_COMPLETE_INSTALL}, restarting`
|
||||
);
|
||||
await restartInstallation({
|
||||
savedObjectsClient,
|
||||
pkgName,
|
||||
pkgVersion,
|
||||
installSource,
|
||||
verificationResult,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
logger.debug(`Package install - Create installation`);
|
||||
|
||||
await createInstallation({
|
||||
savedObjectsClient,
|
||||
packageInfo,
|
||||
installSource,
|
||||
spaceId,
|
||||
verificationResult,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,481 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type {
|
||||
SavedObjectsClientContract,
|
||||
ElasticsearchClient,
|
||||
SavedObject,
|
||||
} from '@kbn/core/server';
|
||||
import { savedObjectsClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants';
|
||||
|
||||
import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../../common/constants';
|
||||
import { ElasticsearchAssetType } from '../../../../../types';
|
||||
|
||||
import type { EsAssetReference, Installation } from '../../../../../../common';
|
||||
import { appContextService } from '../../../../app_context';
|
||||
import { createAppContextStartContractMock } from '../../../../../mocks';
|
||||
import {
|
||||
isTopLevelPipeline,
|
||||
deletePreviousPipelines,
|
||||
} from '../../../elasticsearch/ingest_pipeline';
|
||||
|
||||
import { stepDeletePreviousPipelines } from './step_delete_previous_pipelines';
|
||||
|
||||
jest.mock('../../../elasticsearch/ingest_pipeline');
|
||||
|
||||
const mockedDeletePreviousPipelines = deletePreviousPipelines as jest.MockedFunction<
|
||||
typeof deletePreviousPipelines
|
||||
>;
|
||||
const mockedIsTopLevelPipeline = isTopLevelPipeline as jest.MockedFunction<
|
||||
typeof isTopLevelPipeline
|
||||
>;
|
||||
|
||||
describe('stepDeletePreviousPipelines', () => {
|
||||
let soClient: jest.Mocked<SavedObjectsClientContract>;
|
||||
let esClient: jest.Mocked<ElasticsearchClient>;
|
||||
const getMockInstalledPackageSo = (
|
||||
installedEs: EsAssetReference[] = []
|
||||
): SavedObject<Installation> => {
|
||||
return {
|
||||
id: 'mocked-package',
|
||||
attributes: {
|
||||
name: 'test-package',
|
||||
version: '1.0.0',
|
||||
install_status: 'installing',
|
||||
install_version: '1.0.0',
|
||||
install_started_at: new Date().toISOString(),
|
||||
install_source: 'registry',
|
||||
verification_status: 'verified',
|
||||
installed_kibana: [] as any,
|
||||
installed_es: installedEs,
|
||||
es_index_patterns: {},
|
||||
},
|
||||
type: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
references: [],
|
||||
};
|
||||
};
|
||||
beforeEach(async () => {
|
||||
soClient = savedObjectsClientMock.create();
|
||||
esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
appContextService.start(createAppContextStartContractMock());
|
||||
});
|
||||
afterEach(async () => {
|
||||
jest.mocked(mockedDeletePreviousPipelines).mockReset();
|
||||
jest.mocked(mockedIsTopLevelPipeline).mockReset();
|
||||
});
|
||||
|
||||
describe('Should call deletePreviousPipelines', () => {
|
||||
const packageInstallContext = {
|
||||
packageInfo: {
|
||||
title: 'title',
|
||||
name: 'test-package',
|
||||
version: '1.0.0',
|
||||
description: 'test',
|
||||
type: 'integration',
|
||||
categories: ['cloud', 'custom'],
|
||||
format_version: 'string',
|
||||
release: 'experimental',
|
||||
conditions: { kibana: { version: 'x.y.z' } },
|
||||
owner: { github: 'elastic/fleet' },
|
||||
} as any,
|
||||
assetsMap: new Map(),
|
||||
paths: [],
|
||||
};
|
||||
appContextService.start(
|
||||
createAppContextStartContractMock({
|
||||
internal: {
|
||||
disableILMPolicies: true,
|
||||
fleetServerStandalone: false,
|
||||
onlyAllowAgentUpgradeToKnownVersions: false,
|
||||
retrySetupOnBoot: false,
|
||||
registry: {
|
||||
kibanaVersionCheckEnabled: true,
|
||||
capabilities: [],
|
||||
excludePackages: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
const mockInstalledPackageSo = getMockInstalledPackageSo();
|
||||
const installedPkg = {
|
||||
...mockInstalledPackageSo,
|
||||
attributes: {
|
||||
...mockInstalledPackageSo.attributes,
|
||||
install_started_at: new Date(Date.now() - 1000).toISOString(),
|
||||
},
|
||||
};
|
||||
beforeEach(async () => {
|
||||
jest.mocked(mockedDeletePreviousPipelines).mockResolvedValue([
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
{
|
||||
id: 'something-01',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('if installType is update', async () => {
|
||||
const res = await stepDeletePreviousPipelines({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext,
|
||||
installedPkg,
|
||||
installType: 'update',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(mockedDeletePreviousPipelines).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
installedPkg.attributes.name,
|
||||
installedPkg.attributes.version,
|
||||
[
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
]
|
||||
);
|
||||
expect(res).toEqual({
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
{
|
||||
id: 'something-01',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('if installType is reupdate', async () => {
|
||||
const res = await stepDeletePreviousPipelines({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext,
|
||||
installedPkg,
|
||||
installType: 'reupdate',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(mockedDeletePreviousPipelines).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
installedPkg.attributes.name,
|
||||
installedPkg.attributes.version,
|
||||
[
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
]
|
||||
);
|
||||
expect(res).toEqual({
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
{
|
||||
id: 'something-01',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('if installType is rollback', async () => {
|
||||
const res = await stepDeletePreviousPipelines({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext,
|
||||
installedPkg,
|
||||
installType: 'rollback',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(mockedDeletePreviousPipelines).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
installedPkg.attributes.name,
|
||||
installedPkg.attributes.version,
|
||||
[
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
]
|
||||
);
|
||||
expect(res).toEqual({
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
{
|
||||
id: 'something-01',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Should not call deletePreviousPipelines', () => {
|
||||
const packageInstallContext = {
|
||||
packageInfo: {
|
||||
title: 'title',
|
||||
name: 'test-package',
|
||||
version: '1.0.0',
|
||||
description: 'test',
|
||||
type: 'integration',
|
||||
categories: ['cloud', 'custom'],
|
||||
format_version: 'string',
|
||||
release: 'experimental',
|
||||
conditions: { kibana: { version: 'x.y.z' } },
|
||||
owner: { github: 'elastic/fleet' },
|
||||
} as any,
|
||||
assetsMap: new Map(),
|
||||
paths: [],
|
||||
};
|
||||
appContextService.start(
|
||||
createAppContextStartContractMock({
|
||||
internal: {
|
||||
disableILMPolicies: true,
|
||||
fleetServerStandalone: false,
|
||||
onlyAllowAgentUpgradeToKnownVersions: false,
|
||||
retrySetupOnBoot: false,
|
||||
registry: {
|
||||
kibanaVersionCheckEnabled: true,
|
||||
capabilities: [],
|
||||
excludePackages: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
beforeEach(async () => {
|
||||
jest.mocked(mockedDeletePreviousPipelines).mockResolvedValue([
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
{
|
||||
id: 'something-01',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('if installType is update and installedPkg is not present', async () => {
|
||||
const res = await stepDeletePreviousPipelines({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext,
|
||||
installType: 'update',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(mockedDeletePreviousPipelines).not.toBeCalled();
|
||||
expect(res).toEqual({
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('if installType is reupdate and installedPkg is not present', async () => {
|
||||
const res = await stepDeletePreviousPipelines({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext,
|
||||
installType: 'reupdate',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(mockedDeletePreviousPipelines).not.toBeCalled();
|
||||
expect(res).toEqual({
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('if installType is rollback and installedPkg is not present', async () => {
|
||||
const res = await stepDeletePreviousPipelines({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext,
|
||||
installType: 'rollback',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(mockedDeletePreviousPipelines).not.toBeCalled();
|
||||
expect(res).toEqual({
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('if installType type is of different type', async () => {
|
||||
const mockInstalledPackageSo = getMockInstalledPackageSo();
|
||||
const installedPkg = {
|
||||
...mockInstalledPackageSo,
|
||||
attributes: {
|
||||
...mockInstalledPackageSo.attributes,
|
||||
install_started_at: new Date(Date.now() - 1000).toISOString(),
|
||||
},
|
||||
};
|
||||
jest.mocked(mockedIsTopLevelPipeline).mockImplementation(() => true);
|
||||
|
||||
const res = await stepDeletePreviousPipelines({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext: { ...packageInstallContext, paths: ['some/path/1', 'some/path/2'] },
|
||||
installType: 'install',
|
||||
installedPkg,
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(mockedDeletePreviousPipelines).not.toBeCalled();
|
||||
expect(res).toEqual({
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('if installedPkg is present and there is a top level pipeline', async () => {
|
||||
const mockInstalledPackageSo = getMockInstalledPackageSo();
|
||||
const installedPkg = {
|
||||
...mockInstalledPackageSo,
|
||||
attributes: {
|
||||
...mockInstalledPackageSo.attributes,
|
||||
install_started_at: new Date(Date.now() - 1000).toISOString(),
|
||||
},
|
||||
};
|
||||
jest.mocked(mockedIsTopLevelPipeline).mockImplementation(() => true);
|
||||
|
||||
const res = await stepDeletePreviousPipelines({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext: { ...packageInstallContext, paths: ['some/path/1', 'some/path/2'] },
|
||||
installType: 'update',
|
||||
installedPkg,
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(mockedDeletePreviousPipelines).not.toBeCalled();
|
||||
expect(res).toEqual({
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
isTopLevelPipeline,
|
||||
deletePreviousPipelines,
|
||||
} from '../../../elasticsearch/ingest_pipeline';
|
||||
|
||||
import { withPackageSpan } from '../../utils';
|
||||
|
||||
import type { InstallContext } from '../_state_machine_package_install';
|
||||
|
||||
export async function stepDeletePreviousPipelines(context: InstallContext) {
|
||||
const {
|
||||
packageInstallContext,
|
||||
esClient,
|
||||
savedObjectsClient,
|
||||
logger,
|
||||
esReferences,
|
||||
installType,
|
||||
installedPkg,
|
||||
} = context;
|
||||
const { packageInfo, paths } = packageInstallContext;
|
||||
const { name: pkgName } = packageInfo;
|
||||
let updatedESReferences;
|
||||
// If this is an update or retrying an update, delete the previous version's pipelines
|
||||
// Top-level pipeline assets will not be removed on upgrade as of ml model package addition which requires previous
|
||||
// assets to remain installed. This is a temporary solution - more robust solution tracked here https://github.com/elastic/kibana/issues/115035
|
||||
if (
|
||||
paths.filter((path) => isTopLevelPipeline(path)).length === 0 &&
|
||||
(installType === 'update' || installType === 'reupdate') &&
|
||||
installedPkg
|
||||
) {
|
||||
logger.debug(`Package install - installType ${installType} Deleting previous ingest pipelines`);
|
||||
updatedESReferences = await withPackageSpan('Delete previous ingest pipelines', () =>
|
||||
deletePreviousPipelines(
|
||||
esClient,
|
||||
savedObjectsClient,
|
||||
pkgName,
|
||||
installedPkg!.attributes.version,
|
||||
esReferences || []
|
||||
)
|
||||
);
|
||||
} else if (installType === 'rollback' && installedPkg) {
|
||||
// pipelines from a different version may have been installed during a failed update
|
||||
logger.debug(`Package install - installType ${installType} Deleting previous ingest pipelines`);
|
||||
updatedESReferences = await withPackageSpan('Delete previous ingest pipelines', () =>
|
||||
deletePreviousPipelines(
|
||||
esClient,
|
||||
savedObjectsClient,
|
||||
pkgName,
|
||||
installedPkg!.attributes.install_version,
|
||||
esReferences || []
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// if none of the previous cases, return the original esReferences
|
||||
updatedESReferences = esReferences;
|
||||
}
|
||||
return { esReferences: updatedESReferences };
|
||||
}
|
|
@ -0,0 +1,374 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type {
|
||||
SavedObjectsClientContract,
|
||||
ElasticsearchClient,
|
||||
SavedObject,
|
||||
} from '@kbn/core/server';
|
||||
import { savedObjectsClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants';
|
||||
|
||||
import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../../common/constants';
|
||||
|
||||
import type { Installation } from '../../../../../../common';
|
||||
|
||||
import { appContextService } from '../../../../app_context';
|
||||
import { createAppContextStartContractMock } from '../../../../../mocks';
|
||||
import { installILMPolicy } from '../../../elasticsearch/ilm/install';
|
||||
import { installIlmForDataStream } from '../../../elasticsearch/datastream_ilm/install';
|
||||
import { ElasticsearchAssetType } from '../../../../../types';
|
||||
|
||||
jest.mock('../../../archive/storage');
|
||||
jest.mock('../../../elasticsearch/ilm/install');
|
||||
jest.mock('../../../elasticsearch/datastream_ilm/install');
|
||||
|
||||
import { stepInstallILMPolicies } from './step_install_ilm_policies';
|
||||
|
||||
describe('stepInstallILMPolicies', () => {
|
||||
let soClient: jest.Mocked<SavedObjectsClientContract>;
|
||||
let esClient: jest.Mocked<ElasticsearchClient>;
|
||||
const mockInstalledPackageSo: SavedObject<Installation> = {
|
||||
id: 'mocked-package',
|
||||
attributes: {
|
||||
name: 'test-package',
|
||||
version: '1.0.0',
|
||||
install_status: 'installing',
|
||||
install_version: '1.0.0',
|
||||
install_started_at: new Date().toISOString(),
|
||||
install_source: 'registry',
|
||||
verification_status: 'verified',
|
||||
installed_kibana: [] as any,
|
||||
installed_es: [
|
||||
{
|
||||
id: 'metrics-endpoint.policy-0.1.0-dev.0',
|
||||
type: ElasticsearchAssetType.ingestPipeline,
|
||||
},
|
||||
{
|
||||
id: 'endpoint.metadata_current-default-0.1.0',
|
||||
type: ElasticsearchAssetType.transform,
|
||||
},
|
||||
] as any,
|
||||
es_index_patterns: {},
|
||||
},
|
||||
type: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
references: [],
|
||||
};
|
||||
const packageInstallContext = {
|
||||
packageInfo: {
|
||||
title: 'title',
|
||||
name: 'xyz',
|
||||
version: '4.5.6',
|
||||
description: 'test',
|
||||
type: 'integration',
|
||||
categories: ['cloud', 'custom'],
|
||||
format_version: 'string',
|
||||
release: 'experimental',
|
||||
conditions: { kibana: { version: 'x.y.z' } },
|
||||
owner: { github: 'elastic/fleet' },
|
||||
} as any,
|
||||
assetsMap: new Map(),
|
||||
paths: [],
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
soClient = savedObjectsClientMock.create();
|
||||
esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
appContextService.start(createAppContextStartContractMock());
|
||||
});
|
||||
afterEach(async () => {
|
||||
jest.mocked(installILMPolicy).mockReset();
|
||||
jest.mocked(installIlmForDataStream).mockReset();
|
||||
});
|
||||
|
||||
it('Should not install ILM policies if disabled in config', async () => {
|
||||
appContextService.start(
|
||||
createAppContextStartContractMock({
|
||||
internal: {
|
||||
disableILMPolicies: true,
|
||||
fleetServerStandalone: false,
|
||||
onlyAllowAgentUpgradeToKnownVersions: false,
|
||||
retrySetupOnBoot: false,
|
||||
registry: {
|
||||
kibanaVersionCheckEnabled: true,
|
||||
capabilities: [],
|
||||
excludePackages: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
await stepInstallILMPolicies({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext,
|
||||
installType: 'install',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
});
|
||||
|
||||
expect(installILMPolicy).not.toBeCalled();
|
||||
expect(installIlmForDataStream).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('Should not install ILM policies if disabled in config and should return esReferences form installedPkg', async () => {
|
||||
appContextService.start(
|
||||
createAppContextStartContractMock({
|
||||
internal: {
|
||||
disableILMPolicies: true,
|
||||
fleetServerStandalone: false,
|
||||
onlyAllowAgentUpgradeToKnownVersions: false,
|
||||
retrySetupOnBoot: false,
|
||||
registry: {
|
||||
kibanaVersionCheckEnabled: true,
|
||||
capabilities: [],
|
||||
excludePackages: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
const res = await stepInstallILMPolicies({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext,
|
||||
installedPkg: {
|
||||
...mockInstalledPackageSo,
|
||||
attributes: {
|
||||
...mockInstalledPackageSo.attributes,
|
||||
install_started_at: new Date(Date.now() - 1000).toISOString(),
|
||||
installed_es: [
|
||||
{
|
||||
id: 'metrics-endpoint.policy-0.1.0-dev.0',
|
||||
type: ElasticsearchAssetType.ingestPipeline,
|
||||
},
|
||||
{
|
||||
id: 'endpoint.metadata_current-default-0.1.0',
|
||||
type: ElasticsearchAssetType.transform,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
installType: 'install',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
});
|
||||
|
||||
expect(installILMPolicy).not.toBeCalled();
|
||||
expect(installIlmForDataStream).not.toBeCalled();
|
||||
expect(res?.esReferences).toEqual([
|
||||
{
|
||||
id: 'metrics-endpoint.policy-0.1.0-dev.0',
|
||||
type: ElasticsearchAssetType.ingestPipeline,
|
||||
},
|
||||
{
|
||||
id: 'endpoint.metadata_current-default-0.1.0',
|
||||
type: ElasticsearchAssetType.transform,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('Should installs ILM policies if not disabled in config', async () => {
|
||||
appContextService.start(
|
||||
createAppContextStartContractMock({
|
||||
internal: {
|
||||
disableILMPolicies: false,
|
||||
fleetServerStandalone: false,
|
||||
onlyAllowAgentUpgradeToKnownVersions: false,
|
||||
retrySetupOnBoot: false,
|
||||
registry: {
|
||||
kibanaVersionCheckEnabled: true,
|
||||
capabilities: [],
|
||||
excludePackages: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
jest.mocked(installILMPolicy).mockResolvedValue([]);
|
||||
jest.mocked(installIlmForDataStream).mockResolvedValue({
|
||||
esReferences: [
|
||||
{
|
||||
id: 'metrics-endpoint.policy-0.1.0-dev.0',
|
||||
type: ElasticsearchAssetType.ingestPipeline,
|
||||
},
|
||||
{
|
||||
id: 'endpoint.metadata_current-default-0.1.0',
|
||||
type: ElasticsearchAssetType.transform,
|
||||
},
|
||||
],
|
||||
installedIlms: [],
|
||||
});
|
||||
const res = await stepInstallILMPolicies({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext: {
|
||||
packageInfo: {
|
||||
title: 'title',
|
||||
name: 'xyz',
|
||||
version: '4.5.6',
|
||||
description: 'test',
|
||||
type: 'integration',
|
||||
categories: ['cloud', 'custom'],
|
||||
format_version: 'string',
|
||||
release: 'experimental',
|
||||
conditions: { kibana: { version: 'x.y.z' } },
|
||||
owner: { github: 'elastic/fleet' },
|
||||
} as any,
|
||||
assetsMap: new Map(),
|
||||
paths: [],
|
||||
},
|
||||
installType: 'install',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
});
|
||||
|
||||
expect(installILMPolicy).toHaveBeenCalled();
|
||||
expect(installIlmForDataStream).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
[]
|
||||
);
|
||||
expect(res?.esReferences).toEqual([
|
||||
{
|
||||
id: 'metrics-endpoint.policy-0.1.0-dev.0',
|
||||
type: ElasticsearchAssetType.ingestPipeline,
|
||||
},
|
||||
{
|
||||
id: 'endpoint.metadata_current-default-0.1.0',
|
||||
type: ElasticsearchAssetType.transform,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return updated esReferences', async () => {
|
||||
appContextService.start(
|
||||
createAppContextStartContractMock({
|
||||
internal: {
|
||||
disableILMPolicies: false,
|
||||
fleetServerStandalone: false,
|
||||
onlyAllowAgentUpgradeToKnownVersions: false,
|
||||
retrySetupOnBoot: false,
|
||||
registry: {
|
||||
kibanaVersionCheckEnabled: true,
|
||||
capabilities: [],
|
||||
excludePackages: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
jest.mocked(installILMPolicy).mockResolvedValue([
|
||||
{
|
||||
id: 'metrics-endpoint.policy-0.1.0-dev.0',
|
||||
type: ElasticsearchAssetType.ingestPipeline,
|
||||
},
|
||||
{
|
||||
id: 'endpoint.metadata_current-default-0.1.0',
|
||||
type: ElasticsearchAssetType.transform,
|
||||
},
|
||||
{
|
||||
id: 'endpoint.metadata_current-default-0.2.0',
|
||||
type: ElasticsearchAssetType.transform,
|
||||
},
|
||||
] as any);
|
||||
jest.mocked(installIlmForDataStream).mockResolvedValue({
|
||||
esReferences: [
|
||||
{
|
||||
id: 'metrics-endpoint.policy-0.1.0-dev.0',
|
||||
type: ElasticsearchAssetType.ingestPipeline,
|
||||
},
|
||||
{
|
||||
id: 'endpoint.metadata_current-default-0.1.0',
|
||||
type: ElasticsearchAssetType.transform,
|
||||
},
|
||||
{
|
||||
id: 'endpoint.metadata_current-default-0.2.0',
|
||||
type: ElasticsearchAssetType.transform,
|
||||
},
|
||||
],
|
||||
installedIlms: [],
|
||||
});
|
||||
|
||||
const res = await stepInstallILMPolicies({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext,
|
||||
installedPkg: {
|
||||
...mockInstalledPackageSo,
|
||||
attributes: {
|
||||
...mockInstalledPackageSo.attributes,
|
||||
install_started_at: new Date(Date.now() - 1000).toISOString(),
|
||||
},
|
||||
},
|
||||
installType: 'install',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
});
|
||||
|
||||
expect(installILMPolicy).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
[
|
||||
{
|
||||
id: 'metrics-endpoint.policy-0.1.0-dev.0',
|
||||
type: ElasticsearchAssetType.ingestPipeline,
|
||||
},
|
||||
{
|
||||
id: 'endpoint.metadata_current-default-0.1.0',
|
||||
type: ElasticsearchAssetType.transform,
|
||||
},
|
||||
]
|
||||
);
|
||||
expect(installIlmForDataStream).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
[
|
||||
{
|
||||
id: 'metrics-endpoint.policy-0.1.0-dev.0',
|
||||
type: ElasticsearchAssetType.ingestPipeline,
|
||||
},
|
||||
{
|
||||
id: 'endpoint.metadata_current-default-0.1.0',
|
||||
type: ElasticsearchAssetType.transform,
|
||||
},
|
||||
{
|
||||
id: 'endpoint.metadata_current-default-0.2.0',
|
||||
type: ElasticsearchAssetType.transform,
|
||||
},
|
||||
]
|
||||
);
|
||||
expect(res?.esReferences).toEqual([
|
||||
{
|
||||
id: 'metrics-endpoint.policy-0.1.0-dev.0',
|
||||
type: ElasticsearchAssetType.ingestPipeline,
|
||||
},
|
||||
{
|
||||
id: 'endpoint.metadata_current-default-0.1.0',
|
||||
type: ElasticsearchAssetType.transform,
|
||||
},
|
||||
{
|
||||
id: 'endpoint.metadata_current-default-0.2.0',
|
||||
type: ElasticsearchAssetType.transform,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { appContextService } from '../../../..';
|
||||
|
||||
import { installIlmForDataStream } from '../../../elasticsearch/datastream_ilm/install';
|
||||
import { installILMPolicy } from '../../../elasticsearch/ilm/install';
|
||||
|
||||
import { withPackageSpan } from '../../utils';
|
||||
|
||||
import type { InstallContext } from '../_state_machine_package_install';
|
||||
|
||||
export async function stepInstallILMPolicies(context: InstallContext) {
|
||||
const { logger, packageInstallContext, esClient, savedObjectsClient, installedPkg } = context;
|
||||
|
||||
// Array that gets updated by each operation. This allows each operation to accurately update the
|
||||
// installation object with its references without requiring a refresh of the SO index on each update (faster).
|
||||
let esReferences = installedPkg?.attributes.installed_es ?? [];
|
||||
|
||||
// currently only the base package has an ILM policy
|
||||
// at some point ILM policies can be installed/modified
|
||||
// per data stream and we should then save them
|
||||
const isILMPoliciesDisabled =
|
||||
appContextService.getConfig()?.internal?.disableILMPolicies ?? false;
|
||||
if (!isILMPoliciesDisabled) {
|
||||
esReferences = await withPackageSpan('Install ILM policies', () =>
|
||||
installILMPolicy(packageInstallContext, esClient, savedObjectsClient, logger, esReferences)
|
||||
);
|
||||
({ esReferences } = await withPackageSpan('Install Data Stream ILM policies', () =>
|
||||
installIlmForDataStream(
|
||||
packageInstallContext,
|
||||
esClient,
|
||||
savedObjectsClient,
|
||||
logger,
|
||||
esReferences
|
||||
)
|
||||
));
|
||||
}
|
||||
// always return esReferences even when isILMPoliciesDisabled is false as it's the first time we are writing to it
|
||||
return { esReferences };
|
||||
}
|
|
@ -0,0 +1,592 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type {
|
||||
SavedObjectsClientContract,
|
||||
ElasticsearchClient,
|
||||
SavedObject,
|
||||
} from '@kbn/core/server';
|
||||
import { savedObjectsClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants';
|
||||
|
||||
import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../../common/constants';
|
||||
import { ElasticsearchAssetType } from '../../../../../types';
|
||||
|
||||
import type { EsAssetReference, Installation } from '../../../../../../common';
|
||||
import { appContextService } from '../../../../app_context';
|
||||
import { createAppContextStartContractMock } from '../../../../../mocks';
|
||||
import { installIndexTemplatesAndPipelines } from '../../install_index_template_pipeline';
|
||||
|
||||
jest.mock('../../install_index_template_pipeline');
|
||||
|
||||
import { stepInstallIndexTemplatePipelines } from './step_install_index_template_pipelines';
|
||||
const mockedInstallIndexTemplatesAndPipelines =
|
||||
installIndexTemplatesAndPipelines as jest.MockedFunction<
|
||||
typeof installIndexTemplatesAndPipelines
|
||||
>;
|
||||
|
||||
describe('stepInstallIndexTemplatePipelines', () => {
|
||||
let soClient: jest.Mocked<SavedObjectsClientContract>;
|
||||
let esClient: jest.Mocked<ElasticsearchClient>;
|
||||
const getMockInstalledPackageSo = (
|
||||
installedEs: EsAssetReference[] = []
|
||||
): SavedObject<Installation> => {
|
||||
return {
|
||||
id: 'mocked-package',
|
||||
attributes: {
|
||||
name: 'test-package',
|
||||
version: '1.0.0',
|
||||
install_status: 'installing',
|
||||
install_version: '1.0.0',
|
||||
install_started_at: new Date().toISOString(),
|
||||
install_source: 'registry',
|
||||
verification_status: 'verified',
|
||||
installed_kibana: [] as any,
|
||||
installed_es: installedEs,
|
||||
es_index_patterns: {},
|
||||
},
|
||||
type: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
references: [],
|
||||
};
|
||||
};
|
||||
beforeEach(async () => {
|
||||
soClient = savedObjectsClientMock.create();
|
||||
esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
appContextService.start(createAppContextStartContractMock());
|
||||
});
|
||||
afterEach(async () => {
|
||||
jest.mocked(mockedInstallIndexTemplatesAndPipelines).mockReset();
|
||||
});
|
||||
|
||||
it('Should call installIndexTemplatesAndPipelines if packageInfo type is integration', async () => {
|
||||
mockedInstallIndexTemplatesAndPipelines.mockResolvedValue({
|
||||
installedTemplates: [
|
||||
{
|
||||
templateName: 'template-01',
|
||||
indexTemplate: {
|
||||
priority: 1,
|
||||
index_patterns: [],
|
||||
template: {
|
||||
settings: {},
|
||||
mappings: {},
|
||||
},
|
||||
data_stream: { hidden: false },
|
||||
composed_of: [],
|
||||
_meta: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
{
|
||||
id: 'something-01',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
const packageInstallContext = {
|
||||
packageInfo: {
|
||||
title: 'title',
|
||||
name: 'xyz',
|
||||
version: '4.5.6',
|
||||
description: 'test',
|
||||
type: 'integration',
|
||||
categories: ['cloud', 'custom'],
|
||||
format_version: 'string',
|
||||
release: 'experimental',
|
||||
conditions: { kibana: { version: 'x.y.z' } },
|
||||
owner: { github: 'elastic/fleet' },
|
||||
} as any,
|
||||
assetsMap: new Map(),
|
||||
paths: [],
|
||||
};
|
||||
appContextService.start(
|
||||
createAppContextStartContractMock({
|
||||
internal: {
|
||||
disableILMPolicies: true,
|
||||
fleetServerStandalone: false,
|
||||
onlyAllowAgentUpgradeToKnownVersions: false,
|
||||
retrySetupOnBoot: false,
|
||||
registry: {
|
||||
kibanaVersionCheckEnabled: true,
|
||||
capabilities: [],
|
||||
excludePackages: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
const mockInstalledPackageSo = getMockInstalledPackageSo([
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
]);
|
||||
const installedPkg = {
|
||||
...mockInstalledPackageSo,
|
||||
attributes: {
|
||||
...mockInstalledPackageSo.attributes,
|
||||
install_started_at: new Date(Date.now() - 1000).toISOString(),
|
||||
},
|
||||
};
|
||||
const res = await stepInstallIndexTemplatePipelines({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext,
|
||||
installedPkg,
|
||||
installType: 'install',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(mockedInstallIndexTemplatesAndPipelines).toHaveBeenCalledWith({
|
||||
installedPkg: installedPkg.attributes,
|
||||
packageInstallContext: expect.any(Object),
|
||||
esClient: expect.any(Object),
|
||||
savedObjectsClient: expect.any(Object),
|
||||
logger: expect.any(Object),
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(res).toEqual({
|
||||
indexTemplates: [
|
||||
{
|
||||
templateName: 'template-01',
|
||||
indexTemplate: {
|
||||
priority: 1,
|
||||
index_patterns: [],
|
||||
template: {
|
||||
settings: {},
|
||||
mappings: {},
|
||||
},
|
||||
data_stream: { hidden: false },
|
||||
composed_of: [],
|
||||
_meta: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
{
|
||||
id: 'something-01',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('Should call installIndexTemplatesAndPipelines if packageInfo type is input and installedPkg exists', async () => {
|
||||
mockedInstallIndexTemplatesAndPipelines.mockResolvedValue({
|
||||
installedTemplates: [
|
||||
{
|
||||
templateName: 'template-01',
|
||||
indexTemplate: {
|
||||
priority: 1,
|
||||
index_patterns: [],
|
||||
template: {
|
||||
settings: {},
|
||||
mappings: {},
|
||||
},
|
||||
data_stream: { hidden: false },
|
||||
composed_of: [],
|
||||
_meta: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
{
|
||||
id: 'something-01',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
const packageInstallContext = {
|
||||
packageInfo: {
|
||||
title: 'title',
|
||||
name: 'xyz',
|
||||
version: '4.5.6',
|
||||
description: 'test',
|
||||
type: 'input',
|
||||
categories: ['cloud'],
|
||||
format_version: 'string',
|
||||
release: 'experimental',
|
||||
conditions: { kibana: { version: 'x.y.z' } },
|
||||
owner: { github: 'elastic/fleet' },
|
||||
policy_templates: [
|
||||
{
|
||||
name: 'template_0001',
|
||||
title: 'Template 1',
|
||||
description: 'Template 1',
|
||||
inputs: [
|
||||
{
|
||||
type: 'logs',
|
||||
title: 'Log',
|
||||
description: 'Log Input',
|
||||
vars: [
|
||||
{
|
||||
name: 'path',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'path_2',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as any,
|
||||
assetsMap: new Map(),
|
||||
paths: [],
|
||||
};
|
||||
appContextService.start(
|
||||
createAppContextStartContractMock({
|
||||
internal: {
|
||||
disableILMPolicies: true,
|
||||
fleetServerStandalone: false,
|
||||
onlyAllowAgentUpgradeToKnownVersions: false,
|
||||
retrySetupOnBoot: false,
|
||||
registry: {
|
||||
kibanaVersionCheckEnabled: true,
|
||||
capabilities: [],
|
||||
excludePackages: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
const mockInstalledPackageSo = getMockInstalledPackageSo([
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
{
|
||||
id: 'type-template_0001',
|
||||
type: ElasticsearchAssetType.indexTemplate,
|
||||
},
|
||||
]);
|
||||
const installedPkg = {
|
||||
...mockInstalledPackageSo,
|
||||
attributes: {
|
||||
...mockInstalledPackageSo.attributes,
|
||||
install_started_at: new Date(Date.now() - 1000).toISOString(),
|
||||
},
|
||||
};
|
||||
const res = await stepInstallIndexTemplatePipelines({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext,
|
||||
installedPkg,
|
||||
installType: 'install',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
indexTemplates: [
|
||||
{
|
||||
templateName: 'template-01',
|
||||
indexTemplate: {
|
||||
priority: 1,
|
||||
index_patterns: [],
|
||||
template: {
|
||||
settings: {},
|
||||
mappings: {},
|
||||
},
|
||||
data_stream: { hidden: false },
|
||||
composed_of: [],
|
||||
_meta: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
{
|
||||
id: 'something-01',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('Should not call installIndexTemplatesAndPipelines if packageInfo type is input and no data streams are found', async () => {
|
||||
mockedInstallIndexTemplatesAndPipelines.mockResolvedValue({
|
||||
installedTemplates: [
|
||||
{
|
||||
templateName: 'template-01',
|
||||
indexTemplate: {
|
||||
priority: 1,
|
||||
index_patterns: [],
|
||||
template: {
|
||||
settings: {},
|
||||
mappings: {},
|
||||
},
|
||||
data_stream: { hidden: false },
|
||||
composed_of: [],
|
||||
_meta: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
{
|
||||
id: 'something-01',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
const packageInstallContext = {
|
||||
packageInfo: {
|
||||
title: 'title',
|
||||
name: 'xyz',
|
||||
version: '4.5.6',
|
||||
description: 'test',
|
||||
type: 'input',
|
||||
categories: ['cloud'],
|
||||
format_version: 'string',
|
||||
release: 'experimental',
|
||||
conditions: { kibana: { version: 'x.y.z' } },
|
||||
owner: { github: 'elastic/fleet' },
|
||||
policy_templates: [
|
||||
{
|
||||
name: 'template_0001',
|
||||
title: 'Template 1',
|
||||
description: 'Template 1',
|
||||
inputs: [
|
||||
{
|
||||
type: 'logs',
|
||||
title: 'Log',
|
||||
description: 'Log Input',
|
||||
vars: [
|
||||
{
|
||||
name: 'path',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'path_2',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as any,
|
||||
assetsMap: new Map(),
|
||||
paths: [],
|
||||
};
|
||||
appContextService.start(
|
||||
createAppContextStartContractMock({
|
||||
internal: {
|
||||
disableILMPolicies: true,
|
||||
fleetServerStandalone: false,
|
||||
onlyAllowAgentUpgradeToKnownVersions: false,
|
||||
retrySetupOnBoot: false,
|
||||
registry: {
|
||||
kibanaVersionCheckEnabled: true,
|
||||
capabilities: [],
|
||||
excludePackages: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
const mockInstalledPackageSo = getMockInstalledPackageSo([
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
]);
|
||||
const installedPkg = {
|
||||
...mockInstalledPackageSo,
|
||||
attributes: {
|
||||
...mockInstalledPackageSo.attributes,
|
||||
install_started_at: new Date(Date.now() - 1000).toISOString(),
|
||||
},
|
||||
};
|
||||
await stepInstallIndexTemplatePipelines({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext,
|
||||
installedPkg,
|
||||
installType: 'install',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(mockedInstallIndexTemplatesAndPipelines).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('Should not call installIndexTemplatesAndPipelines if packageInfo type is input and installedPkg does not exist', async () => {
|
||||
const packageInstallContext = {
|
||||
packageInfo: {
|
||||
title: 'title',
|
||||
name: 'xyz',
|
||||
version: '4.5.6',
|
||||
description: 'test',
|
||||
type: 'input',
|
||||
categories: ['cloud'],
|
||||
format_version: 'string',
|
||||
release: 'experimental',
|
||||
conditions: { kibana: { version: 'x.y.z' } },
|
||||
owner: { github: 'elastic/fleet' },
|
||||
policy_templates: [
|
||||
{
|
||||
name: 'template_0001',
|
||||
title: 'Template 1',
|
||||
description: 'Template 1',
|
||||
inputs: [
|
||||
{
|
||||
type: 'logs',
|
||||
title: 'Log',
|
||||
description: 'Log Input',
|
||||
vars: [
|
||||
{
|
||||
name: 'path',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'path_2',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as any,
|
||||
assetsMap: new Map(),
|
||||
paths: [],
|
||||
};
|
||||
appContextService.start(
|
||||
createAppContextStartContractMock({
|
||||
internal: {
|
||||
disableILMPolicies: true,
|
||||
fleetServerStandalone: false,
|
||||
onlyAllowAgentUpgradeToKnownVersions: false,
|
||||
retrySetupOnBoot: false,
|
||||
registry: {
|
||||
kibanaVersionCheckEnabled: true,
|
||||
capabilities: [],
|
||||
excludePackages: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await stepInstallIndexTemplatePipelines({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext,
|
||||
installType: 'install',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(mockedInstallIndexTemplatesAndPipelines).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('Should not call installIndexTemplatesAndPipelines if packageInfo type is undefined', async () => {
|
||||
const packageInstallContext = {
|
||||
packageInfo: {
|
||||
title: 'title',
|
||||
name: 'xyz',
|
||||
version: '4.5.6',
|
||||
description: 'test',
|
||||
type: undefined,
|
||||
categories: ['cloud'],
|
||||
format_version: 'string',
|
||||
release: 'experimental',
|
||||
conditions: { kibana: { version: 'x.y.z' } },
|
||||
owner: { github: 'elastic/fleet' },
|
||||
} as any,
|
||||
assetsMap: new Map(),
|
||||
paths: [],
|
||||
};
|
||||
appContextService.start(
|
||||
createAppContextStartContractMock({
|
||||
internal: {
|
||||
disableILMPolicies: true,
|
||||
fleetServerStandalone: false,
|
||||
onlyAllowAgentUpgradeToKnownVersions: false,
|
||||
retrySetupOnBoot: false,
|
||||
registry: {
|
||||
kibanaVersionCheckEnabled: true,
|
||||
capabilities: [],
|
||||
excludePackages: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await stepInstallIndexTemplatePipelines({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext,
|
||||
installType: 'install',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
esReferences: [],
|
||||
});
|
||||
expect(mockedInstallIndexTemplatesAndPipelines).not.toBeCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getNormalizedDataStreams } from '../../../../../../common/services';
|
||||
|
||||
import { installIndexTemplatesAndPipelines } from '../../install_index_template_pipeline';
|
||||
|
||||
import type { InstallContext } from '../_state_machine_package_install';
|
||||
|
||||
export async function stepInstallIndexTemplatePipelines(context: InstallContext) {
|
||||
const { esClient, savedObjectsClient, packageInstallContext, logger, installedPkg } = context;
|
||||
const { packageInfo } = packageInstallContext;
|
||||
const esReferences = context.esReferences ?? [];
|
||||
|
||||
if (packageInfo.type === 'integration') {
|
||||
logger.debug(
|
||||
`Package install - Installing index templates and pipelines, packageInfo.type: ${packageInfo.type}`
|
||||
);
|
||||
const { installedTemplates, esReferences: templateEsReferences } =
|
||||
await installIndexTemplatesAndPipelines({
|
||||
installedPkg: installedPkg ? installedPkg.attributes : undefined,
|
||||
packageInstallContext,
|
||||
esClient,
|
||||
savedObjectsClient,
|
||||
logger,
|
||||
esReferences,
|
||||
});
|
||||
return {
|
||||
esReferences: templateEsReferences,
|
||||
indexTemplates: installedTemplates,
|
||||
};
|
||||
}
|
||||
|
||||
if (packageInfo.type === 'input' && installedPkg) {
|
||||
// input packages create their data streams during package policy creation
|
||||
// we must use installed_es to infer which streams exist first then
|
||||
// we can install the new index templates
|
||||
logger.debug(`Package install - packageInfo.type: ${packageInfo.type}`);
|
||||
const dataStreamNames = installedPkg.attributes.installed_es
|
||||
.filter((ref) => ref.type === 'index_template')
|
||||
// index templates are named {type}-{dataset}, remove everything before first hyphen
|
||||
.map((ref) => ref.id.replace(/^[^-]+-/, ''));
|
||||
|
||||
const dataStreams = dataStreamNames.flatMap((dataStreamName) =>
|
||||
getNormalizedDataStreams(packageInfo, dataStreamName)
|
||||
);
|
||||
|
||||
if (dataStreams.length) {
|
||||
const { installedTemplates, esReferences: templateEsReferences } =
|
||||
await installIndexTemplatesAndPipelines({
|
||||
installedPkg: installedPkg ? installedPkg.attributes : undefined,
|
||||
packageInstallContext,
|
||||
esClient,
|
||||
savedObjectsClient,
|
||||
logger,
|
||||
esReferences,
|
||||
onlyForDataStreams: dataStreams,
|
||||
});
|
||||
return { esReferences: templateEsReferences, indexTemplates: installedTemplates };
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { SavedObjectsClientContract, ElasticsearchClient } from '@kbn/core/server';
|
||||
import { savedObjectsClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants';
|
||||
|
||||
import { appContextService } from '../../../../app_context';
|
||||
import { createAppContextStartContractMock } from '../../../../../mocks';
|
||||
import { installKibanaAssetsAndReferences } from '../../../kibana/assets/install';
|
||||
|
||||
jest.mock('../../../kibana/assets/install');
|
||||
|
||||
import { stepInstallKibanaAssets } from './step_install_kibana_assets';
|
||||
|
||||
const mockedInstallKibanaAssetsAndReferences =
|
||||
installKibanaAssetsAndReferences as jest.MockedFunction<typeof installKibanaAssetsAndReferences>;
|
||||
|
||||
describe('stepInstallKibanaAssets', () => {
|
||||
let soClient: jest.Mocked<SavedObjectsClientContract>;
|
||||
let esClient: jest.Mocked<ElasticsearchClient>;
|
||||
|
||||
beforeEach(async () => {
|
||||
soClient = savedObjectsClientMock.create();
|
||||
|
||||
soClient.update.mockImplementation(async (type, id, attributes) => {
|
||||
return { id, attributes } as any;
|
||||
});
|
||||
soClient.get.mockImplementation(async (type, id) => {
|
||||
return { id, attributes: {} } as any;
|
||||
});
|
||||
});
|
||||
|
||||
esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
appContextService.start(createAppContextStartContractMock());
|
||||
|
||||
it('Should call installKibanaAssetsAndReferences', async () => {
|
||||
const installationPromise = stepInstallKibanaAssets({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext: {
|
||||
assetsMap: new Map(),
|
||||
paths: [],
|
||||
packageInfo: {
|
||||
title: 'title',
|
||||
name: 'xyz',
|
||||
version: '4.5.6',
|
||||
description: 'test',
|
||||
type: 'integration',
|
||||
categories: ['cloud', 'custom'],
|
||||
format_version: 'string',
|
||||
release: 'experimental',
|
||||
conditions: { kibana: { version: 'x.y.z' } },
|
||||
owner: { github: 'elastic/fleet' },
|
||||
},
|
||||
},
|
||||
installType: 'install',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
});
|
||||
|
||||
await expect(installationPromise).resolves.not.toThrowError();
|
||||
expect(mockedInstallKibanaAssetsAndReferences).toBeCalledTimes(1);
|
||||
});
|
||||
esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
appContextService.start(createAppContextStartContractMock());
|
||||
|
||||
it('Should correctly handle errors', async () => {
|
||||
// force errors from this function
|
||||
mockedInstallKibanaAssetsAndReferences.mockImplementation(async () => {
|
||||
throw new Error('mocked async error A: should be caught');
|
||||
});
|
||||
|
||||
const installationPromise = stepInstallKibanaAssets({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext: {
|
||||
assetsMap: new Map(),
|
||||
paths: [],
|
||||
packageInfo: {
|
||||
title: 'title',
|
||||
name: 'xyz',
|
||||
version: '4.5.6',
|
||||
description: 'test',
|
||||
type: 'integration',
|
||||
categories: ['cloud', 'custom'],
|
||||
format_version: 'string',
|
||||
release: 'experimental',
|
||||
conditions: { kibana: { version: 'x.y.z' } },
|
||||
owner: { github: 'elastic/fleet' },
|
||||
},
|
||||
},
|
||||
installType: 'install',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
});
|
||||
await expect(installationPromise).resolves.not.toThrowError();
|
||||
await expect(installationPromise).resolves.not.toThrowError();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { installKibanaAssetsAndReferences } from '../../../kibana/assets/install';
|
||||
|
||||
import { withPackageSpan } from '../../utils';
|
||||
|
||||
import type { InstallContext } from '../_state_machine_package_install';
|
||||
|
||||
export async function stepInstallKibanaAssets(context: InstallContext) {
|
||||
const {
|
||||
savedObjectsClient,
|
||||
savedObjectsImporter,
|
||||
savedObjectTagAssignmentService,
|
||||
savedObjectTagClient,
|
||||
logger,
|
||||
installedPkg,
|
||||
packageInstallContext,
|
||||
spaceId,
|
||||
} = context;
|
||||
const { packageInfo } = packageInstallContext;
|
||||
const { name: pkgName, title: pkgTitle } = packageInfo;
|
||||
|
||||
const kibanaAssetPromise = withPackageSpan('Install Kibana assets', () =>
|
||||
installKibanaAssetsAndReferences({
|
||||
savedObjectsClient,
|
||||
savedObjectsImporter,
|
||||
savedObjectTagAssignmentService,
|
||||
savedObjectTagClient,
|
||||
pkgName,
|
||||
pkgTitle,
|
||||
packageInstallContext,
|
||||
installedPkg,
|
||||
logger,
|
||||
spaceId,
|
||||
assetTags: packageInfo?.asset_tags,
|
||||
})
|
||||
);
|
||||
// Necessary to avoid async promise rejection warning
|
||||
// See https://stackoverflow.com/questions/40920179/should-i-refrain-from-handling-promise-rejection-asynchronously
|
||||
kibanaAssetPromise.catch(() => {});
|
||||
|
||||
return { kibanaAssetPromise };
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type {
|
||||
SavedObjectsClientContract,
|
||||
ElasticsearchClient,
|
||||
SavedObject,
|
||||
} from '@kbn/core/server';
|
||||
import { savedObjectsClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants';
|
||||
|
||||
import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../../common/constants';
|
||||
import { ElasticsearchAssetType } from '../../../../../types';
|
||||
|
||||
import type { EsAssetReference, Installation } from '../../../../../../common';
|
||||
import { appContextService } from '../../../../app_context';
|
||||
import { createAppContextStartContractMock } from '../../../../../mocks';
|
||||
import { installMlModel } from '../../../elasticsearch/ml_model';
|
||||
|
||||
import { stepInstallMlModel } from './step_install_mlmodel';
|
||||
|
||||
jest.mock('../../../elasticsearch/ml_model');
|
||||
|
||||
const mockedInstallMlModel = installMlModel as jest.MockedFunction<typeof installMlModel>;
|
||||
|
||||
describe('stepInstallMlModel', () => {
|
||||
let soClient: jest.Mocked<SavedObjectsClientContract>;
|
||||
let esClient: jest.Mocked<ElasticsearchClient>;
|
||||
const getMockInstalledPackageSo = (
|
||||
installedEs: EsAssetReference[] = []
|
||||
): SavedObject<Installation> => {
|
||||
return {
|
||||
id: 'mocked-package',
|
||||
attributes: {
|
||||
name: 'test-package',
|
||||
version: '1.0.0',
|
||||
install_status: 'installing',
|
||||
install_version: '1.0.0',
|
||||
install_started_at: new Date().toISOString(),
|
||||
install_source: 'registry',
|
||||
verification_status: 'verified',
|
||||
installed_kibana: [] as any,
|
||||
installed_es: installedEs,
|
||||
es_index_patterns: {},
|
||||
},
|
||||
type: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
references: [],
|
||||
};
|
||||
};
|
||||
beforeEach(async () => {
|
||||
soClient = savedObjectsClientMock.create();
|
||||
esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
appContextService.start(createAppContextStartContractMock());
|
||||
});
|
||||
afterEach(async () => {
|
||||
jest.mocked(mockedInstallMlModel).mockReset();
|
||||
});
|
||||
|
||||
const packageInstallContext = {
|
||||
packageInfo: {
|
||||
title: 'title',
|
||||
name: 'test-package',
|
||||
version: '1.0.0',
|
||||
description: 'test',
|
||||
type: 'integration',
|
||||
categories: ['cloud', 'custom'],
|
||||
format_version: 'string',
|
||||
release: 'experimental',
|
||||
conditions: { kibana: { version: 'x.y.z' } },
|
||||
owner: { github: 'elastic/fleet' },
|
||||
} as any,
|
||||
paths: ['some/path/1', 'some/path/2'],
|
||||
assetsMap: new Map(),
|
||||
};
|
||||
appContextService.start(
|
||||
createAppContextStartContractMock({
|
||||
internal: {
|
||||
disableILMPolicies: true,
|
||||
fleetServerStandalone: false,
|
||||
onlyAllowAgentUpgradeToKnownVersions: false,
|
||||
retrySetupOnBoot: false,
|
||||
registry: {
|
||||
kibanaVersionCheckEnabled: true,
|
||||
capabilities: [],
|
||||
excludePackages: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
const mockInstalledPackageSo = getMockInstalledPackageSo();
|
||||
const installedPkg = {
|
||||
...mockInstalledPackageSo,
|
||||
attributes: {
|
||||
...mockInstalledPackageSo.attributes,
|
||||
install_started_at: new Date(Date.now() - 1000).toISOString(),
|
||||
},
|
||||
};
|
||||
|
||||
it('Should update esReferences', async () => {
|
||||
jest.mocked(mockedInstallMlModel).mockResolvedValue([]);
|
||||
const res = await stepInstallMlModel({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext,
|
||||
installedPkg,
|
||||
installType: 'update',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(mockedInstallMlModel).toHaveBeenCalled();
|
||||
expect(res.esReferences).toEqual([]);
|
||||
});
|
||||
|
||||
it('Should call installTransforms and return updated esReferences', async () => {
|
||||
jest.mocked(mockedInstallMlModel).mockResolvedValue([
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
]);
|
||||
const res = await stepInstallMlModel({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext,
|
||||
installedPkg,
|
||||
installType: 'update',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
esReferences: [],
|
||||
});
|
||||
expect(mockedInstallMlModel).toHaveBeenCalled();
|
||||
expect(res.esReferences).toEqual([
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { installMlModel } from '../../../elasticsearch/ml_model';
|
||||
|
||||
import { withPackageSpan } from '../../utils';
|
||||
|
||||
import type { InstallContext } from '../_state_machine_package_install';
|
||||
|
||||
export async function stepInstallMlModel(context: InstallContext) {
|
||||
const { logger, packageInstallContext, esClient, savedObjectsClient } = context;
|
||||
let esReferences = context.esReferences ?? [];
|
||||
|
||||
esReferences = await withPackageSpan('Install ML models', () =>
|
||||
installMlModel(packageInstallContext, esClient, savedObjectsClient, logger, esReferences)
|
||||
);
|
||||
return { esReferences };
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type {
|
||||
SavedObjectsClientContract,
|
||||
ElasticsearchClient,
|
||||
SavedObject,
|
||||
} from '@kbn/core/server';
|
||||
import { savedObjectsClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants';
|
||||
|
||||
import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../../common/constants';
|
||||
import { ElasticsearchAssetType } from '../../../../../types';
|
||||
|
||||
import type { EsAssetReference, Installation } from '../../../../../../common';
|
||||
import { appContextService } from '../../../../app_context';
|
||||
import { createAppContextStartContractMock } from '../../../../../mocks';
|
||||
import { installTransforms } from '../../../elasticsearch/transform/install';
|
||||
|
||||
import { stepInstallTransforms } from './step_install_transforms';
|
||||
|
||||
jest.mock('../../../elasticsearch/transform/install');
|
||||
|
||||
const mockedInstallTransforms = installTransforms as jest.MockedFunction<typeof installTransforms>;
|
||||
|
||||
describe('stepInstallTransforms', () => {
|
||||
let soClient: jest.Mocked<SavedObjectsClientContract>;
|
||||
let esClient: jest.Mocked<ElasticsearchClient>;
|
||||
const getMockInstalledPackageSo = (
|
||||
installedEs: EsAssetReference[] = []
|
||||
): SavedObject<Installation> => {
|
||||
return {
|
||||
id: 'mocked-package',
|
||||
attributes: {
|
||||
name: 'test-package',
|
||||
version: '1.0.0',
|
||||
install_status: 'installing',
|
||||
install_version: '1.0.0',
|
||||
install_started_at: new Date().toISOString(),
|
||||
install_source: 'registry',
|
||||
verification_status: 'verified',
|
||||
installed_kibana: [] as any,
|
||||
installed_es: installedEs,
|
||||
es_index_patterns: {},
|
||||
},
|
||||
type: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
references: [],
|
||||
};
|
||||
};
|
||||
beforeEach(async () => {
|
||||
soClient = savedObjectsClientMock.create();
|
||||
esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
appContextService.start(createAppContextStartContractMock());
|
||||
});
|
||||
afterEach(async () => {
|
||||
jest.mocked(mockedInstallTransforms).mockReset();
|
||||
});
|
||||
|
||||
const packageInstallContext = {
|
||||
packageInfo: {
|
||||
title: 'title',
|
||||
name: 'test-package',
|
||||
version: '1.0.0',
|
||||
description: 'test',
|
||||
type: 'integration',
|
||||
categories: ['cloud', 'custom'],
|
||||
format_version: 'string',
|
||||
release: 'experimental',
|
||||
conditions: { kibana: { version: 'x.y.z' } },
|
||||
owner: { github: 'elastic/fleet' },
|
||||
} as any,
|
||||
paths: ['some/path/1', 'some/path/2'],
|
||||
assetsMap: new Map(),
|
||||
};
|
||||
appContextService.start(
|
||||
createAppContextStartContractMock({
|
||||
internal: {
|
||||
disableILMPolicies: true,
|
||||
fleetServerStandalone: false,
|
||||
onlyAllowAgentUpgradeToKnownVersions: false,
|
||||
retrySetupOnBoot: false,
|
||||
registry: {
|
||||
kibanaVersionCheckEnabled: true,
|
||||
capabilities: [],
|
||||
excludePackages: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
const mockInstalledPackageSo = getMockInstalledPackageSo();
|
||||
const installedPkg = {
|
||||
...mockInstalledPackageSo,
|
||||
attributes: {
|
||||
...mockInstalledPackageSo.attributes,
|
||||
install_started_at: new Date(Date.now() - 1000).toISOString(),
|
||||
},
|
||||
};
|
||||
|
||||
it('Should update esReferences', async () => {
|
||||
jest.mocked(mockedInstallTransforms).mockResolvedValue({
|
||||
installedTransforms: [],
|
||||
esReferences: [],
|
||||
});
|
||||
const res = await stepInstallTransforms({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext,
|
||||
installedPkg,
|
||||
installType: 'update',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(mockedInstallTransforms).toHaveBeenCalled();
|
||||
expect(res.esReferences).toEqual([]);
|
||||
});
|
||||
|
||||
it('Should call installTransforms and return updated esReferences', async () => {
|
||||
jest.mocked(mockedInstallTransforms).mockResolvedValue({
|
||||
installedTransforms: [],
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
const res = await stepInstallTransforms({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext,
|
||||
installedPkg,
|
||||
installType: 'update',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
esReferences: [],
|
||||
});
|
||||
expect(mockedInstallTransforms).toHaveBeenCalled();
|
||||
expect(res.esReferences).toEqual([
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { installTransforms } from '../../../elasticsearch/transform/install';
|
||||
|
||||
import { withPackageSpan } from '../../utils';
|
||||
|
||||
import type { InstallContext } from '../_state_machine_package_install';
|
||||
|
||||
export async function stepInstallTransforms(context: InstallContext) {
|
||||
const {
|
||||
packageInstallContext,
|
||||
esClient,
|
||||
savedObjectsClient,
|
||||
logger,
|
||||
force,
|
||||
authorizationHeader,
|
||||
} = context;
|
||||
let esReferences = context.esReferences ?? [];
|
||||
|
||||
({ esReferences } = await withPackageSpan('Install transforms', () =>
|
||||
installTransforms({
|
||||
packageInstallContext,
|
||||
esClient,
|
||||
savedObjectsClient,
|
||||
logger,
|
||||
esReferences,
|
||||
force,
|
||||
authorizationHeader,
|
||||
})
|
||||
));
|
||||
return { esReferences };
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type {
|
||||
SavedObjectsClientContract,
|
||||
ElasticsearchClient,
|
||||
SavedObject,
|
||||
} from '@kbn/core/server';
|
||||
import {
|
||||
savedObjectsClientMock,
|
||||
elasticsearchServiceMock,
|
||||
loggingSystemMock,
|
||||
} from '@kbn/core/server/mocks';
|
||||
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants';
|
||||
|
||||
import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../../common/constants';
|
||||
import { ElasticsearchAssetType } from '../../../../../types';
|
||||
|
||||
import type { EsAssetReference, Installation } from '../../../../../../common';
|
||||
import { appContextService } from '../../../../app_context';
|
||||
import { createAppContextStartContractMock } from '../../../../../mocks';
|
||||
import { removeLegacyTemplates } from '../../../elasticsearch/template/remove_legacy';
|
||||
|
||||
import { stepRemoveLegacyTemplates } from './step_remove_legacy_templates';
|
||||
|
||||
jest.mock('../../../elasticsearch/template/remove_legacy');
|
||||
|
||||
const mockedRemoveLegacyTemplates = removeLegacyTemplates as jest.MockedFunction<
|
||||
typeof removeLegacyTemplates
|
||||
>;
|
||||
|
||||
describe('stepRemoveLegacyTemplates', () => {
|
||||
let soClient: jest.Mocked<SavedObjectsClientContract>;
|
||||
let esClient: jest.Mocked<ElasticsearchClient>;
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
|
||||
const getMockInstalledPackageSo = (
|
||||
installedEs: EsAssetReference[] = []
|
||||
): SavedObject<Installation> => {
|
||||
return {
|
||||
id: 'mocked-package',
|
||||
attributes: {
|
||||
name: 'test-package',
|
||||
version: '1.0.0',
|
||||
install_status: 'installing',
|
||||
install_version: '1.0.0',
|
||||
install_started_at: new Date().toISOString(),
|
||||
install_source: 'registry',
|
||||
verification_status: 'verified',
|
||||
installed_kibana: [] as any,
|
||||
installed_es: installedEs,
|
||||
es_index_patterns: {},
|
||||
},
|
||||
type: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
references: [],
|
||||
};
|
||||
};
|
||||
beforeEach(async () => {
|
||||
soClient = savedObjectsClientMock.create();
|
||||
esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
appContextService.start(createAppContextStartContractMock());
|
||||
});
|
||||
afterEach(async () => {
|
||||
jest.mocked(mockedRemoveLegacyTemplates).mockReset();
|
||||
});
|
||||
|
||||
const packageInstallContext = {
|
||||
packageInfo: {
|
||||
title: 'title',
|
||||
name: 'test-package',
|
||||
version: '1.0.0',
|
||||
description: 'test',
|
||||
type: 'integration',
|
||||
categories: ['cloud', 'custom'],
|
||||
format_version: 'string',
|
||||
release: 'experimental',
|
||||
conditions: { kibana: { version: 'x.y.z' } },
|
||||
owner: { github: 'elastic/fleet' },
|
||||
} as any,
|
||||
paths: ['some/path/1', 'some/path/2'],
|
||||
assetsMap: new Map(),
|
||||
};
|
||||
appContextService.start(
|
||||
createAppContextStartContractMock({
|
||||
internal: {
|
||||
disableILMPolicies: true,
|
||||
fleetServerStandalone: false,
|
||||
onlyAllowAgentUpgradeToKnownVersions: false,
|
||||
retrySetupOnBoot: false,
|
||||
registry: {
|
||||
kibanaVersionCheckEnabled: true,
|
||||
capabilities: [],
|
||||
excludePackages: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
const mockInstalledPackageSo = getMockInstalledPackageSo();
|
||||
const installedPkg = {
|
||||
...mockInstalledPackageSo,
|
||||
attributes: {
|
||||
...mockInstalledPackageSo.attributes,
|
||||
install_started_at: new Date(Date.now() - 1000).toISOString(),
|
||||
},
|
||||
};
|
||||
|
||||
it('Should call removeLegacyTemplates', async () => {
|
||||
await stepRemoveLegacyTemplates({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger,
|
||||
packageInstallContext,
|
||||
installedPkg,
|
||||
installType: 'update',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(mockedRemoveLegacyTemplates).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Should catch the error when removeLegacyTemplates fails', async () => {
|
||||
jest.mocked(mockedRemoveLegacyTemplates).mockRejectedValue(Error('Error!'));
|
||||
await stepRemoveLegacyTemplates({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger,
|
||||
packageInstallContext,
|
||||
installedPkg,
|
||||
installType: 'update',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(mockedRemoveLegacyTemplates).toHaveBeenCalled();
|
||||
expect(logger.warn).toHaveBeenCalledWith('Error removing legacy templates: Error!');
|
||||
});
|
||||
});
|
|
@ -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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { removeLegacyTemplates } from '../../../elasticsearch/template/remove_legacy';
|
||||
|
||||
import type { InstallContext } from '../_state_machine_package_install';
|
||||
|
||||
export async function stepRemoveLegacyTemplates(context: InstallContext) {
|
||||
const { esClient, packageInstallContext, logger } = context;
|
||||
const { packageInfo } = packageInstallContext;
|
||||
try {
|
||||
await removeLegacyTemplates({ packageInfo, esClient, logger });
|
||||
} catch (e) {
|
||||
logger.warn(`Error removing legacy templates: ${e.message}`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { InstallContext } from '../_state_machine_package_install';
|
||||
|
||||
export async function stepResolveKibanaPromise(context: InstallContext) {
|
||||
const { kibanaAssetPromise } = context;
|
||||
const installedKibanaAssetsRefs = await kibanaAssetPromise;
|
||||
|
||||
return { installedKibanaAssetsRefs };
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type {
|
||||
SavedObjectsClientContract,
|
||||
ElasticsearchClient,
|
||||
SavedObject,
|
||||
} from '@kbn/core/server';
|
||||
import { savedObjectsClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants';
|
||||
|
||||
import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../../common/constants';
|
||||
import { ElasticsearchAssetType } from '../../../../../types';
|
||||
|
||||
import type { EsAssetReference, Installation } from '../../../../../../common';
|
||||
import { appContextService } from '../../../../app_context';
|
||||
import { createAppContextStartContractMock } from '../../../../../mocks';
|
||||
import { saveArchiveEntriesFromAssetsMap } from '../../../archive/storage';
|
||||
|
||||
import { stepSaveArchiveEntries } from './step_save_archive_entries';
|
||||
|
||||
jest.mock('../../../archive/storage');
|
||||
|
||||
const mockedSaveArchiveEntriesFromAssetsMap =
|
||||
saveArchiveEntriesFromAssetsMap as jest.MockedFunction<typeof saveArchiveEntriesFromAssetsMap>;
|
||||
|
||||
describe('stepSaveArchiveEntries', () => {
|
||||
let soClient: jest.Mocked<SavedObjectsClientContract>;
|
||||
let esClient: jest.Mocked<ElasticsearchClient>;
|
||||
const getMockInstalledPackageSo = (
|
||||
installedEs: EsAssetReference[] = []
|
||||
): SavedObject<Installation> => {
|
||||
return {
|
||||
id: 'mocked-package',
|
||||
attributes: {
|
||||
name: 'test-package',
|
||||
version: '1.0.0',
|
||||
install_status: 'installing',
|
||||
install_version: '1.0.0',
|
||||
install_started_at: new Date().toISOString(),
|
||||
install_source: 'registry',
|
||||
verification_status: 'verified',
|
||||
installed_kibana: [] as any,
|
||||
installed_es: installedEs,
|
||||
es_index_patterns: {},
|
||||
},
|
||||
type: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
references: [],
|
||||
};
|
||||
};
|
||||
beforeEach(async () => {
|
||||
soClient = savedObjectsClientMock.create();
|
||||
esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
appContextService.start(createAppContextStartContractMock());
|
||||
});
|
||||
afterEach(async () => {
|
||||
jest.mocked(mockedSaveArchiveEntriesFromAssetsMap).mockReset();
|
||||
});
|
||||
|
||||
const packageInstallContext = {
|
||||
packageInfo: {
|
||||
title: 'title',
|
||||
name: 'test-package',
|
||||
version: '1.0.0',
|
||||
description: 'test',
|
||||
type: 'integration',
|
||||
categories: ['cloud', 'custom'],
|
||||
format_version: 'string',
|
||||
release: 'experimental',
|
||||
conditions: { kibana: { version: 'x.y.z' } },
|
||||
owner: { github: 'elastic/fleet' },
|
||||
} as any,
|
||||
paths: ['some/path/1', 'some/path/2'],
|
||||
assetsMap: new Map([
|
||||
[
|
||||
'endpoint-0.16.0-dev.0/elasticsearch/transform/metadata_current/default.json',
|
||||
Buffer.from('{"content": "data"}'),
|
||||
],
|
||||
]),
|
||||
};
|
||||
appContextService.start(
|
||||
createAppContextStartContractMock({
|
||||
internal: {
|
||||
disableILMPolicies: true,
|
||||
fleetServerStandalone: false,
|
||||
onlyAllowAgentUpgradeToKnownVersions: false,
|
||||
retrySetupOnBoot: false,
|
||||
registry: {
|
||||
kibanaVersionCheckEnabled: true,
|
||||
capabilities: [],
|
||||
excludePackages: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
const mockInstalledPackageSo = getMockInstalledPackageSo();
|
||||
const installedPkg = {
|
||||
...mockInstalledPackageSo,
|
||||
attributes: {
|
||||
...mockInstalledPackageSo.attributes,
|
||||
install_started_at: new Date(Date.now() - 1000).toISOString(),
|
||||
},
|
||||
};
|
||||
|
||||
it('Should return empty packageAssetRefs if saved_objects were not found', async () => {
|
||||
jest.mocked(mockedSaveArchiveEntriesFromAssetsMap).mockResolvedValue({
|
||||
saved_objects: [],
|
||||
});
|
||||
const res = await stepSaveArchiveEntries({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext,
|
||||
installedPkg,
|
||||
installType: 'update',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
packageAssetRefs: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('Should return packageAssetRefs', async () => {
|
||||
jest.mocked(mockedSaveArchiveEntriesFromAssetsMap).mockResolvedValue({
|
||||
saved_objects: [
|
||||
{
|
||||
id: 'test',
|
||||
attributes: {
|
||||
package_name: 'test-package',
|
||||
package_version: '1.0.0',
|
||||
install_source: 'registry',
|
||||
asset_path: 'some/path',
|
||||
media_type: '',
|
||||
data_utf8: '',
|
||||
data_base64: '',
|
||||
},
|
||||
type: '',
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
const res = await stepSaveArchiveEntries({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext,
|
||||
installedPkg,
|
||||
installType: 'update',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
esReferences: [
|
||||
{
|
||||
id: 'something',
|
||||
type: ElasticsearchAssetType.ilmPolicy,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
packageAssetRefs: [
|
||||
{
|
||||
id: 'test',
|
||||
type: 'epm-packages-assets',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ASSETS_SAVED_OBJECT_TYPE } from '../../../../../constants';
|
||||
import type { PackageAssetReference } from '../../../../../types';
|
||||
|
||||
import { saveArchiveEntriesFromAssetsMap } from '../../../archive/storage';
|
||||
|
||||
import { withPackageSpan } from '../../utils';
|
||||
|
||||
import type { InstallContext } from '../_state_machine_package_install';
|
||||
|
||||
export async function stepSaveArchiveEntries(context: InstallContext) {
|
||||
const { packageInstallContext, savedObjectsClient, installSource } = context;
|
||||
|
||||
const { packageInfo } = packageInstallContext;
|
||||
|
||||
const packageAssetResults = await withPackageSpan('Update archive entries', () =>
|
||||
saveArchiveEntriesFromAssetsMap({
|
||||
savedObjectsClient,
|
||||
assetsMap: packageInstallContext?.assetsMap,
|
||||
paths: packageInstallContext?.paths,
|
||||
packageInfo,
|
||||
installSource,
|
||||
})
|
||||
);
|
||||
const packageAssetRefs: PackageAssetReference[] = packageAssetResults.saved_objects.map(
|
||||
(result) => ({
|
||||
id: result.id,
|
||||
type: ASSETS_SAVED_OBJECT_TYPE,
|
||||
})
|
||||
);
|
||||
|
||||
return { packageAssetRefs };
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { SavedObjectsClientContract, ElasticsearchClient } from '@kbn/core/server';
|
||||
import {
|
||||
savedObjectsClientMock,
|
||||
elasticsearchServiceMock,
|
||||
loggingSystemMock,
|
||||
} from '@kbn/core/server/mocks';
|
||||
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants';
|
||||
|
||||
import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../../common/constants';
|
||||
|
||||
import { appContextService } from '../../../../app_context';
|
||||
import { createAppContextStartContractMock } from '../../../../../mocks';
|
||||
|
||||
import { auditLoggingService } from '../../../../audit_logging';
|
||||
import { packagePolicyService } from '../../../../package_policy';
|
||||
|
||||
import { stepSaveSystemObject } from './step_save_system_object';
|
||||
|
||||
jest.mock('../../../../audit_logging');
|
||||
const mockedAuditLoggingService = auditLoggingService as jest.Mocked<typeof auditLoggingService>;
|
||||
|
||||
jest.mock('../../../../package_policy');
|
||||
const mockedPackagePolicyService = packagePolicyService as jest.Mocked<typeof packagePolicyService>;
|
||||
|
||||
describe('updateLatestExecutedState', () => {
|
||||
let soClient: jest.Mocked<SavedObjectsClientContract>;
|
||||
let esClient: jest.Mocked<ElasticsearchClient>;
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
|
||||
beforeEach(async () => {
|
||||
soClient = savedObjectsClientMock.create();
|
||||
esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
appContextService.start(createAppContextStartContractMock());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockedAuditLoggingService.writeCustomSoAuditLog.mockReset();
|
||||
soClient.get.mockReset();
|
||||
soClient.update.mockReset();
|
||||
});
|
||||
|
||||
it('Should save the SO and should not call packagePolicy upgrade if keep_policies_up_to_date = false', async () => {
|
||||
soClient.get.mockResolvedValue({
|
||||
id: 'test-integration',
|
||||
attributes: {
|
||||
title: 'title',
|
||||
name: 'test-integration',
|
||||
version: '1.0.0',
|
||||
install_source: 'registry',
|
||||
install_status: 'installed',
|
||||
package_assets: [],
|
||||
},
|
||||
} as any);
|
||||
|
||||
await stepSaveSystemObject({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger,
|
||||
packageInstallContext: {
|
||||
assetsMap: new Map(),
|
||||
paths: [],
|
||||
packageInfo: {
|
||||
title: 'title',
|
||||
name: 'test-integration',
|
||||
version: '1.0.0',
|
||||
description: 'test',
|
||||
type: 'integration',
|
||||
categories: ['cloud', 'custom'],
|
||||
format_version: 'string',
|
||||
release: 'experimental',
|
||||
conditions: { kibana: { version: 'x.y.z' } },
|
||||
owner: { github: 'elastic/fleet' },
|
||||
},
|
||||
},
|
||||
installType: 'install',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
});
|
||||
|
||||
expect(soClient.update.mock.calls).toEqual([
|
||||
[
|
||||
'epm-packages',
|
||||
'test-integration',
|
||||
{
|
||||
install_format_schema_version: '1.2.0',
|
||||
install_status: 'installed',
|
||||
install_version: '1.0.0',
|
||||
latest_install_failed_attempts: [],
|
||||
package_assets: undefined,
|
||||
version: '1.0.0',
|
||||
},
|
||||
],
|
||||
]);
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenCalledWith({
|
||||
action: 'update',
|
||||
id: 'test-integration',
|
||||
savedObjectType: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
expect(mockedPackagePolicyService.upgrade).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('Should call packagePolicy upgrade if keep_policies_up_to_date = true', async () => {
|
||||
soClient.get.mockResolvedValue({
|
||||
id: 'test-integration',
|
||||
attributes: {
|
||||
title: 'title',
|
||||
name: 'test-integration',
|
||||
version: '1.0.0',
|
||||
install_source: 'registry',
|
||||
install_status: 'installed',
|
||||
package_assets: [],
|
||||
keep_policies_up_to_date: true,
|
||||
},
|
||||
} as any);
|
||||
mockedPackagePolicyService.listIds.mockReturnValue({
|
||||
items: ['packagePolicy1', 'packagePolicy2'],
|
||||
} as any);
|
||||
|
||||
await stepSaveSystemObject({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger,
|
||||
packageInstallContext: {
|
||||
assetsMap: new Map(),
|
||||
paths: [],
|
||||
packageInfo: {
|
||||
title: 'title',
|
||||
name: 'test-integration',
|
||||
version: '1.0.0',
|
||||
description: 'test',
|
||||
type: 'integration',
|
||||
categories: ['cloud', 'custom'],
|
||||
format_version: 'string',
|
||||
release: 'experimental',
|
||||
conditions: { kibana: { version: 'x.y.z' } },
|
||||
owner: { github: 'elastic/fleet' },
|
||||
},
|
||||
},
|
||||
installType: 'install',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
});
|
||||
|
||||
expect(soClient.update.mock.calls).toEqual([
|
||||
[
|
||||
'epm-packages',
|
||||
'test-integration',
|
||||
{
|
||||
install_format_schema_version: '1.2.0',
|
||||
install_status: 'installed',
|
||||
install_version: '1.0.0',
|
||||
latest_install_failed_attempts: [],
|
||||
package_assets: undefined,
|
||||
version: '1.0.0',
|
||||
},
|
||||
],
|
||||
]);
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenCalledWith({
|
||||
action: 'update',
|
||||
id: 'test-integration',
|
||||
savedObjectType: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
expect(packagePolicyService.upgrade).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
['packagePolicy1', 'packagePolicy2']
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
PACKAGES_SAVED_OBJECT_TYPE,
|
||||
PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
SO_SEARCH_LIMIT,
|
||||
FLEET_INSTALL_FORMAT_VERSION,
|
||||
} from '../../../../../constants';
|
||||
import type { Installation } from '../../../../../types';
|
||||
|
||||
import { packagePolicyService } from '../../../..';
|
||||
|
||||
import { auditLoggingService } from '../../../../audit_logging';
|
||||
|
||||
import { withPackageSpan } from '../../utils';
|
||||
|
||||
import { clearLatestFailedAttempts } from '../../install_errors_helpers';
|
||||
|
||||
import type { InstallContext } from '../_state_machine_package_install';
|
||||
|
||||
export async function stepSaveSystemObject(context: InstallContext) {
|
||||
const {
|
||||
packageInstallContext,
|
||||
savedObjectsClient,
|
||||
logger,
|
||||
esClient,
|
||||
installedPkg,
|
||||
packageAssetRefs,
|
||||
} = context;
|
||||
const { packageInfo } = packageInstallContext;
|
||||
const { name: pkgName, version: pkgVersion } = packageInfo;
|
||||
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'update',
|
||||
id: pkgName,
|
||||
savedObjectType: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
await withPackageSpan('Update install status', () =>
|
||||
savedObjectsClient.update<Installation>(PACKAGES_SAVED_OBJECT_TYPE, pkgName, {
|
||||
version: pkgVersion,
|
||||
install_version: pkgVersion,
|
||||
install_status: 'installed',
|
||||
package_assets: packageAssetRefs,
|
||||
install_format_schema_version: FLEET_INSTALL_FORMAT_VERSION,
|
||||
latest_install_failed_attempts: clearLatestFailedAttempts(
|
||||
pkgVersion,
|
||||
installedPkg?.attributes.latest_install_failed_attempts ?? []
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
// Need to refetch the installation again to retrieve all the attributes
|
||||
const updatedPackage = await savedObjectsClient.get<Installation>(
|
||||
PACKAGES_SAVED_OBJECT_TYPE,
|
||||
pkgName
|
||||
);
|
||||
logger.debug(`Package install - Install status ${updatedPackage?.attributes?.install_status}`);
|
||||
// If the package is flagged with the `keep_policies_up_to_date` flag, upgrade its
|
||||
// associated package policies after installation
|
||||
if (updatedPackage.attributes.keep_policies_up_to_date) {
|
||||
await withPackageSpan('Upgrade package policies', async () => {
|
||||
const policyIdsToUpgrade = await packagePolicyService.listIds(savedObjectsClient, {
|
||||
page: 1,
|
||||
perPage: SO_SEARCH_LIMIT,
|
||||
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${pkgName}`,
|
||||
});
|
||||
logger.debug(
|
||||
`Package install - Package is flagged with keep_policies_up_to_date, upgrading its associated package policies ${policyIdsToUpgrade}`
|
||||
);
|
||||
await packagePolicyService.upgrade(savedObjectsClient, esClient, policyIdsToUpgrade.items);
|
||||
});
|
||||
}
|
||||
logger.debug(
|
||||
`Install status ${updatedPackage?.attributes?.install_status} - Installation complete!`
|
||||
);
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type {
|
||||
SavedObjectsClientContract,
|
||||
ElasticsearchClient,
|
||||
SavedObject,
|
||||
} from '@kbn/core/server';
|
||||
import { savedObjectsClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants';
|
||||
|
||||
import type { IndicesGetIndexTemplateIndexTemplateItem } from '@elastic/elasticsearch/lib/api/types';
|
||||
|
||||
import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../../common/constants';
|
||||
|
||||
import type { EsAssetReference, Installation } from '../../../../../../common';
|
||||
import { appContextService } from '../../../../app_context';
|
||||
import { createAppContextStartContractMock } from '../../../../../mocks';
|
||||
import { updateCurrentWriteIndices } from '../../../elasticsearch/template/template';
|
||||
|
||||
import { stepUpdateCurrentWriteIndices } from './step_update_current_write_indices';
|
||||
|
||||
jest.mock('../../../elasticsearch/template/template');
|
||||
|
||||
const mockedUpdateCurrentWriteIndices = updateCurrentWriteIndices as jest.MockedFunction<
|
||||
typeof updateCurrentWriteIndices
|
||||
>;
|
||||
|
||||
const createMockTemplate = ({ name, composedOf = [] }: { name: string; composedOf?: string[] }) =>
|
||||
({
|
||||
name,
|
||||
index_template: {
|
||||
composed_of: composedOf,
|
||||
},
|
||||
} as IndicesGetIndexTemplateIndexTemplateItem);
|
||||
|
||||
describe('stepUpdateCurrentWriteIndices', () => {
|
||||
let soClient: jest.Mocked<SavedObjectsClientContract>;
|
||||
let esClient: jest.Mocked<ElasticsearchClient>;
|
||||
const getMockInstalledPackageSo = (
|
||||
installedEs: EsAssetReference[] = []
|
||||
): SavedObject<Installation> => {
|
||||
return {
|
||||
id: 'mocked-package',
|
||||
attributes: {
|
||||
name: 'test-package',
|
||||
version: '1.0.0',
|
||||
install_status: 'installing',
|
||||
install_version: '1.0.0',
|
||||
install_started_at: new Date().toISOString(),
|
||||
install_source: 'registry',
|
||||
verification_status: 'verified',
|
||||
installed_kibana: [] as any,
|
||||
installed_es: installedEs,
|
||||
es_index_patterns: {},
|
||||
},
|
||||
type: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
references: [],
|
||||
};
|
||||
};
|
||||
beforeEach(async () => {
|
||||
soClient = savedObjectsClientMock.create();
|
||||
esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
appContextService.start(createAppContextStartContractMock());
|
||||
});
|
||||
afterEach(async () => {
|
||||
jest.mocked(mockedUpdateCurrentWriteIndices).mockReset();
|
||||
});
|
||||
|
||||
const packageInstallContext = {
|
||||
packageInfo: {
|
||||
title: 'title',
|
||||
name: 'test-package',
|
||||
version: '1.0.0',
|
||||
description: 'test',
|
||||
type: 'integration',
|
||||
categories: ['cloud', 'custom'],
|
||||
format_version: 'string',
|
||||
release: 'experimental',
|
||||
conditions: { kibana: { version: 'x.y.z' } },
|
||||
owner: { github: 'elastic/fleet' },
|
||||
} as any,
|
||||
paths: ['some/path/1', 'some/path/2'],
|
||||
assetsMap: new Map(),
|
||||
};
|
||||
appContextService.start(
|
||||
createAppContextStartContractMock({
|
||||
internal: {
|
||||
disableILMPolicies: true,
|
||||
fleetServerStandalone: false,
|
||||
onlyAllowAgentUpgradeToKnownVersions: false,
|
||||
retrySetupOnBoot: false,
|
||||
registry: {
|
||||
kibanaVersionCheckEnabled: true,
|
||||
capabilities: [],
|
||||
excludePackages: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
const mockInstalledPackageSo = getMockInstalledPackageSo();
|
||||
const installedPkg = {
|
||||
...mockInstalledPackageSo,
|
||||
attributes: {
|
||||
...mockInstalledPackageSo.attributes,
|
||||
install_started_at: new Date(Date.now() - 1000).toISOString(),
|
||||
},
|
||||
};
|
||||
|
||||
it('Should call updateCurrentWriteIndices', async () => {
|
||||
await stepUpdateCurrentWriteIndices({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext,
|
||||
installedPkg,
|
||||
installType: 'install',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
esReferences: [],
|
||||
});
|
||||
|
||||
expect(mockedUpdateCurrentWriteIndices).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
[],
|
||||
{ ignoreMappingUpdateErrors: undefined, skipDataStreamRollover: undefined }
|
||||
);
|
||||
});
|
||||
|
||||
it('Should call updateCurrentWriteIndices with passed parameters', async () => {
|
||||
const indexTemplates = [createMockTemplate({ name: 'tmpl1' })] as any;
|
||||
await stepUpdateCurrentWriteIndices({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger: loggerMock.create(),
|
||||
packageInstallContext,
|
||||
installedPkg,
|
||||
installType: 'install',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
esReferences: [],
|
||||
indexTemplates,
|
||||
ignoreMappingUpdateErrors: true,
|
||||
skipDataStreamRollover: true,
|
||||
});
|
||||
|
||||
expect(mockedUpdateCurrentWriteIndices).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
indexTemplates,
|
||||
{ ignoreMappingUpdateErrors: true, skipDataStreamRollover: true }
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { updateCurrentWriteIndices } from '../../../elasticsearch/template/template';
|
||||
|
||||
import { withPackageSpan } from '../../utils';
|
||||
|
||||
import type { InstallContext } from '../_state_machine_package_install';
|
||||
|
||||
export async function stepUpdateCurrentWriteIndices(context: InstallContext) {
|
||||
const { esClient, logger, ignoreMappingUpdateErrors, skipDataStreamRollover, indexTemplates } =
|
||||
context;
|
||||
|
||||
// update current backing indices of each data stream
|
||||
await withPackageSpan('Update write indices', () =>
|
||||
updateCurrentWriteIndices(esClient, logger, indexTemplates || [], {
|
||||
ignoreMappingUpdateErrors,
|
||||
skipDataStreamRollover,
|
||||
})
|
||||
);
|
||||
}
|
|
@ -0,0 +1,195 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
|
||||
import type {
|
||||
SavedObjectsClientContract,
|
||||
ElasticsearchClient,
|
||||
SavedObjectsUpdateResponse,
|
||||
} from '@kbn/core/server';
|
||||
import {
|
||||
savedObjectsClientMock,
|
||||
elasticsearchServiceMock,
|
||||
loggingSystemMock,
|
||||
} from '@kbn/core/server/mocks';
|
||||
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants';
|
||||
|
||||
import {
|
||||
MAX_TIME_COMPLETE_INSTALL,
|
||||
PACKAGES_SAVED_OBJECT_TYPE,
|
||||
} from '../../../../../../common/constants';
|
||||
|
||||
import { appContextService } from '../../../../app_context';
|
||||
import { createAppContextStartContractMock } from '../../../../../mocks';
|
||||
|
||||
import { INSTALL_STATES } from '../../../../../../common/types';
|
||||
|
||||
import { auditLoggingService } from '../../../../audit_logging';
|
||||
|
||||
import type { PackagePolicySOAttributes } from '../../../../../types';
|
||||
|
||||
import { updateLatestExecutedState } from './update_latest_executed_state';
|
||||
|
||||
jest.mock('../../../../audit_logging');
|
||||
const mockedAuditLoggingService = auditLoggingService as jest.Mocked<typeof auditLoggingService>;
|
||||
|
||||
describe('updateLatestExecutedState', () => {
|
||||
let soClient: jest.Mocked<SavedObjectsClientContract>;
|
||||
let esClient: jest.Mocked<ElasticsearchClient>;
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
|
||||
beforeEach(async () => {
|
||||
soClient = savedObjectsClientMock.create();
|
||||
esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
appContextService.start(createAppContextStartContractMock());
|
||||
});
|
||||
afterEach(() => {
|
||||
mockedAuditLoggingService.writeCustomSoAuditLog.mockReset();
|
||||
soClient.update.mockReset();
|
||||
});
|
||||
|
||||
it('Updates the SO after each transition', async () => {
|
||||
await updateLatestExecutedState({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger,
|
||||
packageInstallContext: {
|
||||
assetsMap: new Map(),
|
||||
paths: [],
|
||||
packageInfo: {
|
||||
title: 'title',
|
||||
name: 'xyz',
|
||||
version: '4.5.6',
|
||||
description: 'test',
|
||||
type: 'integration',
|
||||
categories: ['cloud', 'custom'],
|
||||
format_version: 'string',
|
||||
release: 'experimental',
|
||||
conditions: { kibana: { version: 'x.y.z' } },
|
||||
owner: { github: 'elastic/fleet' },
|
||||
},
|
||||
},
|
||||
latestExecutedState: {
|
||||
name: INSTALL_STATES.SAVE_ARCHIVE_ENTRIES,
|
||||
started_at: new Date(Date.now() - MAX_TIME_COMPLETE_INSTALL * 2).toISOString(),
|
||||
},
|
||||
installType: 'install',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
});
|
||||
|
||||
expect(soClient.update.mock.calls).toEqual(
|
||||
expect.objectContaining([
|
||||
[
|
||||
'epm-packages',
|
||||
'xyz',
|
||||
{
|
||||
latest_executed_state: {
|
||||
name: 'save_archive_entries_from_assets_map',
|
||||
started_at: expect.anything(),
|
||||
},
|
||||
},
|
||||
],
|
||||
])
|
||||
);
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenCalledWith({
|
||||
action: 'update',
|
||||
id: 'xyz',
|
||||
savedObjectType: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should not update the SO if the context contains concurrent installation error', async () => {
|
||||
await updateLatestExecutedState({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger,
|
||||
packageInstallContext: {
|
||||
assetsMap: new Map(),
|
||||
paths: [],
|
||||
packageInfo: {
|
||||
title: 'title',
|
||||
name: 'xyz',
|
||||
version: '4.5.6',
|
||||
description: 'test',
|
||||
type: 'integration',
|
||||
categories: ['cloud', 'custom'],
|
||||
format_version: 'string',
|
||||
release: 'experimental',
|
||||
conditions: { kibana: { version: 'x.y.z' } },
|
||||
owner: { github: 'elastic/fleet' },
|
||||
},
|
||||
},
|
||||
latestExecutedState: {
|
||||
name: INSTALL_STATES.SAVE_ARCHIVE_ENTRIES,
|
||||
started_at: new Date(Date.now() - MAX_TIME_COMPLETE_INSTALL * 2).toISOString(),
|
||||
error: `Concurrent installation or upgrade of xyz-4.5.6 detected.`,
|
||||
},
|
||||
installType: 'install',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
});
|
||||
|
||||
expect(soClient.update.mock.calls).toEqual([]);
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Should log error if the update failed', async () => {
|
||||
soClient.update.mockImplementation(
|
||||
async (
|
||||
_type: string,
|
||||
_id: string
|
||||
): Promise<SavedObjectsUpdateResponse<PackagePolicySOAttributes>> => {
|
||||
throw SavedObjectsErrorHelpers.createConflictError('abc', '123');
|
||||
}
|
||||
);
|
||||
|
||||
await updateLatestExecutedState({
|
||||
savedObjectsClient: soClient,
|
||||
// @ts-ignore
|
||||
savedObjectsImporter: jest.fn(),
|
||||
esClient,
|
||||
logger,
|
||||
packageInstallContext: {
|
||||
assetsMap: new Map(),
|
||||
paths: [],
|
||||
packageInfo: {
|
||||
title: 'title',
|
||||
name: 'xyz',
|
||||
version: '4.5.6',
|
||||
description: 'test',
|
||||
type: 'integration',
|
||||
categories: ['cloud', 'custom'],
|
||||
format_version: 'string',
|
||||
release: 'experimental',
|
||||
conditions: { kibana: { version: 'x.y.z' } },
|
||||
owner: { github: 'elastic/fleet' },
|
||||
},
|
||||
},
|
||||
latestExecutedState: {
|
||||
name: INSTALL_STATES.SAVE_ARCHIVE_ENTRIES,
|
||||
started_at: new Date(Date.now() - MAX_TIME_COMPLETE_INSTALL * 2).toISOString(),
|
||||
},
|
||||
installType: 'install',
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
});
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenCalledWith({
|
||||
action: 'update',
|
||||
id: 'xyz',
|
||||
savedObjectType: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
'Failed to update SO with latest executed state: Error: Saved object [abc/123] conflict'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
|
||||
|
||||
import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../constants';
|
||||
|
||||
import { auditLoggingService } from '../../../../audit_logging';
|
||||
|
||||
import type { InstallContext } from '../_state_machine_package_install';
|
||||
|
||||
// Function invoked after each transition
|
||||
export const updateLatestExecutedState = async (context: InstallContext) => {
|
||||
const { logger, savedObjectsClient, packageInstallContext, latestExecutedState } = context;
|
||||
const { packageInfo } = packageInstallContext;
|
||||
const { name: pkgName } = packageInfo;
|
||||
|
||||
try {
|
||||
// If the error is of type ConcurrentInstallationError, don't save it in the SO
|
||||
if (latestExecutedState?.error?.includes('Concurrent installation or upgrade')) return;
|
||||
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'update',
|
||||
id: pkgName,
|
||||
savedObjectType: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
return await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, {
|
||||
latest_executed_state: latestExecutedState,
|
||||
});
|
||||
} catch (err) {
|
||||
if (!SavedObjectsErrorHelpers.isNotFoundError(err)) {
|
||||
logger.error(`Failed to update SO with latest executed state: ${err}`);
|
||||
}
|
||||
}
|
||||
};
|
|
@ -97,6 +97,8 @@ export type {
|
|||
ActionStatusOptions,
|
||||
PackageSpecTags,
|
||||
AssetsMap,
|
||||
InstallResultStatus,
|
||||
InstallLatestExecutedState,
|
||||
} from '../../common/types';
|
||||
export { ElasticsearchAssetType, KibanaAssetType, KibanaSavedObjectType } from '../../common/types';
|
||||
export { dataTypes } from '../../common/constants';
|
||||
|
|
|
@ -27,7 +27,10 @@ export default function (providerContext: FtrProviderContext) {
|
|||
};
|
||||
|
||||
const uninstallPackage = async (pkg: string, version: string) => {
|
||||
await supertest.delete(`/api/fleet/epm/packages/${pkg}/${version}`).set('kbn-xsrf', 'xxxx');
|
||||
await supertest
|
||||
.delete(`/api/fleet/epm/packages/${pkg}/${version}`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({ force: true });
|
||||
};
|
||||
|
||||
const getPackageInfo = async (pkg: string, version: string) => {
|
||||
|
|
|
@ -778,6 +778,10 @@ const expectAssetsInstalled = ({
|
|||
install_started_at: res.attributes.install_started_at,
|
||||
install_source: 'registry',
|
||||
latest_install_failed_attempts: [],
|
||||
latest_executed_state: {
|
||||
name: 'update_so',
|
||||
started_at: res.attributes.latest_executed_state.started_at,
|
||||
},
|
||||
install_format_schema_version: FLEET_INSTALL_FORMAT_VERSION,
|
||||
verification_status: 'unknown',
|
||||
verification_key_id: null,
|
||||
|
|
|
@ -486,6 +486,10 @@ export default function (providerContext: FtrProviderContext) {
|
|||
install_source: 'registry',
|
||||
install_format_schema_version: FLEET_INSTALL_FORMAT_VERSION,
|
||||
latest_install_failed_attempts: [],
|
||||
latest_executed_state: {
|
||||
name: 'update_so',
|
||||
started_at: res.attributes.latest_executed_state.started_at,
|
||||
},
|
||||
verification_status: 'unknown',
|
||||
verification_key_id: null,
|
||||
});
|
||||
|
|
|
@ -76,6 +76,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
'agentTamperProtectionEnabled',
|
||||
'enableStrictKQLValidation',
|
||||
'subfeaturePrivileges',
|
||||
'enablePackagesStateMachine',
|
||||
])}`,
|
||||
`--logging.loggers=${JSON.stringify([
|
||||
...getKibanaCliLoggers(xPackAPITestsConfig.get('kbnTestServer.serverArgs')),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue