diff --git a/web/packages/teleport/src/Discover/Database/CreateDatabase/CreateDatabase.story.tsx b/web/packages/teleport/src/Discover/Database/CreateDatabase/CreateDatabase.story.tsx index d599495e1cb9d..e9bc69d291f02 100644 --- a/web/packages/teleport/src/Discover/Database/CreateDatabase/CreateDatabase.story.tsx +++ b/web/packages/teleport/src/Discover/Database/CreateDatabase/CreateDatabase.story.tsx @@ -79,4 +79,5 @@ const props: State = { prevStep: () => null, nextStep: () => null, createdDb: {} as any, + handleOnTimeout: () => null, }; diff --git a/web/packages/teleport/src/Discover/Database/CreateDatabase/CreateDatabase.tsx b/web/packages/teleport/src/Discover/Database/CreateDatabase/CreateDatabase.tsx index 8fad2dc0ae062..867c3a7d3a2c1 100644 --- a/web/packages/teleport/src/Discover/Database/CreateDatabase/CreateDatabase.tsx +++ b/web/packages/teleport/src/Discover/Database/CreateDatabase/CreateDatabase.tsx @@ -58,6 +58,7 @@ export function CreateDatabaseView({ isDbCreateErr, prevStep, nextStep, + handleOnTimeout, }: State) { const [dbName, setDbName] = useState(''); const [dbUri, setDbUri] = useState(''); @@ -75,7 +76,10 @@ export function CreateDatabaseView({ } }, [isDbCreateErr]); - function handleOnProceed(validator: Validator, retry = false) { + function handleOnProceed( + validator: Validator, + { overwriteDb = false, retry = false } = {} + ) { if (!validator.validate()) { return; } @@ -86,12 +90,15 @@ export function CreateDatabaseView({ return; } - registerDatabase({ - labels, - name: dbName, - uri: `${dbUri}:${dbPort}`, - protocol: getDatabaseProtocol(dbEngine), - }); + registerDatabase( + { + labels, + name: dbName, + uri: `${dbUri}:${dbPort}`, + protocol: getDatabaseProtocol(dbEngine), + }, + { overwriteDb } + ); } return ( @@ -188,7 +195,11 @@ export function CreateDatabaseView({ handleOnProceed(validator, true /* retry */)} + retry={() => handleOnProceed(validator, { retry: true })} + onOverwrite={() => + handleOnProceed(validator, { overwriteDb: true }) + } + onTimeout={handleOnTimeout} close={clearAttempt} dbName={dbName} next={nextStep} diff --git a/web/packages/teleport/src/Discover/Database/CreateDatabase/CreateDatabaseDialog.story.tsx b/web/packages/teleport/src/Discover/Database/CreateDatabase/CreateDatabaseDialog.story.tsx index 30d9437f56413..92698e3b1871c 100644 --- a/web/packages/teleport/src/Discover/Database/CreateDatabase/CreateDatabaseDialog.story.tsx +++ b/web/packages/teleport/src/Discover/Database/CreateDatabase/CreateDatabaseDialog.story.tsx @@ -17,11 +17,13 @@ */ import React from 'react'; +import { Info } from 'design/Alert'; import { CreateDatabaseDialog, CreateDatabaseDialogProps, } from './CreateDatabaseDialog'; +import { dbWithoutDbServerExistsErrorMsg, timeoutErrorMsg } from './const'; export default { title: 'Teleport/Discover/Database/CreateDatabase/Dialog', @@ -40,11 +42,34 @@ export const Success = () => ( ); +export const AllowSkipOnTimeout = () => ( + <> + Devs: it should be same state as success + + +); + +export const AllowOverwrite = () => ( + +); + const props: CreateDatabaseDialogProps = { pollTimeout: 8080000000, attempt: { status: 'processing' }, retry: () => null, close: () => null, next: () => null, + onOverwrite: () => null, + onTimeout: () => null, dbName: 'db-name', }; diff --git a/web/packages/teleport/src/Discover/Database/CreateDatabase/CreateDatabaseDialog.tsx b/web/packages/teleport/src/Discover/Database/CreateDatabase/CreateDatabaseDialog.tsx index a18eb15a6b274..c5de2390ff7e2 100644 --- a/web/packages/teleport/src/Discover/Database/CreateDatabase/CreateDatabaseDialog.tsx +++ b/web/packages/teleport/src/Discover/Database/CreateDatabase/CreateDatabaseDialog.tsx @@ -24,6 +24,7 @@ import { ButtonPrimary, ButtonSecondary, H2, + ButtonWarning, } from 'design'; import * as Icons from 'design/Icon'; import Dialog, { DialogContent } from 'design/DialogConfirmation'; @@ -31,6 +32,8 @@ import Dialog, { DialogContent } from 'design/DialogConfirmation'; import { Timeout } from 'teleport/Discover/Shared/Timeout'; import { TextIcon } from 'teleport/Discover/Shared'; +import { dbWithoutDbServerExistsErrorMsg, timeoutErrorMsg } from './const'; + import type { Attempt } from 'shared/hooks/useAttemptNext'; export type CreateDatabaseDialogProps = { @@ -39,6 +42,8 @@ export type CreateDatabaseDialogProps = { retry(): void; close(): void; next(): void; + onOverwrite(): void; + onTimeout(): void; dbName: string; }; @@ -49,28 +54,53 @@ export function CreateDatabaseDialog({ close, next, dbName, + onOverwrite, + onTimeout, }: CreateDatabaseDialogProps) { let content: JSX.Element; if (attempt.status === 'failed') { - // TODO(bl-nero): Migrate this to alert boxes. - content = ( - <> - - {' '} - - {attempt.statusText} - - - - Retry - - - Close - - - - ); - } else if (attempt.status === 'processing') { + /** + * Most likely cause of timeout is when we found a matching db_service + * but no db_server heartbeats. Most likely cause is because db_service + * has been stopped but is not removed from teleport yet (there is some + * minutes delay on expiry). + * + * We allow the user to proceed to the next step to re-deploy (replace) + * the db_service that has been stopped. + */ + if (attempt.statusText === timeoutErrorMsg) { + content = ; + } else { + // Only allow overwriting if the database error + // states that it's a existing database without a db_server. + const canOverwriteDb = attempt.statusText.includes( + dbWithoutDbServerExistsErrorMsg + ); + + // TODO(bl-nero): Migrate this to alert boxes. + content = ( + <> + + + {attempt.statusText} + + + + Retry + + {canOverwriteDb && ( + + Overwrite + + )} + + Close + + + + ); + } + } else if (attempt.status === 'processing' || attempt.status === '') { content = ( <> @@ -92,19 +122,8 @@ export function CreateDatabaseDialog({ ); - } else { - // success - content = ( - <> - - - Database "{dbName}" successfully registered - - - Next - - - ); + } else if (attempt.status === 'success') { + content = ; } return ( @@ -121,3 +140,15 @@ export function CreateDatabaseDialog({ ); } + +const SuccessContent = ({ dbName, onClick }) => ( + <> + + + Database "{dbName}" successfully registered + + + Next + + +); diff --git a/web/packages/teleport/src/Discover/Database/CreateDatabase/const.ts b/web/packages/teleport/src/Discover/Database/CreateDatabase/const.ts new file mode 100644 index 0000000000000..377e3cf9a2b13 --- /dev/null +++ b/web/packages/teleport/src/Discover/Database/CreateDatabase/const.ts @@ -0,0 +1,23 @@ +/** + * 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 . + */ + +export const timeoutErrorMsg = + 'Teleport could not detect your new database in time. Please try again.'; + +export const dbWithoutDbServerExistsErrorMsg = + 'already exists but there are no Teleport agents proxying it'; diff --git a/web/packages/teleport/src/Discover/Database/CreateDatabase/useCreateDatabase.ts b/web/packages/teleport/src/Discover/Database/CreateDatabase/useCreateDatabase.ts index 6f23d48a33bcb..a9ac3e6499c11 100644 --- a/web/packages/teleport/src/Discover/Database/CreateDatabase/useCreateDatabase.ts +++ b/web/packages/teleport/src/Discover/Database/CreateDatabase/useCreateDatabase.ts @@ -32,6 +32,8 @@ import cfg from 'teleport/config'; import { matchLabels } from '../common'; +import { dbWithoutDbServerExistsErrorMsg, timeoutErrorMsg } from './const'; + import type { CreateDatabaseRequest, Database as DatabaseResource, @@ -101,8 +103,7 @@ export function useCreateDatabase() { setTimedOut(false); setAttempt({ status: 'failed', - statusText: - 'Teleport could not detect your new database in time. Please try again.', + statusText: timeoutErrorMsg, }); emitErrorEvent( `timeout polling for new database with an existing service` @@ -134,6 +135,22 @@ export function useCreateDatabase() { setAttempt({ status: 'success' }); }, [dbPollingResult]); + function handleOnTimeout() { + updateAgentMetaUponRequiringDeployment(createdDb); + } + + function updateAgentMetaUponRequiringDeployment(db: CreateDatabaseRequest) { + updateAgentMeta({ + ...(agentMeta as DbMeta), + resourceName: db.name, + awsRegion: db.awsRegion, + agentMatcherLabels: db.labels, + selectedAwsRdsDb: db.awsRds, + awsVpcId: db.awsVpcId, + }); + handleNextStep(); + } + // fetchDatabaseServer is the callback that is run every interval by the poller. // The poller will stop polling once a result returns (a dbServer). function fetchDatabaseServer(signal: AbortSignal) { @@ -171,15 +188,30 @@ export function useCreateDatabase() { return ctx.databaseService.fetchDatabases(clusterId, request); } - async function registerDatabase(db: CreateDatabaseRequest, newDb = false) { + async function registerDatabase( + db: CreateDatabaseRequest, + { newDb = false, overwriteDb = false } = {} + ) { // Set the timeout now, because this entire registering process // should take less than WAITING_TIMEOUT. setPollTimeout(Date.now() + WAITING_TIMEOUT); setAttempt({ status: 'processing' }); setIsDbCreateErr(false); + if (overwriteDb) { + try { + await ctx.databaseService.createDatabase(clusterId, { + ...db, + overwrite: true, + }); + setCreatedDb(db); + } catch (err) { + handleRequestError(err, 'failed to overwrite database: '); + return; + } + } // Attempt creating a new Database resource. - if (!createdDb || newDb) { + else if (!createdDb || newDb) { try { await ctx.databaseService.createDatabase(clusterId, db); setCreatedDb(db); @@ -196,9 +228,8 @@ export function useCreateDatabase() { return; } } - // Check and see if database resource need to be updated. - if (!newDb && requiresDbUpdate(db)) { + else if (requiresDbUpdate(db)) { try { await ctx.databaseService.updateDatabase(clusterId, { ...db, @@ -218,14 +249,7 @@ export function useCreateDatabase() { await ctx.databaseService.fetchDatabaseServices(clusterId); if (!findActiveDatabaseSvc(db.labels, services)) { - updateAgentMeta({ - ...(agentMeta as DbMeta), - resourceName: db.name, - awsRegion: db.awsRegion, - agentMatcherLabels: db.labels, - selectedAwsRdsDb: db.awsRds, - awsVpcId: db.awsVpcId, - }); + updateAgentMetaUponRequiringDeployment(db); setAttempt({ status: 'success' }); return; } @@ -248,32 +272,29 @@ export function useCreateDatabase() { dbName: string, isAwsRds = false ) { - const preErrMsg = 'failed to register database: '; - const nonAwsMsg = `use a different name and try again`; - const awsMsg = `change (or define) the value of the \ - tag "TeleportDatabaseName" on the RDS instance and try again`; + const selfHostedMsg = `use a different name and retry.`; + const awsMsg = `alternatively upsert the value of the \ + AWS tag "TeleportDatabaseName" on the RDS instance and retry.`; try { await ctx.databaseService.fetchDatabase(clusterId, dbName); - let message = `a database with the name "${dbName}" is already \ - a part of this cluster, ${isAwsRds ? awsMsg : nonAwsMsg}`; - handleRequestError(new Error(message), preErrMsg); + let message = `A database with the name "${dbName}" is already \ + a part of this cluster, ${isAwsRds ? awsMsg : selfHostedMsg}`; + handleRequestError(new Error(message)); } catch (e) { - // No database server were found for the database name. + // No database server were found for the database name + // so it'll be safe to overwrite the database. if (e instanceof ApiError) { if (e.response.status === 404) { - let message = `a database with the name "${dbName}" already exists \ - but there are no database servers for it, you can remove this \ - database using the command, “tctl rm db/${dbName}”, or ${ - isAwsRds ? awsMsg : nonAwsMsg - }`; - handleRequestError(new Error(message), preErrMsg); + let message = `A database with the name "${dbName}" ${dbWithoutDbServerExistsErrorMsg}. \ + You can overwrite it, or ${isAwsRds ? awsMsg : selfHostedMsg}`; + handleRequestError(new Error(message)); } return; } // Display other errors as is. - handleRequestError(e, preErrMsg); + handleRequestError(e, 'failed to register database:'); } setIsDbCreateErr(true); } @@ -364,6 +385,7 @@ export function useCreateDatabase() { clearAttempt, registerDatabase, fetchDatabaseServers, + handleOnTimeout, canCreateDatabase: access.create, pollTimeout, dbEngine: resourceSpec.dbMeta.engine, diff --git a/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/AutoDeploy.test.tsx b/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/AutoDeploy.test.tsx index 8791e794fb45b..6f6df15755cb2 100644 --- a/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/AutoDeploy.test.tsx +++ b/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/AutoDeploy.test.tsx @@ -118,17 +118,6 @@ describe('test AutoDeploy.tsx', () => { await screen.findByText('subnet-id'); } - test('init: labels are rendered, command is not rendered yet', async () => { - const { teleCtx, discoverCtx } = getMockedContexts(); - - renderAutoDeploy(teleCtx, discoverCtx); - await waitForSubnetsAndSecurityGroups(); - - expect(screen.getByText(/env: prod/i)).toBeInTheDocument(); - expect(screen.queryByText(/copy\/paste/i)).not.toBeInTheDocument(); - expect(screen.queryByText(/curl/i)).not.toBeInTheDocument(); - }); - test('clicking button renders command', async () => { const { teleCtx, discoverCtx } = getMockedContexts(); diff --git a/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/AutoDeploy.tsx b/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/AutoDeploy.tsx index da37ae8bcd2a2..3006b0233c948 100644 --- a/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/AutoDeploy.tsx +++ b/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/AutoDeploy.tsx @@ -18,7 +18,17 @@ import React, { useState, useEffect } from 'react'; import styled from 'styled-components'; -import { Box, ButtonSecondary, Link, Text, Mark, H3, Subtitle3 } from 'design'; +import { + Box, + ButtonSecondary, + Link, + Text, + Mark, + H3, + Subtitle3, + Link as ExternalLink, + Flex, +} from 'design'; import * as Icons from 'design/Icon'; import FieldInput from 'shared/components/FieldInput'; import Validation, { Validator } from 'shared/components/Validation'; @@ -34,7 +44,7 @@ import { SuccessBox, WaitingInfo, } from 'teleport/Discover/Shared/HintBox'; -import { integrationService } from 'teleport/services/integrations'; +import { integrationService, Regions } from 'teleport/services/integrations'; import { useDiscover, DbMeta } from 'teleport/Discover/useDiscover'; import { DiscoverEventStatus, @@ -50,12 +60,10 @@ import { TextIcon, useShowHint, Header, - DiscoverLabel, AlternateInstructionButton, } from '../../../Shared'; import { DeployServiceProp } from '../DeployService'; -import { hasMatchingLabels, Labels } from '../../common'; import { SelectSecurityGroups } from './SelectSecurityGroups'; import { SelectSubnetIds } from './SelectSubnetIds'; @@ -66,7 +74,6 @@ export function AutoDeploy({ toggleDeployMethod }: DeployServiceProp) { const { emitErrorEvent, nextStep, emitEvent, agentMeta, updateAgentMeta } = useDiscover(); const { attempt, setAttempt } = useAttempt(''); - const [showLabelMatchErr, setShowLabelMatchErr] = useState(true); const [taskRoleArn, setTaskRoleArn] = useState('TeleportDatabaseAccess'); const [svcDeployedAwsUrl, setSvcDeployedAwsUrl] = useState(''); @@ -82,20 +89,8 @@ export function AutoDeploy({ toggleDeployMethod }: DeployServiceProp) { string[] >([]); - const hasDbLabels = agentMeta?.agentMatcherLabels?.length; - const dbLabels = hasDbLabels ? agentMeta.agentMatcherLabels : []; - const [labels, setLabels] = useState([ - { name: '*', value: '*', isFixed: dbLabels.length === 0 }, - ]); const dbMeta = agentMeta as DbMeta; - useEffect(() => { - // Turn off error once user changes labels. - if (showLabelMatchErr) { - setShowLabelMatchErr(false); - } - }, [labels]); - function manuallyValidateRequiredFields() { if (selectedSubnetIds.length === 0) { setHasNoSubnets(true); @@ -115,6 +110,9 @@ export function AutoDeploy({ toggleDeployMethod }: DeployServiceProp) { } function handleDeploy(validator) { + setSvcDeployedAwsUrl(''); + setDeployFinished(false); + if (!validator.validate()) { return; } @@ -124,20 +122,24 @@ export function AutoDeploy({ toggleDeployMethod }: DeployServiceProp) { } const integrationName = dbMeta.awsIntegration.name; + const { awsAccountId } = splitAwsIamArn( + agentMeta.awsIntegration.spec.roleArn + ); if (wantAutoDiscover) { setAttempt({ status: 'processing' }); - const { awsAccountId } = splitAwsIamArn( - agentMeta.awsIntegration.spec.roleArn - ); integrationService .deployDatabaseServices(integrationName, { region: dbMeta.awsRegion, accountId: awsAccountId, taskRoleArn, deployments: [ - { vpcId: dbMeta.awsVpcId, subnetIds: selectedSubnetIds }, + { + vpcId: dbMeta.awsVpcId, + subnetIds: selectedSubnetIds, + securityGroups: selectedSecurityGroups, + }, ], }) .then(url => { @@ -151,12 +153,6 @@ export function AutoDeploy({ toggleDeployMethod }: DeployServiceProp) { emitErrorEvent(`auto discover deploy request failed: ${err.message}`); }); } else { - if (!hasMatchingLabels(dbLabels, labels)) { - setShowLabelMatchErr(true); - return; - } - - setShowLabelMatchErr(false); setAttempt({ status: 'processing' }); integrationService .deployAwsOidcService(integrationName, { @@ -164,8 +160,9 @@ export function AutoDeploy({ toggleDeployMethod }: DeployServiceProp) { region: dbMeta.awsRegion, subnetIds: selectedSubnetIds, taskRoleArn, - databaseAgentMatcherLabels: labels, securityGroups: selectedSecurityGroups, + vpcId: dbMeta.awsVpcId, + accountId: awsAccountId, }) // The user is still technically in the "processing" // state, because after this call succeeds, we will @@ -215,8 +212,8 @@ export function AutoDeploy({ toggleDeployMethod }: DeployServiceProp) { } const wantAutoDiscover = !!dbMeta.autoDiscovery; - const isProcessing = attempt.status === 'processing' && !!svcDeployedAwsUrl; - const isDeploying = isProcessing && !!svcDeployedAwsUrl; + const isProcessing = attempt.status === 'processing' && !svcDeployedAwsUrl; + const isDeploying = attempt.status === 'processing' && !!svcDeployedAwsUrl; const hasError = attempt.status === 'failed'; return ( @@ -268,36 +265,22 @@ export function AutoDeploy({ toggleDeployMethod }: DeployServiceProp) {
-

Step 4 (Optional)

- Define Matcher Labels -
- -
- - -
-

Step 5

+

Step 4

Deploy the Teleport Database Service
handleDeploy(validator)} - disabled={attempt.status === 'processing'} + disabled={isProcessing} mt={2} mb={2} > - Deploy Teleport Service + {isDeploying + ? 'Redeploy Teleport Service' + : 'Deploy Teleport Service'} {hasError && ( @@ -321,6 +304,7 @@ export function AutoDeploy({ toggleDeployMethod }: DeployServiceProp) { resourceName={agentMeta.resourceName} abortDeploying={abortDeploying} svcDeployedAwsUrl={svcDeployedAwsUrl} + region={dbMeta.awsRegion} /> )} @@ -479,11 +463,13 @@ const DeployHints = ({ deployFinished, abortDeploying, svcDeployedAwsUrl, + region, }: { resourceName: string; deployFinished(dbResult: Database): void; abortDeploying(): void; svcDeployedAwsUrl: string; + region: Regions; }) => { // Starts resource querying interval. const { result, active } = usePingTeleport(resourceName); @@ -499,18 +485,57 @@ const DeployHints = ({ if (showHint && !result) { return ( - - The network may be slow. Try continuing to wait for a few more minutes - or{' '} - - try manually deploying your own service. - {' '} - You can visit your AWS{' '} - - dashboard - {' '} - to see progress details. - + + + Visit your AWS{' '} + + dashboard + {' '} + to see progress details. + + + There are a few possible reasons for why we haven't been able to + detect your database service: + +
    p.theme.space[3]}px; + `} + > +
  • + The subnets you selected do not route to an internet gateway (igw) + or a NAT gateway in a public subnet. +
  • +
  • + The security groups you selected do not allow outbound traffic + (eg: 0.0.0.0/0) to pull the public Teleport image and + to reach your Teleport cluster. +
  • +
  • + The security groups attached to your database(s) neither allow + inbound traffic from the security group you selected nor allow + inbound traffic from all IPs in the subnets you selected. +
  • +
  • + There may be issues in the region you selected ({region}). Check + the{' '} + + AWS Health Dashboard + {' '} + for any problems. +
  • +
  • + The network may be slow. Try waiting for a few more minutes or{' '} + + try manually deploying your own database service. + +
  • +
+
); } diff --git a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/SingleEnrollment.tsx b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/SingleEnrollment.tsx index 69a3a45a4223b..43dede55ed56d 100644 --- a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/SingleEnrollment.tsx +++ b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/SingleEnrollment.tsx @@ -81,9 +81,10 @@ export function SingleEnrollment({ pollTimeout, registerDatabase, attempt, - clearAttempt, // TODO + clearAttempt, nextStep, fetchDatabaseServers, + handleOnTimeout, } = useCreateDatabase(); const { agentMeta, resourceSpec, emitErrorEvent } = useDiscover(); @@ -175,7 +176,10 @@ export function SingleEnrollment({ } } - function handleOnProceed() { + function handleOnProceed({ overwriteDb = false } = {}) { + // Corner case where if registering db fails a user can: + // 1) change region, which will list new databases or + // 2) select a different database before re-trying. const isNewDb = selectedDb.name !== createdDb?.name; registerDatabase( { @@ -187,10 +191,7 @@ export function SingleEnrollment({ awsRegion: region, awsVpcId: vpc.id, }, - // Corner case where if registering db fails a user can: - // 1) change region, which will list new databases or - // 2) select a different database before re-trying. - isNewDb + { newDb: isNewDb, overwriteDb } ); } @@ -222,6 +223,8 @@ export function SingleEnrollment({ next={nextStep} close={clearAttempt} retry={handleOnProceed} + onTimeout={handleOnTimeout} + onOverwrite={() => handleOnProceed({ overwriteDb: true })} dbName={selectedDb.name} /> )} diff --git a/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEKSCluster.test.tsx b/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEKSCluster.test.tsx index b5a5a2fdc412f..19a1d47f9df6c 100644 --- a/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEKSCluster.test.tsx +++ b/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEKSCluster.test.tsx @@ -115,8 +115,11 @@ describe('test EnrollEksCluster.tsx', () => { fireEvent.keyDown(selectEl, { key: 'ArrowDown', keyCode: 40 }); fireEvent.click(screen.getByText('us-east-2')); - // EKS results are rendered. await screen.findByText(/eks1/i); + + // Toggle on auto enroll. + act(() => screen.getByText(/auto-enroll all/i).click()); + // Cloud uses a default discovery group name. expect( screen.queryByText(/define a discovery group name/i) @@ -152,6 +155,12 @@ describe('test EnrollEksCluster.tsx', () => { fireEvent.keyDown(selectEl, { key: 'ArrowDown', keyCode: 40 }); fireEvent.click(screen.getByText('us-east-2')); + await screen.findByText(/eks1/i); + + // Toggle on auto enroll. + act(() => screen.getByText(/auto-enroll all/i).click()); + expect(screen.queryByText(/eks1/i)).not.toBeInTheDocument(); + // Only self-hosted need to define a discovery group name. await screen.findByText(/define a discovery group name/i); // There should be no table rendered. @@ -187,11 +196,6 @@ describe('test EnrollEksCluster.tsx', () => { await screen.findByText(/eks1/i); - // disable auto enroll - expect(screen.getByText('Next')).toBeEnabled(); - act(() => screen.getByText(/auto-enroll all/i).click()); - expect(screen.getByText('Enroll EKS Cluster')).toBeDisabled(); - act(() => screen.getByRole('radio').click()); act(() => screen.getByText('Enroll EKS Cluster').click()); diff --git a/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEksCluster.tsx b/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEksCluster.tsx index 5ead71559623a..aadb78d3af559 100644 --- a/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEksCluster.tsx +++ b/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEksCluster.tsx @@ -62,6 +62,10 @@ import { ConfigureDiscoveryServiceDirections, CreatedDiscoveryConfigDialog, } from 'teleport/Discover/Shared/ConfigureDiscoveryService'; +import { + DiscoverEvent, + DiscoverEventStatus, +} from 'teleport/services/userEvent'; import { ActionButtons, Header } from '../../Shared'; @@ -96,7 +100,8 @@ type EKSClusterEnrollmentState = { }; export function EnrollEksCluster(props: AgentStepProps) { - const { agentMeta, updateAgentMeta, emitErrorEvent } = useDiscover(); + const { agentMeta, updateAgentMeta, emitErrorEvent, emitEvent } = + useDiscover(); const { attempt: fetchClustersAttempt, setAttempt: setFetchClustersAttempt } = useAttempt(''); @@ -113,7 +118,7 @@ export function EnrollEksCluster(props: AgentStepProps) { status: 'notStarted', }); const [isAppDiscoveryEnabled, setAppDiscoveryEnabled] = useState(true); - const [isAutoDiscoveryEnabled, setAutoDiscoveryEnabled] = useState(true); + const [isAutoDiscoveryEnabled, setAutoDiscoveryEnabled] = useState(false); const [isAgentWaitingDialogShown, setIsAgentWaitingDialogShown] = useState(false); const [isManualHelmDialogShown, setIsManualHelmDialogShown] = useState(false); @@ -245,6 +250,12 @@ export function EnrollEksCluster(props: AgentStepProps) { } ); setAutoDiscoveryCfg(discoveryConfig); + emitEvent( + { stepStatus: DiscoverEventStatus.Success }, + { + eventName: DiscoverEvent.CreateDiscoveryConfig, + } + ); } catch (err) { const message = getErrMessage(err); setAutoDiscoverAttempt({ diff --git a/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx index 3b3867e05a104..3e0821699a652 100644 --- a/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx +++ b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx @@ -52,6 +52,10 @@ import { } from 'teleport/services/discovery'; import { splitAwsIamArn } from 'teleport/services/integrations/aws'; import useStickyClusterId from 'teleport/useStickyClusterId'; +import { + DiscoverEvent, + DiscoverEventStatus, +} from 'teleport/services/userEvent'; import { ActionButtons, Header, StyledBox } from '../../Shared'; @@ -62,8 +66,14 @@ import { DiscoveryConfigCreatedDialog } from './DiscoveryConfigCreatedDialog'; const IAM_POLICY_NAME = 'EC2DiscoverWithSSM'; export function DiscoveryConfigSsm() { - const { agentMeta, emitErrorEvent, nextStep, updateAgentMeta, prevStep } = - useDiscover(); + const { + agentMeta, + emitErrorEvent, + nextStep, + updateAgentMeta, + prevStep, + emitEvent, + } = useDiscover(); const { arnResourceName, awsAccountId } = splitAwsIamArn( agentMeta.awsIntegration.spec.roleArn @@ -115,6 +125,13 @@ export function DiscoveryConfigSsm() { ], }); + emitEvent( + { stepStatus: DiscoverEventStatus.Success }, + { + eventName: DiscoverEvent.CreateDiscoveryConfig, + } + ); + updateAgentMeta({ ...agentMeta, awsRegion: selectedRegion, diff --git a/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.test.tsx b/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.test.tsx index 920321be3e8c0..f1dc90188185d 100644 --- a/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.test.tsx +++ b/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.test.tsx @@ -120,11 +120,7 @@ describe('test EnrollEc2Instance.tsx', () => { .mockResolvedValue({ agents: mockFetchedNodes }); renderEc2Instances(ctx, discoverCtx); - await selectARegion({ waitForSelfHosted: true }); - - // toggle off auto enroll, to test the table. - await userEvent.click(screen.getByText(/auto-enroll all/i)); - await screen.findAllByText(/My EC2 Box 1/i); + await selectARegion({ waitForTable: true }); expect(integrationService.fetchAwsEc2Instances).toHaveBeenCalledTimes(1); expect(ctx.nodeService.fetchNodes).toHaveBeenCalledTimes(1); @@ -154,11 +150,7 @@ describe('test EnrollEc2Instance.tsx', () => { .mockResolvedValue({ instances: mockEc2Instances }); renderEc2Instances(ctx, discoverCtx); - await selectARegion({ waitForSelfHosted: true }); - - // toggle off auto enroll - await userEvent.click(screen.getByText(/auto-enroll all/i)); - await screen.findAllByText(/My EC2 Box 1/i); + await selectARegion({ waitForTable: true }); expect(integrationService.fetchAwsEc2Instances).toHaveBeenCalledTimes(1); expect(ctx.nodeService.fetchNodes).toHaveBeenCalledTimes(1); @@ -179,23 +171,18 @@ describe('test EnrollEc2Instance.tsx', () => { .mockResolvedValue({ instances: mockEc2Instances }); renderEc2Instances(ctx, discoverCtx); - await selectARegion({ waitForSelfHosted: true }); - - // default toggler should be checked. - expect(screen.getByTestId('toggle')).toBeChecked(); - expect(screen.queryByText(/My EC2 Box 1/i)).not.toBeInTheDocument(); - expect(screen.getByText(/next/i, { selector: 'button' })).toBeEnabled(); + await selectARegion({ waitForTable: true }); - // toggle off auto enroll, should render table. - await userEvent.click(screen.getByText(/auto-enroll all/i)); + // default toggler should not be checked. expect(screen.getByTestId('toggle')).not.toBeChecked(); expect(screen.getByText(/next/i, { selector: 'button' })).toBeDisabled(); - await screen.findAllByText(/My EC2 Box 1/i); - - // toggle it back on. + // toggle on auto enroll, should render table. await userEvent.click(screen.getByText(/auto-enroll all/i)); expect(screen.getByTestId('toggle')).toBeChecked(); + expect(screen.getByText(/next/i, { selector: 'button' })).toBeEnabled(); + expect(screen.queryByText(/My EC2 Box 1/i)).not.toBeInTheDocument(); + expect(screen.getByText(/create a join token/i)).toBeInTheDocument(); }); test('cloud, auto discover toggling', async () => { @@ -210,20 +197,12 @@ describe('test EnrollEc2Instance.tsx', () => { renderEc2Instances(ctx, discoverCtx); await selectARegion({ waitForTable: true }); - // default toggler should be checked. - expect(screen.queryByText(/create a join token/i)).not.toBeInTheDocument(); - expect(screen.getByTestId('toggle')).toBeChecked(); - expect(screen.getByText(/next/i, { selector: 'button' })).toBeEnabled(); - - // toggle off auto enroll - await userEvent.click(screen.getByText(/auto-enroll all/i)); - await screen.findAllByText(/My EC2 Box 1/i); + // default toggler should be off. expect(screen.getByTestId('toggle')).not.toBeChecked(); - expect(screen.getByText(/next/i, { selector: 'button' })).toBeDisabled(); - // toggle it back on. await userEvent.click(screen.getByText(/auto-enroll all/i)); - expect(screen.getByTestId('toggle')).toBeChecked(); + expect(screen.getByText(/next/i, { selector: 'button' })).toBeEnabled(); + expect(screen.queryByText(/create a join token/i)).not.toBeInTheDocument(); }); test('self-hosted, auto discover without existing endpoints', async () => { @@ -246,7 +225,10 @@ describe('test EnrollEc2Instance.tsx', () => { }); renderEc2Instances(ctx, discoverCtx); - await selectARegion({ waitForSelfHosted: true }); + await selectARegion({ waitForTable: true }); + + // Toggle on. + await userEvent.click(screen.getByText(/auto-enroll all/i)); await userEvent.click(screen.getByText(/next/i, { selector: 'button' })); expect(integrationService.fetchAwsEc2Instances).toHaveBeenCalledWith( @@ -278,7 +260,10 @@ describe('test EnrollEc2Instance.tsx', () => { }); renderEc2Instances(ctx, discoverCtx); - await selectARegion({ waitForSelfHosted: true }); + await selectARegion({ waitForTable: true }); + + // Toggle on. + await userEvent.click(screen.getByText(/auto-enroll all/i)); await userEvent.click(screen.getByText(/next/i, { selector: 'button' })); expect(integrationService.fetchAwsEc2Instances).toHaveBeenCalledTimes(1); @@ -322,6 +307,9 @@ describe('test EnrollEc2Instance.tsx', () => { renderEc2Instances(ctx, discoverCtx); await selectARegion({ waitForTable: true }); + // Toggle on. + await userEvent.click(screen.getByText(/auto-enroll all/i)); + await userEvent.click(screen.getByText(/next/i, { selector: 'button' })); expect(integrationService.fetchAwsEc2Instances).toHaveBeenCalledWith( discoverCtx.agentMeta.awsIntegration.name, @@ -388,6 +376,9 @@ describe('test EnrollEc2Instance.tsx', () => { renderEc2Instances(ctx, discoverCtx); await selectARegion({ waitForTable: true }); + // Toggle on. + await userEvent.click(screen.getByText(/auto-enroll all/i)); + await userEvent.click(screen.getByText(/next/i, { selector: 'button' })); expect(integrationService.fetchAwsEc2Instances).toHaveBeenCalledWith( discoverCtx.agentMeta.awsIntegration.name, @@ -409,7 +400,6 @@ describe('test EnrollEc2Instance.tsx', () => { test('cloud, with partially created endpoints, with already set discovery config', async () => { cfg.isCloud = true; - jest.useFakeTimers(); const { ctx, discoverCtx } = getMockedContexts( true /* withAutoDiscovery */ @@ -469,7 +459,11 @@ describe('test EnrollEc2Instance.tsx', () => { renderEc2Instances(ctx, discoverCtx); await selectARegion({ waitForTable: true }); + await userEvent.click(screen.getByText(/auto-enroll all/i)); + expect(screen.getByTestId('toggle')).toBeChecked(); + // Test it's polling. + jest.useFakeTimers(); fireEvent.click(screen.getByText(/next/i, { selector: 'button' })); await screen.findByText(/this may take a few minutes/i); diff --git a/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx b/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx index a958f7c13483f..2cbb084ecd732 100644 --- a/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx +++ b/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx @@ -98,7 +98,7 @@ export function EnrollEc2Instance() { }); const [autoDiscoveryCfg, setAutoDiscoveryCfg] = useState(); - const [wantAutoDiscover, setWantAutoDiscover] = useState(true); + const [wantAutoDiscover, setWantAutoDiscover] = useState(false); const [discoveryGroupName, setDiscoveryGroupName] = useState(() => cfg.isCloud ? '' : DEFAULT_DISCOVERY_GROUP_NON_CLOUD ); diff --git a/web/packages/teleport/src/services/databases/types.ts b/web/packages/teleport/src/services/databases/types.ts index 589db3486612f..6630646f9e4e4 100644 --- a/web/packages/teleport/src/services/databases/types.ts +++ b/web/packages/teleport/src/services/databases/types.ts @@ -71,6 +71,7 @@ export type CreateDatabaseRequest = { awsRds?: AwsRdsDatabase; awsRegion?: Regions; awsVpcId?: string; + overwrite?: boolean; }; export type DatabaseIamPolicyResponse = { diff --git a/web/packages/teleport/src/services/integrations/types.ts b/web/packages/teleport/src/services/integrations/types.ts index 1eb28993287f7..80dd390f5ec9a 100644 --- a/web/packages/teleport/src/services/integrations/types.ts +++ b/web/packages/teleport/src/services/integrations/types.ts @@ -325,8 +325,9 @@ export type AwsOidcDeployServiceRequest = { region: Regions; subnetIds: string[]; taskRoleArn: string; - databaseAgentMatcherLabels: Label[]; securityGroups?: string[]; + vpcId: string; + accountId: string; }; // DeployDatabaseServiceDeployment identifies the required fields to deploy a DatabaseService.