+
+ 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 && (
-
+
Encountered Error: {attempt.statusText}
@@ -284,6 +300,7 @@ export function AutoDeploy({ toggleDeployMethod }: DeployServiceProp) {
resourceName={agentMeta.resourceName}
abortDeploying={abortDeploying}
svcDeployedAwsUrl={svcDeployedAwsUrl}
+ region={dbMeta.awsRegion}
/>
)}
@@ -293,6 +310,19 @@ export function AutoDeploy({ toggleDeployMethod }: DeployServiceProp) {
/>
)}
+ {hasNoSubnets && selectedSubnetIds.length === 0 && (
+
+
+ At least one subnet selection is required
+
+ )}
+ {hasNoSecurityGroups && selectedSecurityGroups.length === 0 && (
+
+
+ At least one security group selection is required
+
+ )}
+
{
// Starts resource querying interval.
const { result, active } = usePingTeleport(resourceName);
@@ -449,18 +481,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.
+
+
+ The selected security group(s) must allow all outbound traffic
+ (eg: 0.0.0.0/0)
+
+
+ A security group attached to your database(s) must allow inbound
+ traffic from a security group you select or from all IPs in the
+ subnets you selected
+
+
+
+
+
+
+
Select security groups to assign to the Fargate service that will be
- running the database access agent. The security groups you pick must
- allow outbound connectivity to this Teleport cluster. If you don't
- select any security groups, the default one for the VPC will be used.
-
+ running the Teleport Database Service. If you don't select any security
+ groups, the default one for the VPC will be used.
+
+ {/* TODO(bl-nero): Convert this to an alert box with embedded retry button */}
{attempt.status === 'failed' && (
<>
@@ -136,6 +165,23 @@ export const SelectSecurityGroups = ({
onSelectSecurityGroup={onSelectSecurityGroup}
selectedSecurityGroups={selectedSecurityGroups}
/>
+
+
+ fetchSecurityGroups({ refresh: true })}
+ px={2}
+ disabled={disabled}
+ >
+ Refresh
+
+
+
+ {`${selectedSecurityGroups.length} ${pluralize(selectedSecurityGroups.length, 'security group')} selected`}
+
+
)}
>
diff --git a/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSubnetIds.tsx b/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSubnetIds.tsx
new file mode 100644
index 0000000000000..e118eb776324c
--- /dev/null
+++ b/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSubnetIds.tsx
@@ -0,0 +1,185 @@
+/**
+ * Teleport
+ * Copyright (C) 2023 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, useEffect } from 'react';
+
+import {
+ Text,
+ Flex,
+ Box,
+ Indicator,
+ ButtonSecondary,
+ Subtitle3,
+ P3,
+} from 'design';
+import * as Icons from 'design/Icon';
+import { FetchStatus } from 'design/DataTable/types';
+import { HoverTooltip, ToolTipInfo } from 'shared/components/ToolTip';
+import { pluralize } from 'shared/utils/text';
+import useAttempt from 'shared/hooks/useAttemptNext';
+import { getErrMessage } from 'shared/utils/errorType';
+
+import { SubnetIdPicker } from 'teleport/Discover/Shared/SubnetIdPicker';
+import { integrationService, Subnet } from 'teleport/services/integrations';
+import { DbMeta } from 'teleport/Discover/useDiscover';
+import useTeleport from 'teleport/useTeleport';
+
+import { ButtonBlueText } from '../../../Shared';
+
+type TableData = {
+ items: Subnet[];
+ nextToken?: string;
+ fetchStatus: FetchStatus;
+};
+
+export function SelectSubnetIds({
+ selectedSubnetIds,
+ onSelectedSubnetIds,
+ dbMeta,
+ emitErrorEvent,
+ disabled = false,
+}: {
+ selectedSubnetIds: string[];
+ onSelectedSubnetIds: React.Dispatch>;
+ dbMeta: DbMeta;
+ emitErrorEvent(err: string): void;
+ disabled?: boolean;
+}) {
+ const ctx = useTeleport();
+ const clusterId = ctx.storeUser.getClusterId();
+ const [tableData, setTableData] = useState({
+ items: [],
+ nextToken: '',
+ fetchStatus: 'disabled',
+ });
+
+ const { attempt, run } = useAttempt('processing');
+
+ function handleSelectSubnet(
+ subnet: Subnet,
+ e: React.ChangeEvent
+ ) {
+ if (e.target.checked) {
+ return onSelectedSubnetIds(currentSelectedGroups => [
+ ...currentSelectedGroups,
+ subnet.id,
+ ]);
+ } else {
+ onSelectedSubnetIds(selectedSubnetIds.filter(id => id !== subnet.id));
+ }
+ }
+
+ async function fetchSubnets({ refresh = false } = {}) {
+ run(() =>
+ integrationService
+ .fetchAwsSubnets(dbMeta.awsIntegration.name, clusterId, {
+ vpcId: dbMeta.awsVpcId,
+ region: dbMeta.awsRegion,
+ nextToken: tableData.nextToken,
+ })
+ .then(({ subnets, nextToken }) => {
+ const combinedSubnets = [...tableData.items, ...subnets];
+ setTableData({
+ nextToken,
+ fetchStatus: nextToken ? '' : 'disabled',
+ items: refresh ? subnets : combinedSubnets,
+ });
+ if (refresh) {
+ // Reset so user doesn't unintentionally keep a subnet
+ // that no longer exists upon refresh.
+ onSelectedSubnetIds([]);
+ }
+ })
+ .catch((err: Error) => {
+ const errMsg = getErrMessage(err);
+ emitErrorEvent(`fetch subnets error: ${errMsg}`);
+ throw err;
+ })
+ );
+ }
+
+ useEffect(() => {
+ fetchSubnets();
+ }, []);
+
+ return (
+ <>
+
+ Select Subnets
+
+
+ A subnet has an outbound internet route if it has a route to an
+ internet gateway or a NAT gateway in a public subnet.
+
+
+
+
+
+ Select subnets to assign to the Fargate service that will be running the
+ Teleport Database Service. All of the subnets you select must have an
+ outbound internet route and a local route to the database subnets.
+
+ {attempt.status === 'failed' && (
+ <>
+
+
+ {attempt.statusText}
+
+
+ Retry
+
+ >
+ )}
+ {attempt.status === 'processing' && (
+
+
+
+ )}
+ {attempt.status === 'success' && (
+
+
+
+
+ fetchSubnets({ refresh: true })}
+ px={2}
+ disabled={disabled}
+ >
+ Refresh
+
+
+
+ {`${selectedSubnetIds.length} ${pluralize(selectedSubnetIds.length, 'subnet')} selected`}
+
+
+
+ )}
+ >
+ );
+}
diff --git a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/AutoDiscoverToggle.tsx b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/AutoDiscoverToggle.tsx
new file mode 100644
index 0000000000000..6e2224731d9d8
--- /dev/null
+++ b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/AutoDiscoverToggle.tsx
@@ -0,0 +1,51 @@
+/**
+ * Teleport
+ * Copyright (C) 2023 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 { Box, Toggle } from 'design';
+
+import { ToolTipInfo } from 'shared/components/ToolTip';
+
+export function AutoDiscoverToggle({
+ wantAutoDiscover,
+ toggleWantAutoDiscover,
+ disabled = false,
+}: {
+ wantAutoDiscover: boolean;
+ toggleWantAutoDiscover(): void;
+ disabled?: boolean;
+}) {
+ return (
+
+
+
+ Auto-enroll all databases for the selected region
+
+
+ Auto-enroll will automatically identify all RDS databases (e.g.
+ PostgreSQL, MySQL, Aurora) from the selected region and register them
+ as database resources in your infrastructure.
+
+
+
+ );
+}
diff --git a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/AutoEnrollment.tsx b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/AutoEnrollment.tsx
new file mode 100644
index 0000000000000..d0029ebc014fd
--- /dev/null
+++ b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/AutoEnrollment.tsx
@@ -0,0 +1,254 @@
+/**
+ * Teleport
+ * Copyright (C) 2023 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, useEffect } from 'react';
+import { Text, Flex, ButtonSecondary } from 'design';
+import { FetchStatus } from 'design/DataTable/types';
+import useAttempt, { Attempt } from 'shared/hooks/useAttemptNext';
+import { getErrMessage } from 'shared/utils/errorType';
+import Alert from 'design/Alert/Alert';
+
+import { DbMeta, useDiscover } from 'teleport/Discover/useDiscover';
+import {
+ AwsRdsDatabase,
+ Regions,
+ Vpc,
+ integrationService,
+} from 'teleport/services/integrations';
+import cfg from 'teleport/config';
+import {
+ DISCOVERY_GROUP_CLOUD,
+ createDiscoveryConfig,
+} from 'teleport/services/discovery';
+import useTeleport from 'teleport/useTeleport';
+import {
+ DiscoverEvent,
+ DiscoverEventStatus,
+} from 'teleport/services/userEvent';
+import { CreatedDiscoveryConfigDialog } from 'teleport/Discover/Shared/ConfigureDiscoveryService';
+
+import { ActionButtons } from '../../Shared';
+
+import { DatabaseList } from './RdsDatabaseList';
+
+type TableData = {
+ items: AwsRdsDatabase[];
+ fetchStatus: FetchStatus;
+ instancesStartKey?: string;
+ clustersStartKey?: string;
+ oneOfError?: string;
+};
+
+const emptyTableData = (): TableData => ({
+ items: [],
+ fetchStatus: 'disabled',
+ instancesStartKey: '',
+ clustersStartKey: '',
+ oneOfError: '',
+});
+
+export function AutoEnrollment({
+ region,
+ vpc,
+ disableBtns,
+ onFetchAttempt,
+ fetchAttempt,
+}: {
+ region: Regions;
+ vpc?: Vpc;
+ disableBtns: boolean;
+ fetchAttempt: Attempt;
+ onFetchAttempt(a: Attempt): void;
+ /**
+ * key is expected to be set to the ID of the VPC.
+ */
+ key: string;
+}) {
+ const ctx = useTeleport();
+ const clusterId = ctx.storeUser.getClusterId();
+
+ const { agentMeta, updateAgentMeta, emitErrorEvent, nextStep, emitEvent } =
+ useDiscover();
+ const {
+ attempt: createDiscoveryConfigAttempt,
+ setAttempt: setCreateDiscoveryConfigAttempt,
+ } = useAttempt('');
+
+ const [tableData, setTableData] = useState();
+
+ useEffect(() => {
+ if (vpc) {
+ // Start with empty table data for new vpc's.
+ fetchRdsDatabases(emptyTableData(), vpc);
+ }
+ }, [vpc]);
+
+ function fetchNextPage() {
+ fetchRdsDatabases({ ...tableData }, vpc);
+ }
+
+ async function fetchRdsDatabases(data: TableData, vpc: Vpc) {
+ const integrationName = agentMeta.awsIntegration.name;
+
+ setTableData({ ...data, fetchStatus: 'loading' });
+ onFetchAttempt({ status: 'processing' });
+
+ try {
+ const {
+ databases: fetchedDbs,
+ instancesNextToken,
+ clustersNextToken,
+ oneOfError,
+ } = await integrationService.fetchAllAwsRdsEnginesDatabases(
+ integrationName,
+ {
+ region: region,
+ instancesNextToken: data.instancesStartKey,
+ clustersNextToken: data.clustersStartKey,
+ vpcId: vpc.id,
+ }
+ );
+
+ // Abort if there were no rds dbs for the selected region.
+ if (fetchedDbs.length <= 0) {
+ onFetchAttempt({ status: 'success' });
+ setTableData({ ...data, fetchStatus: 'disabled' });
+ return;
+ }
+
+ onFetchAttempt({ status: 'success' });
+ setTableData({
+ instancesStartKey: instancesNextToken,
+ clustersStartKey: clustersNextToken,
+ fetchStatus: instancesNextToken || clustersNextToken ? '' : 'disabled',
+ oneOfError,
+ // concat each page fetch.
+ items: [...data.items, ...fetchedDbs],
+ });
+ } catch (err) {
+ const errMsg = getErrMessage(err);
+ onFetchAttempt({ status: 'failed', statusText: errMsg });
+ setTableData(data); // fallback to previous data
+ emitErrorEvent(`database fetch error: ${errMsg}`);
+ }
+ }
+
+ async function handleOnProceed() {
+ // For self-hosted, discovery config needs to be created
+ // on the next step since self-hosted needs to manually
+ // install a discovery service.
+ if (!cfg.isCloud) {
+ updateAgentMeta({
+ ...(agentMeta as DbMeta),
+ awsVpcId: vpc.id,
+ awsRegion: region,
+ autoDiscovery: {},
+ });
+ nextStep();
+ return;
+ }
+
+ try {
+ setCreateDiscoveryConfigAttempt({ status: 'processing' });
+ // Cloud has a discovery service automatically running so
+ // we have everything we need to create a
+ const discoveryConfig = await createDiscoveryConfig(clusterId, {
+ name: crypto.randomUUID(),
+ discoveryGroup: DISCOVERY_GROUP_CLOUD,
+ aws: [
+ {
+ types: ['rds'],
+ regions: [region],
+ tags: { 'vpc-id': [vpc.id] },
+ integration: agentMeta.awsIntegration.name,
+ },
+ ],
+ });
+
+ emitEvent(
+ { stepStatus: DiscoverEventStatus.Success },
+ {
+ eventName: DiscoverEvent.CreateDiscoveryConfig,
+ }
+ );
+
+ setCreateDiscoveryConfigAttempt({ status: 'success' });
+ updateAgentMeta({
+ ...(agentMeta as DbMeta),
+ autoDiscovery: {
+ config: discoveryConfig,
+ },
+ awsVpcId: vpc.id,
+ awsRegion: region,
+ });
+ } catch (err) {
+ const message = getErrMessage(err);
+ setCreateDiscoveryConfigAttempt({
+ status: 'failed',
+ statusText: `failed to create discovery config: ${message}`,
+ });
+ emitErrorEvent(`failed to create discovery config: ${message}`);
+ return;
+ }
+ }
+
+ const selectedVpc = !!vpc;
+ const showTable = selectedVpc && fetchAttempt.status !== 'failed';
+
+ return (
+ <>
+ {showTable && (
+ <>
+ {tableData?.oneOfError && (
+
+
+ {tableData.oneOfError}
+ fetchRdsDatabases(emptyTableData(), vpc)}
+ >
+ Retry
+
+
+
+ )}
+ List of databases that will be auto enrolled:
+
+ >
+ )}
+
+ {createDiscoveryConfigAttempt.status !== '' && (
+ setCreateDiscoveryConfigAttempt({ status: '' })}
+ retry={handleOnProceed}
+ region={region}
+ notifyAboutDelay={false} // TODO always notify?
+ />
+ )}
+ >
+ );
+}
diff --git a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabaseEnroll.story.tsx b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.story.tsx
similarity index 64%
rename from web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabaseEnroll.story.tsx
rename to web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.story.tsx
index 4841017a33a93..1974723bd48cc 100644
--- a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabaseEnroll.story.tsx
+++ b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.story.tsx
@@ -59,8 +59,8 @@ export default {
],
};
-export const InstanceList = () => ;
-InstanceList.parameters = {
+export const SelfHostedFlow = () => ;
+SelfHostedFlow.parameters = {
msw: {
handlers: [
rest.post(cfg.api.awsRdsDbListPath, (req, res, ctx) =>
@@ -73,26 +73,21 @@ InstanceList.parameters = {
rest.post(cfg.api.discoveryConfigPath, (req, res, ctx) =>
res(ctx.json({}))
),
- rest.get(cfg.api.databaseServicesPath, (req, res, ctx) =>
- res(
- ctx.json({ services: [{ name: 'test', matchers: { '*': ['*'] } }] })
- )
- ),
rest.get(cfg.api.databaseServicesPath, (req, res, ctx) =>
res(ctx.json({}))
),
- rest.post(cfg.api.awsRdsDbRequiredVpcsPath, (req, res, ctx) =>
- res(ctx.json({ vpcMapOfSubnets: {} }))
+ rest.post(cfg.api.awsDatabaseVpcsPath, (req, res, ctx) =>
+ res(ctx.json({ vpcs }))
),
],
},
};
-export const InstanceListForCloud = () => {
+export const CloudFlow = () => {
cfg.isCloud = true;
return ;
};
-InstanceListForCloud.parameters = {
+CloudFlow.parameters = {
msw: {
handlers: [
rest.post(cfg.api.awsRdsDbListPath, (req, res, ctx) =>
@@ -104,35 +99,108 @@ InstanceListForCloud.parameters = {
rest.post(cfg.api.discoveryConfigPath, (req, res, ctx) =>
res(ctx.json({}))
),
+
rest.get(cfg.api.databaseServicesPath, (req, res, ctx) =>
+ res(ctx.json({}))
+ ),
+ rest.post(cfg.api.awsDatabaseVpcsPath, (req, res, ctx) =>
+ res(ctx.json({ vpcs }))
+ ),
+ ],
+ },
+};
+
+export const NoVpcs = () => {
+ return ;
+};
+NoVpcs.parameters = {
+ msw: {
+ handlers: [
+ rest.post(cfg.api.awsRdsDbListPath, (req, res, ctx) =>
+ res(ctx.json({ databases: [] }))
+ ),
+ rest.post(cfg.api.awsDatabaseVpcsPath, (req, res, ctx) =>
+ res.once(ctx.json({ vpcs: [] }))
+ ),
+ rest.post(cfg.api.awsDatabaseVpcsPath, (req, res, ctx) =>
+ res(ctx.json({ vpcs }))
+ ),
+ ],
+ },
+};
+
+export const VpcError = () => {
+ return ;
+};
+VpcError.parameters = {
+ msw: {
+ handlers: [
+ rest.post(cfg.api.awsDatabaseVpcsPath, (req, res, ctx) =>
+ res(
+ ctx.status(404),
+ ctx.json({ message: 'Whoops, error fetching required vpcs.' })
+ )
+ ),
+ ],
+ },
+};
+
+export const SelectedVpcAlreadyExists = () => {
+ return ;
+};
+SelectedVpcAlreadyExists.parameters = {
+ msw: {
+ handlers: [
+ rest.post(cfg.api.awsRdsDbListPath, (req, res, ctx) =>
+ res(ctx.json({ databases: rdsInstances }))
+ ),
+ rest.post(cfg.api.awsDatabaseVpcsPath, (req, res, ctx) =>
res(
ctx.json({
- items: [
- { name: 'test', resource_matchers: [{ labels: { '*': ['*'] } }] },
+ vpcs: [
+ {
+ id: 'Click me, then toggle ON auto enroll',
+ ecsServiceDashboardURL: 'http://some-dashboard-url',
+ },
+ {
+ id: 'vpc-1234',
+ },
],
})
)
),
- rest.get(cfg.api.databaseServicesPath, (req, res, ctx) =>
- res(ctx.json({}))
+ rest.get(cfg.api.databasesPath, (req, res, ctx) =>
+ res(ctx.json({ items: [rdsInstances[2]] }))
),
- rest.post(cfg.api.awsRdsDbRequiredVpcsPath, (req, res, ctx) =>
- res(ctx.json({ vpcMapOfSubnets: { 'vpc-1': ['subnet1'] } }))
+ ],
+ },
+};
+
+export const LoadingVpcs = () => {
+ return ;
+};
+LoadingVpcs.parameters = {
+ msw: {
+ handlers: [
+ rest.post(cfg.api.awsDatabaseVpcsPath, (req, res, ctx) =>
+ res(ctx.delay('infinite'))
),
],
},
};
-export const InstanceListLoading = () => {
- cfg.isCloud = true;
+export const LoadingDatabases = () => {
return ;
};
-InstanceListLoading.parameters = {
+LoadingDatabases.parameters = {
msw: {
handlers: [
rest.post(cfg.api.awsRdsDbListPath, (req, res, ctx) =>
res(ctx.delay('infinite'))
),
+ rest.post(cfg.api.awsDatabaseVpcsPath, (req, res, ctx) =>
+ res(ctx.json({ vpcs }))
+ ),
],
},
};
@@ -142,21 +210,59 @@ WithAwsPermissionsError.parameters = {
msw: {
handlers: [
rest.post(cfg.api.awsRdsDbListPath, (req, res, ctx) =>
- res(
+ res(ctx.json({ databases: [] }))
+ ),
+ rest.post(cfg.api.awsDatabaseVpcsPath, (req, res, ctx) =>
+ res.once(
ctx.status(403),
ctx.json({ message: 'StatusCode: 403, RequestID: operation error' })
)
),
+ rest.post(cfg.api.awsDatabaseVpcsPath, (req, res, ctx) =>
+ res(ctx.json({ vpcs }))
+ ),
],
},
};
-export const WithOtherError = () => ;
-WithOtherError.parameters = {
+export const WithDbListError = () => ;
+WithDbListError.parameters = {
msw: {
handlers: [
+ rest.post(cfg.api.awsDatabaseVpcsPath, (req, res, ctx) =>
+ res(ctx.json({ vpcs }))
+ ),
rest.post(cfg.api.awsRdsDbListPath, (req, res, ctx) =>
- res(ctx.status(404))
+ res(
+ ctx.status(403),
+ ctx.json({ message: 'Whoops, fetching aws databases error' })
+ )
+ ),
+ ],
+ },
+};
+
+export const WithOneOfDbListError = () => ;
+WithOneOfDbListError.parameters = {
+ msw: {
+ handlers: [
+ rest.post(cfg.api.awsRdsDbListPath, (req, res, ctx) =>
+ res.once(ctx.json({ databases: rdsInstances }))
+ ),
+ rest.post(cfg.api.awsRdsDbListPath, (req, res, ctx) =>
+ res.once(
+ ctx.status(403),
+ ctx.json({ message: 'Whoops, fetching another aws databases error' })
+ )
+ ),
+ rest.post(cfg.api.awsRdsDbListPath, (req, res, ctx) =>
+ res(ctx.json({ databases: rdsInstances }))
+ ),
+ rest.get(cfg.api.databasesPath, (req, res, ctx) =>
+ res(ctx.json({ items: [rdsInstances[2]] }))
+ ),
+ rest.post(cfg.api.awsDatabaseVpcsPath, (req, res, ctx) =>
+ res(ctx.json({ vpcs }))
),
],
},
@@ -305,3 +411,26 @@ const rdsInstances = [
},
},
];
+
+const vpcs = [
+ {
+ name: '',
+ id: 'vpc-341c69a6-1bdb-5521-aad1',
+ },
+ {
+ name: '',
+ id: 'vpc-92b8d60f-0f0e-5d31-b5b4',
+ },
+ {
+ name: 'aws-controlsomething-VPC',
+ id: 'vpc-d36151d6-8f0e-588d-87a7',
+ },
+ {
+ name: 'eksctl-bob-test-1-cluster/VPC',
+ id: 'vpc-fe7203d3-e959-57d4-8f87',
+ },
+ {
+ name: 'Default VPC (DO NOT USE)',
+ id: 'vpc-57cbdb9c-0f3e-5efb-bd84',
+ },
+];
diff --git a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.test.tsx b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.test.tsx
index 68f39d10b398a..337bc83a69e36 100644
--- a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.test.tsx
+++ b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.test.tsx
@@ -28,10 +28,7 @@ import DatabaseService from 'teleport/services/databases/databases';
import * as discoveryService from 'teleport/services/discovery/discovery';
import { ComponentWrapper } from 'teleport/Discover/Fixtures/databases';
import cfg from 'teleport/config';
-import {
- DISCOVERY_GROUP_CLOUD,
- DEFAULT_DISCOVERY_GROUP_NON_CLOUD,
-} from 'teleport/services/discovery/discovery';
+import { DISCOVERY_GROUP_CLOUD } from 'teleport/services/discovery/discovery';
import { EnrollRdsDatabase } from './EnrollRdsDatabase';
@@ -60,6 +57,15 @@ describe('test EnrollRdsDatabase.tsx', () => {
jest
.spyOn(DatabaseService.prototype, 'fetchDatabaseServices')
.mockResolvedValue({ services: [] });
+ jest.spyOn(integrationService, 'fetchAwsDatabasesVpcs').mockResolvedValue({
+ nextToken: '',
+ vpcs: [
+ {
+ name: 'vpc-name',
+ id: 'vpc-id',
+ },
+ ],
+ });
});
afterEach(() => {
@@ -67,6 +73,24 @@ describe('test EnrollRdsDatabase.tsx', () => {
jest.restoreAllMocks();
});
+ async function selectRegionAndVpc() {
+ // select a region
+ let selectEl = screen.getByLabelText(/aws region/i);
+ fireEvent.focus(selectEl);
+ fireEvent.keyDown(selectEl, { key: 'ArrowDown' });
+ fireEvent.click(screen.getByText('us-east-2'));
+
+ await screen.findByLabelText(/vpc id/i);
+
+ // select a vpc
+ selectEl = screen.getByText(/select a vpc id/i);
+ fireEvent.focus(selectEl);
+ fireEvent.keyDown(selectEl, { key: 'ArrowDown' });
+ fireEvent.keyDown(selectEl, { key: 'Enter' });
+
+ await screen.findByText(/selected region/i);
+ }
+
test('without rds database result, does not attempt to fetch db servers', async () => {
jest
.spyOn(integrationService, 'fetchAwsRdsDatabases')
@@ -74,14 +98,7 @@ describe('test EnrollRdsDatabase.tsx', () => {
render();
- // select a region from selector.
- const selectEl = screen.getByLabelText(/aws region/i);
- fireEvent.focus(selectEl);
- fireEvent.keyDown(selectEl, { key: 'ArrowDown', keyCode: 40 });
- fireEvent.click(screen.getByText('us-east-2'));
-
- // No results are rendered.
- await screen.findByText(/no result/i);
+ await selectRegionAndVpc();
expect(integrationService.fetchAwsRdsDatabases).toHaveBeenCalledTimes(1);
expect(DatabaseService.prototype.fetchDatabases).not.toHaveBeenCalled();
@@ -94,11 +111,7 @@ describe('test EnrollRdsDatabase.tsx', () => {
render();
- // select a region from selector.
- const selectEl = screen.getByLabelText(/aws region/i);
- fireEvent.focus(selectEl);
- fireEvent.keyDown(selectEl, { key: 'ArrowDown', keyCode: 40 });
- fireEvent.click(screen.getByText('us-east-2'));
+ await selectRegionAndVpc();
// Rds results renders result.
await screen.findByText(/rds-1/i);
@@ -107,32 +120,28 @@ describe('test EnrollRdsDatabase.tsx', () => {
expect(DatabaseService.prototype.fetchDatabases).toHaveBeenCalledTimes(1);
});
- test('auto enroll (cloud) is on by default', async () => {
- jest.spyOn(integrationService, 'fetchAwsRdsDatabases').mockResolvedValue({
- databases: mockAwsDbs,
- });
+ test('auto enrolling with cloud should create discovery config', async () => {
jest
- .spyOn(integrationService, 'fetchAwsRdsRequiredVpcs')
- .mockResolvedValue({});
+ .spyOn(integrationService, 'fetchAwsRdsDatabases')
+ .mockResolvedValue({ databases: [] });
+ jest
+ .spyOn(integrationService, 'fetchAllAwsRdsEnginesDatabases')
+ .mockResolvedValue({
+ databases: mockAwsDbs,
+ });
render();
- // select a region from selector.
- const selectEl = screen.getByLabelText(/aws region/i);
- fireEvent.focus(selectEl);
- fireEvent.keyDown(selectEl, { key: 'ArrowDown', keyCode: 40 });
- fireEvent.click(screen.getByText('us-east-2'));
+ await selectRegionAndVpc();
+
+ // Toggle on auto-enroll
+ act(() => screen.getByText(/auto-enroll all/i).click());
// Rds results renders result.
await screen.findByText(/rds-1/i);
- // Cloud uses a default discovery group name.
- expect(
- screen.queryByText(/define a discovery group name/i)
- ).not.toBeInTheDocument();
act(() => screen.getByText('Next').click());
await screen.findByText(/Creating Auto Discovery Config/i);
- expect(integrationService.fetchAwsRdsRequiredVpcs).toHaveBeenCalledTimes(1);
expect(discoveryService.createDiscoveryConfig).toHaveBeenCalledTimes(1);
// 2D array:
@@ -146,95 +155,43 @@ describe('test EnrollRdsDatabase.tsx', () => {
expect(DatabaseService.prototype.createDatabase).not.toHaveBeenCalled();
});
- test('auto enroll disabled (cloud), creates database', async () => {
- jest.spyOn(integrationService, 'fetchAwsRdsDatabases').mockResolvedValue({
- databases: mockAwsDbs,
- });
+ test('auto enrolling with self-hosted should not create discovery config (its done on the next step)', async () => {
+ cfg.isCloud = false;
+
+ jest
+ .spyOn(integrationService, 'fetchAwsRdsDatabases')
+ .mockResolvedValue({ databases: [] });
+ jest
+ .spyOn(integrationService, 'fetchAllAwsRdsEnginesDatabases')
+ .mockResolvedValue({
+ databases: mockAwsDbs,
+ });
render();
- // select a region from selector.
- const selectEl = screen.getByLabelText(/aws region/i);
- fireEvent.focus(selectEl);
- fireEvent.keyDown(selectEl, { key: 'ArrowDown', keyCode: 40 });
- fireEvent.click(screen.getByText('us-east-2'));
+ await selectRegionAndVpc();
- await screen.findByText(/rds-1/i);
-
- // disable auto enroll
- expect(screen.getByText('Next')).toBeEnabled();
+ // Toggle on auto-enroll
act(() => screen.getByText(/auto-enroll all/i).click());
- expect(screen.getByText('Next')).toBeDisabled();
- act(() => screen.getByRole('radio').click());
+ // Rds results renders result.
+ await screen.findByText(/rds-1/i);
act(() => screen.getByText('Next').click());
- await screen.findByText(/Database "rds-1" successfully registered/i);
-
expect(discoveryService.createDiscoveryConfig).not.toHaveBeenCalled();
- expect(
- DatabaseService.prototype.fetchDatabaseServices
- ).toHaveBeenCalledTimes(1);
- expect(DatabaseService.prototype.createDatabase).toHaveBeenCalledTimes(1);
- });
-
- test('auto enroll (self-hosted) is on by default', async () => {
- cfg.isCloud = false;
- jest.spyOn(integrationService, 'fetchAwsRdsDatabases').mockResolvedValue({
- databases: mockAwsDbs,
- });
- jest
- .spyOn(integrationService, 'fetchAwsRdsRequiredVpcs')
- .mockResolvedValue({});
-
- render();
-
- // select a region from selector.
- const selectEl = screen.getByLabelText(/aws region/i);
- fireEvent.focus(selectEl);
- fireEvent.keyDown(selectEl, { key: 'ArrowDown', keyCode: 40 });
- fireEvent.click(screen.getByText('us-east-2'));
-
- // Only self-hosted need to define a discovery group name.
- await screen.findByText(/define a discovery group name/i);
- // There should be no talbe rendered.
- expect(screen.queryByText(/rds-1/i)).not.toBeInTheDocument();
-
- act(() => screen.getByText('Next').click());
- await screen.findByText(/Creating Auto Discovery Config/i);
- expect(integrationService.fetchAwsRdsRequiredVpcs).toHaveBeenCalledTimes(1);
- expect(discoveryService.createDiscoveryConfig).toHaveBeenCalledTimes(1);
-
- // 2D array:
- // First array is the array of calls, we are only interested in the first.
- // Second array are the parameters that this api got called with,
- // we are interested in the second parameter.
- expect(createDiscoveryConfig.mock.calls[0][1]['discoveryGroup']).toBe(
- DEFAULT_DISCOVERY_GROUP_NON_CLOUD
- );
-
expect(DatabaseService.prototype.createDatabase).not.toHaveBeenCalled();
});
- test('auto enroll disabled (self-hosted), creates database', async () => {
- cfg.isCloud = false;
+ test('auto enroll disabled, creates database', async () => {
jest.spyOn(integrationService, 'fetchAwsRdsDatabases').mockResolvedValue({
databases: mockAwsDbs,
});
render();
- // select a region from selector.
- const selectEl = screen.getByLabelText(/aws region/i);
- fireEvent.focus(selectEl);
- fireEvent.keyDown(selectEl, { key: 'ArrowDown', keyCode: 40 });
- fireEvent.click(screen.getByText('us-east-2'));
-
- await screen.findByText(/define a discovery group name/i);
+ await selectRegionAndVpc();
- // disable auto enroll
- act(() => screen.getByText(/auto-enroll all/i).click());
- expect(screen.getByText('Next')).toBeDisabled();
+ await screen.findByText(/rds-1/i);
act(() => screen.getByRole('radio').click());
diff --git a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.tsx b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.tsx
index a45ea63b1ca63..ab7a884d83bbc 100644
--- a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.tsx
+++ b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.tsx
@@ -17,457 +17,187 @@
*/
import React, { useState } from 'react';
-import { Box, Text, Toggle, Mark } from 'design';
-import { FetchStatus } from 'design/DataTable/types';
+import { Box, Indicator } from 'design';
import { Danger } from 'design/Alert';
-import useAttempt, { Attempt } from 'shared/hooks/useAttemptNext';
-import { ToolTipInfo } from 'shared/components/ToolTip';
+import useAttempt from 'shared/hooks/useAttemptNext';
import { getErrMessage } from 'shared/utils/errorType';
+import { P } from 'design/Text/Text';
-import { DbMeta, useDiscover } from 'teleport/Discover/useDiscover';
+import { useDiscover } from 'teleport/Discover/useDiscover';
import {
- AwsRdsDatabase,
- RdsEngineIdentifier,
Regions,
+ Vpc,
integrationService,
} from 'teleport/services/integrations';
-import { DatabaseEngine } from 'teleport/Discover/SelectResource';
import { AwsRegionSelector } from 'teleport/Discover/Shared/AwsRegionSelector';
-import { Database } from 'teleport/services/databases';
import { ConfigureIamPerms } from 'teleport/Discover/Shared/Aws/ConfigureIamPerms';
import { isIamPermError } from 'teleport/Discover/Shared/Aws/error';
-import cfg from 'teleport/config';
-import {
- DISCOVERY_GROUP_CLOUD,
- DEFAULT_DISCOVERY_GROUP_NON_CLOUD,
- DiscoveryConfig,
- createDiscoveryConfig,
-} from 'teleport/services/discovery';
-import useTeleport from 'teleport/useTeleport';
import { splitAwsIamArn } from 'teleport/services/integrations/aws';
+import useTeleport from 'teleport/useTeleport';
-import {
- AutoEnrollDialog,
- ActionButtons,
- Header,
- SelfHostedAutoDiscoverDirections,
-} from '../../Shared';
-
-import { useCreateDatabase } from '../CreateDatabase/useCreateDatabase';
-import { CreateDatabaseDialog } from '../CreateDatabase/CreateDatabaseDialog';
-
-import { DatabaseList } from './RdsDatabaseList';
-
-type TableData = {
- items: CheckedAwsRdsDatabase[];
- fetchStatus: FetchStatus;
- startKey?: string;
- currRegion?: Regions;
-};
-
-const emptyTableData: TableData = {
- items: [],
- fetchStatus: 'disabled',
- startKey: '',
-};
+import { Header } from '../../Shared';
-// CheckedAwsRdsDatabase is a type to describe that a
-// AwsRdsDatabase has been checked (by its resource id)
-// with the backend whether or not a database server already
-// exists for it.
-export type CheckedAwsRdsDatabase = AwsRdsDatabase & {
- dbServerExists?: boolean;
-};
+import { VpcOption, VpcSelector } from './VpcSelector';
+import { AutoDiscoverToggle } from './AutoDiscoverToggle';
+import { AutoEnrollment } from './AutoEnrollment';
+import { SingleEnrollment } from './SingleEnrollment';
export function EnrollRdsDatabase() {
- const {
- createdDb,
- pollTimeout,
- registerDatabase,
- attempt: registerAttempt,
- clearAttempt: clearRegisterAttempt,
- nextStep,
- fetchDatabaseServers,
- } = useCreateDatabase();
-
const ctx = useTeleport();
const clusterId = ctx.storeUser.getClusterId();
- const { agentMeta, resourceSpec, updateAgentMeta, emitErrorEvent } =
- useDiscover();
- const { attempt: fetchDbAttempt, setAttempt: setFetchDbAttempt } =
- useAttempt('');
- const { attempt: autoDiscoverAttempt, setAttempt: setAutoDiscoverAttempt } =
- useAttempt('');
+ const { agentMeta, emitErrorEvent } = useDiscover();
- const [tableData, setTableData] = useState({
- items: [],
- startKey: '',
- fetchStatus: 'disabled',
- });
- const [selectedDb, setSelectedDb] = useState();
- const [wantAutoDiscover, setWantAutoDiscover] = useState(true);
- const [autoDiscoveryCfg, setAutoDiscoveryCfg] = useState();
- const [requiredVpcs, setRequiredVpcs] = useState>();
- const [discoveryGroupName, setDiscoveryGroupName] = useState(() =>
- cfg.isCloud ? '' : DEFAULT_DISCOVERY_GROUP_NON_CLOUD
- );
+ // This attempt is used for both fetching vpc's and for
+ // fetching databases since each fetching is done at separate
+ // times and relies on one fetch result (vpcs) to be complete
+ // before performing the next fetch (databases, but only after user
+ // has selected a vpc).
+ const { attempt: fetchAttempt, setAttempt: setFetchAttempt } = useAttempt('');
- function fetchDatabasesWithNewRegion(region: Regions) {
- // Clear table when fetching with new region.
- fetchDatabases({ ...emptyTableData, currRegion: region });
- }
+ const [vpcs, setVpcs] = useState();
+ const [selectedVpc, setSelectedVpc] = useState();
+ const [wantAutoDiscover, setWantAutoDiscover] = useState(false);
+ const [selectedRegion, setSelectedRegion] = useState();
- function fetchNextPage() {
- fetchDatabases({ ...tableData });
+ function onNewVpc(selectedVpc: VpcOption) {
+ setSelectedVpc(selectedVpc);
}
- function refreshDatabaseList() {
- setSelectedDb(null);
- // When refreshing, start the table back at page 1.
- fetchDatabases({ ...tableData, startKey: '', items: [] });
+ function onNewRegion(region: Regions) {
+ setSelectedVpc(null);
+ setSelectedRegion(region);
+ fetchVpcs(region);
}
- async function fetchDatabases(data: TableData) {
- const integrationName = agentMeta.awsIntegration.name;
-
- setTableData({ ...data, fetchStatus: 'loading' });
- setFetchDbAttempt({ status: 'processing' });
-
+ async function fetchVpcs(region: Regions) {
+ setFetchAttempt({ status: 'processing' });
try {
- const { databases: fetchedRdsDbs, nextToken } =
- await integrationService.fetchAwsRdsDatabases(
- integrationName,
- getRdsEngineIdentifier(resourceSpec.dbMeta?.engine),
- {
- region: data.currRegion,
- nextToken: data.startKey,
- }
- );
-
- // Abort if there were no rds dbs for the selected region.
- if (fetchedRdsDbs.length <= 0) {
- setFetchDbAttempt({ status: 'success' });
- setTableData({ ...data, fetchStatus: 'disabled' });
- return;
- }
-
- // Check if fetched rds databases have a database
- // server for it, to prevent user from enrolling
- // the same db and getting an error from it.
-
- // Build the predicate string that will query for
- // all the fetched rds dbs by its resource ids.
- const resourceIds: string[] = fetchedRdsDbs.map(
- d => `resource.spec.aws.rds.resource_id == "${d.resourceId}"`
- );
- const query = resourceIds.join(' || ');
- const { agents: fetchedDbServers } = await fetchDatabaseServers(
- query,
- fetchedRdsDbs.length // limit
- );
-
- const dbServerLookupByResourceId: Record = {};
- fetchedDbServers.forEach(
- d => (dbServerLookupByResourceId[d.aws.rds.resourceId] = d)
- );
-
- // Check for db server matches.
- const checkedRdsDbs: CheckedAwsRdsDatabase[] = fetchedRdsDbs.map(rds => {
- const dbServer = dbServerLookupByResourceId[rds.resourceId];
- if (dbServer) {
- return {
- ...rds,
- dbServerExists: true,
- };
- }
- return rds;
- });
-
- setFetchDbAttempt({ status: 'success' });
- setTableData({
- currRegion: data.currRegion,
- startKey: nextToken,
- fetchStatus: nextToken ? '' : 'disabled',
- // concat each page fetch.
- items: [...data.items, ...checkedRdsDbs],
- });
+ const { spec, name: integrationName } = agentMeta.awsIntegration;
+ const { awsAccountId } = splitAwsIamArn(spec.roleArn);
+
+ // Get a list of every vpcs.
+ let fetchedVpcs: Vpc[] = [];
+ let nextPage = '';
+ do {
+ const { vpcs, nextToken } =
+ await integrationService.fetchAwsDatabasesVpcs(
+ integrationName,
+ clusterId,
+ {
+ region: region,
+ accountId: awsAccountId,
+ nextToken: nextPage,
+ }
+ );
+
+ fetchedVpcs = [...fetchedVpcs, ...vpcs];
+ nextPage = nextToken;
+ } while (nextPage);
+
+ setVpcs(fetchedVpcs);
+ setFetchAttempt({ status: '' });
} catch (err) {
- const errMsg = getErrMessage(err);
- setFetchDbAttempt({ status: 'failed', statusText: errMsg });
- setTableData(data); // fallback to previous data
- emitErrorEvent(`database fetch error: ${errMsg}`);
- }
- }
-
- function handleAndEmitRequestError(
- err: Error,
- cfg: { errorPrefix?: string; setAttempt?(attempt: Attempt): void }
- ) {
- const message = getErrMessage(err);
- if (cfg.setAttempt) {
- cfg.setAttempt({
+ const message = getErrMessage(err);
+ setFetchAttempt({
status: 'failed',
- statusText: `${cfg.errorPrefix}${message}`,
+ statusText: message,
});
+ emitErrorEvent(`failed to fetch vpcs: ${message}`);
}
- emitErrorEvent(`${cfg.errorPrefix}${message}`);
}
- async function enableAutoDiscovery() {
- setAutoDiscoverAttempt({ status: 'processing' });
-
- let requiredVpcsAndSubnets = requiredVpcs;
- if (!requiredVpcsAndSubnets) {
- try {
- const { spec, name: integrationName } = agentMeta.awsIntegration;
- const { awsAccountId } = splitAwsIamArn(spec.roleArn);
- requiredVpcsAndSubnets =
- await integrationService.fetchAwsRdsRequiredVpcs(integrationName, {
- region: tableData.currRegion,
- accountId: awsAccountId,
- });
-
- setRequiredVpcs(requiredVpcsAndSubnets);
- } catch (err) {
- handleAndEmitRequestError(err, {
- errorPrefix: 'failed to collect vpc ids and its subnets: ',
- setAttempt: setAutoDiscoverAttempt,
- });
- return;
- }
- }
-
- // Only create a discovery config after successfully fetching
- // required vpcs. This is to avoid creating a unused auto discovery
- // config if user quits in the middle of things not working.
- let discoveryConfig = autoDiscoveryCfg;
- if (!discoveryConfig) {
- try {
- discoveryConfig = await createDiscoveryConfig(clusterId, {
- name: crypto.randomUUID(),
- discoveryGroup: cfg.isCloud
- ? DISCOVERY_GROUP_CLOUD
- : discoveryGroupName,
- aws: [
- {
- types: ['rds'],
- regions: [tableData.currRegion],
- tags: { '*': ['*'] },
- integration: agentMeta.awsIntegration.name,
- },
- ],
- });
- setAutoDiscoveryCfg(discoveryConfig);
- } catch (err) {
- handleAndEmitRequestError(err, {
- errorPrefix: 'failed to create discovery config: ',
- setAttempt: setAutoDiscoverAttempt,
- });
- return;
- }
- }
-
- setAutoDiscoverAttempt({ status: 'success' });
- updateAgentMeta({
- ...(agentMeta as DbMeta),
- autoDiscovery: {
- config: discoveryConfig,
- requiredVpcsAndSubnets,
- },
- serviceDeployedMethod:
- Object.keys(requiredVpcsAndSubnets).length > 0 ? undefined : 'skipped',
- awsRegion: tableData.currRegion,
- });
+ function refreshVpcsAndDatabases() {
+ clear();
+ fetchVpcs(selectedRegion);
}
+ /**
+ * Used when user changes a region.
+ */
function clear() {
- clearRegisterAttempt();
-
- if (fetchDbAttempt.status === 'failed') {
- setFetchDbAttempt({ status: '' });
- }
- if (tableData.items.length > 0) {
- setTableData(emptyTableData);
- }
- if (selectedDb) {
- setSelectedDb(null);
+ setFetchAttempt({ status: '' });
+ if (selectedVpc) {
+ setSelectedVpc(null);
}
}
- function handleOnProceed() {
- if (wantAutoDiscover) {
- enableAutoDiscovery();
- } else {
- const isNewDb = selectedDb.name !== createdDb?.name;
- registerDatabase(
- {
- name: selectedDb.name,
- protocol: selectedDb.engine,
- uri: selectedDb.uri,
- labels: selectedDb.labels,
- awsRds: selectedDb,
- awsRegion: tableData.currRegion,
- },
- // 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
- );
- }
- }
-
- let DialogComponent;
- if (registerAttempt.status !== '') {
- DialogComponent = (
-
- );
- } else if (autoDiscoverAttempt.status !== '') {
- DialogComponent = (
- setAutoDiscoverAttempt({ status: '' })}
- retry={handleOnProceed}
- region={tableData.currRegion}
- notifyAboutDelay={
- requiredVpcs && Object.keys(requiredVpcs).length === 0
- }
- />
- );
- }
-
- const hasIamPermError = isIamPermError(fetchDbAttempt);
- const showContent = !hasIamPermError && tableData.currRegion;
- const showAutoEnrollToggle = fetchDbAttempt.status === 'success';
-
- // (Temp)
- // Self hosted auto enroll is different from cloud.
- // For cloud, we already run the discovery service for customer.
- // For on-prem, user has to run their own discovery service.
- // We hide the RDS table for on-prem if they are wanting auto discover
- // because it takes up so much space to give them instructions.
- // Future work will simply provide user a script so we can show the table then.
- const showTable = cfg.isCloud || !wantAutoDiscover;
+ const hasIamPermError = isIamPermError(fetchAttempt);
+ const showVpcSelector = !hasIamPermError && !!vpcs;
+ const showAutoEnrollToggle =
+ fetchAttempt.status !== 'failed' && !!selectedVpc;
+ const hasVpcs = vpcs?.length > 0;
+
+ const mainContentProps = {
+ vpc: selectedVpc?.value,
+ region: selectedRegion,
+ fetchAttempt,
+ onFetchAttempt: setFetchAttempt,
+ disableBtns:
+ fetchAttempt.status === 'processing' ||
+ hasIamPermError ||
+ fetchAttempt.status === 'failed',
+ };
return (
- Enroll a RDS Database
- {fetchDbAttempt.status === 'failed' && !hasIamPermError && (
- {fetchDbAttempt.statusText}
+ Enroll RDS Database
+ {fetchAttempt.status === 'failed' && !hasIamPermError && (
+ {fetchAttempt.statusText}
)}
-
- Select the AWS Region you would like to see databases for:
-
+
+ Select a AWS Region and a VPC ID you would like to see databases for:
+
- {showContent && (
- <>
- {showAutoEnrollToggle && (
- setWantAutoDiscover(b => !b)}
- discoveryGroupName={discoveryGroupName}
- setDiscoveryGroupName={setDiscoveryGroupName}
- clusterPublicUrl={ctx.storeUser.state.cluster.publicURL}
- />
- )}
- {showTable && (
-
- )}
- >
+ {!vpcs && fetchAttempt.status === 'processing' && (
+
+
+
+ )}
+ {showVpcSelector && hasVpcs && (
+
+ )}
+ {showVpcSelector && !hasVpcs && (
+ // TODO(lisa): negative margin was required since the
+ // AwsRegionSelector added too much bottom margin.
+ // Refactor AwsRegionSelector so margins can be controlled
+ // outside of the component (or use flex columns with gap prop)
+
+ There are no VPCs defined in the selected region. Try another region.
+
)}
{hasIamPermError && (
)}
- {showContent && showAutoEnrollToggle && wantAutoDiscover && (
-
- Note: Auto-enroll will enroll all database engines
- in this region (e.g. PostgreSQL, MySQL, Aurora).
-
+ {showAutoEnrollToggle && (
+ setWantAutoDiscover(b => !b)}
+ disabled={fetchAttempt.status === 'processing'}
+ />
)}
-
- {DialogComponent}
-
- );
-}
-
-function getRdsEngineIdentifier(engine: DatabaseEngine): RdsEngineIdentifier {
- switch (engine) {
- case DatabaseEngine.MySql:
- return 'mysql';
- case DatabaseEngine.Postgres:
- return 'postgres';
- case DatabaseEngine.AuroraMysql:
- return 'aurora-mysql';
- case DatabaseEngine.AuroraPostgres:
- return 'aurora-postgres';
- }
-}
-
-function ToggleSection({
- wantAutoDiscover,
- toggleWantAutoDiscover,
- discoveryGroupName,
- setDiscoveryGroupName,
- clusterPublicUrl,
-}: {
- wantAutoDiscover: boolean;
- toggleWantAutoDiscover(): void;
- discoveryGroupName: string;
- setDiscoveryGroupName(n: string): void;
- clusterPublicUrl: string;
-}) {
- return (
-
-
-
- Auto-enroll all databases for selected region
-
-
- Auto-enroll will automatically identify all RDS databases from the
- selected region and register them as database resources in your
- infrastructure.
-
-
- {!cfg.isCloud && wantAutoDiscover && (
-
+ ) : (
+
)}
diff --git a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/RdsDatabaseList.tsx b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/RdsDatabaseList.tsx
index c341eb46fc055..c5ca31114afea 100644
--- a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/RdsDatabaseList.tsx
+++ b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/RdsDatabaseList.tsx
@@ -29,13 +29,13 @@ import {
labelMatcher,
} from 'teleport/Discover/Shared';
-import { CheckedAwsRdsDatabase } from './EnrollRdsDatabase';
+import { CheckedAwsRdsDatabase } from './SingleEnrollment';
type Props = {
items: CheckedAwsRdsDatabase[];
fetchStatus: FetchStatus;
fetchNextPage(): void;
- onSelectDatabase(item: CheckedAwsRdsDatabase): void;
+ onSelectDatabase?(item: CheckedAwsRdsDatabase): void;
selectedDatabase?: CheckedAwsRdsDatabase;
wantAutoDiscover: boolean;
};
@@ -52,25 +52,30 @@ export const DatabaseList = ({
{
- const isChecked =
- item.name === selectedDatabase?.name &&
- item.engine === selectedDatabase?.engine;
- return (
-
- item={item}
- key={`${item.name}${item.resourceId}`}
- isChecked={isChecked}
- onChange={onSelectDatabase}
- value={item.name}
- {...disabledStates(item, wantAutoDiscover)}
- />
- );
- },
- },
+ // Hide the selector when choosing to auto enroll
+ ...(!wantAutoDiscover
+ ? [
+ {
+ altKey: 'radio-select',
+ headerText: 'Select',
+ render: item => {
+ const isChecked =
+ item.name === selectedDatabase?.name &&
+ item.engine === selectedDatabase?.engine;
+ return (
+
+ item={item}
+ key={`${item.name}${item.resourceId}`}
+ isChecked={isChecked}
+ onChange={onSelectDatabase}
+ value={item.name}
+ {...disabledStates(item, wantAutoDiscover)}
+ />
+ );
+ },
+ },
+ ]
+ : []),
{
key: 'name',
headerText: 'Name',
@@ -135,13 +140,10 @@ function disabledStates(
const disabled =
item.status === 'failed' ||
item.status === 'deleting' ||
- wantAutoDiscover ||
- item.dbServerExists;
+ (!wantAutoDiscover && item.dbServerExists);
let disabledText = `This RDS database is already enrolled and is a part of this cluster`;
- if (wantAutoDiscover) {
- disabledText = 'All RDS databases will be enrolled automatically';
- } else if (item.status === 'failed') {
+ if (item.status === 'failed') {
disabledText = 'Not available, try refreshing the list';
} else if (item.status === 'deleting') {
disabledText = 'Not available';
diff --git a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/SingleEnrollment.tsx b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/SingleEnrollment.tsx
new file mode 100644
index 0000000000000..43dede55ed56d
--- /dev/null
+++ b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/SingleEnrollment.tsx
@@ -0,0 +1,233 @@
+/**
+ * Teleport
+ * Copyright (C) 2023 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, useEffect } from 'react';
+import { Text } from 'design';
+import { FetchStatus } from 'design/DataTable/types';
+import { Attempt } from 'shared/hooks/useAttemptNext';
+import { getErrMessage } from 'shared/utils/errorType';
+
+import { useDiscover } from 'teleport/Discover/useDiscover';
+import {
+ AwsRdsDatabase,
+ Regions,
+ Vpc,
+ integrationService,
+} from 'teleport/services/integrations';
+import { Database } from 'teleport/services/databases';
+import { getRdsEngineIdentifier } from 'teleport/Discover/SelectResource/types';
+
+import { ActionButtons } from '../../Shared';
+
+import { useCreateDatabase } from '../CreateDatabase/useCreateDatabase';
+import { CreateDatabaseDialog } from '../CreateDatabase/CreateDatabaseDialog';
+
+import { DatabaseList } from './RdsDatabaseList';
+
+type TableData = {
+ items: CheckedAwsRdsDatabase[];
+ fetchStatus: FetchStatus;
+ startKey?: string;
+};
+
+const emptyTableData = (): TableData => ({
+ items: [],
+ fetchStatus: 'disabled',
+ startKey: '',
+});
+
+// CheckedAwsRdsDatabase is a type to describe that a
+// AwsRdsDatabase has been checked (by its resource id)
+// with the backend whether or not a database server already
+// exists for it.
+export type CheckedAwsRdsDatabase = AwsRdsDatabase & {
+ dbServerExists?: boolean;
+};
+
+export function SingleEnrollment({
+ region,
+ vpc,
+ disableBtns,
+ onFetchAttempt,
+ fetchAttempt,
+}: {
+ region: Regions;
+ vpc?: Vpc;
+ disableBtns: boolean;
+ fetchAttempt: Attempt;
+ onFetchAttempt(a: Attempt): void;
+ /**
+ * key is expected to be set to the ID of the VPC.
+ */
+ key: string;
+}) {
+ const {
+ createdDb,
+ pollTimeout,
+ registerDatabase,
+ attempt,
+ clearAttempt,
+ nextStep,
+ fetchDatabaseServers,
+ handleOnTimeout,
+ } = useCreateDatabase();
+
+ const { agentMeta, resourceSpec, emitErrorEvent } = useDiscover();
+
+ const [tableData, setTableData] = useState();
+ const [selectedDb, setSelectedDb] = useState();
+
+ useEffect(() => {
+ if (vpc) {
+ // Start with empty table data for new vpc's.
+ fetchRdsDatabases(emptyTableData(), vpc);
+ }
+ }, [vpc]);
+
+ function fetchNextPage() {
+ fetchRdsDatabases({ ...tableData }, vpc);
+ }
+
+ async function fetchRdsDatabases(data: TableData, vpc: Vpc) {
+ const integrationName = agentMeta.awsIntegration.name;
+
+ setTableData({ ...data, fetchStatus: 'loading' });
+ onFetchAttempt({ status: 'processing' });
+
+ try {
+ const { databases: fetchedDbs, nextToken } =
+ await integrationService.fetchAwsRdsDatabases(
+ integrationName,
+ getRdsEngineIdentifier(resourceSpec.dbMeta?.engine),
+ {
+ region: region,
+ nextToken: data.startKey,
+ vpcId: vpc.id,
+ }
+ );
+
+ // Abort early if there were no rds dbs for the selected region.
+ if (fetchedDbs.length <= 0) {
+ onFetchAttempt({ status: 'success' });
+ setTableData({ ...data, fetchStatus: 'disabled' });
+ return;
+ }
+
+ // Check if fetched rds databases have a database
+ // server for it, to prevent user from enrolling
+ // the same db and getting an error from it.
+
+ // Build the predicate string that will query for
+ // all the fetched rds dbs by its resource ids.
+ const resourceIds: string[] = fetchedDbs.map(
+ d => `resource.spec.aws.rds.resource_id == "${d.resourceId}"`
+ );
+ const query = resourceIds.join(' || ');
+
+ const { agents: fetchedDbServers } = await fetchDatabaseServers(query);
+
+ const dbServerLookupByResourceId: Record = {};
+ fetchedDbServers.forEach(
+ d => (dbServerLookupByResourceId[d.aws.rds.resourceId] = d)
+ );
+
+ // Check for db server matches.
+ const checkedRdsDbs: CheckedAwsRdsDatabase[] = fetchedDbs.map(rds => {
+ const dbServer = dbServerLookupByResourceId[rds.resourceId];
+ if (dbServer) {
+ return {
+ ...rds,
+ dbServerExists: true,
+ };
+ }
+ return rds;
+ });
+
+ onFetchAttempt({ status: 'success' });
+ setTableData({
+ startKey: nextToken,
+ fetchStatus: nextToken ? '' : 'disabled',
+ // concat each page fetch.
+ items: [
+ ...data.items,
+ ...checkedRdsDbs.sort((a, b) => a.name.localeCompare(b.name)),
+ ],
+ });
+ } catch (err) {
+ const errMsg = getErrMessage(err);
+ onFetchAttempt({ status: 'failed', statusText: errMsg });
+ setTableData(data); // fallback to previous data
+ emitErrorEvent(`database fetch error: ${errMsg}`);
+ }
+ }
+
+ 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(
+ {
+ name: selectedDb.name,
+ protocol: selectedDb.engine,
+ uri: selectedDb.uri,
+ labels: selectedDb.labels,
+ awsRds: selectedDb,
+ awsRegion: region,
+ awsVpcId: vpc.id,
+ },
+ { newDb: isNewDb, overwriteDb }
+ );
+ }
+
+ const showTable = !!vpc && fetchAttempt.status !== 'failed';
+
+ return (
+ <>
+ {showTable && (
+ <>
+ Select an RDS to enroll:
+
+ >
+ )}
+
+ {attempt.status !== '' && (
+ handleOnProceed({ overwriteDb: true })}
+ dbName={selectedDb.name}
+ />
+ )}
+ >
+ );
+}
diff --git a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/VpcSelector.tsx b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/VpcSelector.tsx
new file mode 100644
index 0000000000000..792624858e17b
--- /dev/null
+++ b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/VpcSelector.tsx
@@ -0,0 +1,87 @@
+/**
+ * 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 { components } from 'react-select';
+import { Box, Flex, LabelInput, Link, ButtonIcon } from 'design';
+import Select from 'shared/components/Select';
+import { NewTab } from 'design/Icon';
+
+import { Regions, Vpc } from 'teleport/services/integrations';
+
+export type VpcOption = { value: Vpc; label: string; link: string };
+
+export function VpcSelector({
+ vpcs,
+ selectedVpc,
+ onSelectedVpc,
+ selectedRegion,
+}: {
+ vpcs: Vpc[];
+ selectedVpc: VpcOption;
+ onSelectedVpc(o: VpcOption): void;
+ selectedRegion: Regions;
+}) {
+ const options: VpcOption[] = vpcs?.map(vpc => {
+ return {
+ value: vpc,
+ label: `${vpc.id} ${vpc.name && `(${vpc.name})`}`,
+ link: `https://${selectedRegion}.console.aws.amazon.com/vpcconsole/home?region=${selectedRegion}#VpcDetails:VpcId=${vpc.id}`,
+ };
+ });
+
+ return (
+ // TODO(lisa): negative margin was required since the
+ // AwsRegionSelector added too much bottom margin.
+ // Refactor AwsRegionSelector so margins can be controlled
+ // outside of the component (or use flex columns with gap prop)
+
+
+ VPC ID
+
+
+
+
+
+ );
+}
+
+const Option = props => {
+ const { value, link } = props.data;
+ const { id, name } = value;
+ return (
+
+
+
+ {id} {name && {name}}
+
+
+
+
+
+
+ );
+};
diff --git a/web/packages/teleport/src/Discover/Database/index.tsx b/web/packages/teleport/src/Discover/Database/index.tsx
index 06d1b2676b385..a3fa5a1194a25 100644
--- a/web/packages/teleport/src/Discover/Database/index.tsx
+++ b/web/packages/teleport/src/Discover/Database/index.tsx
@@ -25,6 +25,7 @@ import {
ResourceSpec,
DatabaseLocation,
} from 'teleport/Discover/SelectResource';
+import cfg from 'teleport/config';
import { CreateDatabase } from 'teleport/Discover/Database/CreateDatabase';
import { SetupAccess } from 'teleport/Discover/Database/SetupAccess';
@@ -36,6 +37,8 @@ import { DiscoverEvent } from 'teleport/services/userEvent';
import { EnrollRdsDatabase } from 'teleport/Discover/Database/EnrollRdsDatabase';
import { IamPolicy } from 'teleport/Discover/Database/IamPolicy';
+import { ConfigureDiscoveryService } from '../Shared/ConfigureDiscoveryService';
+
export const DatabaseResource: ResourceViewConfig = {
kind: ResourceKind.Database,
wrapper(component: React.ReactNode) {
@@ -68,6 +71,20 @@ export const DatabaseResource: ResourceViewConfig = {
component: EnrollRdsDatabase,
eventName: DiscoverEvent.DatabaseRDSEnrollEvent,
},
+ // Self hosted requires user to manually install a discovery service
+ // for auto discovery.
+ // Cloud already has a discovery service running, so this step is not required.
+ ...(!cfg.isCloud
+ ? [
+ {
+ title: 'Configure Discovery Service',
+ component: () => (
+
+ ),
+ eventName: DiscoverEvent.CreateDiscoveryConfig,
+ },
+ ]
+ : []),
// There are two types of deploy service methods:
// - manual: user deploys it whereever they want OR
// - auto (default): we deploy for them using aws
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 ac0a80847f580..0577ffa9f678b 100644
--- a/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEksCluster.tsx
+++ b/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEksCluster.tsx
@@ -50,13 +50,16 @@ import { Kube } from 'teleport/services/kube';
import { JoinToken } from 'teleport/services/joinToken';
import cfg from 'teleport/config';
-
import {
- ActionButtons,
- Header,
- SelfHostedAutoDiscoverDirections,
- AutoEnrollDialog,
-} from '../../Shared';
+ ConfigureDiscoveryServiceDirections,
+ CreatedDiscoveryConfigDialog,
+} from 'teleport/Discover/Shared/ConfigureDiscoveryService';
+import {
+ DiscoverEvent,
+ DiscoverEventStatus,
+} from 'teleport/services/userEvent';
+
+import { ActionButtons, Header } from '../../Shared';
import { ClustersList } from './EksClustersList';
import ManualHelmDialog from './ManualHelmDialog';
@@ -89,7 +92,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('');
@@ -106,7 +110,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);
@@ -238,6 +242,12 @@ export function EnrollEksCluster(props: AgentStepProps) {
}
);
setAutoDiscoveryCfg(discoveryConfig);
+ emitEvent(
+ { stepStatus: DiscoverEventStatus.Success },
+ {
+ eventName: DiscoverEvent.CreateDiscoveryConfig,
+ }
+ );
} catch (err) {
const message = getErrMessage(err);
setAutoDiscoverAttempt({
@@ -439,7 +449,7 @@ export function EnrollEksCluster(props: AgentStepProps) {
/>
)}
{!cfg.isCloud && isAutoDiscoveryEnabled && (
-
)}
{autoDiscoverAttempt.status !== '' && (
- setAutoDiscoverAttempt({ status: '' })}
diff --git a/web/packages/teleport/src/Discover/SelectResource/types.ts b/web/packages/teleport/src/Discover/SelectResource/types.ts
index 0de8856a15501..165cc6142fe40 100644
--- a/web/packages/teleport/src/Discover/SelectResource/types.ts
+++ b/web/packages/teleport/src/Discover/SelectResource/types.ts
@@ -21,6 +21,7 @@ import { Platform } from 'design/platform';
import { Resource } from 'gen-proto-ts/teleport/userpreferences/v1/onboard_pb';
import { AuthType } from 'teleport/services/user';
+import { RdsEngineIdentifier } from 'teleport/services/integrations';
import { ResourceKind } from '../Shared/ResourceKind';
@@ -145,3 +146,18 @@ export type PrioritizedResources = {
preferredResources: Resource[];
hasPreferredResources: boolean;
};
+
+export function getRdsEngineIdentifier(
+ engine: DatabaseEngine
+): RdsEngineIdentifier {
+ switch (engine) {
+ case DatabaseEngine.MySql:
+ return 'mysql';
+ case DatabaseEngine.Postgres:
+ return 'postgres';
+ case DatabaseEngine.AuroraMysql:
+ return 'aurora-mysql';
+ case DatabaseEngine.AuroraPostgres:
+ return 'aurora-postgres';
+ }
+}
diff --git a/web/packages/teleport/src/Discover/Server/ConfigureDiscoveryService/ConfigureDiscoveryService.tsx b/web/packages/teleport/src/Discover/Server/ConfigureDiscoveryService/ConfigureDiscoveryService.tsx
deleted file mode 100644
index 1511701659bc2..0000000000000
--- a/web/packages/teleport/src/Discover/Server/ConfigureDiscoveryService/ConfigureDiscoveryService.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-/**
- * 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/DiscoveryConfigSsm.tsx b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx
index cd0e53ac3814f..c15e0b462370a 100644
--- a/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx
+++ b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx
@@ -48,6 +48,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';
@@ -58,8 +62,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
@@ -111,6 +121,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 90e56163c3b72..3e982d9d63c27 100644
--- a/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx
+++ b/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx
@@ -55,12 +55,9 @@ import {
isIamPermError,
} from 'teleport/Discover/Shared/Aws/error';
import { ConfigureIamPerms } from 'teleport/Discover/Shared/Aws/ConfigureIamPerms';
+import { ConfigureDiscoveryServiceDirections } from 'teleport/Discover/Shared/ConfigureDiscoveryService';
-import {
- ActionButtons,
- Header,
- SelfHostedAutoDiscoverDirections,
-} from '../../Shared';
+import { ActionButtons, Header } from '../../Shared';
import { CreateEc2IceDialog } from '../CreateEc2Ice/CreateEc2IceDialog';
@@ -103,7 +100,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
);
@@ -523,7 +520,7 @@ export function EnrollEc2Instance() {
)}
{!cfg.isCloud && wantAutoDiscover && (
- = {
kind: ResourceKind.Server,
diff --git a/web/packages/teleport/src/Discover/Shared/AwsAccount/AwsAccount.tsx b/web/packages/teleport/src/Discover/Shared/AwsAccount/AwsAccount.tsx
index a22debf052ef1..fa4e2f5eb82c6 100644
--- a/web/packages/teleport/src/Discover/Shared/AwsAccount/AwsAccount.tsx
+++ b/web/packages/teleport/src/Discover/Shared/AwsAccount/AwsAccount.tsx
@@ -272,7 +272,7 @@ export function AwsAccount() {
options={awsIntegrations.map(makeAwsIntegrationOption)}
/>
-
+
Or click here to set up a different AWS account
>
diff --git a/web/packages/teleport/src/Discover/Shared/AwsRegionSelector/AwsRegionSelector.tsx b/web/packages/teleport/src/Discover/Shared/AwsRegionSelector/AwsRegionSelector.tsx
index bd4b2394fe1c9..abcc8b2ca1cee 100644
--- a/web/packages/teleport/src/Discover/Shared/AwsRegionSelector/AwsRegionSelector.tsx
+++ b/web/packages/teleport/src/Discover/Shared/AwsRegionSelector/AwsRegionSelector.tsx
@@ -62,7 +62,7 @@ export function AwsRegionSelector({
{
- return ;
+export const Server = () => {
+ return ;
};
-const Component = () => {
+export const Database = () => {
+ return ;
+};
+
+export const WithCreateConfig = () => {
+ return ;
+};
+WithCreateConfig.parameters = {
+ msw: {
+ handlers: [
+ rest.post(cfg.api.discoveryConfigPath, (req, res, ctx) =>
+ res(ctx.json({}))
+ ),
+ ],
+ },
+};
+
+export const WithCreateConfigFailed = () => {
+ return ;
+};
+WithCreateConfigFailed.parameters = {
+ msw: {
+ handlers: [
+ rest.post(cfg.api.discoveryConfigPath, (req, res, ctx) =>
+ res.once(
+ ctx.status(403),
+ ctx.json({ message: 'Whoops, creating config error' })
+ )
+ ),
+ rest.post(cfg.api.discoveryConfigPath, (req, res, ctx) =>
+ res(ctx.json({}))
+ ),
+ ],
+ },
+};
+
+const Component = ({
+ kind,
+ withCreateConfig = false,
+}: {
+ kind: ResourceKind;
+ withCreateConfig?: boolean;
+}) => {
const ctx = createTeleportContext();
const discoverCtx: DiscoverContextState = {
agentMeta: {
@@ -79,7 +123,7 @@ const Component = () => {
onSelectResource: () => null,
resourceSpec: {
name: '',
- kind: ResourceKind.Application,
+ kind,
icon: null,
keywords: '',
event: DiscoverEventResource.Ec2Instance,
@@ -102,12 +146,15 @@ const Component = () => {
return (
-
+ {withCreateConfig && (
+ Devs: Click next to see create config dialog
+ )}
+
diff --git a/web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/ConfigureDiscoveryService.tsx b/web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/ConfigureDiscoveryService.tsx
new file mode 100644
index 0000000000000..72aaf398dd154
--- /dev/null
+++ b/web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/ConfigureDiscoveryService.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, { useState } from 'react';
+import { Link as InternalLink } from 'react-router-dom';
+import { Box, Text, Mark } from 'design';
+import { OutlineInfo } from 'design/Alert/Alert';
+import useAttempt from 'shared/hooks/useAttemptNext';
+import { getErrMessage } from 'shared/utils/errorType';
+
+import { useDiscover } from 'teleport/Discover/useDiscover';
+import useTeleport from 'teleport/useTeleport';
+
+import {
+ DEFAULT_DISCOVERY_GROUP_NON_CLOUD,
+ createDiscoveryConfig,
+} from 'teleport/services/discovery';
+import cfg from 'teleport/config';
+
+import { ActionButtons, Header, ResourceKind } from '../../Shared';
+import { InfoIcon } from '../InfoIcon';
+
+import { CreatedDiscoveryConfigDialog } from './CreatedDiscoveryConfigDialog';
+import { ConfigureDiscoveryServiceDirections } from './ConfigureDiscoveryServiceDirections';
+
+export function ConfigureDiscoveryService({
+ withCreateConfig = false,
+}: {
+ /**
+ * if true, creates a discovery config resource when clicking on
+ * nextStep button
+ */
+ withCreateConfig?: boolean;
+}) {
+ const ctx = useTeleport();
+ const clusterId = ctx.storeUser.getClusterId();
+ const {
+ nextStep,
+ prevStep,
+ agentMeta,
+ updateAgentMeta,
+ resourceSpec,
+ emitErrorEvent,
+ } = useDiscover();
+
+ const {
+ attempt: createDiscoveryConfigAttempt,
+ setAttempt: setCreateDiscoveryConfigAttempt,
+ } = useAttempt('');
+
+ const [discoveryGroupName, setDiscoveryGroupName] = useState(
+ DEFAULT_DISCOVERY_GROUP_NON_CLOUD
+ );
+
+ const { storeUser } = useTeleport();
+
+ function handleNextStep() {
+ if (withCreateConfig) {
+ createDiscoveryCfg();
+ nextStep();
+ return;
+ }
+
+ updateAgentMeta({
+ ...agentMeta,
+ autoDiscovery: {
+ config: { name: '', aws: [], discoveryGroup: discoveryGroupName },
+ },
+ });
+ nextStep();
+ }
+
+ async function createDiscoveryCfg() {
+ try {
+ setCreateDiscoveryConfigAttempt({ status: 'processing' });
+ const discoveryConfig = await createDiscoveryConfig(clusterId, {
+ name: crypto.randomUUID(),
+ discoveryGroup: discoveryGroupName,
+ aws: [
+ {
+ types: ['rds'],
+ regions: [agentMeta.awsRegion],
+ tags: { 'vpc-id': [agentMeta.awsVpcId] },
+ integration: agentMeta.awsIntegration.name,
+ },
+ ],
+ });
+ setCreateDiscoveryConfigAttempt({ status: 'success' });
+ updateAgentMeta({
+ ...agentMeta,
+ autoDiscovery: {
+ config: discoveryConfig,
+ },
+ });
+ } catch (err) {
+ const message = getErrMessage(err);
+ setCreateDiscoveryConfigAttempt({
+ status: 'failed',
+ statusText: `failed to create discovery config: ${message}`,
+ });
+ emitErrorEvent(`failed to create discovery config: ${message}`);
+ return;
+ }
+ }
+
+ return (
+
+ Configure Teleport Discovery Service
+
+
+
+ {createDiscoveryConfigAttempt.status !== '' && (
+ setCreateDiscoveryConfigAttempt({ status: '' })}
+ retry={handleNextStep}
+ region={agentMeta.awsRegion}
+ notifyAboutDelay={false}
+ />
+ )}
+
+ );
+}
+
+function EnrollInfo({ kind }: { kind: ResourceKind }) {
+ if (kind === ResourceKind.Database) {
+ return (
+
+ The Teleport Discovery Service can connect to Amazon RDS and
+ automatically discover and enroll RDS instances and clusters.
+
+ );
+ }
+
+ if (kind === ResourceKind.Server) {
+ return (
+ <>
+
+ The Teleport Discovery Service can connect to Amazon EC2 and
+ automatically discover and enroll EC2 instances.
+
+
+
+
+
+
+ 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
+
+ .
+
+
+ >
+ );
+ }
+
+ return null;
+}
diff --git a/web/packages/teleport/src/Discover/Shared/AutoDiscovery/SelfHostedAutoDiscoverDirections.tsx b/web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/ConfigureDiscoveryServiceDirections.tsx
similarity index 99%
rename from web/packages/teleport/src/Discover/Shared/AutoDiscovery/SelfHostedAutoDiscoverDirections.tsx
rename to web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/ConfigureDiscoveryServiceDirections.tsx
index 318b0d17f5b93..e3e7086b28dcc 100644
--- a/web/packages/teleport/src/Discover/Shared/AutoDiscovery/SelfHostedAutoDiscoverDirections.tsx
+++ b/web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/ConfigureDiscoveryServiceDirections.tsx
@@ -33,7 +33,7 @@ const discoveryServiceToolTip = `The Discovery Service is responsible for watchi
cloud provider and checking if there are any new resources or if there have been any \
modifications to previously discovered resources.`;
-export const SelfHostedAutoDiscoverDirections = ({
+export const ConfigureDiscoveryServiceDirections = ({
clusterPublicUrl,
discoveryGroupName,
setDiscoveryGroupName,
diff --git a/web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/CreatedDiscoveryConfigDialog.story.tsx b/web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/CreatedDiscoveryConfigDialog.story.tsx
new file mode 100644
index 0000000000000..73af471af56dd
--- /dev/null
+++ b/web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/CreatedDiscoveryConfigDialog.story.tsx
@@ -0,0 +1,69 @@
+/**
+ * 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 { CreatedDiscoveryConfigDialog } from './CreatedDiscoveryConfigDialog';
+
+export default {
+ title: 'Teleport/Discover/Shared/ConfigureDiscoveryService/CreatedDialog',
+};
+
+export const Success = () => (
+ null}
+ close={() => null}
+ next={() => null}
+ region="us-east-1"
+ notifyAboutDelay={false}
+ attempt={{ status: 'success' }}
+ />
+);
+
+export const SuccessWithDelay = () => (
+ null}
+ close={() => null}
+ next={() => null}
+ region="us-east-1"
+ notifyAboutDelay={true}
+ attempt={{ status: 'success' }}
+ />
+);
+
+export const Loading = () => (
+ null}
+ close={() => null}
+ next={() => null}
+ region="us-east-1"
+ notifyAboutDelay={false}
+ attempt={{ status: 'processing' }}
+ />
+);
+
+export const Failed = () => (
+ null}
+ close={() => null}
+ next={() => null}
+ region="us-east-1"
+ notifyAboutDelay={false}
+ attempt={{ status: 'failed', statusText: 'some kind of error message' }}
+ />
+);
diff --git a/web/packages/teleport/src/Discover/Shared/AutoDiscovery/AutoEnrollDialog.tsx b/web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/CreatedDiscoveryConfigDialog.tsx
similarity index 90%
rename from web/packages/teleport/src/Discover/Shared/AutoDiscovery/AutoEnrollDialog.tsx
rename to web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/CreatedDiscoveryConfigDialog.tsx
index 7486205d09438..ee7bf1db8e2d0 100644
--- a/web/packages/teleport/src/Discover/Shared/AutoDiscovery/AutoEnrollDialog.tsx
+++ b/web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/CreatedDiscoveryConfigDialog.tsx
@@ -30,7 +30,7 @@ import Dialog, { DialogContent } from 'design/DialogConfirmation';
import type { Attempt } from 'shared/hooks/useAttemptNext';
-export type AutoEnrollDialog = {
+export type CreatedDiscoveryConfigDialog = {
attempt: Attempt;
retry(): void;
close(): void;
@@ -43,14 +43,14 @@ export type AutoEnrollDialog = {
notifyAboutDelay: boolean;
};
-export function AutoEnrollDialog({
+export function CreatedDiscoveryConfigDialog({
attempt,
retry,
close,
next,
region,
notifyAboutDelay,
-}: AutoEnrollDialog) {
+}: CreatedDiscoveryConfigDialog) {
let content: JSX.Element;
if (attempt.status === 'failed') {
content = (
@@ -59,11 +59,11 @@ export function AutoEnrollDialog({
{attempt.statusText}
-
-
+
+
Retry
-
+
Close
diff --git a/web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/index.ts b/web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/index.ts
new file mode 100644
index 0000000000000..2456ea71f121f
--- /dev/null
+++ b/web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/index.ts
@@ -0,0 +1,21 @@
+/**
+ * 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 { ConfigureDiscoveryService } from './ConfigureDiscoveryService';
+export { ConfigureDiscoveryServiceDirections } from './ConfigureDiscoveryServiceDirections';
+export { CreatedDiscoveryConfigDialog } from './CreatedDiscoveryConfigDialog';
diff --git a/web/packages/teleport/src/Discover/Shared/SecurityGroupPicker/SecurityGroupPicker.tsx b/web/packages/teleport/src/Discover/Shared/SecurityGroupPicker/SecurityGroupPicker.tsx
index 009824e058d12..6beb33f21cbd8 100644
--- a/web/packages/teleport/src/Discover/Shared/SecurityGroupPicker/SecurityGroupPicker.tsx
+++ b/web/packages/teleport/src/Discover/Shared/SecurityGroupPicker/SecurityGroupPicker.tsx
@@ -172,6 +172,7 @@ function CheckboxCell({
onChange(item, e);
}}
checked={isChecked}
+ data-testid={item.id}
/>
diff --git a/web/packages/teleport/src/Discover/Shared/SubnetIdPicker/SubnetIdPicker.tsx b/web/packages/teleport/src/Discover/Shared/SubnetIdPicker/SubnetIdPicker.tsx
new file mode 100644
index 0000000000000..625a34e5c810f
--- /dev/null
+++ b/web/packages/teleport/src/Discover/Shared/SubnetIdPicker/SubnetIdPicker.tsx
@@ -0,0 +1,136 @@
+/**
+ * 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 { Flex, Link, ButtonIcon } from 'design';
+import Table, { Cell } from 'design/DataTable';
+import { Danger } from 'design/Alert';
+import { CheckboxInput } from 'design/Checkbox';
+import { FetchStatus } from 'design/DataTable/types';
+import { NewTab } from 'design/Icon';
+
+import { Attempt } from 'shared/hooks/useAttemptNext';
+
+import { Regions, Subnet } from 'teleport/services/integrations';
+
+export function SubnetIdPicker({
+ region,
+ attempt,
+ subnets = [],
+ fetchStatus = '',
+ fetchNextPage,
+ onSelectSubnet,
+ selectedSubnets,
+}: {
+ region: Regions;
+ attempt: Attempt;
+ subnets: Subnet[];
+ fetchStatus: FetchStatus;
+ fetchNextPage(): void;
+ onSelectSubnet: (
+ subnet: Subnet,
+ e: React.ChangeEvent
+ ) => void;
+ selectedSubnets: string[];
+}) {
+ if (attempt.status === 'failed') {
+ return {attempt.statusText};
+ }
+
+ return (
+
{
+ const isChecked = selectedSubnets.includes(item.id);
+ return (
+
+ );
+ },
+ },
+ {
+ key: 'name',
+ headerText: 'Name',
+ },
+ {
+ key: 'id',
+ headerText: 'ID',
+ },
+ {
+ key: 'availabilityZone',
+ headerText: 'Availability Zone',
+ },
+ {
+ altKey: 'link-out',
+ render: subnet => {
+ return (
+
+
+
+
+
+ );
+ },
+ },
+ ]}
+ emptyText="No Subnets Found"
+ pagination={{ pageSize: 5 }}
+ fetching={{ onFetchMore: fetchNextPage, fetchStatus }}
+ isSearchable
+ />
+ );
+}
+
+function CheckboxCell({
+ item,
+ isChecked,
+ onChange,
+}: {
+ item: Subnet;
+ isChecked: boolean;
+ onChange(selectedItem: Subnet, e: React.ChangeEvent): void;
+}) {
+ return (
+
+
+ {
+ onChange(item, e);
+ }}
+ checked={isChecked}
+ data-testid={item.id}
+ />
+
+
+ );
+}
diff --git a/web/packages/teleport/src/Discover/Shared/AutoDiscovery/index.ts b/web/packages/teleport/src/Discover/Shared/SubnetIdPicker/index.ts
similarity index 83%
rename from web/packages/teleport/src/Discover/Shared/AutoDiscovery/index.ts
rename to web/packages/teleport/src/Discover/Shared/SubnetIdPicker/index.ts
index 387da8a33da5d..fede7765a6cfe 100644
--- a/web/packages/teleport/src/Discover/Shared/AutoDiscovery/index.ts
+++ b/web/packages/teleport/src/Discover/Shared/SubnetIdPicker/index.ts
@@ -16,5 +16,4 @@
* along with this program. If not, see .
*/
-export { AutoEnrollDialog } from './AutoEnrollDialog';
-export { SelfHostedAutoDiscoverDirections } from './SelfHostedAutoDiscoverDirections';
+export { SubnetIdPicker } from './SubnetIdPicker';
diff --git a/web/packages/teleport/src/Discover/Shared/index.ts b/web/packages/teleport/src/Discover/Shared/index.ts
index 8082ff6f96b12..a8cc71627ff9c 100644
--- a/web/packages/teleport/src/Discover/Shared/index.ts
+++ b/web/packages/teleport/src/Discover/Shared/index.ts
@@ -42,10 +42,6 @@ export {
RadioCell,
StatusCell,
} from './Aws';
-export {
- AutoEnrollDialog,
- SelfHostedAutoDiscoverDirections,
-} from './AutoDiscovery';
export { StyledBox } from './StyledBox';
export type { DiscoverLabel } from './LabelsCreater';
diff --git a/web/packages/teleport/src/Discover/useDiscover.tsx b/web/packages/teleport/src/Discover/useDiscover.tsx
index ed226bf8e2066..61e493aa3b44f 100644
--- a/web/packages/teleport/src/Discover/useDiscover.tsx
+++ b/web/packages/teleport/src/Discover/useDiscover.tsx
@@ -526,13 +526,20 @@ type BaseMeta = {
* in customers infrastructure such as Kubernetes clusters or databases hosted
* on cloud platforms like AWS, Azure, etc.
*/
- autoDiscovery?: {
- config: DiscoveryConfig;
- // requiredVpcsAndSubnets is a map of required vpcs for auto discovery.
- // If this is empty, then a user can skip deploying db agents.
- // If >0, auto discovery requires deploying db agents.
- requiredVpcsAndSubnets?: Record;
- };
+ autoDiscovery?: AutoDiscovery;
+ /**
+ * If this field is defined, it means the user selected a specific vpc ID.
+ * Not all flows will allow a user to select a vpc ID.
+ */
+ awsVpcId?: string;
+};
+
+export type AutoDiscovery = {
+ config?: DiscoveryConfig;
+ // requiredVpcsAndSubnets is a map of required vpcs for auto discovery.
+ // If this is empty, then a user can skip deploying db agents.
+ // If >0, auto discovery requires deploying db agents.
+ requiredVpcsAndSubnets?: Record;
};
// NodeMeta describes the fields for node resource
diff --git a/web/packages/teleport/src/config.ts b/web/packages/teleport/src/config.ts
index 60e7f0dea4eb8..92fba138f8808 100644
--- a/web/packages/teleport/src/config.ts
+++ b/web/packages/teleport/src/config.ts
@@ -314,12 +314,16 @@ const cfg = {
'/v1/webapi/sites/:clusterId/integrations/aws-oidc/:name/deploydatabaseservices',
awsRdsDbRequiredVpcsPath:
'/v1/webapi/sites/:clusterId/integrations/aws-oidc/:name/requireddatabasesvpcs',
+ awsDatabaseVpcsPath:
+ '/webapi/sites/:clusterId/integrations/aws-oidc/:name/databasevpcs',
awsRdsDbListPath:
'/v1/webapi/sites/:clusterId/integrations/aws-oidc/:name/databases',
awsDeployTeleportServicePath:
'/v1/webapi/sites/:clusterId/integrations/aws-oidc/:name/deployservice',
awsSecurityGroupsListPath:
'/v1/webapi/sites/:clusterId/integrations/aws-oidc/:name/securitygroups',
+ awsSubnetListPath:
+ '/v1/webapi/sites/:clusterId/integrations/aws-oidc/:name/subnets',
awsAppAccessPath:
'/v1/webapi/sites/:clusterId/integrations/aws-oidc/:name/aws-app-access',
@@ -900,6 +904,13 @@ const cfg = {
});
},
+ getAwsDatabaseVpcsUrl(integrationName: string, clusterId: string) {
+ return generatePath(cfg.api.awsDatabaseVpcsPath, {
+ clusterId,
+ name: integrationName,
+ });
+ },
+
getAwsRdsDbsDeployServicesUrl(integrationName: string) {
const clusterId = cfg.proxyCluster;
@@ -1004,6 +1015,13 @@ const cfg = {
});
},
+ getAwsSubnetListUrl(integrationName: string, clusterId: string) {
+ return generatePath(cfg.api.awsSubnetListPath, {
+ clusterId,
+ name: integrationName,
+ });
+ },
+
getEc2InstanceConnectIAMConfigureScriptUrl(
params: UrlAwsConfigureIamScriptParams
) {
diff --git a/web/packages/teleport/src/services/databases/types.ts b/web/packages/teleport/src/services/databases/types.ts
index 2247532da0b7d..6630646f9e4e4 100644
--- a/web/packages/teleport/src/services/databases/types.ts
+++ b/web/packages/teleport/src/services/databases/types.ts
@@ -70,6 +70,8 @@ export type CreateDatabaseRequest = {
labels?: ResourceLabel[];
awsRds?: AwsRdsDatabase;
awsRegion?: Regions;
+ awsVpcId?: string;
+ overwrite?: boolean;
};
export type DatabaseIamPolicyResponse = {
diff --git a/web/packages/teleport/src/services/integrations/fetchAllAwsRdsEnginesDatabase.test.ts b/web/packages/teleport/src/services/integrations/fetchAllAwsRdsEnginesDatabase.test.ts
new file mode 100644
index 0000000000000..26e89012ebff4
--- /dev/null
+++ b/web/packages/teleport/src/services/integrations/fetchAllAwsRdsEnginesDatabase.test.ts
@@ -0,0 +1,154 @@
+/**
+ * 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 api from 'teleport/services/api';
+
+import { integrationService, makeAwsDatabase } from './integrations';
+
+const testCases: {
+ name: string;
+ fetch: 'instance' | 'cluster' | 'both';
+ err?: string;
+ instancesNextKey?: string;
+ clustersNextKey?: string;
+}[] = [
+ {
+ name: 'fetch only rds instances',
+ fetch: 'instance',
+ },
+ {
+ name: 'fetch only clusters instances',
+ fetch: 'cluster',
+ },
+ {
+ name: 'fetch both clusters and instances',
+ fetch: 'both',
+ instancesNextKey: 'instance-key',
+ clustersNextKey: 'cluster-key',
+ },
+];
+
+test.each(testCases)('$name', async tc => {
+ let instances;
+ let clusters;
+
+ if (tc.fetch === 'cluster') {
+ clusters = [
+ {
+ protocol: 'sql',
+ name: 'rds-cluster',
+ },
+ ];
+ }
+ if (tc.fetch === 'instance') {
+ instances = [
+ {
+ protocol: 'postgres',
+ name: 'rds-instance',
+ },
+ ];
+ }
+ jest
+ .spyOn(api, 'post')
+ .mockResolvedValueOnce({
+ databases: instances || [],
+ nextToken: tc.instancesNextKey,
+ })
+ .mockResolvedValueOnce({
+ databases: clusters || [],
+ nextToken: tc.clustersNextKey,
+ });
+
+ const resp = await integrationService.fetchAllAwsRdsEnginesDatabases(
+ 'some-name',
+ {
+ region: 'us-east-1',
+ }
+ );
+
+ expect(resp).toStrictEqual({
+ databases: [
+ ...(clusters ? clusters.map(makeAwsDatabase) : []),
+ ...(instances ? instances.map(makeAwsDatabase) : []),
+ ],
+ instancesNextToken: tc.instancesNextKey,
+ clustersNextToken: tc.clustersNextKey,
+ });
+});
+
+test('failed to fetch both clusters and instances should throw error', async () => {
+ jest.spyOn(api, 'post').mockRejectedValue(new Error('some error'));
+
+ await expect(
+ integrationService.fetchAllAwsRdsEnginesDatabases('some-name', {
+ region: 'us-east-1',
+ })
+ ).rejects.toThrow('some error');
+});
+
+test('fetching instances but failed fetch clusters', async () => {
+ const instance = {
+ protocol: 'postgres',
+ name: 'rds-instance',
+ };
+ jest
+ .spyOn(api, 'post')
+ .mockResolvedValueOnce({
+ databases: [instance],
+ })
+ .mockRejectedValue(new Error('some error'));
+
+ const resp = await integrationService.fetchAllAwsRdsEnginesDatabases(
+ 'some-name',
+ {
+ region: 'us-east-1',
+ }
+ );
+
+ expect(resp).toStrictEqual({
+ databases: [makeAwsDatabase(instance)],
+ oneOfError: 'Failed to fetch RDS clusters: Error: some error',
+ instancesNextToken: undefined,
+ });
+});
+
+test('fetching clusters but failed fetch instances', async () => {
+ const cluster = {
+ protocol: 'postgres',
+ name: 'rds-cluster',
+ };
+ jest
+ .spyOn(api, 'post')
+ .mockRejectedValueOnce(new Error('some error'))
+ .mockResolvedValue({
+ databases: [cluster],
+ });
+
+ const resp = await integrationService.fetchAllAwsRdsEnginesDatabases(
+ 'some-name',
+ {
+ region: 'us-east-1',
+ }
+ );
+
+ expect(resp).toStrictEqual({
+ databases: [makeAwsDatabase(cluster)],
+ oneOfError: 'Failed to fetch RDS instances: Error: some error',
+ clustersNextToken: undefined,
+ });
+});
diff --git a/web/packages/teleport/src/services/integrations/integrations.ts b/web/packages/teleport/src/services/integrations/integrations.ts
index a884debcd6d76..f45b436394c43 100644
--- a/web/packages/teleport/src/services/integrations/integrations.ts
+++ b/web/packages/teleport/src/services/integrations/integrations.ts
@@ -50,6 +50,12 @@ import {
EnrollEksClustersRequest,
ListEksClustersRequest,
AwsOidcDeployDatabaseServicesRequest,
+ Regions,
+ ListAwsRdsFromAllEnginesResponse,
+ ListAwsSubnetsRequest,
+ ListAwsSubnetsResponse,
+ Subnet,
+ AwsDatabaseVpcsResponse,
} from './types';
export const integrationService = {
@@ -95,12 +101,114 @@ export const integrationService = {
.then(resp => resp.vpcMapOfSubnets);
},
+ fetchAwsDatabasesVpcs(
+ integrationName: string,
+ clusterId: string,
+ body: { region: string; accountId: string; nextToken: string }
+ ): Promise {
+ return api
+ .post(cfg.getAwsDatabaseVpcsUrl(integrationName, clusterId), body)
+ .then(resp => {
+ const vpcs = resp.vpcs || [];
+ return { vpcs, nextToken: resp.nextToken };
+ });
+ },
+
+ /**
+ * Grabs a page for rds instances and rds clusters.
+ * Used with auto discovery to display "all" the
+ * rds's in a region by page.
+ */
+ fetchAllAwsRdsEnginesDatabases(
+ integrationName: string,
+ req: {
+ region: Regions;
+ instancesNextToken?: string;
+ clustersNextToken?: string;
+ vpcId?: string;
+ }
+ ): Promise {
+ const makeResponse = response => {
+ const dbs = response?.databases ?? [];
+ const madeResponse: ListAwsRdsDatabaseResponse = {
+ databases: dbs.map(makeAwsDatabase),
+ nextToken: response?.nextToken,
+ };
+ return madeResponse;
+ };
+
+ return Promise.allSettled([
+ api
+ .post(cfg.getAwsRdsDbListUrl(integrationName), {
+ region: req.region,
+ vpcId: req.vpcId,
+ nextToken: req.instancesNextToken,
+ rdsType: 'instance',
+ engines: ['mysql', 'mariadb', 'postgres'],
+ })
+ .then(makeResponse),
+ api
+ .post(cfg.getAwsRdsDbListUrl(integrationName), {
+ region: req.region,
+ vpcId: req.vpcId,
+ nextToken: req.clustersNextToken,
+ rdsType: 'cluster',
+ engines: ['aurora-mysql', 'aurora-postgresql'],
+ })
+ .then(makeResponse),
+ ]).then(response => {
+ const [instances, clusters] = response;
+
+ if (instances.status === 'rejected' && clusters.status === 'rejected') {
+ // Just return one error message, likely the other will be the same error.
+ throw new Error(instances.reason);
+ }
+
+ let madeResponse: ListAwsRdsFromAllEnginesResponse = {
+ databases: [],
+ };
+
+ if (instances.status === 'fulfilled') {
+ madeResponse = {
+ databases: instances.value.databases,
+ instancesNextToken: instances.value.nextToken,
+ };
+ } else {
+ madeResponse = {
+ ...madeResponse,
+ oneOfError: `Failed to fetch RDS instances: ${instances.reason}`,
+ };
+ }
+
+ if (clusters.status === 'fulfilled') {
+ madeResponse = {
+ ...madeResponse,
+ databases: [...madeResponse.databases, ...clusters.value.databases],
+ clustersNextToken: clusters.value.nextToken,
+ };
+ } else {
+ madeResponse = {
+ ...madeResponse,
+ oneOfError: `Failed to fetch RDS clusters: ${clusters.reason}`,
+ };
+ }
+
+ // Sort databases by their names
+ madeResponse.databases = madeResponse.databases.sort((a, b) =>
+ a.name.localeCompare(b.name)
+ );
+
+ return madeResponse;
+ });
+ },
+
fetchAwsRdsDatabases(
integrationName: string,
rdsEngineIdentifier: RdsEngineIdentifier,
req: {
- region: AwsOidcListDatabasesRequest['region'];
- nextToken?: AwsOidcListDatabasesRequest['nextToken'];
+ region: Regions;
+ nextToken?: string;
+ vpcId?: string;
}
): Promise {
let body: AwsOidcListDatabasesRequest;
@@ -275,6 +383,23 @@ export const integrationService = {
};
});
},
+
+ fetchAwsSubnets(
+ integrationName: string,
+ clusterId: string,
+ req: ListAwsSubnetsRequest
+ ): Promise {
+ return api
+ .post(cfg.getAwsSubnetListUrl(integrationName, clusterId), req)
+ .then(json => {
+ const subnets = json?.subnets ?? [];
+
+ return {
+ subnets: subnets.map(makeAwsSubnets),
+ nextToken: json?.nextToken,
+ };
+ });
+ },
};
export function makeIntegrations(json: any): Integration[] {
@@ -348,3 +473,15 @@ function makeSecurityGroup(json: any): SecurityGroup {
outboundRules: outboundRules ?? [],
};
}
+
+function makeAwsSubnets(json: any): Subnet {
+ json = json ?? {};
+
+ const { name, id, availability_zone } = json;
+
+ return {
+ name,
+ id,
+ availabilityZone: availability_zone,
+ };
+}
diff --git a/web/packages/teleport/src/services/integrations/types.ts b/web/packages/teleport/src/services/integrations/types.ts
index 7ed7769195c6d..80dd390f5ec9a 100644
--- a/web/packages/teleport/src/services/integrations/types.ts
+++ b/web/packages/teleport/src/services/integrations/types.ts
@@ -297,6 +297,23 @@ export type ListAwsRdsDatabaseResponse = {
nextToken?: string;
};
+export type ListAwsRdsFromAllEnginesResponse = {
+ databases: AwsRdsDatabase[];
+ /**
+ * next page for rds instances.
+ */
+ instancesNextToken?: string;
+ /**
+ * next page for rds clusters.
+ */
+ clustersNextToken?: string;
+ /**
+ * set if fetching rds instances OR rds clusters
+ * returned an error
+ */
+ oneOfError?: string;
+};
+
export type IntegrationUpdateRequest = {
awsoidc: {
roleArn: string;
@@ -308,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.
@@ -467,6 +485,36 @@ export type DeployEc2InstanceConnectEndpointResponse = {
endpoints: AwsEc2InstanceConnectEndpoint[];
};
+export type Subnet = {
+ /**
+ * Subnet name.
+ * This is just a friendly name and should not be used for further API calls.
+ * It can be empty if the subnet was not given a "Name" tag.
+ */
+ name?: string;
+ /**
+ * Subnet ID, for example "subnet-0b3ca383195ad2cc7".
+ * This is the value that should be used when doing further API calls.
+ */
+ id: string;
+ /**
+ * AWS availability zone of the subnet, for example
+ * "us-west-1a".
+ */
+ availabilityZone: string;
+};
+
+export type ListAwsSubnetsRequest = {
+ vpcId: string;
+ region: Regions;
+ nextToken?: string;
+};
+
+export type ListAwsSubnetsResponse = {
+ subnets: Subnet[];
+ nextToken?: string;
+};
+
export type ListAwsSecurityGroupsRequest = {
// VPCID is the VPC to filter Security Groups.
vpcId: string;
@@ -521,3 +569,17 @@ export type IntegrationUrlLocationState = {
kind: IntegrationKind;
redirectText: string;
};
+
+export type Vpc = {
+ id: string;
+ name?: string;
+ /**
+ * if defined, a database service is already deployed for this vpc.
+ */
+ ecsServiceDashboardURL?: string;
+};
+
+export type AwsDatabaseVpcsResponse = {
+ vpcs: Vpc[];
+ nextToken: string;
+};