mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Fleet] Refactor setup to load default packages/policies with preconfiguration (#97328)
Co-authored-by: Nicolas Chaulet <nicolas.chaulet@elastic.co> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
97ebe11aac
commit
57f84f8593
19 changed files with 412 additions and 547 deletions
|
@ -5,44 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { AgentPolicy } from '../types';
|
||||
|
||||
import { defaultPackages } from './epm';
|
||||
|
||||
export const AGENT_POLICY_SAVED_OBJECT_TYPE = 'ingest-agent-policies';
|
||||
export const AGENT_POLICY_INDEX = '.fleet-policies';
|
||||
export const agentPolicyStatuses = {
|
||||
Active: 'active',
|
||||
Inactive: 'inactive',
|
||||
} as const;
|
||||
|
||||
export const DEFAULT_AGENT_POLICY: Omit<
|
||||
AgentPolicy,
|
||||
'id' | 'updated_at' | 'updated_by' | 'revision'
|
||||
> = {
|
||||
name: 'Default policy',
|
||||
namespace: 'default',
|
||||
description: 'Default agent policy created by Kibana',
|
||||
status: agentPolicyStatuses.Active,
|
||||
package_policies: [],
|
||||
is_default: true,
|
||||
is_managed: false,
|
||||
monitoring_enabled: ['logs', 'metrics'] as Array<'logs' | 'metrics'>,
|
||||
};
|
||||
|
||||
export const DEFAULT_FLEET_SERVER_AGENT_POLICY: Omit<
|
||||
AgentPolicy,
|
||||
'id' | 'updated_at' | 'updated_by' | 'revision'
|
||||
> = {
|
||||
name: 'Default Fleet Server policy',
|
||||
namespace: 'default',
|
||||
description: 'Default Fleet Server agent policy created by Kibana',
|
||||
status: agentPolicyStatuses.Active,
|
||||
package_policies: [],
|
||||
is_default: false,
|
||||
is_default_fleet_server: true,
|
||||
is_managed: false,
|
||||
monitoring_enabled: ['logs', 'metrics'] as Array<'logs' | 'metrics'>,
|
||||
};
|
||||
|
||||
export const DEFAULT_AGENT_POLICIES_PACKAGES = [defaultPackages.System];
|
||||
|
|
|
@ -15,6 +15,7 @@ export const requiredPackages = {
|
|||
System: 'system',
|
||||
Endpoint: 'endpoint',
|
||||
ElasticAgent: 'elastic_agent',
|
||||
FleetServer: FLEET_SERVER_PACKAGE,
|
||||
SecurityDetectionEngine: 'security_detection_engine',
|
||||
} as const;
|
||||
|
||||
|
|
|
@ -5,7 +5,67 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { PreconfiguredAgentPolicy } from '../types';
|
||||
|
||||
import { defaultPackages } from './epm';
|
||||
|
||||
export const PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE =
|
||||
'fleet-preconfiguration-deletion-record';
|
||||
|
||||
export const PRECONFIGURATION_LATEST_KEYWORD = 'latest';
|
||||
|
||||
type PreconfiguredAgentPolicyWithDefaultInputs = Omit<
|
||||
PreconfiguredAgentPolicy,
|
||||
'package_policies' | 'id'
|
||||
> & {
|
||||
package_policies: Array<Omit<PreconfiguredAgentPolicy['package_policies'][0], 'inputs'>>;
|
||||
};
|
||||
|
||||
export const DEFAULT_AGENT_POLICY: PreconfiguredAgentPolicyWithDefaultInputs = {
|
||||
name: 'Default policy',
|
||||
namespace: 'default',
|
||||
description: 'Default agent policy created by Kibana',
|
||||
package_policies: [
|
||||
{
|
||||
name: `${defaultPackages.System}-1`,
|
||||
package: {
|
||||
name: defaultPackages.System,
|
||||
},
|
||||
},
|
||||
],
|
||||
is_default: true,
|
||||
is_managed: false,
|
||||
monitoring_enabled: ['logs', 'metrics'] as Array<'logs' | 'metrics'>,
|
||||
};
|
||||
|
||||
export const DEFAULT_FLEET_SERVER_AGENT_POLICY: PreconfiguredAgentPolicyWithDefaultInputs = {
|
||||
name: 'Default Fleet Server policy',
|
||||
namespace: 'default',
|
||||
description: 'Default Fleet Server agent policy created by Kibana',
|
||||
package_policies: [
|
||||
{
|
||||
name: `${defaultPackages.FleetServer}-1`,
|
||||
package: {
|
||||
name: defaultPackages.FleetServer,
|
||||
},
|
||||
},
|
||||
],
|
||||
is_default: false,
|
||||
is_default_fleet_server: true,
|
||||
is_managed: false,
|
||||
monitoring_enabled: ['logs', 'metrics'] as Array<'logs' | 'metrics'>,
|
||||
};
|
||||
|
||||
export const DEFAULT_PACKAGES = Object.values(defaultPackages).map((name) => ({
|
||||
name,
|
||||
version: PRECONFIGURATION_LATEST_KEYWORD,
|
||||
}));
|
||||
|
||||
// these are currently identical. we can separate if they later diverge
|
||||
export const REQUIRED_PACKAGES = DEFAULT_PACKAGES;
|
||||
|
||||
export interface PreconfigurationError {
|
||||
package?: { name: string; version: string };
|
||||
agentPolicy?: { name: string };
|
||||
error: Error;
|
||||
}
|
||||
|
|
|
@ -20,9 +20,9 @@ export interface PreconfiguredAgentPolicy extends Omit<NewAgentPolicy, 'namespac
|
|||
id: string | number;
|
||||
namespace?: string;
|
||||
package_policies: Array<
|
||||
Partial<Omit<NewPackagePolicy, 'inputs'>> & {
|
||||
Partial<Omit<NewPackagePolicy, 'inputs' | 'package'>> & {
|
||||
name: string;
|
||||
package: Partial<PackagePolicyPackage>;
|
||||
package: Partial<PackagePolicyPackage> & { name: string };
|
||||
inputs?: InputsOverride[];
|
||||
}
|
||||
>;
|
||||
|
|
|
@ -5,10 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { DefaultPackagesInstallationError } from '../models/epm';
|
||||
|
||||
export interface PostIngestSetupResponse {
|
||||
isInitialized: boolean;
|
||||
preconfigurationError?: { name: string; message: string };
|
||||
nonFatalPackageUpgradeErrors?: DefaultPackagesInstallationError[];
|
||||
nonFatalErrors?: Array<{ error: Error }>;
|
||||
}
|
||||
|
|
|
@ -83,20 +83,13 @@ export const WithPermissionsAndSetup: React.FC = memo(({ children }) => {
|
|||
if (setupResponse.error) {
|
||||
setInitializationError(setupResponse.error);
|
||||
}
|
||||
if (setupResponse.data?.preconfigurationError) {
|
||||
notifications.toasts.addError(setupResponse.data.preconfigurationError, {
|
||||
if (setupResponse.data?.nonFatalErrors?.length) {
|
||||
notifications.toasts.addError(setupResponse.data.nonFatalErrors[0], {
|
||||
title: i18n.translate('xpack.fleet.setup.uiPreconfigurationErrorTitle', {
|
||||
defaultMessage: 'Configuration error',
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (setupResponse.data?.nonFatalPackageUpgradeErrors) {
|
||||
notifications.toasts.addError(setupResponse.data.nonFatalPackageUpgradeErrors, {
|
||||
title: i18n.translate('xpack.fleet.setup.nonFatalPackageErrorsTitle', {
|
||||
defaultMessage: 'One or more packages could not be successfully upgraded',
|
||||
}),
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
setInitializationError(err);
|
||||
}
|
||||
|
|
|
@ -47,11 +47,15 @@ export {
|
|||
GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
// Defaults
|
||||
DEFAULT_AGENT_POLICY,
|
||||
DEFAULT_FLEET_SERVER_AGENT_POLICY,
|
||||
DEFAULT_OUTPUT,
|
||||
DEFAULT_PACKAGES,
|
||||
REQUIRED_PACKAGES,
|
||||
// Fleet Server index
|
||||
FLEET_SERVER_SERVERS_INDEX,
|
||||
ENROLLMENT_API_KEYS_INDEX,
|
||||
AGENTS_INDEX,
|
||||
// Preconfiguration
|
||||
PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE,
|
||||
PRECONFIGURATION_LATEST_KEYWORD,
|
||||
} from '../../common';
|
||||
|
|
|
@ -58,8 +58,8 @@ export const config: PluginConfigDescriptor = {
|
|||
})
|
||||
),
|
||||
}),
|
||||
packages: schema.maybe(PreconfiguredPackagesSchema),
|
||||
agentPolicies: schema.maybe(PreconfiguredAgentPoliciesSchema),
|
||||
packages: PreconfiguredPackagesSchema,
|
||||
agentPolicies: PreconfiguredAgentPoliciesSchema,
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
|
@ -48,13 +48,12 @@ describe('FleetSetupHandler', () => {
|
|||
mockSetupIngestManager.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
isInitialized: true,
|
||||
preconfigurationError: undefined,
|
||||
nonFatalPackageUpgradeErrors: [],
|
||||
nonFatalErrors: [],
|
||||
})
|
||||
);
|
||||
await fleetSetupHandler(context, request, response);
|
||||
|
||||
const expectedBody: PostIngestSetupResponse = { isInitialized: true };
|
||||
const expectedBody: PostIngestSetupResponse = { isInitialized: true, nonFatalErrors: [] };
|
||||
expect(response.customError).toHaveBeenCalledTimes(0);
|
||||
expect(response.ok).toHaveBeenCalledWith({ body: expectedBody });
|
||||
});
|
||||
|
|
|
@ -48,12 +48,18 @@ export const fleetSetupHandler: RequestHandler = async (context, request, respon
|
|||
const esClient = context.core.elasticsearch.client.asCurrentUser;
|
||||
const body: PostIngestSetupResponse = await setupIngestManager(soClient, esClient);
|
||||
|
||||
if (body.nonFatalPackageUpgradeErrors?.length === 0) {
|
||||
delete body.nonFatalPackageUpgradeErrors;
|
||||
}
|
||||
|
||||
return response.ok({
|
||||
body,
|
||||
body: {
|
||||
...body,
|
||||
nonFatalErrors: body.nonFatalErrors?.map((e) => {
|
||||
// JSONify the error object so it can be displayed properly in the UI
|
||||
const error = e.error ?? e;
|
||||
return {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
};
|
||||
}),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return defaultIngestErrorHandler({ error, response });
|
||||
|
|
|
@ -16,7 +16,6 @@ import type {
|
|||
|
||||
import type { AuthenticatedUser } from '../../../security/server';
|
||||
import {
|
||||
DEFAULT_AGENT_POLICY,
|
||||
AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
AGENT_SAVED_OBJECT_TYPE,
|
||||
PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE,
|
||||
|
@ -37,7 +36,6 @@ import {
|
|||
dataTypes,
|
||||
packageToPackagePolicy,
|
||||
AGENT_POLICY_INDEX,
|
||||
DEFAULT_FLEET_SERVER_AGENT_POLICY,
|
||||
} from '../../common';
|
||||
import type {
|
||||
DeleteAgentPolicyResponse,
|
||||
|
@ -106,39 +104,6 @@ class AgentPolicyService {
|
|||
return (await this.get(soClient, id)) as AgentPolicy;
|
||||
}
|
||||
|
||||
public async ensureDefaultAgentPolicy(
|
||||
soClient: SavedObjectsClientContract,
|
||||
esClient: ElasticsearchClient
|
||||
): Promise<{
|
||||
created: boolean;
|
||||
policy: AgentPolicy;
|
||||
}> {
|
||||
const searchParams = {
|
||||
searchFields: ['is_default'],
|
||||
search: 'true',
|
||||
};
|
||||
return await this.ensureAgentPolicy(soClient, esClient, DEFAULT_AGENT_POLICY, searchParams);
|
||||
}
|
||||
|
||||
public async ensureDefaultFleetServerAgentPolicy(
|
||||
soClient: SavedObjectsClientContract,
|
||||
esClient: ElasticsearchClient
|
||||
): Promise<{
|
||||
created: boolean;
|
||||
policy: AgentPolicy;
|
||||
}> {
|
||||
const searchParams = {
|
||||
searchFields: ['is_default_fleet_server'],
|
||||
search: 'true',
|
||||
};
|
||||
return await this.ensureAgentPolicy(
|
||||
soClient,
|
||||
esClient,
|
||||
DEFAULT_FLEET_SERVER_AGENT_POLICY,
|
||||
searchParams
|
||||
);
|
||||
}
|
||||
|
||||
public async ensurePreconfiguredAgentPolicy(
|
||||
soClient: SavedObjectsClientContract,
|
||||
esClient: ElasticsearchClient,
|
||||
|
@ -148,22 +113,44 @@ class AgentPolicyService {
|
|||
policy?: AgentPolicy;
|
||||
}> {
|
||||
const { id, ...preconfiguredAgentPolicy } = omit(config, 'package_policies');
|
||||
const preconfigurationId = String(id);
|
||||
const searchParams = {
|
||||
searchFields: ['preconfiguration_id'],
|
||||
search: escapeSearchQueryPhrase(preconfigurationId),
|
||||
};
|
||||
|
||||
const newAgentPolicyDefaults: Partial<NewAgentPolicy> = {
|
||||
namespace: 'default',
|
||||
monitoring_enabled: ['logs', 'metrics'],
|
||||
};
|
||||
|
||||
const newAgentPolicy = {
|
||||
...newAgentPolicyDefaults,
|
||||
...preconfiguredAgentPolicy,
|
||||
preconfiguration_id: preconfigurationId,
|
||||
} as NewAgentPolicy;
|
||||
let searchParams;
|
||||
let newAgentPolicy;
|
||||
if (id) {
|
||||
const preconfigurationId = String(id);
|
||||
searchParams = {
|
||||
searchFields: ['preconfiguration_id'],
|
||||
search: escapeSearchQueryPhrase(preconfigurationId),
|
||||
};
|
||||
|
||||
newAgentPolicy = {
|
||||
...newAgentPolicyDefaults,
|
||||
...preconfiguredAgentPolicy,
|
||||
preconfiguration_id: preconfigurationId,
|
||||
} as NewAgentPolicy;
|
||||
} else if (
|
||||
preconfiguredAgentPolicy.is_default ||
|
||||
preconfiguredAgentPolicy.is_default_fleet_server
|
||||
) {
|
||||
searchParams = {
|
||||
searchFields: [
|
||||
preconfiguredAgentPolicy.is_default_fleet_server
|
||||
? 'is_default_fleet_server'
|
||||
: 'is_default',
|
||||
],
|
||||
search: 'true',
|
||||
};
|
||||
|
||||
newAgentPolicy = {
|
||||
...newAgentPolicyDefaults,
|
||||
...preconfiguredAgentPolicy,
|
||||
} as NewAgentPolicy;
|
||||
}
|
||||
if (!newAgentPolicy || !searchParams) throw new Error('Missing ID');
|
||||
|
||||
return await this.ensureAgentPolicy(soClient, esClient, newAgentPolicy, searchParams);
|
||||
}
|
||||
|
@ -554,13 +541,14 @@ class AgentPolicyService {
|
|||
throw new HostedAgentPolicyRestrictionRelatedError(`Cannot delete hosted agent policy ${id}`);
|
||||
}
|
||||
|
||||
const {
|
||||
policy: { id: defaultAgentPolicyId },
|
||||
} = await this.ensureDefaultAgentPolicy(soClient, esClient);
|
||||
if (id === defaultAgentPolicyId) {
|
||||
if (agentPolicy.is_default) {
|
||||
throw new Error('The default agent policy cannot be deleted');
|
||||
}
|
||||
|
||||
if (agentPolicy.is_default_fleet_server) {
|
||||
throw new Error('The default fleet server agent policy cannot be deleted');
|
||||
}
|
||||
|
||||
const { total } = await getAgentsByKuery(esClient, {
|
||||
showInactive: false,
|
||||
perPage: 0,
|
||||
|
|
|
@ -11,38 +11,68 @@ import { appContextService } from '../../app_context';
|
|||
import * as Registry from '../registry';
|
||||
import { installIndexPatterns } from '../kibana/index_pattern/install';
|
||||
|
||||
import { installPackage } from './install';
|
||||
import type { InstallResult } from '../../../types';
|
||||
|
||||
import { installPackage, isPackageVersionOrLaterInstalled } from './install';
|
||||
import type { BulkInstallResponse, IBulkInstallPackageError } from './install';
|
||||
|
||||
interface BulkInstallPackagesParams {
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
packagesToInstall: string[];
|
||||
packagesToInstall: Array<string | { name: string; version: string }>;
|
||||
esClient: ElasticsearchClient;
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
export async function bulkInstallPackages({
|
||||
savedObjectsClient,
|
||||
packagesToInstall,
|
||||
esClient,
|
||||
force,
|
||||
}: BulkInstallPackagesParams): Promise<BulkInstallResponse[]> {
|
||||
const logger = appContextService.getLogger();
|
||||
const installSource = 'registry';
|
||||
const latestPackagesResults = await Promise.allSettled(
|
||||
packagesToInstall.map((packageName) => Registry.fetchFindLatestPackage(packageName))
|
||||
const packagesResults = await Promise.allSettled(
|
||||
packagesToInstall.map((pkg) => {
|
||||
if (typeof pkg === 'string') return Registry.fetchFindLatestPackage(pkg);
|
||||
return Promise.resolve(pkg);
|
||||
})
|
||||
);
|
||||
|
||||
logger.debug(`kicking off bulk install of ${packagesToInstall.join(', ')} from registry`);
|
||||
const bulkInstallResults = await Promise.allSettled(
|
||||
latestPackagesResults.map(async (result, index) => {
|
||||
const packageName = packagesToInstall[index];
|
||||
packagesResults.map(async (result, index) => {
|
||||
const packageName = getNameFromPackagesToInstall(packagesToInstall, index);
|
||||
if (result.status === 'fulfilled') {
|
||||
const latestPackage = result.value;
|
||||
const pkgKeyProps = result.value;
|
||||
const installedPackageResult = await isPackageVersionOrLaterInstalled({
|
||||
savedObjectsClient,
|
||||
pkgName: pkgKeyProps.name,
|
||||
pkgVersion: pkgKeyProps.version,
|
||||
});
|
||||
if (installedPackageResult) {
|
||||
const {
|
||||
name,
|
||||
version,
|
||||
installed_es: installedEs,
|
||||
installed_kibana: installedKibana,
|
||||
} = installedPackageResult.package;
|
||||
return {
|
||||
name,
|
||||
version,
|
||||
result: {
|
||||
assets: [...installedEs, ...installedKibana],
|
||||
status: 'already_installed',
|
||||
installType: installedPackageResult.installType,
|
||||
} as InstallResult,
|
||||
};
|
||||
}
|
||||
const installResult = await installPackage({
|
||||
savedObjectsClient,
|
||||
esClient,
|
||||
pkgkey: Registry.pkgToPkgKey(latestPackage),
|
||||
pkgkey: Registry.pkgToPkgKey(pkgKeyProps),
|
||||
installSource,
|
||||
skipPostInstall: true,
|
||||
force,
|
||||
});
|
||||
if (installResult.error) {
|
||||
return {
|
||||
|
@ -53,7 +83,7 @@ export async function bulkInstallPackages({
|
|||
} else {
|
||||
return {
|
||||
name: packageName,
|
||||
version: latestPackage.version,
|
||||
version: pkgKeyProps.version,
|
||||
result: installResult,
|
||||
};
|
||||
}
|
||||
|
@ -76,7 +106,7 @@ export async function bulkInstallPackages({
|
|||
}
|
||||
|
||||
return bulkInstallResults.map((result, index) => {
|
||||
const packageName = packagesToInstall[index];
|
||||
const packageName = getNameFromPackagesToInstall(packagesToInstall, index);
|
||||
if (result.status === 'fulfilled') {
|
||||
if (result.value && result.value.error) {
|
||||
return {
|
||||
|
@ -98,3 +128,12 @@ export function isBulkInstallError(
|
|||
): installResponse is IBulkInstallPackageError {
|
||||
return 'error' in installResponse && installResponse.error instanceof Error;
|
||||
}
|
||||
|
||||
function getNameFromPackagesToInstall(
|
||||
packagesToInstall: BulkInstallPackagesParams['packagesToInstall'],
|
||||
index: number
|
||||
) {
|
||||
const entry = packagesToInstall[index];
|
||||
if (typeof entry === 'string') return entry;
|
||||
return entry.name;
|
||||
}
|
||||
|
|
|
@ -1,147 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { SavedObject, SavedObjectsClientContract } from 'src/core/server';
|
||||
|
||||
import { ElasticsearchAssetType, KibanaSavedObjectType } from '../../../types';
|
||||
import type { Installation } from '../../../types';
|
||||
|
||||
jest.mock('./install');
|
||||
jest.mock('./bulk_install_packages');
|
||||
jest.mock('./get');
|
||||
|
||||
const { ensureInstalledDefaultPackages } = jest.requireActual('./install');
|
||||
const { isBulkInstallError: actualIsBulkInstallError } = jest.requireActual(
|
||||
'./bulk_install_packages'
|
||||
);
|
||||
// eslint-disable-next-line import/order
|
||||
import { savedObjectsClientMock } from 'src/core/server/mocks';
|
||||
|
||||
import { appContextService } from '../../app_context';
|
||||
import { createAppContextStartContractMock } from '../../../mocks';
|
||||
|
||||
import { getInstallation } from './get';
|
||||
import { bulkInstallPackages, isBulkInstallError } from './bulk_install_packages';
|
||||
|
||||
// if we add this assertion, TS will type check the return value
|
||||
// and the editor will also know about .mockImplementation, .mock.calls, etc
|
||||
const mockedBulkInstallPackages = bulkInstallPackages as jest.MockedFunction<
|
||||
typeof bulkInstallPackages
|
||||
>;
|
||||
const mockedIsBulkInstallError = isBulkInstallError as jest.MockedFunction<
|
||||
typeof isBulkInstallError
|
||||
>;
|
||||
const mockedGetInstallation = getInstallation as jest.MockedFunction<typeof getInstallation>;
|
||||
|
||||
// I was unable to get the actual implementation set in the `jest.mock()` call at the top to work
|
||||
// so this will set the `isBulkInstallError` function back to the actual implementation
|
||||
mockedIsBulkInstallError.mockImplementation(actualIsBulkInstallError);
|
||||
|
||||
const mockInstallation: SavedObject<Installation> = {
|
||||
id: 'test-pkg',
|
||||
references: [],
|
||||
type: 'epm-packages',
|
||||
attributes: {
|
||||
id: 'test-pkg',
|
||||
installed_kibana: [{ type: KibanaSavedObjectType.dashboard, id: 'dashboard-1' }],
|
||||
installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }],
|
||||
package_assets: [],
|
||||
es_index_patterns: { pattern: 'pattern-name' },
|
||||
name: 'test package',
|
||||
version: '1.0.0',
|
||||
install_status: 'installed',
|
||||
install_version: '1.0.0',
|
||||
install_started_at: new Date().toISOString(),
|
||||
install_source: 'registry',
|
||||
},
|
||||
};
|
||||
|
||||
describe('ensureInstalledDefaultPackages', () => {
|
||||
let soClient: jest.Mocked<SavedObjectsClientContract>;
|
||||
beforeEach(async () => {
|
||||
soClient = savedObjectsClientMock.create();
|
||||
appContextService.start(createAppContextStartContractMock());
|
||||
});
|
||||
afterEach(async () => {
|
||||
appContextService.stop();
|
||||
});
|
||||
it('should return an array of Installation objects when successful', async () => {
|
||||
mockedGetInstallation.mockImplementation(async () => {
|
||||
return mockInstallation.attributes;
|
||||
});
|
||||
mockedBulkInstallPackages.mockImplementationOnce(async function () {
|
||||
return [
|
||||
{
|
||||
name: mockInstallation.attributes.name,
|
||||
result: { assets: [], status: 'installed', installType: 'install' },
|
||||
version: '',
|
||||
statusCode: 200,
|
||||
},
|
||||
];
|
||||
});
|
||||
const resp = await ensureInstalledDefaultPackages(soClient, jest.fn());
|
||||
expect(resp.installations).toEqual([mockInstallation.attributes]);
|
||||
});
|
||||
it('should throw the first Error it finds', async () => {
|
||||
class SomeCustomError extends Error {}
|
||||
mockedGetInstallation.mockImplementation(async () => {
|
||||
return mockInstallation.attributes;
|
||||
});
|
||||
mockedBulkInstallPackages.mockImplementationOnce(async function () {
|
||||
return [
|
||||
{
|
||||
name: 'success one',
|
||||
result: { assets: [], status: 'installed', installType: 'install' },
|
||||
version: '',
|
||||
statusCode: 200,
|
||||
},
|
||||
{
|
||||
name: 'success two',
|
||||
result: { assets: [], status: 'installed', installType: 'install' },
|
||||
version: '',
|
||||
statusCode: 200,
|
||||
},
|
||||
{
|
||||
name: 'failure one',
|
||||
error: new SomeCustomError('abc 123'),
|
||||
},
|
||||
{
|
||||
name: 'success three',
|
||||
result: { assets: [], status: 'installed', installType: 'install' },
|
||||
version: '',
|
||||
statusCode: 200,
|
||||
},
|
||||
{
|
||||
name: 'failure two',
|
||||
error: new Error('zzz'),
|
||||
},
|
||||
];
|
||||
});
|
||||
const installPromise = ensureInstalledDefaultPackages(soClient, jest.fn());
|
||||
expect.assertions(2);
|
||||
expect(installPromise).rejects.toThrow(SomeCustomError);
|
||||
expect(installPromise).rejects.toThrow('abc 123');
|
||||
});
|
||||
it('should throw an error when get installation returns undefined', async () => {
|
||||
mockedGetInstallation.mockImplementation(async () => {
|
||||
return undefined;
|
||||
});
|
||||
mockedBulkInstallPackages.mockImplementationOnce(async function () {
|
||||
return [
|
||||
{
|
||||
name: 'undefined package',
|
||||
result: { assets: [], status: 'installed', installType: 'install' },
|
||||
version: '',
|
||||
statusCode: 200,
|
||||
},
|
||||
];
|
||||
});
|
||||
const installPromise = ensureInstalledDefaultPackages(soClient, jest.fn());
|
||||
expect.assertions(1);
|
||||
expect(installPromise).rejects.toThrow();
|
||||
});
|
||||
});
|
|
@ -5,19 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import semverLt from 'semver/functions/lt';
|
||||
import type Boom from '@hapi/boom';
|
||||
import type { ElasticsearchClient, SavedObject, SavedObjectsClientContract } from 'src/core/server';
|
||||
|
||||
import { generateESIndexPatterns } from '../elasticsearch/template/template';
|
||||
|
||||
import { defaultPackages } from '../../../../common';
|
||||
import type {
|
||||
BulkInstallPackageInfo,
|
||||
InstallablePackage,
|
||||
InstallSource,
|
||||
DefaultPackagesInstallationError,
|
||||
} from '../../../../common';
|
||||
import type { BulkInstallPackageInfo, InstallablePackage, InstallSource } from '../../../../common';
|
||||
import {
|
||||
IngestManagerError,
|
||||
PackageOperationNotSupportedError,
|
||||
|
@ -39,71 +34,30 @@ import { toAssetReference } from '../kibana/assets/install';
|
|||
import type { ArchiveAsset } from '../kibana/assets/install';
|
||||
import { installIndexPatterns } from '../kibana/index_pattern/install';
|
||||
|
||||
import {
|
||||
isRequiredPackage,
|
||||
getInstallation,
|
||||
getInstallationObject,
|
||||
bulkInstallPackages,
|
||||
isBulkInstallError,
|
||||
} from './index';
|
||||
import { isRequiredPackage, getInstallation, getInstallationObject } from './index';
|
||||
import { removeInstallation } from './remove';
|
||||
import { getPackageSavedObjects } from './get';
|
||||
import { _installPackage } from './_install_package';
|
||||
|
||||
export interface DefaultPackagesInstallationResult {
|
||||
installations: Installation[];
|
||||
nonFatalPackageUpgradeErrors: DefaultPackagesInstallationError[];
|
||||
}
|
||||
|
||||
export async function ensureInstalledDefaultPackages(
|
||||
savedObjectsClient: SavedObjectsClientContract,
|
||||
esClient: ElasticsearchClient
|
||||
): Promise<DefaultPackagesInstallationResult> {
|
||||
const installations = [];
|
||||
const nonFatalPackageUpgradeErrors = [];
|
||||
const bulkResponse = await bulkInstallPackages({
|
||||
savedObjectsClient,
|
||||
packagesToInstall: Object.values(defaultPackages),
|
||||
esClient,
|
||||
});
|
||||
|
||||
for (const resp of bulkResponse) {
|
||||
if (isBulkInstallError(resp)) {
|
||||
if (resp.installType && (resp.installType === 'update' || resp.installType === 'reupdate')) {
|
||||
nonFatalPackageUpgradeErrors.push({ installType: resp.installType, error: resp.error });
|
||||
} else {
|
||||
throw resp.error;
|
||||
}
|
||||
} else {
|
||||
installations.push(getInstallation({ savedObjectsClient, pkgName: resp.name }));
|
||||
}
|
||||
}
|
||||
|
||||
const retrievedInstallations = await Promise.all(installations);
|
||||
const verifiedInstallations = retrievedInstallations.map((installation, index) => {
|
||||
if (!installation) {
|
||||
throw new Error(`could not get installation ${bulkResponse[index].name}`);
|
||||
}
|
||||
return installation;
|
||||
});
|
||||
return {
|
||||
installations: verifiedInstallations,
|
||||
nonFatalPackageUpgradeErrors,
|
||||
};
|
||||
}
|
||||
|
||||
async function isPackageVersionOrLaterInstalled(options: {
|
||||
export async function isPackageVersionOrLaterInstalled(options: {
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
pkgName: string;
|
||||
pkgVersion: string;
|
||||
}): Promise<Installation | false> {
|
||||
}): Promise<{ package: Installation; installType: InstallType } | false> {
|
||||
const { savedObjectsClient, pkgName, pkgVersion } = options;
|
||||
const installedPackage = await getInstallation({ savedObjectsClient, pkgName });
|
||||
const installedPackageObject = await getInstallationObject({ savedObjectsClient, pkgName });
|
||||
const installedPackage = installedPackageObject?.attributes;
|
||||
if (
|
||||
installedPackage &&
|
||||
(installedPackage.version === pkgVersion || semverLt(pkgVersion, installedPackage.version))
|
||||
) {
|
||||
return installedPackage;
|
||||
let installType: InstallType;
|
||||
try {
|
||||
installType = getInstallType({ pkgVersion, installedPkg: installedPackageObject });
|
||||
} catch (e) {
|
||||
installType = 'unknown';
|
||||
}
|
||||
return { package: installedPackage, installType };
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -121,16 +75,16 @@ export async function ensureInstalledPackage(options: {
|
|||
? { name: pkgName, version: pkgVersion }
|
||||
: await Registry.fetchFindLatestPackage(pkgName);
|
||||
|
||||
const installedPackage = await isPackageVersionOrLaterInstalled({
|
||||
const installedPackageResult = await isPackageVersionOrLaterInstalled({
|
||||
savedObjectsClient,
|
||||
pkgName: pkgKeyProps.name,
|
||||
pkgVersion: pkgKeyProps.version,
|
||||
});
|
||||
if (installedPackage) {
|
||||
return installedPackage;
|
||||
if (installedPackageResult) {
|
||||
return installedPackageResult.package;
|
||||
}
|
||||
const pkgkey = Registry.pkgToPkgKey(pkgKeyProps);
|
||||
await installPackage({
|
||||
const installResult = await installPackage({
|
||||
installSource: 'registry',
|
||||
savedObjectsClient,
|
||||
pkgkey,
|
||||
|
@ -138,6 +92,26 @@ export async function ensureInstalledPackage(options: {
|
|||
force: true, // Always force outdated packages to be installed if a later version isn't installed
|
||||
});
|
||||
|
||||
if (installResult.error) {
|
||||
const errorPrefix =
|
||||
installResult.installType === 'update' || installResult.installType === 'reupdate'
|
||||
? i18n.translate('xpack.fleet.epm.install.packageUpdateError', {
|
||||
defaultMessage: 'Error updating {pkgName} to {pkgVersion}',
|
||||
values: {
|
||||
pkgName: pkgKeyProps.name,
|
||||
pkgVersion: pkgKeyProps.version,
|
||||
},
|
||||
})
|
||||
: i18n.translate('xpack.fleet.epm.install.packageInstallError', {
|
||||
defaultMessage: 'Error installing {pkgName} {pkgVersion}',
|
||||
values: {
|
||||
pkgName: pkgKeyProps.name,
|
||||
pkgVersion: pkgKeyProps.version,
|
||||
},
|
||||
});
|
||||
throw new Error(`${errorPrefix}: ${installResult.error.message}`);
|
||||
}
|
||||
|
||||
const installation = await getInstallation({ savedObjectsClient, pkgName });
|
||||
if (!installation) throw new Error(`could not get installation ${pkgName}`);
|
||||
return installation;
|
||||
|
|
|
@ -71,15 +71,8 @@ function getPutPreconfiguredPackagesMock() {
|
|||
}
|
||||
|
||||
jest.mock('./epm/packages/install', () => ({
|
||||
ensureInstalledPackage({
|
||||
pkgName,
|
||||
pkgVersion,
|
||||
force,
|
||||
}: {
|
||||
pkgName: string;
|
||||
pkgVersion: string;
|
||||
force?: boolean;
|
||||
}) {
|
||||
installPackage({ pkgkey, force }: { pkgkey: string; force?: boolean }) {
|
||||
const [pkgName, pkgVersion] = pkgkey.split('-');
|
||||
const installedPackage = mockInstalledPackages.get(pkgName);
|
||||
if (installedPackage) {
|
||||
if (installedPackage.version === pkgVersion) return installedPackage;
|
||||
|
@ -87,8 +80,15 @@ jest.mock('./epm/packages/install', () => ({
|
|||
|
||||
const packageInstallation = { name: pkgName, version: pkgVersion, title: pkgName };
|
||||
mockInstalledPackages.set(pkgName, packageInstallation);
|
||||
|
||||
return packageInstallation;
|
||||
},
|
||||
ensurePackagesCompletedInstall() {
|
||||
return [];
|
||||
},
|
||||
isPackageVersionOrLaterInstalled() {
|
||||
return false;
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('./epm/packages/get', () => ({
|
||||
|
@ -117,6 +117,20 @@ jest.mock('./package_policy', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
jest.mock('./app_context', () => ({
|
||||
appContextService: {
|
||||
getLogger: () =>
|
||||
new Proxy(
|
||||
{},
|
||||
{
|
||||
get() {
|
||||
return jest.fn();
|
||||
},
|
||||
}
|
||||
),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('policy preconfiguration', () => {
|
||||
beforeEach(() => {
|
||||
mockInstalledPackages.clear();
|
||||
|
|
|
@ -18,6 +18,7 @@ import type {
|
|||
NewPackagePolicyInputStream,
|
||||
PreconfiguredAgentPolicy,
|
||||
PreconfiguredPackage,
|
||||
PreconfigurationError,
|
||||
} from '../../common';
|
||||
import {
|
||||
PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE,
|
||||
|
@ -28,9 +29,16 @@ import { escapeSearchQueryPhrase } from './saved_object';
|
|||
|
||||
import { pkgToPkgKey } from './epm/registry';
|
||||
import { getInstallation } from './epm/packages';
|
||||
import { ensureInstalledPackage } from './epm/packages/install';
|
||||
import { ensurePackagesCompletedInstall } from './epm/packages/install';
|
||||
import { bulkInstallPackages } from './epm/packages/bulk_install_packages';
|
||||
import { agentPolicyService, addPackageToAgentPolicy } from './agent_policy';
|
||||
|
||||
interface PreconfigurationResult {
|
||||
policies: Array<{ id: string; updated_at: string }>;
|
||||
packages: string[];
|
||||
nonFatalErrors?: PreconfigurationError[];
|
||||
}
|
||||
|
||||
export type InputsOverride = Partial<NewPackagePolicyInput> & {
|
||||
vars?: Array<NewPackagePolicyInput['vars'] & { name: string }>;
|
||||
};
|
||||
|
@ -41,7 +49,7 @@ export async function ensurePreconfiguredPackagesAndPolicies(
|
|||
policies: PreconfiguredAgentPolicy[] = [],
|
||||
packages: PreconfiguredPackage[] = [],
|
||||
defaultOutput: Output
|
||||
) {
|
||||
): Promise<PreconfigurationResult> {
|
||||
// Validate configured packages to ensure there are no version conflicts
|
||||
const packageNames = groupBy(packages, (pkg) => pkg.name);
|
||||
const duplicatePackages = Object.entries(packageNames).filter(
|
||||
|
@ -66,28 +74,64 @@ export async function ensurePreconfiguredPackagesAndPolicies(
|
|||
}
|
||||
|
||||
// Preinstall packages specified in Kibana config
|
||||
const preconfiguredPackages = await Promise.all(
|
||||
packages.map(({ name, version }) =>
|
||||
ensureInstalledPreconfiguredPackage(soClient, esClient, name, version)
|
||||
)
|
||||
);
|
||||
const preconfiguredPackages = await bulkInstallPackages({
|
||||
savedObjectsClient: soClient,
|
||||
esClient,
|
||||
packagesToInstall: packages.map((pkg) =>
|
||||
pkg.version === PRECONFIGURATION_LATEST_KEYWORD ? pkg.name : pkg
|
||||
),
|
||||
force: true, // Always force outdated packages to be installed if a later version isn't installed
|
||||
});
|
||||
|
||||
const fulfilledPackages = [];
|
||||
const rejectedPackages = [];
|
||||
for (let i = 0; i < preconfiguredPackages.length; i++) {
|
||||
const packageResult = preconfiguredPackages[i];
|
||||
if ('error' in packageResult)
|
||||
rejectedPackages.push({
|
||||
package: { name: packages[i].name, version: packages[i].version },
|
||||
error: packageResult.error,
|
||||
} as PreconfigurationError);
|
||||
else fulfilledPackages.push(packageResult);
|
||||
}
|
||||
|
||||
// Keeping this outside of the Promise.all because it introduces a race condition.
|
||||
// If one of the required packages fails to install/upgrade it might get stuck in the installing state.
|
||||
// On the next call to the /setup API, if there is a upgrade available for one of the required packages a race condition
|
||||
// will occur between upgrading the package and reinstalling the previously failed package.
|
||||
// By moving this outside of the Promise.all, the upgrade will occur first, and then we'll attempt to reinstall any
|
||||
// packages that are stuck in the installing state.
|
||||
await ensurePackagesCompletedInstall(soClient, esClient);
|
||||
|
||||
// Create policies specified in Kibana config
|
||||
const preconfiguredPolicies = await Promise.all(
|
||||
const preconfiguredPolicies = await Promise.allSettled(
|
||||
policies.map(async (preconfiguredAgentPolicy) => {
|
||||
// Check to see if a preconfigured policy with the same preconfigurationId was already deleted by the user
|
||||
const preconfigurationId = String(preconfiguredAgentPolicy.id);
|
||||
const searchParams = {
|
||||
searchFields: ['preconfiguration_id'],
|
||||
search: escapeSearchQueryPhrase(preconfigurationId),
|
||||
};
|
||||
const deletionRecords = await soClient.find({
|
||||
type: PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE,
|
||||
...searchParams,
|
||||
});
|
||||
const wasDeleted = deletionRecords.total > 0;
|
||||
if (wasDeleted) {
|
||||
return { created: false, deleted: preconfigurationId };
|
||||
if (preconfiguredAgentPolicy.id) {
|
||||
// Check to see if a preconfigured policy with the same preconfigurationId was already deleted by the user
|
||||
const preconfigurationId = String(preconfiguredAgentPolicy.id);
|
||||
const searchParams = {
|
||||
searchFields: ['preconfiguration_id'],
|
||||
search: escapeSearchQueryPhrase(preconfigurationId),
|
||||
};
|
||||
const deletionRecords = await soClient.find({
|
||||
type: PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE,
|
||||
...searchParams,
|
||||
});
|
||||
const wasDeleted = deletionRecords.total > 0;
|
||||
if (wasDeleted) {
|
||||
return { created: false, deleted: preconfigurationId };
|
||||
}
|
||||
} else if (
|
||||
!preconfiguredAgentPolicy.is_default &&
|
||||
!preconfiguredAgentPolicy.is_default_fleet_server
|
||||
) {
|
||||
throw new Error(
|
||||
i18n.translate('xpack.fleet.preconfiguration.missingIDError', {
|
||||
defaultMessage:
|
||||
'{agentPolicyName} is missing an `id` field. `id` is required, except for policies marked is_default or is_default_fleet_server.',
|
||||
values: { agentPolicyName: preconfiguredAgentPolicy.name },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const { created, policy } = await agentPolicyService.ensurePreconfiguredAgentPolicy(
|
||||
|
@ -132,13 +176,24 @@ export async function ensurePreconfiguredPackagesAndPolicies(
|
|||
})
|
||||
);
|
||||
|
||||
for (const preconfiguredPolicy of preconfiguredPolicies) {
|
||||
const fulfilledPolicies = [];
|
||||
const rejectedPolicies = [];
|
||||
for (let i = 0; i < preconfiguredPolicies.length; i++) {
|
||||
const policyResult = preconfiguredPolicies[i];
|
||||
if (policyResult.status === 'rejected') {
|
||||
rejectedPolicies.push({
|
||||
error: policyResult.reason as Error,
|
||||
agentPolicy: { name: policies[i].name },
|
||||
} as PreconfigurationError);
|
||||
continue;
|
||||
}
|
||||
fulfilledPolicies.push(policyResult.value);
|
||||
const {
|
||||
created,
|
||||
policy,
|
||||
installedPackagePolicies,
|
||||
shouldAddIsManagedFlag,
|
||||
} = preconfiguredPolicy;
|
||||
} = policyResult.value;
|
||||
if (created) {
|
||||
await addPreconfiguredPolicyPackages(
|
||||
soClient,
|
||||
|
@ -155,21 +210,22 @@ export async function ensurePreconfiguredPackagesAndPolicies(
|
|||
}
|
||||
|
||||
return {
|
||||
policies: preconfiguredPolicies.map((p) =>
|
||||
policies: fulfilledPolicies.map((p) =>
|
||||
p.policy
|
||||
? {
|
||||
id: p.policy.id,
|
||||
id: p.policy.id!,
|
||||
updated_at: p.policy.updated_at,
|
||||
}
|
||||
: {
|
||||
id: p.deleted,
|
||||
id: p.deleted!,
|
||||
updated_at: i18n.translate('xpack.fleet.preconfiguration.policyDeleted', {
|
||||
defaultMessage: 'Preconfigured policy {id} was deleted; skipping creation',
|
||||
values: { id: p.deleted },
|
||||
}),
|
||||
}
|
||||
),
|
||||
packages: preconfiguredPackages.map((pkg) => pkgToPkgKey(pkg)),
|
||||
packages: fulfilledPackages.map((pkg) => pkgToPkgKey(pkg)),
|
||||
nonFatalErrors: [...rejectedPackages, ...rejectedPolicies],
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -201,21 +257,6 @@ async function addPreconfiguredPolicyPackages(
|
|||
}
|
||||
}
|
||||
|
||||
async function ensureInstalledPreconfiguredPackage(
|
||||
soClient: SavedObjectsClientContract,
|
||||
esClient: ElasticsearchClient,
|
||||
pkgName: string,
|
||||
pkgVersion: string
|
||||
) {
|
||||
const isLatest = pkgVersion === PRECONFIGURATION_LATEST_KEYWORD;
|
||||
return ensureInstalledPackage({
|
||||
savedObjectsClient: soClient,
|
||||
pkgName,
|
||||
esClient,
|
||||
pkgVersion: isLatest ? undefined : pkgVersion,
|
||||
});
|
||||
}
|
||||
|
||||
function overridePackageInputs(
|
||||
basePackagePolicy: NewPackagePolicy,
|
||||
inputsOverride?: InputsOverride[]
|
||||
|
@ -228,15 +269,19 @@ function overridePackageInputs(
|
|||
for (const override of inputsOverride) {
|
||||
const originalInput = inputs.find((i) => i.type === override.type);
|
||||
if (!originalInput) {
|
||||
throw new Error(
|
||||
i18n.translate('xpack.fleet.packagePolicyInputOverrideError', {
|
||||
defaultMessage: 'Input type {inputType} does not exist on package {packageName}',
|
||||
values: {
|
||||
inputType: override.type,
|
||||
packageName,
|
||||
},
|
||||
})
|
||||
);
|
||||
const e = {
|
||||
error: new Error(
|
||||
i18n.translate('xpack.fleet.packagePolicyInputOverrideError', {
|
||||
defaultMessage: 'Input type {inputType} does not exist on package {packageName}',
|
||||
values: {
|
||||
inputType: override.type,
|
||||
packageName,
|
||||
},
|
||||
})
|
||||
),
|
||||
package: { name: packageName, version: basePackagePolicy.package!.version },
|
||||
};
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (typeof override.enabled !== 'undefined') originalInput.enabled = override.enabled;
|
||||
|
@ -245,16 +290,21 @@ function overridePackageInputs(
|
|||
try {
|
||||
deepMergeVars(override, originalInput);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
i18n.translate('xpack.fleet.packagePolicyVarOverrideError', {
|
||||
defaultMessage: 'Var {varName} does not exist on {inputType} of package {packageName}',
|
||||
values: {
|
||||
varName: e.message,
|
||||
inputType: override.type,
|
||||
packageName,
|
||||
},
|
||||
})
|
||||
);
|
||||
const err = {
|
||||
error: new Error(
|
||||
i18n.translate('xpack.fleet.packagePolicyVarOverrideError', {
|
||||
defaultMessage:
|
||||
'Var {varName} does not exist on {inputType} of package {packageName}',
|
||||
values: {
|
||||
varName: e.message,
|
||||
inputType: override.type,
|
||||
packageName,
|
||||
},
|
||||
})
|
||||
),
|
||||
package: { name: packageName, version: basePackagePolicy.package!.version },
|
||||
};
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -264,17 +314,21 @@ function overridePackageInputs(
|
|||
(s) => s.data_stream.dataset === stream.data_stream.dataset
|
||||
);
|
||||
if (!originalStream) {
|
||||
throw new Error(
|
||||
i18n.translate('xpack.fleet.packagePolicyStreamOverrideError', {
|
||||
defaultMessage:
|
||||
'Data stream {streamSet} does not exist on {inputType} of package {packageName}',
|
||||
values: {
|
||||
streamSet: stream.data_stream.dataset,
|
||||
inputType: override.type,
|
||||
packageName,
|
||||
},
|
||||
})
|
||||
);
|
||||
const e = {
|
||||
error: new Error(
|
||||
i18n.translate('xpack.fleet.packagePolicyStreamOverrideError', {
|
||||
defaultMessage:
|
||||
'Data stream {streamSet} does not exist on {inputType} of package {packageName}',
|
||||
values: {
|
||||
streamSet: stream.data_stream.dataset,
|
||||
inputType: override.type,
|
||||
packageName,
|
||||
},
|
||||
})
|
||||
),
|
||||
package: { name: packageName, version: basePackagePolicy.package!.version },
|
||||
};
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (typeof stream.enabled !== 'undefined') originalStream.enabled = stream.enabled;
|
||||
|
@ -283,18 +337,22 @@ function overridePackageInputs(
|
|||
try {
|
||||
deepMergeVars(stream as InputsOverride, originalStream);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
i18n.translate('xpack.fleet.packagePolicyStreamVarOverrideError', {
|
||||
defaultMessage:
|
||||
'Var {varName} does not exist on {streamSet} for {inputType} of package {packageName}',
|
||||
values: {
|
||||
varName: e.message,
|
||||
streamSet: stream.data_stream.dataset,
|
||||
inputType: override.type,
|
||||
packageName,
|
||||
},
|
||||
})
|
||||
);
|
||||
const err = {
|
||||
error: new Error(
|
||||
i18n.translate('xpack.fleet.packagePolicyStreamVarOverrideError', {
|
||||
defaultMessage:
|
||||
'Var {varName} does not exist on {streamSet} for {inputType} of package {packageName}',
|
||||
values: {
|
||||
varName: e.message,
|
||||
streamSet: stream.data_stream.dataset,
|
||||
inputType: override.type,
|
||||
packageName,
|
||||
},
|
||||
})
|
||||
),
|
||||
package: { name: packageName, version: basePackagePolicy.package!.version },
|
||||
};
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,23 +6,15 @@
|
|||
*/
|
||||
|
||||
import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { DEFAULT_AGENT_POLICIES_PACKAGES, FLEET_SERVER_PACKAGE } from '../../common';
|
||||
|
||||
import type { PackagePolicy, DefaultPackagesInstallationError } from '../../common';
|
||||
|
||||
import { SO_SEARCH_LIMIT } from '../constants';
|
||||
import type { DefaultPackagesInstallationError, PreconfigurationError } from '../../common';
|
||||
import { SO_SEARCH_LIMIT, REQUIRED_PACKAGES } from '../constants';
|
||||
|
||||
import { appContextService } from './app_context';
|
||||
import { agentPolicyService, addPackageToAgentPolicy } from './agent_policy';
|
||||
import { agentPolicyService } from './agent_policy';
|
||||
import { ensurePreconfiguredPackagesAndPolicies } from './preconfiguration';
|
||||
import { outputService } from './output';
|
||||
import {
|
||||
ensureInstalledDefaultPackages,
|
||||
ensureInstalledPackage,
|
||||
ensurePackagesCompletedInstall,
|
||||
} from './epm/packages/install';
|
||||
|
||||
import { generateEnrollmentAPIKey, hasEnrollementAPIKeysForPolicy } from './api_keys';
|
||||
import { settingsService } from '.';
|
||||
import { awaitIfPending } from './setup_utils';
|
||||
|
@ -32,8 +24,7 @@ import { awaitIfFleetServerSetupPending } from './fleet_server';
|
|||
|
||||
export interface SetupStatus {
|
||||
isInitialized: boolean;
|
||||
preconfigurationError: { name: string; message: string } | undefined;
|
||||
nonFatalPackageUpgradeErrors: DefaultPackagesInstallationError[];
|
||||
nonFatalErrors?: Array<PreconfigurationError | DefaultPackagesInstallationError>;
|
||||
}
|
||||
|
||||
export async function setupIngestManager(
|
||||
|
@ -47,9 +38,7 @@ async function createSetupSideEffects(
|
|||
soClient: SavedObjectsClientContract,
|
||||
esClient: ElasticsearchClient
|
||||
): Promise<SetupStatus> {
|
||||
const [defaultPackagesResult, defaultOutput] = await Promise.all([
|
||||
// packages installed by default
|
||||
ensureInstalledDefaultPackages(soClient, esClient),
|
||||
const [defaultOutput] = await Promise.all([
|
||||
outputService.ensureDefaultOutput(soClient),
|
||||
settingsService.getSettings(soClient).catch((e: any) => {
|
||||
if (e.isBoom && e.output.statusCode === 404) {
|
||||
|
@ -61,122 +50,35 @@ async function createSetupSideEffects(
|
|||
}),
|
||||
]);
|
||||
|
||||
// Keeping this outside of the Promise.all because it introduces a race condition.
|
||||
// If one of the required packages fails to install/upgrade it might get stuck in the installing state.
|
||||
// On the next call to the /setup API, if there is a upgrade available for one of the required packages a race condition
|
||||
// will occur between upgrading the package and reinstalling the previously failed package.
|
||||
// By moving this outside of the Promise.all, the upgrade will occur first, and then we'll attempt to reinstall any
|
||||
// packages that are stuck in the installing state.
|
||||
await ensurePackagesCompletedInstall(soClient, esClient);
|
||||
|
||||
await awaitIfFleetServerSetupPending();
|
||||
|
||||
const fleetServerPackage = await ensureInstalledPackage({
|
||||
savedObjectsClient: soClient,
|
||||
pkgName: FLEET_SERVER_PACKAGE,
|
||||
esClient,
|
||||
});
|
||||
|
||||
const { agentPolicies: policiesOrUndefined, packages: packagesOrUndefined } =
|
||||
appContextService.getConfig() ?? {};
|
||||
|
||||
const policies = policiesOrUndefined ?? [];
|
||||
const packages = packagesOrUndefined ?? [];
|
||||
let preconfigurationError;
|
||||
|
||||
try {
|
||||
await ensurePreconfiguredPackagesAndPolicies(
|
||||
soClient,
|
||||
esClient,
|
||||
policies,
|
||||
packages,
|
||||
defaultOutput
|
||||
);
|
||||
} catch (e) {
|
||||
preconfigurationError = { name: e.name, message: e.message };
|
||||
}
|
||||
let packages = packagesOrUndefined ?? [];
|
||||
// Ensure that required packages are always installed even if they're left out of the config
|
||||
const preconfiguredPackageNames = new Set(packages.map((pkg) => pkg.name));
|
||||
packages = [
|
||||
...packages,
|
||||
...REQUIRED_PACKAGES.filter((pkg) => !preconfiguredPackageNames.has(pkg.name)),
|
||||
];
|
||||
|
||||
// Ensure the predefined default policies AFTER loading preconfigured policies. This allows the kibana config
|
||||
// to override the default agent policies.
|
||||
|
||||
const [
|
||||
{ created: defaultAgentPolicyCreated, policy: defaultAgentPolicy },
|
||||
{ created: defaultFleetServerPolicyCreated, policy: defaultFleetServerPolicy },
|
||||
] = await Promise.all([
|
||||
agentPolicyService.ensureDefaultAgentPolicy(soClient, esClient),
|
||||
agentPolicyService.ensureDefaultFleetServerAgentPolicy(soClient, esClient),
|
||||
]);
|
||||
|
||||
// If we just created the default fleet server policy add the fleet server package
|
||||
if (defaultFleetServerPolicyCreated) {
|
||||
await addPackageToAgentPolicy(
|
||||
soClient,
|
||||
esClient,
|
||||
fleetServerPackage,
|
||||
defaultFleetServerPolicy,
|
||||
defaultOutput
|
||||
);
|
||||
}
|
||||
|
||||
// If we just created the default policy, ensure default packages are added to it
|
||||
if (defaultAgentPolicyCreated) {
|
||||
const agentPolicyWithPackagePolicies = await agentPolicyService.get(
|
||||
soClient,
|
||||
defaultAgentPolicy.id,
|
||||
true
|
||||
);
|
||||
if (!agentPolicyWithPackagePolicies) {
|
||||
throw new Error(
|
||||
i18n.translate('xpack.fleet.setup.policyNotFoundError', {
|
||||
defaultMessage: 'Policy not found',
|
||||
})
|
||||
);
|
||||
}
|
||||
if (
|
||||
agentPolicyWithPackagePolicies.package_policies.length &&
|
||||
typeof agentPolicyWithPackagePolicies.package_policies[0] === 'string'
|
||||
) {
|
||||
throw new Error(
|
||||
i18n.translate('xpack.fleet.setup.policyNotFoundError', {
|
||||
defaultMessage: 'Policy not found',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
for (const installedPackage of defaultPackagesResult.installations) {
|
||||
const packageShouldBeInstalled = DEFAULT_AGENT_POLICIES_PACKAGES.some(
|
||||
(packageName) => installedPackage.name === packageName
|
||||
);
|
||||
if (!packageShouldBeInstalled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isInstalled = agentPolicyWithPackagePolicies.package_policies.some(
|
||||
(d: PackagePolicy | string) => {
|
||||
return typeof d !== 'string' && d.package?.name === installedPackage.name;
|
||||
}
|
||||
);
|
||||
|
||||
if (!isInstalled) {
|
||||
await addPackageToAgentPolicy(
|
||||
soClient,
|
||||
esClient,
|
||||
installedPackage,
|
||||
agentPolicyWithPackagePolicies,
|
||||
defaultOutput
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
const { nonFatalErrors } = await ensurePreconfiguredPackagesAndPolicies(
|
||||
soClient,
|
||||
esClient,
|
||||
policies,
|
||||
packages,
|
||||
defaultOutput
|
||||
);
|
||||
|
||||
await ensureDefaultEnrollmentAPIKeysExists(soClient, esClient);
|
||||
|
||||
await ensureAgentActionPolicyChangeExists(soClient, esClient);
|
||||
|
||||
return {
|
||||
isInitialized: true,
|
||||
preconfigurationError,
|
||||
nonFatalPackageUpgradeErrors: defaultPackagesResult.nonFatalPackageUpgradeErrors,
|
||||
nonFatalErrors,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,12 @@ import { i18n } from '@kbn/i18n';
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import semverValid from 'semver/functions/valid';
|
||||
|
||||
import { PRECONFIGURATION_LATEST_KEYWORD } from '../../constants';
|
||||
import {
|
||||
PRECONFIGURATION_LATEST_KEYWORD,
|
||||
DEFAULT_AGENT_POLICY,
|
||||
DEFAULT_FLEET_SERVER_AGENT_POLICY,
|
||||
DEFAULT_PACKAGES,
|
||||
} from '../../constants';
|
||||
|
||||
import { AgentPolicyBaseSchema } from './agent_policy';
|
||||
import { NamespaceSchema } from './package_policy';
|
||||
|
@ -36,14 +41,17 @@ export const PreconfiguredPackagesSchema = schema.arrayOf(
|
|||
}
|
||||
},
|
||||
}),
|
||||
})
|
||||
}),
|
||||
{
|
||||
defaultValue: DEFAULT_PACKAGES,
|
||||
}
|
||||
);
|
||||
|
||||
export const PreconfiguredAgentPoliciesSchema = schema.arrayOf(
|
||||
schema.object({
|
||||
...AgentPolicyBaseSchema,
|
||||
namespace: schema.maybe(NamespaceSchema),
|
||||
id: schema.oneOf([schema.string(), schema.number()]),
|
||||
id: schema.maybe(schema.oneOf([schema.string(), schema.number()])),
|
||||
is_default: schema.maybe(schema.boolean()),
|
||||
is_default_fleet_server: schema.maybe(schema.boolean()),
|
||||
package_policies: schema.arrayOf(
|
||||
|
@ -77,5 +85,8 @@ export const PreconfiguredAgentPoliciesSchema = schema.arrayOf(
|
|||
),
|
||||
})
|
||||
),
|
||||
})
|
||||
}),
|
||||
{
|
||||
defaultValue: [DEFAULT_AGENT_POLICY, DEFAULT_FLEET_SERVER_AGENT_POLICY],
|
||||
}
|
||||
);
|
||||
|
|
|
@ -41,6 +41,7 @@ export default function (providerContext: FtrProviderContext) {
|
|||
expect(body).to.eql({
|
||||
packages: [],
|
||||
policies: [],
|
||||
nonFatalErrors: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue