diff --git a/packages/cloud/connection_details/kibana/kibana_connection_details_provider.tsx b/packages/cloud/connection_details/kibana/kibana_connection_details_provider.tsx index 444ff70ff9128..d400f67993f8a 100644 --- a/packages/cloud/connection_details/kibana/kibana_connection_details_provider.tsx +++ b/packages/cloud/connection_details/kibana/kibana_connection_details_provider.tsx @@ -21,6 +21,7 @@ const createOpts = async (props: KibanaConnectionDetailsProviderProps) => { const { http, docLinks, analytics } = start.core; const locator = start.plugins?.share?.url?.locators.get('MANAGEMENT_APP_LOCATOR'); const manageKeysLink = await locator?.getUrl({ sectionId: 'security', appId: 'api_keys' }); + const elasticsearchConfig = await start.plugins?.cloud?.fetchElasticsearchConfig(); const result: ConnectionDetailsOpts = { ...options, navigateToUrl: start.core.application @@ -35,7 +36,7 @@ const createOpts = async (props: KibanaConnectionDetailsProviderProps) => { }, endpoints: { id: start.plugins?.cloud?.cloudId, - url: start.plugins?.cloud?.elasticsearchUrl, + url: elasticsearchConfig?.elasticsearchUrl, cloudIdLearMoreLink: docLinks?.links?.cloud?.beatsAndLogstashConfiguration, ...options?.endpoints, }, diff --git a/packages/cloud/deployment_details/services.tsx b/packages/cloud/deployment_details/services.tsx index d3ca0a340b600..73959627e98e6 100644 --- a/packages/cloud/deployment_details/services.tsx +++ b/packages/cloud/deployment_details/services.tsx @@ -6,8 +6,7 @@ * your election, the "Elastic License 2.0", the "GNU Affero General Public * License v3.0 only", or the "Server Side Public License, v 1". */ - -import React, { FC, PropsWithChildren, useContext } from 'react'; +import React, { FC, PropsWithChildren, useContext, useEffect } from 'react'; export interface DeploymentDetailsContextValue { cloudId?: string; @@ -58,7 +57,7 @@ export interface DeploymentDetailsKibanaDependencies { cloud: { isCloudEnabled: boolean; cloudId?: string; - elasticsearchUrl?: string; + fetchElasticsearchConfig: () => Promise<{ elasticsearchUrl?: string }>; }; /** DocLinksStart contract */ docLinks: { @@ -79,11 +78,19 @@ export interface DeploymentDetailsKibanaDependencies { export const DeploymentDetailsKibanaProvider: FC< PropsWithChildren > = ({ children, ...services }) => { + const [elasticsearchUrl, setElasticsearchUrl] = React.useState(''); + + useEffect(() => { + services.cloud.fetchElasticsearchConfig().then((config) => { + setElasticsearchUrl(config.elasticsearchUrl || ''); + }); + }, [services.cloud]); + const { core: { application: { navigateToUrl }, }, - cloud: { isCloudEnabled, cloudId, elasticsearchUrl }, + cloud: { isCloudEnabled, cloudId }, share: { url: { locators }, }, diff --git a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_config.test.ts b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_config.test.ts index 34a8bc07e0b5c..b7d7b40c49806 100644 --- a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_config.test.ts +++ b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_config.test.ts @@ -47,6 +47,7 @@ test('set correct defaults', () => { "maxSockets": 800, "password": undefined, "pingTimeout": "PT30S", + "publicBaseUrl": undefined, "requestHeadersWhitelist": Array [ "authorization", "es-client-authentication", diff --git a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_config.ts b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_config.ts index 46b7a02768e7a..93fb64baf46d0 100644 --- a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_config.ts +++ b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_config.ts @@ -359,6 +359,13 @@ export class ElasticsearchConfig implements IElasticsearchConfig { */ public readonly hosts: string[]; + /** + * Optional host that users can use to connect to your Elasticsearch cluster, + * this URL will be shown in Kibana as the Elasticsearch URL + */ + + public readonly publicBaseUrl?: string; + /** * List of Kibana client-side headers to send to Elasticsearch when request * scoped cluster client is used. If this is an empty array then *no* client-side @@ -473,6 +480,7 @@ export class ElasticsearchConfig implements IElasticsearchConfig { this.skipStartupConnectionCheck = rawConfig.skipStartupConnectionCheck; this.apisToRedactInLogs = rawConfig.apisToRedactInLogs; this.dnsCacheTtl = rawConfig.dnsCacheTtl; + this.publicBaseUrl = rawConfig.publicBaseUrl; const { alwaysPresentCertificate, verificationMode } = rawConfig.ssl; const { key, keyPassphrase, certificate, certificateAuthorities } = readKeyAndCerts(rawConfig); diff --git a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_service.ts b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_service.ts index 5a3f34d0565f8..83eb04832121e 100644 --- a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_service.ts +++ b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_service.ts @@ -135,6 +135,7 @@ export class ElasticsearchService agentStatsProvider: { getAgentsStats: agentManager.getAgentsStats.bind(agentManager), }, + publicBaseUrl: config.publicBaseUrl, }; } @@ -194,6 +195,7 @@ export class ElasticsearchService metrics: { elasticsearchWaitTime, }, + publicBaseUrl: config.publicBaseUrl, }; } diff --git a/packages/core/elasticsearch/core-elasticsearch-server/src/contracts.ts b/packages/core/elasticsearch/core-elasticsearch-server/src/contracts.ts index f0a3a62d08f18..bc712a61a535e 100644 --- a/packages/core/elasticsearch/core-elasticsearch-server/src/contracts.ts +++ b/packages/core/elasticsearch/core-elasticsearch-server/src/contracts.ts @@ -138,6 +138,12 @@ export interface ElasticsearchServiceStart { * Returns the capabilities for the default cluster. */ getCapabilities: () => ElasticsearchCapabilities; + + /** + * The public base URL (if any) that should be used by end users to access the Elasticsearch cluster. + */ + + readonly publicBaseUrl?: string; } /** diff --git a/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts b/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts index 539b629974982..2fcdf384cb897 100644 --- a/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts +++ b/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts @@ -212,6 +212,7 @@ export function createPluginSetupContext({ docLinks: deps.docLinks, elasticsearch: { legacy: deps.elasticsearch.legacy, + publicBaseUrl: deps.elasticsearch.publicBaseUrl, setUnauthorizedErrorHandler: deps.elasticsearch.setUnauthorizedErrorHandler, }, executionContext: { diff --git a/x-pack/plugins/cloud/common/constants.ts b/x-pack/plugins/cloud/common/constants.ts index f2009223f8ac1..27d755ab08214 100644 --- a/x-pack/plugins/cloud/common/constants.ts +++ b/x-pack/plugins/cloud/common/constants.ts @@ -11,3 +11,5 @@ export const ELASTIC_SUPPORT_LINK = 'https://support.elastic.co/'; * This is the page for managing your snapshots on Cloud. */ export const CLOUD_SNAPSHOTS_PATH = 'elasticsearch/snapshots/'; + +export const ELASTICSEARCH_CONFIG_ROUTE = '/api/internal/cloud/elasticsearch_config'; diff --git a/x-pack/plugins/cloud/common/types.ts b/x-pack/plugins/cloud/common/types.ts index ac4593fd0e259..0f72caf515058 100644 --- a/x-pack/plugins/cloud/common/types.ts +++ b/x-pack/plugins/cloud/common/types.ts @@ -6,3 +6,7 @@ */ export type OnBoardingDefaultSolution = 'es' | 'oblt' | 'security'; + +export interface ElasticsearchConfigType { + elasticsearch_url?: string; +} diff --git a/x-pack/plugins/cloud/public/mocks.tsx b/x-pack/plugins/cloud/public/mocks.tsx index dd5c5eced618a..b9f6d850b9acf 100644 --- a/x-pack/plugins/cloud/public/mocks.tsx +++ b/x-pack/plugins/cloud/public/mocks.tsx @@ -19,7 +19,9 @@ function createSetupMock(): jest.Mocked { deploymentUrl: 'deployment-url', profileUrl: 'profile-url', organizationUrl: 'organization-url', - elasticsearchUrl: 'elasticsearch-url', + fetchElasticsearchConfig: jest + .fn() + .mockResolvedValue({ elasticsearchUrl: 'elasticsearch-url' }), kibanaUrl: 'kibana-url', cloudHost: 'cloud-host', cloudDefaultPort: '443', @@ -53,6 +55,7 @@ const createStartMock = (): jest.Mocked => ({ serverless: { projectId: undefined, }, + fetchElasticsearchConfig: jest.fn().mockResolvedValue({ elasticsearchUrl: 'elasticsearch-url' }), }); export const cloudMock = { diff --git a/x-pack/plugins/cloud/public/plugin.test.ts b/x-pack/plugins/cloud/public/plugin.test.ts index 2c32ac8fe972a..583d274db77d8 100644 --- a/x-pack/plugins/cloud/public/plugin.test.ts +++ b/x-pack/plugins/cloud/public/plugin.test.ts @@ -37,6 +37,7 @@ describe('Cloud Plugin', () => { const plugin = new CloudPlugin(initContext); const coreSetup = coreMock.createSetup(); + coreSetup.http.get.mockResolvedValue({ elasticsearch_url: 'elasticsearch-url' }); const setup = plugin.setup(coreSetup); return { setup }; @@ -110,8 +111,8 @@ describe('Cloud Plugin', () => { it('exposes components decoded from the cloudId', () => { const decodedId: DecodedCloudId = { defaultPort: '9000', - host: 'host', elasticsearchUrl: 'elasticsearch-url', + host: 'host', kibanaUrl: 'kibana-url', }; decodeCloudIdMock.mockReturnValue(decodedId); @@ -120,7 +121,6 @@ describe('Cloud Plugin', () => { expect.objectContaining({ cloudDefaultPort: '9000', cloudHost: 'host', - elasticsearchUrl: 'elasticsearch-url', kibanaUrl: 'kibana-url', }) ); @@ -184,6 +184,11 @@ describe('Cloud Plugin', () => { }); expect(setup.serverless.projectType).toBe('security'); }); + it('exposes fetchElasticsearchConfig', async () => { + const { setup } = setupPlugin(); + const result = await setup.fetchElasticsearchConfig(); + expect(result).toEqual({ elasticsearchUrl: 'elasticsearch-url' }); + }); }); }); @@ -307,5 +312,13 @@ describe('Cloud Plugin', () => { const start = plugin.start(coreStart); expect(start.serverless.projectName).toBe('My Awesome Project'); }); + it('exposes fetchElasticsearchConfig', async () => { + const { plugin } = startPlugin(); + const coreStart = coreMock.createStart(); + coreStart.http.get.mockResolvedValue({ elasticsearch_url: 'elasticsearch-url' }); + const start = plugin.start(coreStart); + const result = await start.fetchElasticsearchConfig(); + expect(result).toEqual({ elasticsearchUrl: 'elasticsearch-url' }); + }); }); }); diff --git a/x-pack/plugins/cloud/public/plugin.tsx b/x-pack/plugins/cloud/public/plugin.tsx index a661933955060..e89e63dc1c15b 100644 --- a/x-pack/plugins/cloud/public/plugin.tsx +++ b/x-pack/plugins/cloud/public/plugin.tsx @@ -12,12 +12,13 @@ import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kb import { registerCloudDeploymentMetadataAnalyticsContext } from '../common/register_cloud_deployment_id_analytics_context'; import { getIsCloudEnabled } from '../common/is_cloud_enabled'; import { parseDeploymentIdFromDeploymentUrl } from '../common/parse_deployment_id_from_deployment_url'; -import { CLOUD_SNAPSHOTS_PATH } from '../common/constants'; +import { CLOUD_SNAPSHOTS_PATH, ELASTICSEARCH_CONFIG_ROUTE } from '../common/constants'; import { decodeCloudId, type DecodedCloudId } from '../common/decode_cloud_id'; import { getFullCloudUrl } from '../common/utils'; import { parseOnboardingSolution } from '../common/parse_onboarding_default_solution'; -import type { CloudSetup, CloudStart } from './types'; +import type { CloudSetup, CloudStart, PublicElasticsearchConfigType } from './types'; import { getSupportUrl } from './utils'; +import { ElasticsearchConfigType } from '../common/types'; export interface CloudConfigType { id?: string; @@ -66,12 +67,14 @@ export class CloudPlugin implements Plugin { private readonly isServerlessEnabled: boolean; private readonly contextProviders: Array>> = []; private readonly logger: Logger; + private elasticsearchConfig?: PublicElasticsearchConfigType; constructor(private readonly initializerContext: PluginInitializerContext) { this.config = this.initializerContext.config.get(); this.isCloudEnabled = getIsCloudEnabled(this.config.id); this.isServerlessEnabled = !!this.config.serverless?.project_id; this.logger = initializerContext.logger.get(); + this.elasticsearchConfig = undefined; } public setup(core: CoreSetup): CloudSetup { @@ -99,7 +102,6 @@ export class CloudPlugin implements Plugin { csp, baseUrl, ...this.getCloudUrls(), - elasticsearchUrl: decodedId?.elasticsearchUrl, kibanaUrl: decodedId?.kibanaUrl, cloudHost: decodedId?.host, cloudDefaultPort: decodedId?.defaultPort, @@ -119,6 +121,7 @@ export class CloudPlugin implements Plugin { registerCloudService: (contextProvider) => { this.contextProviders.push(contextProvider); }, + fetchElasticsearchConfig: this.fetchElasticsearchConfig.bind(this, core.http), }; } @@ -166,7 +169,6 @@ export class CloudPlugin implements Plugin { profileUrl, organizationUrl, projectsUrl, - elasticsearchUrl: decodedId?.elasticsearchUrl, kibanaUrl: decodedId?.kibanaUrl, isServerlessEnabled: this.isServerlessEnabled, serverless: { @@ -176,6 +178,7 @@ export class CloudPlugin implements Plugin { }, performanceUrl, usersAndRolesUrl, + fetchElasticsearchConfig: this.fetchElasticsearchConfig.bind(this, coreStart.http), }; } @@ -216,4 +219,26 @@ export class CloudPlugin implements Plugin { projectsUrl: fullCloudProjectsUrl, }; } + + private async fetchElasticsearchConfig( + http: CoreStart['http'] + ): Promise { + if (this.elasticsearchConfig !== undefined) { + // This config should be fully populated on first fetch, so we should avoid refetching from server + return this.elasticsearchConfig; + } + try { + const result = await http.get(ELASTICSEARCH_CONFIG_ROUTE, { + version: '1', + }); + + this.elasticsearchConfig = { elasticsearchUrl: result.elasticsearch_url || undefined }; + return this.elasticsearchConfig; + } catch { + this.logger.error('Failed to fetch Elasticsearch config'); + return { + elasticsearchUrl: undefined, + }; + } + } } diff --git a/x-pack/plugins/cloud/public/types.ts b/x-pack/plugins/cloud/public/types.ts index 8df1ba645cb48..1428e887f1b9f 100644 --- a/x-pack/plugins/cloud/public/types.ts +++ b/x-pack/plugins/cloud/public/types.ts @@ -58,9 +58,9 @@ export interface CloudStart { */ projectsUrl?: string; /** - * The full URL to the elasticsearch cluster. + * Fetches the full URL to the elasticsearch cluster. */ - elasticsearchUrl?: string; + fetchElasticsearchConfig: () => Promise; /** * The full URL to the Kibana deployment. */ @@ -150,9 +150,9 @@ export interface CloudSetup { */ snapshotsUrl?: string; /** - * The full URL to the elasticsearch cluster. + * Fetches the full URL to the elasticsearch cluster. */ - elasticsearchUrl?: string; + fetchElasticsearchConfig: () => Promise; /** * The full URL to the Kibana deployment. */ @@ -225,3 +225,12 @@ export interface CloudSetup { orchestratorTarget?: string; }; } + +export interface PublicElasticsearchConfigType { + /** + * The URL to the Elasticsearch cluster, derived from xpack.elasticsearch.publicBaseUrl if populated + * Otherwise this is based on the cloudId + * If neither is populated, this will be undefined + */ + elasticsearchUrl?: string; +} diff --git a/x-pack/plugins/cloud/server/plugin.ts b/x-pack/plugins/cloud/server/plugin.ts index a03878b760dd4..9f45b5398ac22 100644 --- a/x-pack/plugins/cloud/server/plugin.ts +++ b/x-pack/plugins/cloud/server/plugin.ts @@ -18,6 +18,7 @@ import { decodeCloudId, DecodedCloudId } from '../common/decode_cloud_id'; import { parseOnboardingSolution } from '../common/parse_onboarding_default_solution'; import { getFullCloudUrl } from '../common/utils'; import { readInstanceSizeMb } from './env'; +import { defineRoutes } from './routes/elasticsearch_routes'; interface PluginsSetup { usageCollection?: UsageCollectionSetup; @@ -201,6 +202,9 @@ export class CloudPlugin implements Plugin { if (this.config.id) { decodedId = decodeCloudId(this.config.id, this.logger); } + const router = core.http.createRouter(); + const elasticsearchUrl = core.elasticsearch.publicBaseUrl || decodedId?.elasticsearchUrl; + defineRoutes({ logger: this.logger, router, elasticsearchUrl }); return { ...this.getCloudUrls(), @@ -209,7 +213,7 @@ export class CloudPlugin implements Plugin { organizationId, instanceSizeMb: readInstanceSizeMb(), deploymentId, - elasticsearchUrl: core.elasticsearch.publicBaseUrl || decodedId?.elasticsearchUrl, + elasticsearchUrl, kibanaUrl: decodedId?.kibanaUrl, cloudHost: decodedId?.host, cloudDefaultPort: decodedId?.defaultPort, diff --git a/x-pack/plugins/cloud/server/routes/elasticsearch_routes.ts b/x-pack/plugins/cloud/server/routes/elasticsearch_routes.ts new file mode 100644 index 0000000000000..5cdc2f90559cc --- /dev/null +++ b/x-pack/plugins/cloud/server/routes/elasticsearch_routes.ts @@ -0,0 +1,35 @@ +/* + * 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 { IRouter } from '@kbn/core/server'; +import { Logger } from '@kbn/logging'; +import { ElasticsearchConfigType } from '../../common/types'; +import { ELASTICSEARCH_CONFIG_ROUTE } from '../../common/constants'; + +export function defineRoutes({ + elasticsearchUrl, + logger, + router, +}: { + elasticsearchUrl?: string; + logger: Logger; + router: IRouter; +}) { + router.versioned + .get({ + path: ELASTICSEARCH_CONFIG_ROUTE, + access: 'internal', + }) + .addVersion({ version: '1', validate: {} }, async (context, request, response) => { + const body: ElasticsearchConfigType = { + elasticsearch_url: elasticsearchUrl, + }; + return response.ok({ + body, + }); + }); +} diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate.test.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate.test.tsx index 9bb23b677f743..f55034f72ccd6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate.test.tsx @@ -22,6 +22,12 @@ jest.mock('../../../../shared/enterprise_search_url', () => ({ getEnterpriseSearchUrl: () => 'http://localhost:3002', })); +jest.mock('../../../../shared/cloud_details/cloud_details', () => ({ + useCloudDetails: () => ({ + elasticsearchUrl: 'your_deployment_url', + }), +})); + describe('AnalyticsCollectionIntegrate', () => { const analyticsCollections: AnalyticsCollection = { events_datastream: 'analytics-events-example', @@ -55,7 +61,7 @@ describe('AnalyticsCollectionIntegrate', () => { .toMatchInlineSnapshot(` "