From 9ced2add7b5ef37aa428ed6e93439ff86260610f Mon Sep 17 00:00:00 2001 From: Lisa Kim Date: Mon, 8 Jan 2024 16:25:04 -0800 Subject: [PATCH] Web: Support auto discovery for self-hosted (#36027) * Web: Support auto discovery for self-hosted * Move doc instructions into web UI * fix test --- .../EnrollRdsDatabase.test.tsx | 102 +++++++- .../EnrollRdsDatabase/EnrollRdsDatabase.tsx | 223 ++++++++++++++++-- .../src/Discover/Fixtures/databases.tsx | 4 +- 3 files changed, 301 insertions(+), 28 deletions(-) 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 5beeed370a38c..74f63d9ee3d08 100644 --- a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.test.tsx +++ b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.test.tsx @@ -26,12 +26,14 @@ 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 } from 'teleport/services/discovery/discovery'; import { EnrollRdsDatabase } from './EnrollRdsDatabase'; const defaultIsCloud = cfg.isCloud; describe('test EnrollRdsDatabase.tsx', () => { + let createDiscoveryConfig; beforeEach(() => { cfg.isCloud = true; jest @@ -43,11 +45,13 @@ describe('test EnrollRdsDatabase.tsx', () => { jest .spyOn(userEventService, 'captureDiscoverEvent') .mockResolvedValue(undefined as never); - jest.spyOn(discoveryService, 'createDiscoveryConfig').mockResolvedValue({ - name: '', - discoveryGroup: '', - aws: [], - }); + createDiscoveryConfig = jest + .spyOn(discoveryService, 'createDiscoveryConfig') + .mockResolvedValue({ + name: '', + discoveryGroup: '', + aws: [], + }); jest .spyOn(DatabaseService.prototype, 'fetchDatabaseServices') .mockResolvedValue({ services: [] }); @@ -98,7 +102,7 @@ describe('test EnrollRdsDatabase.tsx', () => { expect(DatabaseService.prototype.fetchDatabases).toHaveBeenCalledTimes(1); }); - test('auto enroll is on by default with no database services', async () => { + test('auto enroll (cloud) is on by default', async () => { jest.spyOn(integrationService, 'fetchAwsRdsDatabases').mockResolvedValue({ databases: mockAwsDbs, }); @@ -116,16 +120,28 @@ describe('test EnrollRdsDatabase.tsx', () => { // 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(discoveryService.createDiscoveryConfig).toHaveBeenCalledTimes(1); 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']).toEqual( + DISCOVERY_GROUP_CLOUD + ); expect(DatabaseService.prototype.createDatabase).not.toHaveBeenCalled(); }); - test('auto enroll disabled, creates database', async () => { + test('auto enroll disabled (cloud), creates database', async () => { jest.spyOn(integrationService, 'fetchAwsRdsDatabases').mockResolvedValue({ databases: mockAwsDbs, }); @@ -156,6 +172,76 @@ describe('test EnrollRdsDatabase.tsx', () => { ).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( + 'aws-prod' + ); + + expect(DatabaseService.prototype.createDatabase).not.toHaveBeenCalled(); + }); + + test('auto enroll disabled (self-hosted), creates database', async () => { + cfg.isCloud = false; + 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); + + // disable auto enroll + act(() => screen.getByText(/auto-enroll all/i).click()); + expect(screen.getByText('Next')).toBeDisabled(); + + act(() => screen.getByRole('radio').click()); + + 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); + }); }); const mockAwsDbs: AwsRdsDatabase[] = [ diff --git a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.tsx b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.tsx index 69919bc78b2eb..0b9c6dbc60df5 100644 --- a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.tsx +++ b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.tsx @@ -15,14 +15,14 @@ */ import React, { useState } from 'react'; -import { Box, Text, Toggle } from 'design'; +import { Box, Flex, Input, Text, Toggle } from 'design'; import { FetchStatus } from 'design/DataTable/types'; import { Danger } from 'design/Alert'; - import useAttempt, { Attempt } from 'shared/hooks/useAttemptNext'; import { ToolTipInfo } from 'shared/components/ToolTip'; import { getErrMessage } from 'shared/utils/errorType'; +import { TextSelectCopyMulti } from 'teleport/components/TextSelectCopy'; import { DbMeta, useDiscover } from 'teleport/Discover/useDiscover'; import { AwsRdsDatabase, @@ -42,8 +42,9 @@ import { createDiscoveryConfig, } from 'teleport/services/discovery'; import useTeleport from 'teleport/useTeleport'; +import { Tabs } from 'teleport/components/Tabs'; -import { ActionButtons, Header, Mark } from '../../Shared'; +import { ActionButtons, Header, Mark, StyledBox } from '../../Shared'; import { useCreateDatabase } from '../CreateDatabase/useCreateDatabase'; import { CreateDatabaseDialog } from '../CreateDatabase/CreateDatabaseDialog'; @@ -99,9 +100,12 @@ export function EnrollRdsDatabase() { fetchStatus: 'disabled', }); const [selectedDb, setSelectedDb] = useState(); - const [wantAutoDiscover, setWantAutoDiscover] = useState(() => cfg.isCloud); + const [wantAutoDiscover, setWantAutoDiscover] = useState(true); const [autoDiscoveryCfg, setAutoDiscoveryCfg] = useState(); const [requiredVpcs, setRequiredVpcs] = useState>(); + const [discoveryGroupName, setDiscoveryGroupName] = useState(() => + cfg.isCloud ? '' : 'aws-prod' + ); function fetchDatabasesWithNewRegion(region: Regions) { // Clear table when fetching with new region. @@ -238,7 +242,9 @@ export function EnrollRdsDatabase() { try { discoveryConfig = await createDiscoveryConfig(clusterId, { name: crypto.randomUUID(), - discoveryGroup: DISCOVERY_GROUP_CLOUD, + discoveryGroup: cfg.isCloud + ? DISCOVERY_GROUP_CLOUD + : discoveryGroupName, aws: [ { types: ['rds'], @@ -333,7 +339,17 @@ export function EnrollRdsDatabase() { } const hasIamPermError = isIamPermError(fetchDbAttempt); - const showTable = !hasIamPermError && tableData.currRegion; + 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; return ( @@ -350,23 +366,28 @@ export function EnrollRdsDatabase() { clear={clear} disableSelector={fetchDbAttempt.status === 'processing'} /> - {showTable && ( + {showContent && ( <> - {cfg.isCloud && ( + {showAutoEnrollToggle && ( setWantAutoDiscover(b => !b)} isDisabled={tableData.items.length === 0} + discoveryGroupName={discoveryGroupName} + setDiscoveryGroupName={setDiscoveryGroupName} + clusterPublicUrl={ctx.storeUser.state.cluster.publicURL} + /> + )} + {showTable && ( + )} - )} {hasIamPermError && ( @@ -378,7 +399,7 @@ export function EnrollRdsDatabase() { /> )} - {showTable && wantAutoDiscover && ( + {showContent && showAutoEnrollToggle && wantAutoDiscover && ( Note: Auto-enroll will enroll all database engines in this region (e.g. PostgreSQL, MySQL, Aurora). @@ -390,7 +411,8 @@ export function EnrollRdsDatabase() { fetchDbAttempt.status === 'processing' || (!wantAutoDiscover && !selectedDb) || hasIamPermError || - fetchDbAttempt.status === 'failed' + fetchDbAttempt.status === 'failed' || + (!cfg.isCloud && !discoveryGroupName) } /> {DialogComponent} @@ -411,14 +433,28 @@ function getRdsEngineIdentifier(engine: DatabaseEngine): RdsEngineIdentifier { } } +const discoveryGroupToolTip = `Discovery group name is used to group discovered resources into different sets. \ +This parameter is used to prevent Discovery Agents watching different sets of cloud resources from \ +colliding against each other and deleting resources created by another services.`; + +const discoveryServiceToolTip = `The Discovery Service, is responsible for watching your \ +cloud provider and checking if there are any new databases or if there have been any \ +modifications to previously discovered databases.`; + function ToggleSection({ wantAutoDiscover, toggleWantAutoDiscover, isDisabled, + discoveryGroupName, + setDiscoveryGroupName, + clusterPublicUrl, }: { wantAutoDiscover: boolean; isDisabled: boolean; toggleWantAutoDiscover(): void; + discoveryGroupName: string; + setDiscoveryGroupName(n: string): void; + clusterPublicUrl: string; }) { return ( @@ -436,6 +472,157 @@ function ToggleSection({ infrastructure. + {!cfg.isCloud && wantAutoDiscover && ( + + )} ); } + +const SelfHostedAutoDiscoverDirections = ({ + clusterPublicUrl, + discoveryGroupName, + setDiscoveryGroupName, +}: { + clusterPublicUrl: string; + discoveryGroupName: string; + setDiscoveryGroupName(n: string): void; +}) => { + const yamlContent = `version: v3 +teleport: + join_params: + token_name: "" + method: token + proxy_server: "${clusterPublicUrl}" +auth_service: + enabled: off +proxy_service: + enabled: off +ssh_service: + enabled: off +discovery_service: + enabled: "yes" + discovery_group: "${discoveryGroupName}"`; + + return ( + + + + Auto-enrolling requires you to configure a{' '} + Discovery Service + + + +
+ + Step 1: Create a Join Token + + Run the following command against your Teleport Auth Service and save + it in /tmp/token on the host that will run the Discovery + Service. + + + + + + + Step 2: Define a Discovery Group name{' '} + + + + + setDiscoveryGroupName(e.target.value)} + hasError={discoveryGroupName.length == 0} + /> + + + + + Step 3: Create a teleport.yaml file + + + Use this template to create a teleport.yaml on the host + that will run the Discovery Service. + + + + + + Step 4: Start Discovery Service + + + Configure the Discovery Service to start automatically when the host + boots up by creating a systemd service for it. The instructions depend + on how you installed the Discovery Service. + + + + On the host where you will run the Discovery Service, enable + and start Teleport: + + +
+ ), + }, + { + title: `TAR Archive`, + content: ( + + + On the host where you will run the Discovery Service, create + a systemd service configuration for Teleport, enable the + Teleport service, and start Teleport: + + + + ), + }, + ]} + /> + + You can check the status of the Discovery Service with{' '} + systemctl status teleport and view its logs with{' '} + journalctl -fu teleport. + + + + ); +}; diff --git a/web/packages/teleport/src/Discover/Fixtures/databases.tsx b/web/packages/teleport/src/Discover/Fixtures/databases.tsx index 5902959d9ee04..4a1d735ed6b9d 100644 --- a/web/packages/teleport/src/Discover/Fixtures/databases.tsx +++ b/web/packages/teleport/src/Discover/Fixtures/databases.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { PropsWithChildren } from 'react'; +import React from 'react'; import { DatabaseEngine, @@ -94,7 +94,7 @@ export function getDbMeta(): DbMeta { }; } -export const ComponentWrapper: React.FC = ({ children }) => ( +export const ComponentWrapper: React.FC = ({ children }) => (