From bfe1e5f08d4ba8e06b34502d2b2537c117678345 Mon Sep 17 00:00:00 2001 From: Lisa Kim Date: Thu, 30 May 2024 14:05:32 -0700 Subject: [PATCH] Web: Add alternate EC2 auto discover flow using AWS Systems Manager (SSM) (#42038) * Define ssm endpoint and related types * Define ssm as a selectable resource - Make refreshing optional for aws region selector - Define ssm ec2 flow * Create a manual configure discovery service view for self hosted * Create a discovery config with ssm view * Address design review: remove ec2 instance eice flow * Address CR --- .../SelectResource.story.test.tsx.snap | 8 +- .../src/Discover/SelectResource/resources.tsx | 15 +- .../src/Discover/SelectResource/types.ts | 10 +- .../ConfigureDiscoveryService.story.tsx | 115 ++++++ .../ConfigureDiscoveryService.tsx | 67 +++ .../DiscoveryConfigCreatedDialog.tsx | 53 +++ .../DiscoveryConfigSsm.story.tsx | 187 +++++++++ .../DiscoveryConfigSsm/DiscoveryConfigSsm.tsx | 380 ++++++++++++++++++ .../teleport/src/Discover/Server/Shared.tsx | 49 +++ .../teleport/src/Discover/Server/index.tsx | 44 +- .../SelfHostedAutoDiscoverDirections.tsx | 22 +- .../AwsRegionSelector/AwsRegionSelector.tsx | 36 +- .../teleport/src/Discover/Shared/InfoIcon.tsx | 29 ++ web/packages/teleport/src/config.ts | 20 + .../src/services/discovery/discovery.ts | 8 + .../teleport/src/services/discovery/types.ts | 28 ++ 16 files changed, 1034 insertions(+), 37 deletions(-) create mode 100644 web/packages/teleport/src/Discover/Server/ConfigureDiscoveryService/ConfigureDiscoveryService.story.tsx create mode 100644 web/packages/teleport/src/Discover/Server/ConfigureDiscoveryService/ConfigureDiscoveryService.tsx create mode 100644 web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigCreatedDialog.tsx create mode 100644 web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.story.tsx create mode 100644 web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx create mode 100644 web/packages/teleport/src/Discover/Server/Shared.tsx create mode 100644 web/packages/teleport/src/Discover/Shared/InfoIcon.tsx diff --git a/web/packages/teleport/src/Discover/SelectResource/__snapshots__/SelectResource.story.test.tsx.snap b/web/packages/teleport/src/Discover/SelectResource/__snapshots__/SelectResource.story.test.tsx.snap index ff4f0a3b3346e..f4ecf2f4f6c8a 100644 --- a/web/packages/teleport/src/Discover/SelectResource/__snapshots__/SelectResource.story.test.tsx.snap +++ b/web/packages/teleport/src/Discover/SelectResource/__snapshots__/SelectResource.story.test.tsx.snap @@ -437,7 +437,7 @@ exports[`render with URL loc state set to "server" 1`] = `
- EC2 Instance + EC2 Auto Enrollment
@@ -1256,7 +1256,7 @@ exports[`render with all access 1`] = `
- EC2 Instance + EC2 Auto Enrollment
@@ -3622,7 +3622,7 @@ exports[`render with no access 1`] = `
- EC2 Instance + EC2 Auto Enrollment
@@ -5350,7 +5350,7 @@ exports[`render with partial access 1`] = `
- EC2 Instance + EC2 Auto Enrollment
diff --git a/web/packages/teleport/src/Discover/SelectResource/resources.tsx b/web/packages/teleport/src/Discover/SelectResource/resources.tsx index cb6d1449da646..d5ebe43810ede 100644 --- a/web/packages/teleport/src/Discover/SelectResource/resources.tsx +++ b/web/packages/teleport/src/Discover/SelectResource/resources.tsx @@ -20,7 +20,10 @@ import { Platform } from 'design/platform'; import { assertUnreachable } from 'shared/utils/assertUnreachable'; -import { DiscoverEventResource } from 'teleport/services/userEvent'; +import { + DiscoverDiscoveryConfigMethod, + DiscoverEventResource, +} from 'teleport/services/userEvent'; import { ResourceKind } from '../Shared/ResourceKind'; @@ -81,13 +84,17 @@ export const SERVERS: ResourceSpec[] = [ platform: Platform.macOS, }, { - name: 'EC2 Instance', + name: 'EC2 Auto Enrollment', kind: ResourceKind.Server, keywords: - baseServerKeywords + 'ec2 instance connect endpoint aws amazon eice', + baseServerKeywords + + 'ec2 instance aws amazon simple systems manager ssm auto enrollment', icon: 'Aws', event: DiscoverEventResource.Ec2Instance, - nodeMeta: { location: ServerLocation.Aws }, + nodeMeta: { + location: ServerLocation.Aws, + discoveryConfigMethod: DiscoverDiscoveryConfigMethod.AwsEc2Ssm, + }, }, { name: 'Connect My Computer', diff --git a/web/packages/teleport/src/Discover/SelectResource/types.ts b/web/packages/teleport/src/Discover/SelectResource/types.ts index b5d033d42f372..cce21de26d7f1 100644 --- a/web/packages/teleport/src/Discover/SelectResource/types.ts +++ b/web/packages/teleport/src/Discover/SelectResource/types.ts @@ -24,7 +24,10 @@ import { AuthType } from 'teleport/services/user'; import { ResourceKind } from '../Shared/ResourceKind'; -import type { DiscoverEventResource } from 'teleport/services/userEvent'; +import type { + DiscoverDiscoveryConfigMethod, + DiscoverEventResource, +} from 'teleport/services/userEvent'; import type { ResourceIconName } from 'design/ResourceIcon'; @@ -77,8 +80,11 @@ export enum SamlServiceProviderPreset { export interface ResourceSpec { dbMeta?: { location: DatabaseLocation; engine: DatabaseEngine }; - nodeMeta?: { location: ServerLocation }; appMeta?: { awsConsole?: boolean }; + nodeMeta?: { + location: ServerLocation; + discoveryConfigMethod: DiscoverDiscoveryConfigMethod; + }; kubeMeta?: { location: KubeLocation }; samlMeta?: { preset: SamlServiceProviderPreset }; name: string; diff --git a/web/packages/teleport/src/Discover/Server/ConfigureDiscoveryService/ConfigureDiscoveryService.story.tsx b/web/packages/teleport/src/Discover/Server/ConfigureDiscoveryService/ConfigureDiscoveryService.story.tsx new file mode 100644 index 0000000000000..1e22bfabee8d0 --- /dev/null +++ b/web/packages/teleport/src/Discover/Server/ConfigureDiscoveryService/ConfigureDiscoveryService.story.tsx @@ -0,0 +1,115 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import React from 'react'; +import { MemoryRouter } from 'react-router'; + +import { ContextProvider } from 'teleport'; +import cfg from 'teleport/config'; +import { createTeleportContext } from 'teleport/mocks/contexts'; +import { + DiscoverProvider, + DiscoverContextState, +} from 'teleport/Discover/useDiscover'; +import { + IntegrationKind, + IntegrationStatusCode, +} from 'teleport/services/integrations'; +import { ResourceKind } from 'teleport/Discover/Shared'; +import { + DiscoverDiscoveryConfigMethod, + DiscoverEventResource, +} from 'teleport/services/userEvent'; +import { ServerLocation } from 'teleport/Discover/SelectResource'; + +import { ConfigureDiscoveryService as Comp } from './ConfigureDiscoveryService'; + +export default { + title: 'Teleport/Discover/Server/EC2', +}; + +export const ConfigureDiscoveryService = () => { + return ; +}; + +const Component = () => { + const ctx = createTeleportContext(); + const discoverCtx: DiscoverContextState = { + agentMeta: { + resourceName: 'aws-console', + agentMatcherLabels: [], + awsRegion: 'ap-south-1', + awsIntegration: { + kind: IntegrationKind.AwsOidc, + name: 'some-oidc-name', + resourceType: 'integration', + spec: { + roleArn: 'arn:aws:iam::123456789012:role/test-role-arn', + issuerS3Bucket: '', + issuerS3Prefix: '', + }, + statusCode: IntegrationStatusCode.Running, + }, + autoDiscovery: { + config: { + name: 'discovery-config-name', + discoveryGroup: 'discovery-group-name', + aws: [], + }, + }, + }, + currentStep: 0, + nextStep: () => null, + prevStep: () => null, + onSelectResource: () => null, + resourceSpec: { + name: '', + kind: ResourceKind.Application, + icon: null, + keywords: '', + event: DiscoverEventResource.Ec2Instance, + nodeMeta: { + location: ServerLocation.Aws, + discoveryConfigMethod: DiscoverDiscoveryConfigMethod.AwsEc2Ssm, + }, + }, + exitFlow: () => null, + viewConfig: null, + indexedViews: [], + setResourceSpec: () => null, + updateAgentMeta: () => null, + emitErrorEvent: () => null, + emitEvent: () => null, + eventState: null, + }; + + cfg.proxyCluster = 'localhost'; + return ( + + + + + + + + ); +}; diff --git a/web/packages/teleport/src/Discover/Server/ConfigureDiscoveryService/ConfigureDiscoveryService.tsx b/web/packages/teleport/src/Discover/Server/ConfigureDiscoveryService/ConfigureDiscoveryService.tsx new file mode 100644 index 0000000000000..1511701659bc2 --- /dev/null +++ b/web/packages/teleport/src/Discover/Server/ConfigureDiscoveryService/ConfigureDiscoveryService.tsx @@ -0,0 +1,67 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import React, { useState } from 'react'; +import { Box, Text } from 'design'; + +import { useDiscover } from 'teleport/Discover/useDiscover'; +import useTeleport from 'teleport/useTeleport'; + +import { SelfHostedAutoDiscoverDirections } from 'teleport/Discover/Shared/AutoDiscovery/SelfHostedAutoDiscoverDirections'; +import { DEFAULT_DISCOVERY_GROUP_NON_CLOUD } from 'teleport/services/discovery'; + +import { ActionButtons, Header } from '../../Shared'; +import { SingleEc2InstanceInstallation } from '../Shared'; + +export function ConfigureDiscoveryService() { + const { nextStep, prevStep, agentMeta, updateAgentMeta } = useDiscover(); + + const [discoveryGroupName, setDiscoveryGroupName] = useState( + DEFAULT_DISCOVERY_GROUP_NON_CLOUD + ); + + const { storeUser } = useTeleport(); + + function handleNextStep() { + updateAgentMeta({ + ...agentMeta, + autoDiscovery: { + config: { name: '', aws: [], discoveryGroup: discoveryGroupName }, + }, + }); + nextStep(); + } + + return ( + +
Configure Teleport Discovery Service
+ + The Teleport Discovery Service can connect to Amazon EC2 and + automatically discover and enroll EC2 instances. + + + + +
+ ); +} diff --git a/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigCreatedDialog.tsx b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigCreatedDialog.tsx new file mode 100644 index 0000000000000..4146162067ad5 --- /dev/null +++ b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigCreatedDialog.tsx @@ -0,0 +1,53 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import React from 'react'; +import { Text, Flex, ButtonPrimary, Box } from 'design'; +import * as Icons from 'design/Icon'; +import Dialog, { DialogContent } from 'design/DialogConfirmation'; + +export function DiscoveryConfigCreatedDialog({ + toNextStep, +}: { + toNextStep: () => void; +}) { + return ( + + + + + + Discovery configuration successfully created. + + The discovery service can take a few minutes to finish + auto-enrolling resources. + + + + toNextStep()}> + Next + + + + ); +} diff --git a/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.story.tsx b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.story.tsx new file mode 100644 index 0000000000000..bc4a4db243854 --- /dev/null +++ b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.story.tsx @@ -0,0 +1,187 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import React, { useEffect } from 'react'; +import { MemoryRouter } from 'react-router'; +import { initialize, mswLoader } from 'msw-storybook-addon'; +import { rest } from 'msw'; +import { Info } from 'design/Alert'; + +import { ContextProvider } from 'teleport'; +import cfg from 'teleport/config'; +import { createTeleportContext } from 'teleport/mocks/contexts'; +import { + DiscoverProvider, + DiscoverContextState, +} from 'teleport/Discover/useDiscover'; +import { + IntegrationKind, + IntegrationStatusCode, +} from 'teleport/services/integrations'; +import { ResourceKind } from 'teleport/Discover/Shared'; +import { + DiscoverDiscoveryConfigMethod, + DiscoverEventResource, +} from 'teleport/services/userEvent'; +import { ServerLocation } from 'teleport/Discover/SelectResource'; + +import { DiscoveryConfigSsm } from './DiscoveryConfigSsm'; + +initialize(); + +const defaultIsCloud = cfg.isCloud; +export default { + title: 'Teleport/Discover/Server/EC2/DiscoveryConfigSsm', + loaders: [mswLoader], + decorators: [ + Story => { + useEffect(() => { + // Clean up + return () => { + cfg.isCloud = defaultIsCloud; + }; + }, []); + return ; + }, + ], +}; + +export const SuccessCloud = () => { + cfg.isCloud = true; + return ; +}; +SuccessCloud.parameters = { + msw: { + handlers: [ + rest.post(cfg.api.joinTokenPath, (req, res, ctx) => + res(ctx.json({ id: 'token-id' })) + ), + rest.post(cfg.api.discoveryConfigPath, (req, res, ctx) => + res(ctx.json({ name: 'discovery-cfg-name' })) + ), + ], + }, +}; + +export const SuccessSelfHosted = () => ; +SuccessSelfHosted.parameters = { + msw: { + handlers: [ + rest.post(cfg.api.joinTokenPath, (req, res, ctx) => + res(ctx.json({ id: 'token-id' })) + ), + rest.post(cfg.api.discoveryConfigPath, (req, res, ctx) => + res(ctx.json({ name: 'discovery-cfg-name' })) + ), + ], + }, +}; + +export const Loading = () => { + cfg.isCloud = true; + return ; +}; +Loading.parameters = { + msw: { + handlers: [ + rest.post(cfg.api.joinTokenPath, (req, res, ctx) => + res(ctx.json({ id: 'token-id' })) + ), + rest.post(cfg.api.discoveryConfigPath, (req, res, ctx) => + res(ctx.delay('infinite')) + ), + ], + }, +}; + +export const Failed = () => ; +Failed.parameters = { + msw: { + handlers: [ + rest.post(cfg.api.joinTokenPath, (req, res, ctx) => + res(ctx.json({ id: 'token-id' })) + ), + rest.post(cfg.api.discoveryConfigPath, (req, res, ctx) => + res( + ctx.status(403), + ctx.json({ message: 'Some kind of error message' }) + ) + ), + ], + }, +}; + +const Component = () => { + const ctx = createTeleportContext(); + const discoverCtx: DiscoverContextState = { + agentMeta: { + resourceName: 'aws-console', + agentMatcherLabels: [], + awsIntegration: { + kind: IntegrationKind.AwsOidc, + name: 'some-oidc-name', + resourceType: 'integration', + spec: { + roleArn: 'arn:aws:iam::123456789012:role/test-role-arn', + issuerS3Bucket: '', + issuerS3Prefix: '', + }, + statusCode: IntegrationStatusCode.Running, + }, + }, + currentStep: 0, + nextStep: () => null, + prevStep: () => null, + onSelectResource: () => null, + resourceSpec: { + name: '', + kind: ResourceKind.Application, + icon: null, + keywords: '', + event: DiscoverEventResource.Ec2Instance, + nodeMeta: { + location: ServerLocation.Aws, + discoveryConfigMethod: DiscoverDiscoveryConfigMethod.AwsEc2Ssm, + }, + }, + exitFlow: () => null, + viewConfig: null, + indexedViews: [], + setResourceSpec: () => null, + updateAgentMeta: () => null, + emitErrorEvent: () => null, + emitEvent: () => null, + eventState: null, + }; + + cfg.proxyCluster = 'localhost'; + return ( + + + + Devs: Click next to see next state + + + + + ); +}; diff --git a/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx new file mode 100644 index 0000000000000..799846063ec9c --- /dev/null +++ b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx @@ -0,0 +1,380 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import React, { useState, useRef } from 'react'; +import { Box, Link as ExternalLink, Text, Flex, ButtonSecondary } from 'design'; +import styled from 'styled-components'; +import { Danger, Info } from 'design/Alert'; +import TextEditor from 'shared/components/TextEditor'; +import { ToolTipInfo } from 'shared/components/ToolTip'; +import FieldInput from 'shared/components/FieldInput'; +import { Rule } from 'shared/components/Validation/rules'; +import Validation, { Validator } from 'shared/components/Validation'; +import { makeEmptyAttempt, useAsync } from 'shared/hooks/useAsync'; +import { TextSelectCopyMulti } from 'shared/components/TextSelectCopy'; + +import cfg from 'teleport/config'; +import { useDiscover } from 'teleport/Discover/useDiscover'; +import { Regions } from 'teleport/services/integrations'; +import { AwsRegionSelector } from 'teleport/Discover/Shared/AwsRegionSelector'; +import JoinTokenService, { JoinToken } from 'teleport/services/joinToken'; + +import { + DISCOVERY_GROUP_CLOUD, + createDiscoveryConfig, + InstallParamEnrollMode, +} from 'teleport/services/discovery'; +import { splitAwsIamArn } from 'teleport/services/integrations/aws'; +import useStickyClusterId from 'teleport/useStickyClusterId'; + +import { ActionButtons, Header, Mark, StyledBox } from '../../Shared'; + +import { SingleEc2InstanceInstallation } from '../Shared'; + +import { DiscoveryConfigCreatedDialog } from './DiscoveryConfigCreatedDialog'; + +const IAM_POLICY_NAME = 'EC2DiscoverWithSSM'; + +export function DiscoveryConfigSsm() { + const { agentMeta, emitErrorEvent, nextStep, updateAgentMeta, prevStep } = + useDiscover(); + + const { arnResourceName, awsAccountId } = splitAwsIamArn( + agentMeta.awsIntegration.spec.roleArn + ); + + const { clusterId } = useStickyClusterId(); + + const [selectedRegion, setSelectedRegion] = useState(); + const [ssmDocumentName, setSsmDocumentName] = useState( + 'TeleportDiscoveryInstaller' + ); + const [scriptUrl, setScriptUrl] = useState(''); + const joinTokenRef = useRef(); + const [showRestOfSteps, setShowRestOfSteps] = useState(false); + + const [attempt, createJoinTokenAndDiscoveryConfig, setAttempt] = useAsync( + async () => { + try { + const joinTokenService = new JoinTokenService(); + // Don't create another token if token was already created. + // This can happen if creating discovery config attempt failed + // and the user retries. + if (!joinTokenRef.current) { + joinTokenRef.current = await joinTokenService.fetchJoinToken({ + roles: ['Node'], + method: 'iam', + rules: [{ awsAccountId }], + }); + } + + const config = await createDiscoveryConfig(clusterId, { + name: crypto.randomUUID(), + discoveryGroup: cfg.isCloud + ? DISCOVERY_GROUP_CLOUD + : agentMeta.autoDiscovery.config.discoveryGroup, + aws: [ + { + types: ['ec2'], + regions: [selectedRegion], + tags: { '*': ['*'] }, + integration: agentMeta.awsIntegration.name, + ssm: { documentName: ssmDocumentName }, + install: { + enrollMode: InstallParamEnrollMode.Script, + installTeleport: true, + joinToken: joinTokenRef.current.id, + }, + }, + ], + }); + + updateAgentMeta({ + ...agentMeta, + awsRegion: selectedRegion, + autoDiscovery: { + config, + }, + }); + } catch (err) { + emitErrorEvent(err.message); + throw err; + } + } + ); + + function generateScriptUrl(validator: Validator) { + if (!validator.validate()) return; + + const scriptUrl = cfg.getAwsIamConfigureScriptEc2AutoDiscoverWithSsmUrl({ + iamRoleName: arnResourceName, + region: selectedRegion, + ssmDocument: ssmDocumentName, + }); + setScriptUrl(scriptUrl); + } + + function clear() { + setAttempt(makeEmptyAttempt); + joinTokenRef.current = undefined; + } + + function handleOnSubmit( + e: React.MouseEvent, + validator: Validator + ) { + e.preventDefault(); + if (scriptUrl) { + setScriptUrl(''); + return; + } + generateScriptUrl(validator); + } + + return ( + +
Setup Discovery Config for Teleport Discovery Service
+ {cfg.isCloud ? ( + + The Teleport Discovery Service can connect to Amazon EC2 and + automatically discover and enroll EC2 instances. + + ) : ( + + Discovery config defines the setup that enables Teleport to + automatically discover and register instances. + + )} + {cfg.isCloud && } + {attempt.status === 'error' && ( + {attempt.statusText} + )} + + Step 1 + + + Select the AWS Region that contains the EC2 instances that you would + like to enroll: + + setSelectedRegion(region)} + clear={clear} + disableSelector={!!scriptUrl} + /> + + {!showRestOfSteps && ( + setShowRestOfSteps(true)} + disabled={!selectedRegion} + mt={3} + > + Next + + )} + {scriptUrl && ( + setScriptUrl('')} mt={3}> + Edit + + )} + + {showRestOfSteps && ( + <> + + Step 2 + + Attach AWS managed{' '} + + AmazonSSMManagedInstanceCore + {' '} + policy to EC2 instances IAM profile. The policy enables EC2 + instances to use SSM core functionality. + + + + Step 3 + Each EC2 instance requires{' '} + + SSM Agent + {' '} + to be running. The SSM{' '} + + Nodes Manager dashboard + {' '} + will list all instances that have SSM agent already running. Ensure + ping statuses are Online. + + If you do not see your instances listed in the dashboard, it might + take up to 30 minutes for your instances to use the IAM + credentials you updated in step 2. + + + + {({ validator }) => ( +
+ + Step 4 + + + Give a name for the{' '} + + AWS SSM Document + {' '} + that will be created on your behalf. Required to run the + installer script on each discovered instances. + + setSsmDocumentName(e.target.value)} + placeholder="ssm-document-name" + disabled={!!scriptUrl} + /> + + handleOnSubmit(e, validator)} + disabled={!selectedRegion} + > + {scriptUrl ? 'Edit' : 'Next'} + + +
+ )} +
+ {scriptUrl && ( + + Step 5 + + + Run the command below on your{' '} + + AWS CloudShell + {' '} + to configure your IAM permissions. + + + The following IAM permissions will be added as an inline + policy named {IAM_POLICY_NAME} to IAM role{' '} + {arnResourceName} + + + + + + + + + + )} + + )} + + {attempt.status === 'success' && ( + + )} + + +
+ ); +} + +const EditorWrapper = styled(Flex)` + flex-directions: column; + height: ${p => p.$height}px; + margin-top: ${p => p.theme.space[3]}px; + width: 450px; +`; + +const inlinePolicyJson = `{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "ec2:DescribeInstances", + "ssm:DescribeInstanceInformation", + "ssm:GetCommandInvocation", + "ssm:ListCommandInvocations", + "ssm:SendCommand" + ], + "Resource": "*" + } + ] +}`; + +const SSM_DOCUMENT_NAME_REGEX = new RegExp('^[0-9A-Za-z._-]*$'); +const requiredSsmDocument: Rule = name => () => { + if (!name || name.length < 3 || name.length > 128) { + return { + valid: false, + message: 'name must be between 3 and 128 characters', + }; + } + + const match = name.match(SSM_DOCUMENT_NAME_REGEX); + if (!match) { + return { + valid: false, + message: 'valid characters are a-z, A-Z, 0-9, and _, -, and . only', + }; + } + + return { + valid: true, + }; +}; + +const SsmInfoHeaderText = () => ( + <> + The service will execute an install script on these discovered instances + using{' '} + + AWS Systems Manager + {' '} + that will install Teleport, start it and join the cluster. + +); diff --git a/web/packages/teleport/src/Discover/Server/Shared.tsx b/web/packages/teleport/src/Discover/Server/Shared.tsx new file mode 100644 index 0000000000000..32ddfc2aed77e --- /dev/null +++ b/web/packages/teleport/src/Discover/Server/Shared.tsx @@ -0,0 +1,49 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import React from 'react'; +import { Link as InternalLink } from 'react-router-dom'; +import { OutlineInfo } from 'design/Alert/Alert'; +import { Box } from 'design'; + +import cfg from 'teleport/config'; + +import { InfoIcon } from '../Shared/InfoIcon'; +import { Mark } from '../Shared'; + +export const SingleEc2InstanceInstallation = () => ( + + + + + + Auto discovery will enroll all EC2 instances found in a region. If you + want to enroll a single EC2 instance instead, consider + following the{' '} + + Teleport service installation + {' '} + flow. + + +); diff --git a/web/packages/teleport/src/Discover/Server/index.tsx b/web/packages/teleport/src/Discover/Server/index.tsx index 9e18226035d4d..1e449b88a8434 100644 --- a/web/packages/teleport/src/Discover/Server/index.tsx +++ b/web/packages/teleport/src/Discover/Server/index.tsx @@ -23,7 +23,11 @@ import { DownloadScript } from 'teleport/Discover/Server/DownloadScript'; import { SetupAccess } from 'teleport/Discover/Server/SetupAccess'; import { TestConnection } from 'teleport/Discover/Server/TestConnection'; import { AwsAccount, ResourceKind, Finished } from 'teleport/Discover/Shared'; -import { DiscoverEvent } from 'teleport/services/userEvent'; +import { + DiscoverDiscoveryConfigMethod, + DiscoverEvent, +} from 'teleport/services/userEvent'; +import cfg from 'teleport/config'; import { ResourceSpec, ServerLocation } from '../SelectResource'; @@ -31,6 +35,8 @@ import { EnrollEc2Instance } from './EnrollEc2Instance/EnrollEc2Instance'; import { CreateEc2Ice } from './CreateEc2Ice/CreateEc2Ice'; import { ServerWrapper } from './ServerWrapper'; +import { DiscoveryConfigSsm } from './DiscoveryConfigSsm/DiscoveryConfigSsm'; +import { ConfigureDiscoveryService } from './ConfigureDiscoveryService/ConfigureDiscoveryService'; export const ServerResource: ResourceViewConfig = { kind: ResourceKind.Server, @@ -51,7 +57,12 @@ export const ServerResource: ResourceViewConfig = { views(resource) { let configureResourceViews; - if (resource && resource.nodeMeta?.location === ServerLocation.Aws) { + const { nodeMeta } = resource; + if ( + nodeMeta?.location === ServerLocation.Aws && + nodeMeta.discoveryConfigMethod === + DiscoverDiscoveryConfigMethod.AwsEc2Eice + ) { configureResourceViews = [ { title: 'Connect AWS Account', @@ -70,6 +81,35 @@ export const ServerResource: ResourceViewConfig = { manuallyEmitSuccessEvent: true, }, ]; + } else if ( + nodeMeta?.location === ServerLocation.Aws && + nodeMeta.discoveryConfigMethod === DiscoverDiscoveryConfigMethod.AwsEc2Ssm + ) { + configureResourceViews = [ + { + title: 'Connect AWS Account', + component: AwsAccount, + eventName: DiscoverEvent.IntegrationAWSOIDCConnectEvent, + }, + // Self hosted requires user to manually install a discovery service. + // Cloud already has a discovery service running, so this step is not required. + ...(!cfg.isCloud + ? [ + { + title: 'Configure Discovery Service', + component: ConfigureDiscoveryService, + eventName: DiscoverEvent.DeployService, + }, + ] + : []), + { + title: cfg.isCloud + ? 'Configure Auto Discovery Service' + : 'Create Discovery Config', + component: DiscoveryConfigSsm, + eventName: DiscoverEvent.CreateDiscoveryConfig, + }, + ]; } else { configureResourceViews = [ { diff --git a/web/packages/teleport/src/Discover/Shared/AutoDiscovery/SelfHostedAutoDiscoverDirections.tsx b/web/packages/teleport/src/Discover/Shared/AutoDiscovery/SelfHostedAutoDiscoverDirections.tsx index ac21c13d20238..992dae8f82780 100644 --- a/web/packages/teleport/src/Discover/Shared/AutoDiscovery/SelfHostedAutoDiscoverDirections.tsx +++ b/web/packages/teleport/src/Discover/Shared/AutoDiscovery/SelfHostedAutoDiscoverDirections.tsx @@ -38,10 +38,12 @@ export const SelfHostedAutoDiscoverDirections = ({ clusterPublicUrl, discoveryGroupName, setDiscoveryGroupName, + showSubHeader = true, }: { clusterPublicUrl: string; discoveryGroupName: string; setDiscoveryGroupName(n: string): void; + showSubHeader?: boolean; }) => { const yamlContent = `version: v3 teleport: @@ -61,14 +63,18 @@ discovery_service: return ( - - - Auto-enrolling requires you to configure a{' '} - Discovery Service - - - -
+ {showSubHeader && ( + <> + + + Auto-enrolling requires you to configure a{' '} + Discovery Service + + + +
+ + )} Step 1: Create a Join Token diff --git a/web/packages/teleport/src/Discover/Shared/AwsRegionSelector/AwsRegionSelector.tsx b/web/packages/teleport/src/Discover/Shared/AwsRegionSelector/AwsRegionSelector.tsx index 544e66fef4bba..bd4b2394fe1c9 100644 --- a/web/packages/teleport/src/Discover/Shared/AwsRegionSelector/AwsRegionSelector.tsx +++ b/web/packages/teleport/src/Discover/Shared/AwsRegionSelector/AwsRegionSelector.tsx @@ -30,7 +30,7 @@ export function AwsRegionSelector({ clear, }: { onFetch(region: Regions): void; - onRefresh(): void; + onRefresh?(): void; disableSelector: boolean; clear(): void; }) { @@ -58,22 +58,24 @@ export function AwsRegionSelector({ isDisabled={disableSelector} />
- - - + {onRefresh && ( + + + + )} ); diff --git a/web/packages/teleport/src/Discover/Shared/InfoIcon.tsx b/web/packages/teleport/src/Discover/Shared/InfoIcon.tsx new file mode 100644 index 0000000000000..bdec4998f8f64 --- /dev/null +++ b/web/packages/teleport/src/Discover/Shared/InfoIcon.tsx @@ -0,0 +1,29 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import styled from 'styled-components'; +import { Info } from 'design/Icon'; + +export const InfoIcon = styled(Info)` + background-color: ${p => p.theme.colors.link}; + border-radius: 100px; + height: 32px; + width: 32px; + color: ${p => p.theme.colors.text.primaryInverse}; + margin-right: ${p => p.theme.space[2]}px; +`; diff --git a/web/packages/teleport/src/config.ts b/web/packages/teleport/src/config.ts index b92d8d24285a4..d52c6cc2fc86c 100644 --- a/web/packages/teleport/src/config.ts +++ b/web/packages/teleport/src/config.ts @@ -319,6 +319,9 @@ const cfg = { awsConfigureIamAppAccessPath: '/v1/webapi/scripts/integrations/configure/aws-app-access-iam.sh?role=:iamRoleName', + awsConfigureIamEc2AutoDiscoverWithSsmPath: + '/v1/webapi/scripts/integrations/configure/ec2-ssm-iam.sh?role=:iamRoleName&awsRegion=:region&ssmDocument=:ssmDocument', + eksClustersListPath: '/v1/webapi/sites/:clusterId/integrations/aws-oidc/:name/eksclusters', eksEnrollClustersPath: @@ -1089,6 +1092,17 @@ const cfg = { ); }, + getAwsIamConfigureScriptEc2AutoDiscoverWithSsmUrl( + params: UrlAwsConfigureIamEc2AutoDiscoverWithSsmScriptParams + ) { + return ( + cfg.baseUrl + + generatePath(cfg.api.awsConfigureIamEc2AutoDiscoverWithSsmPath, { + ...params, + }) + ); + }, + getAwsConfigureIamScriptListDatabasesUrl( params: UrlAwsConfigureIamScriptParams ) { @@ -1248,6 +1262,12 @@ export interface UrlAwsConfigureIamScriptParams { iamRoleName: string; } +export interface UrlAwsConfigureIamEc2AutoDiscoverWithSsmScriptParams { + region: Regions; + iamRoleName: string; + ssmDocument: string; +} + export interface UrlGcpWorkforceConfigParam { orgId: string; poolName: string; diff --git a/web/packages/teleport/src/services/discovery/discovery.ts b/web/packages/teleport/src/services/discovery/discovery.ts index 3ce7042a353be..38a497080dde6 100644 --- a/web/packages/teleport/src/services/discovery/discovery.ts +++ b/web/packages/teleport/src/services/discovery/discovery.ts @@ -73,5 +73,13 @@ function makeAwsMatchersReq(inputMatchers: AwsMatcher[]) { tags: a.tags || {}, integration: a.integration, kube_app_discovery: !!a.kubeAppDiscovery, + ssm: a.ssm ? { document_name: a.ssm.documentName } : undefined, + install: a.install + ? { + enroll_mode: a.install.enrollMode, + install_teleport: a.install.installTeleport, + join_token: a.install.joinToken, + } + : undefined, })); } diff --git a/web/packages/teleport/src/services/discovery/types.ts b/web/packages/teleport/src/services/discovery/types.ts index b117c8d9d5778..70a82daf03a7e 100644 --- a/web/packages/teleport/src/services/discovery/types.ts +++ b/web/packages/teleport/src/services/discovery/types.ts @@ -29,6 +29,11 @@ export type DiscoveryConfig = { type AwsMatcherTypes = 'rds' | 'eks' | 'ec2'; +export enum InstallParamEnrollMode { + Script = 1, + Eice = 2, +} + // AWSMatcher matches AWS EC2 instances, AWS EKS clusters and AWS Databases export type AwsMatcher = { // types are AWS types to match, "ec2", "eks", "rds", "redshift", "elasticache", @@ -43,6 +48,29 @@ export type AwsMatcher = { integration: string; // kubeAppDiscovery specifies if Kubernetes App Discovery should be enabled for a discovered cluster. kubeAppDiscovery?: boolean; + /** + * install sets the join method when installing on + * discovered EC2 nodes + */ + install?: { + /** + * enrollMode indicates the mode used to enroll the node into Teleport. + */ + enrollMode: InstallParamEnrollMode; + /** + * installTeleport disables agentless discovery + */ + installTeleport: boolean; + /** + * joinToken is the token to use when joining the cluster + */ + joinToken: string; + }; + /** + * ssm provides options to use when sending a document command to + * an EC2 node + */ + ssm?: { documentName: string }; }; type Labels = Record;