diff --git a/docs/settings/fleet-settings.asciidoc b/docs/settings/fleet-settings.asciidoc index 9c054fbc0022..134d9de3f49d 100644 --- a/docs/settings/fleet-settings.asciidoc +++ b/docs/settings/fleet-settings.asciidoc @@ -39,8 +39,8 @@ See the {fleet-guide}/index.html[{fleet}] docs for more information. |=== | `xpack.fleet.agents.fleet_server.hosts` | Hostnames used by {agent} for accessing {fleet-server}. -| `xpack.fleet.agents.elasticsearch.host` - | The hostname used by {agent} for accessing {es}. +| `xpack.fleet.agents.elasticsearch.hosts` + | Hostnames used by {agent} for accessing {es}. |=== [NOTE] diff --git a/x-pack/plugins/fleet/common/types/index.ts b/x-pack/plugins/fleet/common/types/index.ts index 7117973baa13..95f91165aaf9 100644 --- a/x-pack/plugins/fleet/common/types/index.ts +++ b/x-pack/plugins/fleet/common/types/index.ts @@ -16,7 +16,7 @@ export interface FleetConfigType { agents: { enabled: boolean; elasticsearch: { - host?: string; + hosts?: string[]; ca_sha256?: string; }; fleet_server?: { diff --git a/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts index 7f0b71de779d..a9ad6b1bd879 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts @@ -15,7 +15,7 @@ export const createConfigurationMock = (): FleetConfigType => { agents: { enabled: true, elasticsearch: { - host: '', + hosts: [''], ca_sha256: '', }, }, diff --git a/x-pack/plugins/fleet/server/index.ts b/x-pack/plugins/fleet/server/index.ts index e83617413b74..0a886ffedbd6 100644 --- a/x-pack/plugins/fleet/server/index.ts +++ b/x-pack/plugins/fleet/server/index.ts @@ -41,6 +41,23 @@ export const config: PluginConfigDescriptor = { unused('agents.pollingRequestTimeout'), unused('agents.tlsCheckDisabled'), unused('agents.fleetServerEnabled'), + (fullConfig, fromPath, addDeprecation) => { + const oldValue = fullConfig?.xpack?.fleet?.agents?.elasticsearch?.host; + if (oldValue) { + delete fullConfig.xpack.fleet.agents.elasticsearch.host; + fullConfig.xpack.fleet.agents.elasticsearch.hosts = [oldValue]; + addDeprecation({ + message: `Config key [xpack.fleet.agents.elasticsearch.host] is deprecated and replaced by [xpack.fleet.agents.elasticsearch.hosts]`, + correctiveActions: { + manualSteps: [ + `Use [xpack.fleet.agents.elasticsearch.hosts] with an array of host instead.`, + ], + }, + }); + } + + return fullConfig; + }, ], schema: schema.object({ enabled: schema.boolean({ defaultValue: true }), @@ -49,7 +66,7 @@ export const config: PluginConfigDescriptor = { agents: schema.object({ enabled: schema.boolean({ defaultValue: true }), elasticsearch: schema.object({ - host: schema.maybe(schema.string()), + hosts: schema.maybe(schema.arrayOf(schema.uri({ scheme: ['http', 'https'] }))), ca_sha256: schema.maybe(schema.string()), }), fleet_server: schema.maybe( diff --git a/x-pack/plugins/fleet/server/services/output.test.ts b/x-pack/plugins/fleet/server/services/output.test.ts new file mode 100644 index 000000000000..26e3955607ad --- /dev/null +++ b/x-pack/plugins/fleet/server/services/output.test.ts @@ -0,0 +1,85 @@ +/* + * 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 { outputService } from './output'; + +import { appContextService } from './app_context'; + +jest.mock('./app_context'); + +const mockedAppContextService = appContextService as jest.Mocked; + +const CLOUD_ID = + 'dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRjZWM2ZjI2MWE3NGJmMjRjZTMzYmI4ODExYjg0Mjk0ZiRjNmMyY2E2ZDA0MjI0OWFmMGNjN2Q3YTllOTYyNTc0Mw=='; + +const CONFIG_WITH_ES_HOSTS = { + enabled: true, + agents: { + enabled: true, + elasticsearch: { + hosts: ['http://host1.com'], + }, + }, +}; + +const CONFIG_WITHOUT_ES_HOSTS = { + enabled: true, + agents: { + enabled: true, + elasticsearch: {}, + }, +}; + +describe('Output Service', () => { + describe('getDefaultESHosts', () => { + afterEach(() => { + mockedAppContextService.getConfig.mockReset(); + mockedAppContextService.getConfig.mockReset(); + }); + it('Should use cloud ID as the source of truth for ES hosts', () => { + // @ts-expect-error + mockedAppContextService.getCloud.mockReturnValue({ + isCloudEnabled: true, + cloudId: CLOUD_ID, + }); + + mockedAppContextService.getConfig.mockReturnValue(CONFIG_WITH_ES_HOSTS); + + const hosts = outputService.getDefaultESHosts(); + + expect(hosts).toEqual([ + 'https://cec6f261a74bf24ce33bb8811b84294f.us-east-1.aws.found.io:443', + ]); + }); + + it('Should use the value from the config if not in cloud', () => { + // @ts-expect-error + mockedAppContextService.getCloud.mockReturnValue({ + isCloudEnabled: false, + }); + + mockedAppContextService.getConfig.mockReturnValue(CONFIG_WITH_ES_HOSTS); + + const hosts = outputService.getDefaultESHosts(); + + expect(hosts).toEqual(['http://host1.com']); + }); + + it('Should use the default value if there is no config', () => { + // @ts-expect-error + mockedAppContextService.getCloud.mockReturnValue({ + isCloudEnabled: false, + }); + + mockedAppContextService.getConfig.mockReturnValue(CONFIG_WITHOUT_ES_HOSTS); + + const hosts = outputService.getDefaultESHosts(); + + expect(hosts).toEqual(['http://localhost:9200']); + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/output.ts b/x-pack/plugins/fleet/server/services/output.ts index b3857ba5c0ef..0c7b086f78fd 100644 --- a/x-pack/plugins/fleet/server/services/output.ts +++ b/x-pack/plugins/fleet/server/services/output.ts @@ -16,6 +16,8 @@ import { normalizeHostsForAgents } from './hosts_utils'; const SAVED_OBJECT_TYPE = OUTPUT_SAVED_OBJECT_TYPE; +const DEFAULT_ES_HOSTS = ['http://localhost:9200']; + class OutputService { public async getDefaultOutput(soClient: SavedObjectsClientContract) { return await soClient.find({ @@ -27,17 +29,11 @@ class OutputService { public async ensureDefaultOutput(soClient: SavedObjectsClientContract) { const outputs = await this.getDefaultOutput(soClient); - const cloud = appContextService.getCloud(); - const cloudId = cloud?.isCloudEnabled && cloud.cloudId; - const cloudUrl = cloudId && decodeCloudId(cloudId)?.elasticsearchUrl; - const flagsUrl = appContextService.getConfig()!.agents.elasticsearch.host; - const defaultUrl = 'http://localhost:9200'; - const defaultOutputUrl = cloudUrl || flagsUrl || defaultUrl; if (!outputs.saved_objects.length) { const newDefaultOutput = { ...DEFAULT_OUTPUT, - hosts: [defaultOutputUrl], + hosts: this.getDefaultESHosts(), ca_sha256: appContextService.getConfig()!.agents.elasticsearch.ca_sha256, } as NewOutput; @@ -50,6 +46,20 @@ class OutputService { }; } + public getDefaultESHosts(): string[] { + const cloud = appContextService.getCloud(); + const cloudId = cloud?.isCloudEnabled && cloud.cloudId; + const cloudUrl = cloudId && decodeCloudId(cloudId)?.elasticsearchUrl; + const cloudHosts = cloudUrl ? [cloudUrl] : undefined; + const flagHosts = + appContextService.getConfig()!.agents?.elasticsearch?.hosts && + appContextService.getConfig()!.agents.elasticsearch.hosts?.length + ? appContextService.getConfig()!.agents.elasticsearch.hosts + : undefined; + + return cloudHosts || flagHosts || DEFAULT_ES_HOSTS; + } + public async getDefaultOutputId(soClient: SavedObjectsClientContract) { const outputs = await this.getDefaultOutput(soClient);