diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/common/aws_cloudforwarder.ts b/x-pack/solutions/observability/plugins/observability_onboarding/common/aws_cloudforwarder.ts new file mode 100644 index 0000000000000..a236b06f9e9a3 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability_onboarding/common/aws_cloudforwarder.ts @@ -0,0 +1,29 @@ +/* + * 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 CLOUDFORWARDER_CLOUDFORMATION_TEMPLATE_URL = + 'https://edot-cloud-forwarder.s3.amazonaws.com/v1/latest/cloudformation/s3_logs-cloudformation.yaml'; + +/** + * CloudFormation stack configurations for different AWS log types. + */ +export const CLOUDFORMATION_STACK_CONFIGS = { + vpcflow: { + stackName: 'edot-cloud-forwarder-vpcflow', + logType: 'vpcflow', + }, + elbaccess: { + stackName: 'edot-cloud-forwarder-elbaccess', + logType: 'elbaccess', + }, + cloudtrail: { + stackName: 'edot-cloud-forwarder-cloudtrail', + logType: 'cloudtrail', + }, +} as const; + +export type LogType = keyof typeof CLOUDFORMATION_STACK_CONFIGS; diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/common/telemetry_events.ts b/x-pack/solutions/observability/plugins/observability_onboarding/common/telemetry_events.ts index c7f901ad6f015..40b4431d6e51d 100644 --- a/x-pack/solutions/observability/plugins/observability_onboarding/common/telemetry_events.ts +++ b/x-pack/solutions/observability/plugins/observability_onboarding/common/telemetry_events.ts @@ -157,6 +157,11 @@ interface OnboardingAutoDetectEventContext { title: string; } +interface OnboardingCloudForwarderEventContext { + selectedLogType?: string; + cloudServiceProvider?: string; +} + /** * Additional flow-specific context that might * be attached to telemetry events. @@ -164,6 +169,7 @@ interface OnboardingAutoDetectEventContext { export interface OnboardingFlowEventContext { autoDetect?: OnboardingAutoDetectEventContext; firehose?: OnboardingFirehoseFlowEventContext; + cloudforwarder?: OnboardingCloudForwarderEventContext; } const flowContextSchema: SchemaValue = { @@ -210,6 +216,29 @@ const flowContextSchema: SchemaValue = { optional: true, }, }, + cloudforwarder: { + properties: { + selectedLogType: { + type: 'keyword', + _meta: { + description: + 'Which log type is selected in the UI (e.g. vpcflow, elbaccess, cloudtrail). Serves as a good indication of the type of logs the user chose to forward.', + optional: true, + }, + }, + cloudServiceProvider: { + type: 'keyword', + _meta: { + description: + "The cloud service provider where the cloud forwarder is deployed. Can be 'aws', 'gcp' or 'azure'", + optional: true, + }, + }, + }, + _meta: { + optional: true, + }, + }, }, _meta: { optional: true, @@ -241,7 +270,7 @@ export const OBSERVABILITY_ONBOARDING_FLOW_PROGRESS_TELEMETRY_EVENT: EventTypeOp type: 'keyword', _meta: { description: - 'The current step in the onboarding flow. Possible values: "in_progress", "awaiting_data", "data_received"', + 'The current step in the onboarding flow. Possible values: "in_progress", "awaiting_data", "data_received", "aws_launch_stack"', }, }, context: flowContextSchema, diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/playwright/stateful/pom/pages/onboarding_home.page.ts b/x-pack/solutions/observability/plugins/observability_onboarding/e2e/playwright/stateful/pom/pages/onboarding_home.page.ts index 6055b345a0d1e..a69faa1ff4f39 100644 --- a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/playwright/stateful/pom/pages/onboarding_home.page.ts +++ b/x-pack/solutions/observability/plugins/observability_onboarding/e2e/playwright/stateful/pom/pages/onboarding_home.page.ts @@ -20,6 +20,7 @@ export class OnboardingHomePage { private readonly otelHostCard: Locator; readonly awsCollectionCard: Locator; readonly firehoseQuickstartCard: Locator; + readonly cloudforwarderQuickstartCard: Locator; constructor(page: Page) { this.page = page; @@ -46,6 +47,9 @@ export class OnboardingHomePage { this.otelHostCard = this.page.getByTestId('integration-card:otel-logs'); this.awsCollectionCard = this.page.getByTestId('integration-card:aws-logs-virtual'); this.firehoseQuickstartCard = this.page.getByTestId('integration-card:firehose-quick-start'); + this.cloudforwarderQuickstartCard = this.page.getByTestId( + 'integration-card:cloudforwarder-quick-start' + ); } public async selectHostUseCase() { diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/observability_onboarding_flow.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/observability_onboarding_flow.tsx index f12bee7bdc245..2d835963005b4 100644 --- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/observability_onboarding_flow.tsx +++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/observability_onboarding_flow.tsx @@ -18,6 +18,7 @@ import { OtelKubernetesPage, FirehosePage, OtelApmPage, + CloudForwarderPage, } from './pages'; import type { ObservabilityOnboardingAppServices } from '..'; import { useFlowBreadcrumb } from './shared/use_flow_breadcrumbs'; @@ -29,7 +30,7 @@ export function ObservabilityOnboardingFlow() { const { pathname } = useLocation(); const { services: { - context: { isDev, isCloud }, + context: { isDev, isCloud, isServerless }, }, } = useKibana(); @@ -66,6 +67,11 @@ export function ObservabilityOnboardingFlow() { )} + {(isServerless || isDev) && ( + + + + )} diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/use_custom_cards.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/use_custom_cards.tsx index b7df85adb58cb..07667ddb6a743 100644 --- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/use_custom_cards.tsx +++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/use_custom_cards.tsx @@ -27,7 +27,7 @@ export function useCustomCards( services: { application, http, - context: { isServerless, isCloud }, + context: { isServerless, isCloud, isDev }, share, }, } = useKibana(); @@ -51,6 +51,10 @@ export function useCustomCards( history, `/otel-apm/${location.search}` ); + const { href: cloudforwarderUrl } = reactRouterNavigate( + history, + `/cloudforwarder/${location.search}` + ); const apmUrl = `${getUrlForApp?.('apm')}/${isServerless ? 'onboarding' : 'tutorial'}`; const otelApmUrl = isManagedOtlpServiceAvailable ? otelApmQuickstartUrl : apmUrl; @@ -86,6 +90,33 @@ export function useCustomCards( isQuickstart: true, }; + const cloudforwarderQuickstartCard: IntegrationCardItem = { + id: 'cloudforwarder-quick-start', + name: 'cloudforwarder-quick-start', + type: 'virtual', + title: i18n.translate('xpack.observability_onboarding.packageList.cloudforwarderTitle', { + defaultMessage: 'EDOT Cloud Forwarder', + }), + description: i18n.translate( + 'xpack.observability_onboarding.packageList.cloudforwarderDescription', + { + defaultMessage: + 'Forward logs from AWS S3 to Elastic using the EDOT Cloud Forwarder, running as a Lambda function.', + } + ), + categories: ['observability'], + icons: [ + { + type: 'svg', + src: http?.staticAssets.getPluginAssetHref('opentelemetry.svg') ?? '', + }, + ], + url: cloudforwarderUrl, + version: '', + integration: '', + isQuickstart: true, + }; + return [ { id: 'auto-detect-logs', @@ -462,8 +493,15 @@ export function useCustomCards( * The new Firehose card should only be visible on Cloud * as Firehose integration requires additional proxy, * which is not available for on-prem customers. + * Also visible in dev mode for local development. + */ + ...(isCloud || isDev ? [firehoseQuickstartCard] : []), + /** + * The EDOT Cloud Forwarder card should only be visible on Serverless + * as it requires Elastic Cloud infrastructure. + * Also visible in dev mode for local development. */ - ...(isCloud ? [firehoseQuickstartCard] : []), + ...(isServerless || isDev ? [cloudforwarderQuickstartCard] : []), ]; } diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/pages/cloudforwarder.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/pages/cloudforwarder.tsx new file mode 100644 index 0000000000000..ac68c325e56c1 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/pages/cloudforwarder.tsx @@ -0,0 +1,37 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React from 'react'; +import { CloudForwarderPanel } from '../quickstart_flows/cloudforwarder'; +import { PageTemplate } from './template'; +import { CustomHeader } from '../header'; + +export const CloudForwarderPage = () => ( + + } + > + + +); diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/pages/index.ts b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/pages/index.ts index 0d01b8742c955..4f0c1e4dcdb52 100644 --- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/pages/index.ts +++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/pages/index.ts @@ -12,3 +12,4 @@ export { LandingPage } from './landing'; export { OtelLogsPage } from './otel_logs'; export { FirehosePage } from './firehose'; export { OtelApmPage } from './otel_apm'; +export { CloudForwarderPage } from './cloudforwarder'; diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx new file mode 100644 index 0000000000000..cd65f37d60612 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/index.tsx @@ -0,0 +1,364 @@ +/* + * 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 React, { useEffect, useState } from 'react'; +import { + EuiButton, + EuiButtonGroup, + EuiCallOut, + EuiFieldText, + EuiFormRow, + EuiLink, + EuiPanel, + EuiSkeletonRectangle, + EuiSkeletonText, + EuiSpacer, + EuiSteps, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { usePerformanceContext } from '@kbn/ebt-tools'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import type { ObservabilityOnboardingAppServices } from '../../..'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; +import { FeedbackButtons } from '../shared/feedback_buttons'; +import { useFlowBreadcrumb } from '../../shared/use_flow_breadcrumbs'; +import { useCloudForwarderFlow } from './use_cloudforwarder_flow'; +import { EmptyPrompt } from '../shared/empty_prompt'; +import { OBSERVABILITY_ONBOARDING_FLOW_PROGRESS_TELEMETRY_EVENT } from '../../../../common/telemetry_events'; +import { type LogType } from '../../../../common/aws_cloudforwarder'; +import { isValidS3BucketName, buildS3BucketArn, buildCloudFormationUrl } from './utils'; + +const EDOT_CLOUD_FORWARDER_DOCS_URL = + 'https://www.elastic.co/docs/reference/opentelemetry/edot-cloud-forwarder/aws'; + +export function CloudForwarderPanel() { + useFlowBreadcrumb({ + text: i18n.translate( + 'xpack.observability_onboarding.cloudforwarderPanel.breadcrumbs.cloudforwarder', + { + defaultMessage: 'EDOT Cloud Forwarder', + } + ), + }); + + const { + services: { + analytics, + context: { cloudServiceProvider }, + }, + } = useKibana(); + const [selectedLogType, setSelectedLogType] = useState('vpcflow'); + const [s3BucketName, setS3BucketName] = useState(''); + const { data, status, error, refetch } = useCloudForwarderFlow(); + + const trimmedBucketName = s3BucketName.trim(); + const isBucketNameInvalid = + trimmedBucketName.length > 0 && !isValidS3BucketName(trimmedBucketName); + const { onPageReady } = usePerformanceContext(); + + useEffect(() => { + if (data) { + onPageReady({ + meta: { + description: `[ttfmp_onboarding] Request to create the onboarding flow succeeded and the flow's UI has rendered`, + }, + }); + } + }, [data, onPageReady]); + + if (error !== undefined) { + return ( + + ); + } + + const logTypeOptions: Array<{ id: LogType; label: string }> = [ + { + id: 'vpcflow', + label: i18n.translate('xpack.observability_onboarding.cloudforwarder.logType.vpcflow', { + defaultMessage: 'VPC Flow Logs', + }), + }, + { + id: 'elbaccess', + label: i18n.translate('xpack.observability_onboarding.cloudforwarder.logType.elbaccess', { + defaultMessage: 'ELB Access Logs', + }), + }, + { + id: 'cloudtrail', + label: i18n.translate('xpack.observability_onboarding.cloudforwarder.logType.cloudtrail', { + defaultMessage: 'CloudTrail Logs', + }), + }, + ]; + + const steps = [ + { + title: i18n.translate( + 'xpack.observability_onboarding.cloudforwarderPanel.prerequisitesTitle', + { + defaultMessage: 'Prerequisites', + } + ), + children: ( + +

+ +

+
    +
  • + +
  • +
  • + +
  • +
+

+ + {i18n.translate( + 'xpack.observability_onboarding.cloudforwarderPanel.documentationLinkLabel', + { defaultMessage: 'Check the documentation' } + )} + + ), + }} + /> +

+
+ ), + }, + { + title: i18n.translate( + 'xpack.observability_onboarding.cloudforwarderPanel.configureForwarderTitle', + { + defaultMessage: 'Configure the Cloud Forwarder', + } + ), + children: ( + <> + {status !== FETCH_STATUS.SUCCESS && ( + <> + + + + + + + + + )} + {status === FETCH_STATUS.SUCCESS && data !== undefined && ( + <> + +

+ +

+
+ + + setS3BucketName(e.target.value)} + isInvalid={isBucketNameInvalid} + placeholder={i18n.translate( + 'xpack.observability_onboarding.cloudforwarderPanel.s3BucketNamePlaceholder', + { + defaultMessage: 'my-logs-bucket', + } + )} + /> + + + +

+ +

+
+ + setSelectedLogType(id as LogType)} + buttonSize="m" + /> + + )} + + ), + }, + { + title: i18n.translate('xpack.observability_onboarding.cloudforwarderPanel.launchStackTitle', { + defaultMessage: 'Deploy the EDOT Cloud Forwarder in AWS', + }), + children: ( + <> + {status !== FETCH_STATUS.SUCCESS && ( + <> + + + + + )} + {status === FETCH_STATUS.SUCCESS && data !== undefined && ( + <> + +

+ +

+
+ + { + analytics?.reportEvent( + OBSERVABILITY_ONBOARDING_FLOW_PROGRESS_TELEMETRY_EVENT.eventType, + { + onboardingFlowType: 'cloudforwarder', + onboardingId: data.onboardingId, + step: 'aws_launch_stack', + context: { + cloudforwarder: { + cloudServiceProvider, + selectedLogType, + }, + }, + } + ); + }} + > + {i18n.translate( + 'xpack.observability_onboarding.cloudforwarderPanel.launchStackButtonLabel', + { defaultMessage: 'Launch Stack in AWS' } + )} + + + )} + + ), + }, + { + title: i18n.translate( + 'xpack.observability_onboarding.cloudforwarderPanel.visualizeDataTitle', + { + defaultMessage: 'Visualize your data', + } + ), + children: ( + +

+ + {i18n.translate( + 'xpack.observability_onboarding.cloudforwarderPanel.strong.logsawsLabel', + { defaultMessage: 'logs-aws.*' } + )} + + ), + }} + /> +

+
+ ), + }, + ]; + + return ( + + + + + ); +} diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/use_cloudforwarder_flow.ts b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/use_cloudforwarder_flow.ts new file mode 100644 index 0000000000000..dcbf1c7a4ae31 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/use_cloudforwarder_flow.ts @@ -0,0 +1,45 @@ +/* + * 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 { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useEffect } from 'react'; +import { OBSERVABILITY_ONBOARDING_FLOW_PROGRESS_TELEMETRY_EVENT } from '../../../../common/telemetry_events'; +import type { ObservabilityOnboardingAppServices } from '../../..'; +import { useFetcher } from '../../../hooks/use_fetcher'; + +export function useCloudForwarderFlow() { + const { + services: { + analytics, + context: { cloudServiceProvider }, + }, + } = useKibana(); + const { data, status, error, refetch } = useFetcher( + (callApi) => { + return callApi('POST /internal/observability_onboarding/cloudforwarder/flow'); + }, + [], + { showToastOnError: false } + ); + + useEffect(() => { + if (data?.onboardingId !== undefined) { + analytics?.reportEvent(OBSERVABILITY_ONBOARDING_FLOW_PROGRESS_TELEMETRY_EVENT.eventType, { + onboardingFlowType: 'cloudforwarder', + onboardingId: data?.onboardingId, + step: 'in_progress', + context: { + cloudforwarder: { + cloudServiceProvider, + }, + }, + }); + } + }, [analytics, cloudServiceProvider, data?.onboardingId]); + + return { data, status, error, refetch } as const; +} diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/utils.test.ts b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/utils.test.ts new file mode 100644 index 0000000000000..9337ee0701b9b --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/utils.test.ts @@ -0,0 +1,65 @@ +/* + * 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 expect from 'expect'; +import { isValidS3BucketName, buildS3BucketArn, buildCloudFormationUrl } from './utils'; + +describe('isValidS3BucketName()', () => { + it('accepts valid bucket names', () => { + expect(isValidS3BucketName('my-bucket')).toBe(true); + expect(isValidS3BucketName('my.bucket.123')).toBe(true); + expect(isValidS3BucketName('abc')).toBe(true); // min length + }); + + it('rejects invalid bucket names', () => { + expect(isValidS3BucketName('')).toBe(false); // empty + expect(isValidS3BucketName('ab')).toBe(false); // too short + expect(isValidS3BucketName('My-Bucket')).toBe(false); // uppercase + expect(isValidS3BucketName('my..bucket')).toBe(false); // consecutive periods + }); + + it('rejects AWS reserved patterns', () => { + // These are obscure AWS rules worth testing explicitly + expect(isValidS3BucketName('xn--my-bucket')).toBe(false); // S3 access point prefix + expect(isValidS3BucketName('my-bucket-s3alias')).toBe(false); // S3 alias suffix + }); +}); + +describe('buildS3BucketArn()', () => { + it('builds correct ARN from bucket name', () => { + expect(buildS3BucketArn('my-bucket')).toBe('arn:aws:s3:::my-bucket'); + }); +}); + +describe('buildCloudFormationUrl()', () => { + it('builds correct CloudFormation URL with all parameters', () => { + const url = buildCloudFormationUrl( + 'vpcflow', + 'https://otlp.example.com', + 'api-key', + 'arn:aws:s3:::bucket' + ); + + expect(url).toContain('console.aws.amazon.com/cloudformation'); + expect(url).toContain('stackName=edot-cloud-forwarder-vpcflow'); + expect(url).toContain('param_EdotCloudForwarderS3LogsType=vpcflow'); + expect(url).toContain('param_ElasticAPIKey=api-key'); + expect(url).toContain('param_SourceS3BucketARN='); + }); + + it('properly encodes special characters in parameters', () => { + const url = buildCloudFormationUrl( + 'cloudtrail', + 'https://otlp.example.com/v1/logs', + 'key+with/special=chars', + 'arn:aws:s3:::bucket' + ); + + expect(url).toContain('param_ElasticAPIKey=key%2Bwith%2Fspecial%3Dchars'); + expect(url).toContain('param_OTLPEndpoint=https%3A%2F%2Fotlp.example.com%2Fv1%2Flogs'); + }); +}); diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/utils.ts b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/utils.ts new file mode 100644 index 0000000000000..758c70e3eabb0 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/cloudforwarder/utils.ts @@ -0,0 +1,71 @@ +/* + * 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 { + CLOUDFORWARDER_CLOUDFORMATION_TEMPLATE_URL, + CLOUDFORMATION_STACK_CONFIGS, + type LogType, +} from '../../../../common/aws_cloudforwarder'; + +export type { LogType }; + +/** + * Validates S3 bucket names according to AWS naming rules: + * - 3-63 characters long + * - Only lowercase letters, numbers, hyphens, and periods + * - Must start and end with a letter or number + * - Cannot contain consecutive periods + * - Cannot start with 'xn--' (reserved for S3 access points) + * - Cannot end with '-s3alias' (reserved for S3 access point aliases) + * + * @see https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html + */ +const S3_BUCKET_NAME_REGEX = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/; + +export function isValidS3BucketName(bucketName: string): boolean { + return ( + S3_BUCKET_NAME_REGEX.test(bucketName) && + !bucketName.includes('..') && + !bucketName.startsWith('xn--') && + !bucketName.endsWith('-s3alias') + ); +} + +/** + * Builds an S3 bucket ARN from a bucket name. + * Format: arn:aws:s3:::bucket-name + */ +export function buildS3BucketArn(bucketName: string): string { + return `arn:aws:s3:::${bucketName}`; +} + +/** + * Builds a CloudFormation console URL with pre-filled parameters for deploying + * the EDOT Cloud Forwarder. The URL includes the template URL, stack name, log type, + * OTLP endpoint, API key, and S3 bucket ARN as hash parameters for the AWS CloudFormation console. + */ +export function buildCloudFormationUrl( + logType: LogType, + otlpEndpoint: string, + apiKey: string, + s3BucketArn: string +): string { + const config = CLOUDFORMATION_STACK_CONFIGS[logType]; + const url = new URL('https://console.aws.amazon.com/cloudformation/home'); + // The param_* names below must match the CloudFormation template parameter names exactly + const params = new URLSearchParams({ + templateURL: CLOUDFORWARDER_CLOUDFORMATION_TEMPLATE_URL, + stackName: config.stackName, + param_EdotCloudForwarderS3LogsType: config.logType, // eslint-disable-line @typescript-eslint/naming-convention + param_OTLPEndpoint: otlpEndpoint, // eslint-disable-line @typescript-eslint/naming-convention + param_ElasticAPIKey: apiKey, // eslint-disable-line @typescript-eslint/naming-convention + param_SourceS3BucketARN: s3BucketArn, // eslint-disable-line @typescript-eslint/naming-convention + }); + + url.hash = `/stacks/create/review?${params.toString()}`; + return url.toString(); +} diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/server/routes/cloudforwarder/route.ts b/x-pack/solutions/observability/plugins/observability_onboarding/server/routes/cloudforwarder/route.ts new file mode 100644 index 0000000000000..494b56051201a --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability_onboarding/server/routes/cloudforwarder/route.ts @@ -0,0 +1,63 @@ +/* + * 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 { v4 as uuidv4 } from 'uuid'; +import Boom from '@hapi/boom'; +import { createManagedOtlpServiceApiKey } from '../../lib/api_key/create_managed_otlp_service_api_key'; +import { hasLogMonitoringPrivileges } from '../../lib/api_key/has_log_monitoring_privileges'; +import { createObservabilityOnboardingServerRoute } from '../create_observability_onboarding_server_route'; +import { getManagedOtlpServiceUrl } from '../../lib/get_managed_otlp_service_url'; + +export interface CreateCloudForwarderOnboardingFlowRouteResponse { + onboardingId: string; + apiKeyEncoded: string; + managedOtlpServiceUrl: string; +} + +const createCloudForwarderOnboardingFlowRoute = createObservabilityOnboardingServerRoute({ + endpoint: 'POST /internal/observability_onboarding/cloudforwarder/flow', + security: { + authz: { + enabled: false, + reason: 'This route has custom authorization logic using Elasticsearch client', + }, + }, + async handler(resources): Promise { + const { context, plugins } = resources; + const { + elasticsearch: { client }, + } = await context.core; + + /** + * Check for log monitoring privileges (logs and metrics only, no traces). + * CloudForwarder only forwards logs from AWS S3 (VPC Flow Logs, ELB Access Logs, CloudTrail). + * This is consistent with other log-only flows (firehose, otel_host). + */ + const hasPrivileges = await hasLogMonitoringPrivileges(client.asCurrentUser); + + if (!hasPrivileges) { + throw Boom.forbidden( + "You don't have enough privileges to start a new onboarding flow. Contact your system administrator to grant you the required privileges." + ); + } + + const { encoded: apiKeyEncoded } = await createManagedOtlpServiceApiKey( + client.asCurrentUser, + 'ingest-cloudforwarder' + ); + + return { + onboardingId: uuidv4(), + apiKeyEncoded, + managedOtlpServiceUrl: getManagedOtlpServiceUrl(plugins), + }; + }, +}); + +export const cloudforwarderOnboardingRouteRepository = { + ...createCloudForwarderOnboardingFlowRoute, +}; diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/server/routes/index.ts b/x-pack/solutions/observability/plugins/observability_onboarding/server/routes/index.ts index acc2414d9d3c1..10adaccd826d1 100644 --- a/x-pack/solutions/observability/plugins/observability_onboarding/server/routes/index.ts +++ b/x-pack/solutions/observability/plugins/observability_onboarding/server/routes/index.ts @@ -10,6 +10,7 @@ import { kubernetesOnboardingRouteRepository } from './kubernetes/route'; import { firehoseOnboardingRouteRepository } from './firehose/route'; import { otelHostOnboardingRouteRepository } from './otel_host/route'; import { otelApmOnboardingRouteRepository } from './otel_apm/route'; +import { cloudforwarderOnboardingRouteRepository } from './cloudforwarder/route'; function getTypedObservabilityOnboardingServerRouteRepository() { const repository = { @@ -18,6 +19,7 @@ function getTypedObservabilityOnboardingServerRouteRepository() { ...firehoseOnboardingRouteRepository, ...otelHostOnboardingRouteRepository, ...otelApmOnboardingRouteRepository, + ...cloudforwarderOnboardingRouteRepository, }; return repository; diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/test/scout/ui/fixtures/page_objects/onboarding_app.ts b/x-pack/solutions/observability/plugins/observability_onboarding/test/scout/ui/fixtures/page_objects/onboarding_app.ts index 7de1047cc64f9..687f87e04030f 100644 --- a/x-pack/solutions/observability/plugins/observability_onboarding/test/scout/ui/fixtures/page_objects/onboarding_app.ts +++ b/x-pack/solutions/observability/plugins/observability_onboarding/test/scout/ui/fixtures/page_objects/onboarding_app.ts @@ -75,6 +75,10 @@ export class OnboardingApp { return this.page.getByTestId('integration-card:firehose-quick-start'); } + public get cloudforwarderQuickstartCard() { + return this.page.getByTestId('integration-card:cloudforwarder-quick-start'); + } + public get useCaseGrid() { return this.page.getByRole('group', { name: 'What do you want to monitor?' }); }