diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index db36c9cf248e7..505390a17f683 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -200,6 +200,8 @@ kibana_vars=( xpack.fleet.agents.elasticsearch.host xpack.fleet.agents.kibana.host xpack.fleet.agents.tlsCheckDisabled + xpack.fleet.agentPolicies + xpack.fleet.packages xpack.fleet.registryUrl xpack.graph.canEditDrillDownUrls xpack.graph.enabled diff --git a/x-pack/plugins/fleet/common/constants/index.ts b/x-pack/plugins/fleet/common/constants/index.ts index 5598e63219776..3704533e79b4a 100644 --- a/x-pack/plugins/fleet/common/constants/index.ts +++ b/x-pack/plugins/fleet/common/constants/index.ts @@ -15,6 +15,7 @@ export * from './epm'; export * from './output'; export * from './enrollment_api_key'; export * from './settings'; +export * from './preconfiguration'; // TODO: This is the default `index.max_result_window` ES setting, which dictates // the maximum amount of results allowed to be returned from a search. It's possible diff --git a/x-pack/plugins/fleet/common/constants/preconfiguration.ts b/x-pack/plugins/fleet/common/constants/preconfiguration.ts new file mode 100644 index 0000000000000..376ba551b1359 --- /dev/null +++ b/x-pack/plugins/fleet/common/constants/preconfiguration.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE = + 'fleet-preconfiguration-deletion-record'; diff --git a/x-pack/plugins/fleet/common/types/index.ts b/x-pack/plugins/fleet/common/types/index.ts index 1984de79a6357..cdea56448f3a2 100644 --- a/x-pack/plugins/fleet/common/types/index.ts +++ b/x-pack/plugins/fleet/common/types/index.ts @@ -7,6 +7,7 @@ export * from './models'; export * from './rest_spec'; +import type { PreconfiguredAgentPolicy, PreconfiguredPackage } from './models/preconfiguration'; export interface FleetConfigType { enabled: boolean; @@ -32,6 +33,8 @@ export interface FleetConfigType { agentPolicyRolloutRateLimitIntervalMs: number; agentPolicyRolloutRateLimitRequestPerInterval: number; }; + agentPolicies?: PreconfiguredAgentPolicy[]; + packages?: PreconfiguredPackage[]; } // Calling Object.entries(PackagesGroupedByStatus) gave `status: string` diff --git a/x-pack/plugins/fleet/common/types/rest_spec/ingest_setup.ts b/x-pack/plugins/fleet/common/types/rest_spec/ingest_setup.ts index 12054aff124f7..2180b66908498 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/ingest_setup.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/ingest_setup.ts @@ -7,4 +7,5 @@ export interface PostIngestSetupResponse { isInitialized: boolean; + preconfigurationError?: { name: string; message: string }; } diff --git a/x-pack/plugins/fleet/public/applications/fleet/app.tsx b/x-pack/plugins/fleet/public/applications/fleet/app.tsx index 2c24468b14782..5663bd4768d5c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/app.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/app.tsx @@ -28,6 +28,7 @@ import { sendSetup, useBreadcrumbs, useConfig, + useStartServices, } from './hooks'; import { Error, Loading } from './components'; import { IntraAppStateProvider } from './hooks/use_intra_app_state'; @@ -59,6 +60,7 @@ const Panel = styled(EuiPanel)` export const WithPermissionsAndSetup: React.FC = memo(({ children }) => { useBreadcrumbs('base'); + const { notifications } = useStartServices(); const [isPermissionsLoading, setIsPermissionsLoading] = useState(false); const [permissionsError, setPermissionsError] = useState(); @@ -81,6 +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, { + title: i18n.translate('xpack.fleet.setup.uiPreconfigurationErrorTitle', { + defaultMessage: 'Configuration error', + }), + }); + } } catch (err) { setInitializationError(err); } @@ -92,7 +101,7 @@ export const WithPermissionsAndSetup: React.FC = memo(({ children }) => { setPermissionsError('REQUEST_ERROR'); } })(); - }, []); + }, [notifications.toasts]); if (isPermissionsLoading || permissionsError) { return ( diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts index 7f5586fb0f034..27af46d0a757d 100644 --- a/x-pack/plugins/fleet/server/constants/index.ts +++ b/x-pack/plugins/fleet/server/constants/index.ts @@ -52,4 +52,5 @@ export { // Fleet Server index ENROLLMENT_API_KEYS_INDEX, AGENTS_INDEX, + PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, } from '../../common'; diff --git a/x-pack/plugins/fleet/server/index.ts b/x-pack/plugins/fleet/server/index.ts index 0178b801f4d2f..c66dd471690eb 100644 --- a/x-pack/plugins/fleet/server/index.ts +++ b/x-pack/plugins/fleet/server/index.ts @@ -15,6 +15,8 @@ import { AGENT_POLLING_REQUEST_TIMEOUT_MS, } from '../common'; +import { PreconfiguredPackagesSchema, PreconfiguredAgentPoliciesSchema } from './types'; + import { FleetPlugin } from './plugin'; export { default as apm } from 'elastic-apm-node'; @@ -77,6 +79,8 @@ export const config: PluginConfigDescriptor = { defaultValue: AGENT_POLICY_ROLLOUT_RATE_LIMIT_REQUEST_PER_INTERVAL, }), }), + packages: schema.maybe(PreconfiguredPackagesSchema), + agentPolicies: schema.maybe(PreconfiguredAgentPoliciesSchema), }), }; diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 20cfae6bc1cf2..d25b1e13904db 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -48,6 +48,7 @@ import { AGENT_SAVED_OBJECT_TYPE, AGENT_EVENT_SAVED_OBJECT_TYPE, ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, } from './constants'; import { registerSavedObjects, registerEncryptedSavedObjects } from './saved_objects'; import { @@ -133,6 +134,7 @@ const allSavedObjectTypes = [ AGENT_SAVED_OBJECT_TYPE, AGENT_EVENT_SAVED_OBJECT_TYPE, ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, ]; /** diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts index 469b2409f140a..2618f3de0d534 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts @@ -45,7 +45,9 @@ describe('FleetSetupHandler', () => { }); it('POST /setup succeeds w/200 and body of resolved value', async () => { - mockSetupIngestManager.mockImplementation(() => Promise.resolve({ isIntialized: true })); + mockSetupIngestManager.mockImplementation(() => + Promise.resolve({ isInitialized: true, preconfigurationError: undefined }) + ); await FleetSetupHandler(context, request, response); const expectedBody: PostIngestSetupResponse = { isInitialized: true }; diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.ts index a7fdcf78f4be9..e94c9470dd350 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.ts @@ -63,13 +63,13 @@ export const createFleetSetupHandler: RequestHandler< try { const soClient = context.core.savedObjects.client; const esClient = context.core.elasticsearch.client.asCurrentUser; - await setupIngestManager(soClient, esClient); + const body = await setupIngestManager(soClient, esClient); await setupFleet(soClient, esClient, { forceRecreate: request.body?.forceRecreate ?? false, }); return response.ok({ - body: { isInitialized: true }, + body, }); } catch (error) { return defaultIngestErrorHandler({ error, response }); @@ -81,8 +81,7 @@ export const FleetSetupHandler: RequestHandler = async (context, request, respon const esClient = context.core.elasticsearch.client.asCurrentUser; try { - const body: PostIngestSetupResponse = { isInitialized: true }; - await setupIngestManager(soClient, esClient); + const body: PostIngestSetupResponse = await setupIngestManager(soClient, esClient); return response.ok({ body, }); diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index 8554c0702f733..58ec3972ca517 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -19,6 +19,7 @@ import { AGENT_ACTION_SAVED_OBJECT_TYPE, ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, + PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, } from '../constants'; import { @@ -358,6 +359,19 @@ const getSavedObjectTypes = ( }, }, }, + [PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE]: { + name: PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, + hidden: false, + namespaceType: 'agnostic', + management: { + importableAndExportable: false, + }, + mappings: { + properties: { + preconfiguration_id: { type: 'keyword' }, + }, + }, + }, }); export function registerSavedObjects( diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 7f793a41ab985..59214e287c873 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -19,6 +19,7 @@ import { DEFAULT_AGENT_POLICY, AGENT_POLICY_SAVED_OBJECT_TYPE, AGENT_SAVED_OBJECT_TYPE, + PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, } from '../constants'; import type { PackagePolicy, @@ -150,7 +151,7 @@ class AgentPolicyService { config: PreconfiguredAgentPolicy ): Promise<{ created: boolean; - policy: AgentPolicy; + policy?: AgentPolicy; }> { const { id, ...preconfiguredAgentPolicy } = omit(config, 'package_policies'); const preconfigurationId = String(id); @@ -582,6 +583,13 @@ class AgentPolicyService { } ); } + + if (agentPolicy.preconfiguration_id) { + await soClient.create(PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, { + preconfiguration_id: String(agentPolicy.preconfiguration_id), + }); + } + await soClient.delete(SAVED_OBJECT_TYPE, id); await this.triggerAgentPolicyUpdatedEvent(soClient, esClient, 'deleted', id); return { @@ -819,5 +827,6 @@ export async function addPackageToAgentPolicy( await packagePolicyService.create(soClient, esClient, newPackagePolicy, { bumpRevision: false, + skipEnsureInstalled: true, }); } diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 7d12aad6f32b5..1d2295a553462 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -60,7 +60,13 @@ class PackagePolicyService { soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, packagePolicy: NewPackagePolicy, - options?: { id?: string; user?: AuthenticatedUser; bumpRevision?: boolean; force?: boolean } + options?: { + id?: string; + user?: AuthenticatedUser; + bumpRevision?: boolean; + force?: boolean; + skipEnsureInstalled?: boolean; + } ): Promise { // Check that its agent policy does not have a package policy with the same name const parentAgentPolicy = await agentPolicyService.get(soClient, packagePolicy.policy_id); @@ -90,18 +96,25 @@ class PackagePolicyService { // Make sure the associated package is installed if (packagePolicy.package?.name) { - const [, pkgInfo] = await Promise.all([ - ensureInstalledPackage({ - savedObjectsClient: soClient, - pkgName: packagePolicy.package.name, - esClient, - }), - getPackageInfo({ - savedObjectsClient: soClient, - pkgName: packagePolicy.package.name, - pkgVersion: packagePolicy.package.version, - }), - ]); + const pkgInfoPromise = getPackageInfo({ + savedObjectsClient: soClient, + pkgName: packagePolicy.package.name, + pkgVersion: packagePolicy.package.version, + }); + + let pkgInfo; + if (options?.skipEnsureInstalled) pkgInfo = await pkgInfoPromise; + else { + const [, packageInfo] = await Promise.all([ + ensureInstalledPackage({ + savedObjectsClient: soClient, + pkgName: packagePolicy.package.name, + esClient, + }), + pkgInfoPromise, + ]); + pkgInfo = packageInfo; + } // Check if it is a limited package, and if so, check that the corresponding agent policy does not // already contain a package policy for this package diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts index 8a885f9c5c821..94865f5d3d917 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts @@ -10,6 +10,8 @@ import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/serve import type { PreconfiguredAgentPolicy } from '../../common/types'; import type { AgentPolicy, NewPackagePolicy, Output } from '../types'; +import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../constants'; + import { ensurePreconfiguredPackagesAndPolicies } from './preconfiguration'; const mockInstalledPackages = new Map(); @@ -27,30 +29,31 @@ const mockDefaultOutput: Output = { function getPutPreconfiguredPackagesMock() { const soClient = savedObjectsClientMock.create(); soClient.find.mockImplementation(async ({ type, search }) => { - const attributes = mockConfiguredPolicies.get(search!.replace(/"/g, '')); - if (attributes) { - return { - saved_objects: [ - { - id: `mocked-${attributes.preconfiguration_id}`, - attributes, - type: type as string, - score: 1, - references: [], - }, - ], - total: 1, - page: 1, - per_page: 1, - }; - } else { - return { - saved_objects: [], - total: 0, - page: 1, - per_page: 0, - }; + if (type === AGENT_POLICY_SAVED_OBJECT_TYPE) { + const attributes = mockConfiguredPolicies.get(search!.replace(/"/g, '')); + if (attributes) { + return { + saved_objects: [ + { + id: `mocked-${attributes.preconfiguration_id}`, + attributes, + type: type as string, + score: 1, + references: [], + }, + ], + total: 1, + page: 1, + per_page: 1, + }; + } } + return { + saved_objects: [], + total: 0, + page: 1, + per_page: 0, + }; }); soClient.create.mockImplementation(async (type, policy) => { const attributes = policy as AgentPolicy; diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts index 97480fcf6b2a8..3bd3169673b31 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts @@ -19,6 +19,9 @@ import type { PreconfiguredAgentPolicy, PreconfiguredPackage, } from '../../common'; +import { PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE } from '../constants'; + +import { escapeSearchQueryPhrase } from './saved_object'; import { pkgToPkgKey } from './epm/registry'; import { getInstallation } from './epm/packages'; @@ -69,6 +72,21 @@ export async function ensurePreconfiguredPackagesAndPolicies( // Create policies specified in Kibana config const preconfiguredPolicies = await Promise.all( 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 }; + } + const { created, policy } = await agentPolicyService.ensurePreconfiguredAgentPolicy( soClient, esClient, @@ -122,22 +140,32 @@ export async function ensurePreconfiguredPackagesAndPolicies( await addPreconfiguredPolicyPackages( soClient, esClient, - policy, + policy!, installedPackagePolicies!, defaultOutput ); // Add the is_managed flag after configuring package policies to avoid errors if (shouldAddIsManagedFlag) { - agentPolicyService.update(soClient, esClient, policy.id, { is_managed: true }); + agentPolicyService.update(soClient, esClient, policy!.id, { is_managed: true }); } } } return { - policies: preconfiguredPolicies.map((p) => ({ - id: p.policy.id, - updated_at: p.policy.updated_at, - })), + policies: preconfiguredPolicies.map((p) => + p.policy + ? { + id: p.policy.id, + updated_at: p.policy.updated_at, + } + : { + 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)), }; } @@ -155,20 +183,19 @@ async function addPreconfiguredPolicyPackages( >, defaultOutput: Output ) { - return await Promise.all( - installedPackagePolicies.map(async ({ installedPackage, name, description, inputs }) => - addPackageToAgentPolicy( - soClient, - esClient, - installedPackage, - agentPolicy, - defaultOutput, - name, - description, - (policy) => overridePackageInputs(policy, inputs) - ) - ) - ); + // Add packages synchronously to avoid overwriting + for (const { installedPackage, name, description, inputs } of installedPackagePolicies) { + await addPackageToAgentPolicy( + soClient, + esClient, + installedPackage, + agentPolicy, + defaultOutput, + name, + description, + (policy) => overridePackageInputs(policy, inputs) + ); + } } async function ensureInstalledPreconfiguredPackage( diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index b5e2326386e02..6d98bc4263a16 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -15,7 +15,9 @@ import type { PackagePolicy } from '../../common'; import { SO_SEARCH_LIMIT } from '../constants'; +import { appContextService } from './app_context'; import { agentPolicyService, addPackageToAgentPolicy } from './agent_policy'; +import { ensurePreconfiguredPackagesAndPolicies } from './preconfiguration'; import { outputService } from './output'; import { ensureInstalledDefaultPackages, @@ -34,7 +36,8 @@ const FLEET_ENROLL_USERNAME = 'fleet_enroll'; const FLEET_ENROLL_ROLE = 'fleet_enroll'; export interface SetupStatus { - isIntialized: true | undefined; + isInitialized: boolean; + preconfigurationError: { name: string; message: string } | undefined; } export async function setupIngestManager( @@ -48,17 +51,10 @@ async function createSetupSideEffects( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient ): Promise { - const [ - installedPackages, - defaultOutput, - { created: defaultAgentPolicyCreated, policy: defaultAgentPolicy }, - { created: defaultFleetServerPolicyCreated, policy: defaultFleetServerPolicy }, - ] = await Promise.all([ + const [installedPackages, defaultOutput] = await Promise.all([ // packages installed by default ensureInstalledDefaultPackages(soClient, esClient), outputService.ensureDefaultOutput(soClient), - agentPolicyService.ensureDefaultAgentPolicy(soClient, esClient), - agentPolicyService.ensureDefaultFleetServerAgentPolicy(soClient, esClient), updateFleetRoleIfExists(esClient), settingsService.getSettings(soClient).catch((e: any) => { if (e.isBoom && e.output.statusCode === 404) { @@ -86,6 +82,37 @@ async function createSetupSideEffects( 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 }; + } + + // 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, @@ -96,8 +123,6 @@ async function createSetupSideEffects( ); } - // If we just created the default fleet server policy add the fleet server package - // If we just created the default policy, ensure default packages are added to it if (defaultAgentPolicyCreated) { const agentPolicyWithPackagePolicies = await agentPolicyService.get( @@ -151,7 +176,7 @@ async function createSetupSideEffects( await ensureAgentActionPolicyChangeExists(soClient); - return { isIntialized: true }; + return { isInitialized: true, preconfigurationError }; } async function updateFleetRoleIfExists(esClient: ElasticsearchClient) {