[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:
Zacqary Adam Xeper 2021-04-22 11:25:10 -05:00 committed by GitHub
parent 97ebe11aac
commit 57f84f8593
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 412 additions and 547 deletions

View file

@ -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];

View file

@ -15,6 +15,7 @@ export const requiredPackages = {
System: 'system',
Endpoint: 'endpoint',
ElasticAgent: 'elastic_agent',
FleetServer: FLEET_SERVER_PACKAGE,
SecurityDetectionEngine: 'security_detection_engine',
} as const;

View file

@ -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;
}

View file

@ -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[];
}
>;

View file

@ -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 }>;
}

View file

@ -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);
}

View file

@ -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';

View file

@ -58,8 +58,8 @@ export const config: PluginConfigDescriptor = {
})
),
}),
packages: schema.maybe(PreconfiguredPackagesSchema),
agentPolicies: schema.maybe(PreconfiguredAgentPoliciesSchema),
packages: PreconfiguredPackagesSchema,
agentPolicies: PreconfiguredAgentPoliciesSchema,
}),
};

View file

@ -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 });
});

View file

@ -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 });

View file

@ -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,

View file

@ -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;
}

View file

@ -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();
});
});

View file

@ -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;

View file

@ -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();

View file

@ -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;
}
}
}

View file

@ -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,
};
}

View file

@ -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],
}
);

View file

@ -41,6 +41,7 @@ export default function (providerContext: FtrProviderContext) {
expect(body).to.eql({
packages: [],
policies: [],
nonFatalErrors: [],
});
});
});