diff --git a/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/Dialogs.story.tsx b/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/Dialogs.story.tsx index c90f093de298b..39e9ad759c92a 100644 --- a/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/Dialogs.story.tsx +++ b/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/Dialogs.story.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { MemoryRouter } from 'react-router'; import { rest } from 'msw'; import { mswLoader } from 'msw-storybook-addon'; @@ -27,11 +27,26 @@ import { ResourceKind } from 'teleport/Discover/Shared'; import { PingTeleportProvider } from 'teleport/Discover/Shared/PingTeleportContext'; import { ContextProvider } from 'teleport'; +import { + INTERNAL_RESOURCE_ID_LABEL_KEY, + JoinToken, +} from 'teleport/services/joinToken'; +import { clearCachedJoinTokenResult } from 'teleport/Discover/Shared/useJoinTokenSuspender'; +import { + DiscoverContextState, + DiscoverProvider, +} from 'teleport/Discover/useDiscover'; +import { + IntegrationKind, + IntegrationStatusCode, +} from 'teleport/services/integrations'; +import { DiscoverEventResource } from 'teleport/services/userEvent'; + import { generateCmd } from 'teleport/Discover/Kubernetes/HelmChart/HelmChart'; -import { ManualHelmDialog } from './ManualHelmDialog'; -import { AgentWaitingDialog } from './AgentWaitingDialog'; import { EnrollmentDialog } from './EnrollmentDialog'; +import { AgentWaitingDialog } from './AgentWaitingDialog'; +import { ManualHelmDialog } from './ManualHelmDialog'; export default { title: 'Teleport/Discover/Kube/EnrollEksClusters/Dialogs', @@ -110,7 +125,7 @@ AgentWaitingDialogSuccess.parameters = { }, }; -const helmCommand = generateCmd({ +const helmCommandProps = { namespace: 'teleport-agent', clusterName: 'EKS1', proxyAddr: 'teleport-proxy.example.com:1234', @@ -126,15 +141,98 @@ const helmCommand = generateCmd({ { name: 'region', value: 'us-east-1' }, { name: 'account-id', value: '1234567789012' }, ], -}); +}; -export const ManualHelmDialogStory = () => ( - - {}} - cancel={() => {}} - /> - -); +export const ManualHelmDialogStory = () => { + const discoverCtx: DiscoverContextState = { + agentMeta: { + resourceName: 'kube-name', + agentMatcherLabels: [], + kube: { + kind: 'kube_cluster', + name: '', + labels: [], + }, + awsIntegration: { + kind: IntegrationKind.AwsOidc, + name: 'test-oidc', + resourceType: 'integration', + spec: { + roleArn: 'arn:aws:iam::123456789012:role/test-role-arn', + }, + statusCode: IntegrationStatusCode.Running, + }, + }, + currentStep: 0, + nextStep: () => null, + prevStep: () => null, + onSelectResource: () => null, + resourceSpec: { + name: 'Eks', + kind: ResourceKind.Kubernetes, + icon: 'Eks', + keywords: '', + event: DiscoverEventResource.KubernetesEks, + }, + exitFlow: () => null, + viewConfig: null, + indexedViews: [], + setResourceSpec: () => null, + updateAgentMeta: () => null, + emitErrorEvent: () => null, + emitEvent: () => null, + eventState: null, + }; + + useEffect(() => { + return () => { + clearCachedJoinTokenResult([ + ResourceKind.Kubernetes, + ResourceKind.Application, + ResourceKind.Discovery, + ]); + }; + }, []); + + const [, setToken] = useState(); + + return ( + + + + { + // Emulate real usage of ManualHelmDialog where setJoinTokenAndGetCommand updates the + // state of a parent. + setToken(token); + return generateCmd(helmCommandProps); + }} + confirmedCommands={() => {}} + cancel={() => {}} + /> + + + + ); +}; ManualHelmDialogStory.storyName = 'ManualHelmDialog'; +ManualHelmDialogStory.parameters = { + msw: { + handlers: [ + rest.post(cfg.api.joinTokenPath, (req, res, ctx) => { + return res( + ctx.json({ + id: 'token-id', + suggestedLabels: [ + { name: INTERNAL_RESOURCE_ID_LABEL_KEY, value: 'resource-id' }, + ], + }) + ); + }), + ], + }, +}; diff --git a/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEksCluster.tsx b/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEksCluster.tsx index 583c391fd252a..699e3456d7ca0 100644 --- a/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEksCluster.tsx +++ b/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEksCluster.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import React, { useState } from 'react'; +import React, { useState, useCallback } from 'react'; import { Box, ButtonSecondary, ButtonText, Text, Toggle } from 'design'; import styled from 'styled-components'; import { FetchStatus } from 'design/DataTable/types'; @@ -39,14 +39,15 @@ import { isIamPermError } from 'teleport/Discover/Shared/Aws/error'; import { AgentStepProps } from 'teleport/Discover/types'; import useTeleport from 'teleport/useTeleport'; -import { useJoinTokenSuspender } from 'teleport/Discover/Shared/useJoinTokenSuspender'; import { generateCmd } from 'teleport/Discover/Kubernetes/HelmChart/HelmChart'; import { Kube } from 'teleport/services/kube'; -import { Header, ResourceKind } from '../../Shared'; +import { JoinToken } from 'teleport/services/joinToken'; + +import { Header } from '../../Shared'; import { ClustersList } from './EksClustersList'; -import { ManualHelmDialog } from './ManualHelmDialog'; +import ManualHelmDialog from './ManualHelmDialog'; import { EnrollmentDialog } from './EnrollmentDialog'; import { AgentWaitingDialog } from './AgentWaitingDialog'; @@ -97,14 +98,11 @@ export function EnrollEksCluster(props: AgentStepProps) { useState(false); const [isManualHelmDialogShown, setIsManualHelmDialogShown] = useState(false); const [waitingResourceId, setWaitingResourceId] = useState(''); + // join token will be set only if user opens ManualHelmDialog, + // we delay it to avoid premature admin action MFA confirmation request. + const [joinToken, setJoinToken] = useState(null); const ctx = useTeleport(); - const { joinToken } = useJoinTokenSuspender([ - ResourceKind.Kubernetes, - ResourceKind.Application, - ResourceKind.Discovery, - ]); - function fetchClustersWithNewRegion(region: Regions) { setSelectedRegion(region); // Clear table when fetching with new region. @@ -205,8 +203,6 @@ export function EnrollEksCluster(props: AgentStepProps) { { region: selectedRegion, enableAppDiscovery: isAppDiscoveryEnabled, - joinToken: joinToken.id, - resourceId: joinToken.internalResourceId, clusterNames: [selectedCluster.name], } ); @@ -262,23 +258,34 @@ export function EnrollEksCluster(props: AgentStepProps) { !selectedCluster || enrollmentState.status !== 'notStarted'; - let command = ''; - if (selectedCluster) { - command = generateCmd({ - namespace: 'teleport-agent', - clusterName: selectedCluster.name, - proxyAddr: ctx.storeUser.state.cluster.publicURL, - tokenId: joinToken.id, - clusterVersion: ctx.storeUser.state.cluster.authVersion, - resourceId: joinToken.internalResourceId, - isEnterprise: ctx.isEnterprise, - isCloud: ctx.isCloud, - automaticUpgradesEnabled: ctx.automaticUpgradesEnabled, - automaticUpgradesTargetVersion: ctx.automaticUpgradesTargetVersion, - joinLabels: [...selectedCluster.labels, ...selectedCluster.joinLabels], - disableAppDiscovery: !isAppDiscoveryEnabled, - }); - } + const setJoinTokenAndGetCommand = useCallback( + (token: JoinToken) => { + setJoinToken(token); + return generateCmd({ + namespace: 'teleport-agent', + clusterName: selectedCluster.name, + proxyAddr: ctx.storeUser.state.cluster.publicURL, + clusterVersion: ctx.storeUser.state.cluster.authVersion, + tokenId: token.id, + resourceId: token.internalResourceId, + isEnterprise: ctx.isEnterprise, + isCloud: ctx.isCloud, + automaticUpgradesEnabled: ctx.automaticUpgradesEnabled, + automaticUpgradesTargetVersion: ctx.automaticUpgradesTargetVersion, + joinLabels: [...selectedCluster.labels, ...selectedCluster.joinLabels], + disableAppDiscovery: !isAppDiscoveryEnabled, + }); + }, + [ + ctx.automaticUpgradesEnabled, + ctx.automaticUpgradesTargetVersion, + ctx.isCloud, + ctx.isEnterprise, + ctx.storeUser.state.cluster, + isAppDiscoveryEnabled, + selectedCluster, + ] + ); return ( @@ -366,7 +373,7 @@ export function EnrollEksCluster(props: AgentStepProps) { )} {isManualHelmDialogShown && ( setIsManualHelmDialogShown(false)} confirmedCommands={() => { setEnrollmentState({ status: 'awaitingAgent' }); @@ -377,7 +384,7 @@ export function EnrollEksCluster(props: AgentStepProps) { )} {isAgentWaitingDialogShown && ( { diff --git a/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/ManualHelmDialog.tsx b/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/ManualHelmDialog.tsx index ce2a0428fd4b8..593f1722b3227 100644 --- a/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/ManualHelmDialog.tsx +++ b/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/ManualHelmDialog.tsx @@ -17,62 +17,77 @@ */ import Dialog, { DialogContent, DialogFooter } from 'design/DialogConfirmation'; -import { Box, Flex, ButtonPrimary, ButtonSecondary, Text } from 'design'; +import { + Box, + Flex, + ButtonPrimary, + ButtonSecondary, + Text, + Indicator, +} from 'design'; -import React from 'react'; +import React, { Suspense, useState, useEffect } from 'react'; import styled from 'styled-components'; +import * as Icons from 'design/Icon'; + import { TextSelectCopyMulti } from 'teleport/components/TextSelectCopy'; import { CommandBox } from 'teleport/Discover/Shared/CommandBox'; +import { useJoinTokenSuspender } from 'teleport/Discover/Shared/useJoinTokenSuspender'; +import { ResourceKind, TextIcon } from 'teleport/Discover/Shared'; +import { JoinToken } from 'teleport/services/joinToken'; +import { CatchError } from 'teleport/components/CatchError'; + type ManualHelmDialogProps = { - command: string; + setJoinTokenAndGetCommand(token: JoinToken): string; confirmedCommands(): void; cancel(): void; }; -export function ManualHelmDialog({ - command, +export default function Container(props: ManualHelmDialogProps) { + return ( + ( + + )} + > + } + > + {/**/} + fallback={} + + + ); +} + +type FallbackDialogProps = { + cancel: () => void; + error?: Error; + showSpinner?: boolean; +}; + +const DialogWrapper = ({ + children, cancel, - confirmedCommands, -}: ManualHelmDialogProps) { + next, +}: { + children: React.ReactNode; + cancel: () => void; + next?: () => void; +}) => { return ( Manual EKS Cluster Enrollment - - Step 1 - - Add teleport-agent chart to your charts repository - - - - - Step 2 - - Run the command below on the server your target EKS cluster is - at. It may take up to a minute for the Teleport Service to join - after running the command. - - - } - > - - + {children} - + I ran these commands @@ -81,6 +96,81 @@ export function ManualHelmDialog({ ); +}; + +const FallbackDialog = ({ + error, + cancel, + showSpinner, +}: FallbackDialogProps) => { + return ( + + {showSpinner && ( + + + + )} + {error && ( + + + + Encountered an error: {error.message} + + + )} + + ); +}; + +export function ManualHelmDialog({ + setJoinTokenAndGetCommand, + cancel, + confirmedCommands, +}: ManualHelmDialogProps) { + const { joinToken } = useJoinTokenSuspender([ + ResourceKind.Kubernetes, + ResourceKind.Application, + ResourceKind.Discovery, + ]); + const [command, setCommand] = useState(''); + + useEffect(() => { + if (joinToken && !command) { + setCommand(setJoinTokenAndGetCommand(joinToken)); + } + }, [joinToken, command, setJoinTokenAndGetCommand]); + + return ( + + + Step 1 + + Add teleport-agent chart to your charts repository + + + + + Step 2 + + Run the command below on the server your target EKS cluster is at. + It may take up to a minute for the Teleport Service to join after + running the command. + + + } + > + + + + ); } const StyledBox = styled(Box)` diff --git a/web/packages/teleport/src/services/integrations/types.ts b/web/packages/teleport/src/services/integrations/types.ts index 8094c0072e3d1..cf99cdc3c75a6 100644 --- a/web/packages/teleport/src/services/integrations/types.ts +++ b/web/packages/teleport/src/services/integrations/types.ts @@ -317,8 +317,6 @@ export type AwsEksCluster = { export type EnrollEksClustersRequest = { region: string; enableAppDiscovery: boolean; - joinToken: string; - resourceId: string; clusterNames: string[]; };