diff --git a/web/packages/teleport/src/Discover/Database/ConnectAwsAccount/ConnectAwsAccount.tsx b/web/packages/teleport/src/Discover/Database/ConnectAwsAccount/ConnectAwsAccount.tsx index a5de9dd826ee0..21873c62500fb 100644 --- a/web/packages/teleport/src/Discover/Database/ConnectAwsAccount/ConnectAwsAccount.tsx +++ b/web/packages/teleport/src/Discover/Database/ConnectAwsAccount/ConnectAwsAccount.tsx @@ -18,7 +18,7 @@ import React, { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import { Box, - ButtonLink, + ButtonText, Text, ButtonPrimary, Indicator, @@ -42,12 +42,23 @@ import useTeleport from 'teleport/useTeleport'; import { ActionButtons, HeaderSubtitle, HeaderWithBackBtn } from '../../Shared'; -import { DbMeta, useDiscover } from '../../useDiscover'; +import { + DbMeta, + DiscoverUrlLocationState, + useDiscover, +} from '../../useDiscover'; export function ConnectAwsAccount() { const { storeUser } = useTeleport(); - const { prevStep, nextStep, agentMeta, updateAgentMeta, eventState } = - useDiscover(); + const { + prevStep, + nextStep, + agentMeta, + updateAgentMeta, + eventState, + resourceSpec, + currentStep, + } = useDiscover(); const integrationAccess = storeUser.getIntegrationsAccess(); const databaseAccess = storeUser.getDatabaseAccess(); @@ -140,14 +151,24 @@ export function ConnectAwsAccount() { integrationName: selectedAwsIntegration.value, }); - // TODO(lisa): Need to add a new event to emit for this screen. nextStep(); } const hasAwsIntegrations = awsIntegrations.length > 0; + + // When a user clicks to create a new AWS integration, we + // define location state to preserve all the states required + // to resume from this step when the user comes back to discover route + // after successfully finishing enrolling integration. const locationState = { pathname: cfg.getIntegrationEnrollRoute(IntegrationKind.AwsOidc), - state: { discoverEventId: eventState?.id }, + state: { + discover: { + eventState, + resourceSpec, + currentStep, + }, + } as DiscoverUrlLocationState, }; return ( @@ -174,9 +195,9 @@ export function ConnectAwsAccount() { options={awsIntegrations} /> - + Or click here to set up a different AWS account - + > ) : ( - - Database Register Failed - - Error: {attempt.statusText} + Register Failed: {attempt.statusText} @@ -70,11 +67,9 @@ export function CreateDatabaseDialog({ } else if (attempt.status === 'processing') { content = ( <> - - Registering Database - - + + + Next + > ); } else { // success content = ( <> - - Successfully Registered Database - Database "{dbName}" successfully registered - + Next > @@ -114,6 +109,9 @@ export function CreateDatabaseDialog({ mb={0} textAlign="center" > + + Database Register + {content} diff --git a/web/packages/teleport/src/Discover/Database/CreateDatabase/useCreateDatabase.test.tsx b/web/packages/teleport/src/Discover/Database/CreateDatabase/useCreateDatabase.test.tsx index 080a3fabe0917..97e1b1c05ef31 100644 --- a/web/packages/teleport/src/Discover/Database/CreateDatabase/useCreateDatabase.test.tsx +++ b/web/packages/teleport/src/Discover/Database/CreateDatabase/useCreateDatabase.test.tsx @@ -87,7 +87,19 @@ const services = [ const testCases = [ { - name: 'match in multiple services', + name: 'match with a service', + newLabels: dbLabels, + services: [ + { + name: 'svc4', + matcherLabels: { env: ['prod'] }, + awsIdentity: emptyAwsIdentity, + }, + ], + expectedMatch: 'svc4', + }, + { + name: 'match among multple service', newLabels: dbLabels, services, expectedMatch: 'svc2', @@ -96,26 +108,14 @@ const testCases = [ name: 'no match despite matching all labels when a svc has a non-matching label', newLabels: dbLabels, services: [ - { - name: 'svc1', - matcherLabels: { os: ['windows', 'mac'], env: ['staging'] }, - awsIdentity: emptyAwsIdentity, - }, { name: 'svc2', matcherLabels: { os: ['windows', 'mac', 'linux'], - tag: ['v11.0.0'], - env: ['staging', 'prod'], fruit: ['apple', '*'], // the non-matching label }, awsIdentity: emptyAwsIdentity, }, - { - name: 'svc3', - matcherLabels: { env: ['prod'], fruit: ['orange'] }, - awsIdentity: emptyAwsIdentity, - }, ], expectedMatch: undefined, }, @@ -161,7 +161,7 @@ const testCases = [ name: 'svc2', matcherLabels: { os: ['linux', 'mac'], - '*': ['prod', 'apple', 'v11.0.0'], + '*': ['prod', 'apple'], }, awsIdentity: emptyAwsIdentity, }, @@ -174,7 +174,7 @@ const testCases = [ services: [ { name: 'svc1', - matcherLabels: { '*': ['windows', 'mac'] }, + matcherLabels: { '*': ['windows'] }, awsIdentity: emptyAwsIdentity, }, ], @@ -204,7 +204,7 @@ const testCases = [ name: 'svc1', matcherLabels: { fruit: ['*'], - os: ['mac'], + os: ['windows'], }, awsIdentity: emptyAwsIdentity, }, @@ -239,9 +239,11 @@ const testCases = [ }, ]; -test.each(testCases)('$name', ({ newLabels, services, expectedMatch }) => { - const foundSvc = findActiveDatabaseSvc(newLabels, services); - expect(foundSvc?.name).toEqual(expectedMatch); +describe('findActiveDatabaseSvc()', () => { + test.each(testCases)('$name', ({ newLabels, services, expectedMatch }) => { + const foundSvc = findActiveDatabaseSvc(newLabels, services); + expect(foundSvc?.name).toEqual(expectedMatch); + }); }); const newDatabaseReq: CreateDatabaseRequest = { @@ -284,9 +286,9 @@ describe('registering new databases, mainly error checking', () => { jest .spyOn(userEventService, 'captureDiscoverEvent') .mockResolvedValue(null as never); // return value does not matter but required by ts - jest - .spyOn(teleCtx.databaseService, 'fetchDatabases') - .mockResolvedValue({ agents: [{ name: 'new-db' } as any] }); + jest.spyOn(teleCtx.databaseService, 'fetchDatabases').mockResolvedValue({ + agents: [{ name: 'new-db', labels: dbLabels } as any], + }); jest .spyOn(teleCtx.databaseService, 'createDatabase') .mockResolvedValue(null); // ret val not used @@ -339,7 +341,7 @@ describe('registering new databases, mainly error checking', () => { expect(discoverCtx.updateAgentMeta).toHaveBeenCalledWith({ resourceName: 'db-name', agentMatcherLabels: dbLabels, - db: { name: 'new-db' }, + db: { name: 'new-db', labels: dbLabels }, }); // Test the dynamic definition of nextStep is called with a number diff --git a/web/packages/teleport/src/Discover/Database/CreateDatabase/useCreateDatabase.ts b/web/packages/teleport/src/Discover/Database/CreateDatabase/useCreateDatabase.ts index f71be652de845..afbcc75d995d8 100644 --- a/web/packages/teleport/src/Discover/Database/CreateDatabase/useCreateDatabase.ts +++ b/web/packages/teleport/src/Discover/Database/CreateDatabase/useCreateDatabase.ts @@ -22,7 +22,7 @@ import { useDiscover } from 'teleport/Discover/useDiscover'; import { usePoll } from 'teleport/Discover/Shared/usePoll'; import { compareByString } from 'teleport/lib/util'; -import { matchLabels, makeLabelMaps } from '../util'; +import { matchLabels } from '../util'; import type { CreateDatabaseRequest, @@ -111,7 +111,7 @@ export function useCreateDatabase() { updateAgentMeta({ ...(agentMeta as DbMeta), resourceName: createdDb.name, - agentMatcherLabels: createdDb.labels, + agentMatcherLabels: dbPollingResult.labels, db: dbPollingResult, }); @@ -257,21 +257,10 @@ export function findActiveDatabaseSvc( return null; } - // Create maps for easy lookup and matching. - const { labelKeysToMatchMap, labelValsToMatchMap, labelToMatchSeenMap } = - makeLabelMaps(newDbLabels); - - const hasLabelsToMatch = newDbLabels.length > 0; for (let i = 0; i < dbServices.length; i++) { // Loop through the current service label keys and its value set. const currService = dbServices[i]; - const match = matchLabels({ - hasLabelsToMatch, - labelKeysToMatchMap, - labelValsToMatchMap, - labelToMatchSeenMap, - matcherLabels: currService.matcherLabels, - }); + const match = matchLabels(newDbLabels, currService.matcherLabels); if (match) { return currService; diff --git a/web/packages/teleport/src/Discover/Database/DownloadScript/DownloadScript.story.tsx b/web/packages/teleport/src/Discover/Database/DownloadScript/DownloadScript.story.tsx index 8758cb2cf20cd..62dc97e359394 100644 --- a/web/packages/teleport/src/Discover/Database/DownloadScript/DownloadScript.story.tsx +++ b/web/packages/teleport/src/Discover/Database/DownloadScript/DownloadScript.story.tsx @@ -55,7 +55,10 @@ export const InitWithLabels = () => { {...props} agentMeta={{ ...props.agentMeta, - agentMatcherLabels: [{ name: 'env', value: 'prod' }], + agentMatcherLabels: [ + { name: 'env', value: 'staging' }, + { name: 'os', value: 'windows' }, + ], }} /> diff --git a/web/packages/teleport/src/Discover/Database/DownloadScript/DownloadScript.tsx b/web/packages/teleport/src/Discover/Database/DownloadScript/DownloadScript.tsx index 56880b337eb4d..b45aa97e68c20 100644 --- a/web/packages/teleport/src/Discover/Database/DownloadScript/DownloadScript.tsx +++ b/web/packages/teleport/src/Discover/Database/DownloadScript/DownloadScript.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ -import React, { Suspense, useState } from 'react'; -import { Box, ButtonSecondary, Text } from 'design'; +import React, { Suspense, useState, useEffect } from 'react'; +import { Box, ButtonSecondary, Label as Pill, Text } from 'design'; import * as Icons from 'design/Icon'; -import Validation, { useRule, Validator } from 'shared/components/Validation'; +import Validation, { Validator } from 'shared/components/Validation'; import { CatchError } from 'teleport/components/CatchError'; import { @@ -50,7 +50,7 @@ import { TextIcon, useShowHint, } from '../../Shared'; -import { makeLabelMaps, matchLabels } from '../util'; +import { matchLabels } from '../util'; import type { AgentStepProps } from '../../types'; @@ -58,12 +58,11 @@ export default function Container(props: AgentStepProps) { const hasDbLabels = props.agentMeta?.agentMatcherLabels?.length; const dbLabels = hasDbLabels ? props.agentMeta.agentMatcherLabels : []; const [labels, setLabels] = useState( - hasDbLabels - ? dbLabels - : // If user did not define any labels from previous step (create db) - // then the only way the agent will know how to pick up the - // new db is through asteriks. - [{ name: '*', value: '*', isFixed: true }] + // TODO(lisa): we will always be defaulting to asterisks, so + // instead of defining the default here, define it inside + // the LabelsCreator (component responsible for rendering + // label input boxes) + [{ name: '*', value: '*', isFixed: dbLabels.length === 0 }] ); const [showScript, setShowScript] = useState(false); @@ -99,17 +98,7 @@ export default function Container(props: AgentStepProps) { } > {!showScript && ( - - - - setShowScript(true)} - > - Generate Command - - null} disableProceed={true} /> - + )} {showScript && ( { return ( <> - Optionally Deploy a Database Service + Deploy a Database Service - This step is optional if you already have a Teleport Database Service - running. - On the host where you will run the Teleport Database Service, execute the generated command that will install and start Teleport with the appropriate configuration. @@ -248,32 +234,65 @@ export const Labels = ({ setLabels, disableBtns = false, dbLabels, + showLabelMatchErr = false, }: { labels: AgentLabel[]; setLabels(l: AgentLabel[]): void; disableBtns?: boolean; dbLabels: AgentLabel[]; + showLabelMatchErr?: boolean; }) => { - const { valid, message } = useRule(requireMatchingLabels(dbLabels, labels)); - const hasError = !valid; - + const hasDbLabels = dbLabels.length > 0; return ( - Labels - - At least one label is required to help this service identify your - database. + Optionally Define Matcher Labels + + {!hasDbLabels && ( + <> + Since no labels were defined for the registered database from the + previous step, the matcher labels are defaulted to wildcards which + will allow this database service to match any database. + > + )} + {hasDbLabels && ( + <> + Default wildcards label allows this database service to match any + database. + + You can define your own labels that this database service will use + to identify your registered database. The labels you define must + match the labels that were defined for the registered database (from + previous step): + > + )} + {hasDbLabels && ( + + {dbLabels.map((label, index) => { + const labelText = `${label.name}: ${label.value}`; + return ( + + {labelText} + + ); + })} + + )} - - {hasError && ( - + + {showLabelMatchErr && ( + - {message} + The matcher labels must be able to match with the labels defined for + the registered database. Use wildcards to match with any labels. )} @@ -285,18 +304,6 @@ function createBashCommand(tokenId: string) { return `sudo bash -c "$(curl -fsSL ${cfg.getDbScriptUrl(tokenId)})"`; } -const requireMatchingLabels = - (dbLabels: AgentLabel[], agentLabels: AgentLabel[]) => () => { - if (!hasMatchingLabels(dbLabels, agentLabels)) { - return { - valid: false, - message: `Labels must match with the labels defined for the database resource. \ - To match any key, and/or any value, asteriks can be used.`, - }; - } - return { valid: true }; - }; - // hasMatchingLabels will go through each 'agentLabels' and find matches from // 'dbLabels'. The 'agentLabels' must have same amount of matching labels // with 'dbLabels' either with asteriks (match all) or by exact match. @@ -322,15 +329,42 @@ export function hasMatchingLabels( matcherLabels[l.name] = [...matcherLabels[l.name], l.value]; }); - // Create maps for easy lookup and matching. - const { labelKeysToMatchMap, labelValsToMatchMap, labelToMatchSeenMap } = - makeLabelMaps(dbLabels); + return matchLabels(dbLabels, matcherLabels); +} - return matchLabels({ - hasLabelsToMatch: dbLabels.length > 0, - labelKeysToMatchMap, - labelValsToMatchMap, - labelToMatchSeenMap, - matcherLabels, - }); +function LoadedView({ labels, setLabels, dbLabels, setShowScript }) { + const [showLabelMatchErr, setShowLabelMatchErr] = useState(true); + + useEffect(() => { + // Turn off error once user changes labels. + if (showLabelMatchErr) { + setShowLabelMatchErr(false); + } + }, [labels]); + + function handleGenerateCommand() { + if (!hasMatchingLabels(dbLabels, labels)) { + setShowLabelMatchErr(true); + return; + } + + setShowLabelMatchErr(false); + setShowScript(true); + } + + return ( + + + + + Generate Command + + null} disableProceed={true} /> + + ); } diff --git a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/AwsRegionSelector.tsx b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/AwsRegionSelector.tsx index 0965d0245330c..9754fb1567806 100644 --- a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/AwsRegionSelector.tsx +++ b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/AwsRegionSelector.tsx @@ -15,22 +15,25 @@ */ import React, { useState } from 'react'; -import { Box, ButtonPrimary, Text, Flex } from 'design'; +import { Box, ButtonPrimary, Text, Flex, ButtonSecondary } from 'design'; import FieldSelect from 'shared/components/FieldSelect'; import { Option } from 'shared/components/Select'; import { requiredField } from 'shared/components/Validation/rules'; import Validation, { Validator } from 'shared/components/Validation'; +import { Refresh as RefreshIcon } from 'design/Icon'; import { awsRegionMap, Regions } from 'teleport/services/integrations'; export function AwsRegionSelector({ onFetch, - disableBtn, + onRefresh, + disableFetch, disableSelector, clear, }: { onFetch(region: Regions): void; - disableBtn: boolean; + onRefresh(): void; + disableFetch: boolean; disableSelector: boolean; clear(): void; }) { @@ -51,7 +54,7 @@ export function AwsRegionSelector({ return ( {({ validator }) => ( - <> + Select the AWS Region you would like to see databases for: @@ -69,17 +72,36 @@ export function AwsRegionSelector({ isDisabled={disableSelector} /> - handleFetch(validator)} - width="160px" - height="40px" - mt={1} - > - Fetch Databases - + + handleFetch(validator)} + width="160px" + height="40px" + mt={1} + > + Fetch Databases + + + + + - > + )} ); diff --git a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.story.tsx b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.story.tsx index 18e745e236cfe..92cc7a7d4599e 100644 --- a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.story.tsx +++ b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.story.tsx @@ -28,7 +28,8 @@ export default { export const AwsRegionsSelectorDisabled = () => ( null} - disableBtn={true} + disableFetch={true} + onRefresh={() => null} disableSelector={true} clear={() => null} /> @@ -37,7 +38,18 @@ export const AwsRegionsSelectorDisabled = () => ( export const AwsRegionsSelectorEnabled = () => ( null} - disableBtn={false} + disableFetch={false} + onRefresh={() => null} + disableSelector={false} + clear={() => null} + /> +); + +export const AwsRegionsSelectorRefreshEnabled = () => ( + null} + disableFetch={true} + onRefresh={() => null} disableSelector={false} clear={() => null} /> @@ -79,7 +91,7 @@ const fixtures: AwsRdsDatabase[] = [ engine: 'postgres', uri: '', labels: [], - status: 'Available', + status: 'available', accountId: '', resourceId: '', }, @@ -88,7 +100,7 @@ const fixtures: AwsRdsDatabase[] = [ engine: 'mysql', uri: '', labels: [], - status: 'Available', + status: 'available', accountId: '', resourceId: '', }, @@ -100,7 +112,7 @@ const fixtures: AwsRdsDatabase[] = [ { name: 'env', value: 'prod' }, { name: 'os', value: 'windows' }, ], - status: 'Deleting', + status: 'deleting', accountId: '', resourceId: '', }, @@ -109,7 +121,7 @@ const fixtures: AwsRdsDatabase[] = [ engine: 'postgres', uri: '', labels: [], - status: 'Failed', + status: 'failed', accountId: '', resourceId: '', }, @@ -131,7 +143,7 @@ const fixtures: AwsRdsDatabase[] = [ engine: 'postgres', uri: '', labels: [{ name: 'testing-name', value: 'testing-value' }], - status: 'Available', + status: 'available', accountId: '', resourceId: '', }, diff --git a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.tsx b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.tsx index a33bfbbde038d..b7ed9612bc2dd 100644 --- a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.tsx +++ b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.tsx @@ -62,7 +62,7 @@ export function EnrollRdsDatabase() { nextStep, } = useCreateDatabase(); - const { agentMeta, resourceSpec } = useDiscover(); + const { agentMeta, resourceSpec, emitErrorEvent } = useDiscover(); const { attempt: fetchDbAttempt, setAttempt: setFetchDbAttempt } = useAttempt(''); @@ -82,6 +82,11 @@ export function EnrollRdsDatabase() { fetchDatabases({ ...tableData }); } + function refreshDatabaseList() { + // When refreshing, start the table back at page 1. + fetchDatabases({ ...tableData, startKey: '', items: [] }); + } + function fetchDatabases(data: TableData) { const integrationName = (agentMeta as DbMeta).integrationName; @@ -110,6 +115,7 @@ export function EnrollRdsDatabase() { .catch((err: Error) => { setFetchDbAttempt({ status: 'failed', statusText: err.message }); setTableData(data); // fallback to previous data + emitErrorEvent(`failed to fetch aws rds list: ${err.message}`); }); } @@ -128,9 +134,6 @@ export function EnrollRdsDatabase() { } function handleOnProceed() { - // Append `teleport_` to the RDS db name to - // lower the chance of duplicate db name. - registerDatabase( { name: selectedDb.name, @@ -157,9 +160,10 @@ export function EnrollRdsDatabase() { )} 0 } /> @@ -177,7 +181,7 @@ export function EnrollRdsDatabase() { {registerAttempt.status !== '' && ( - Database users must allow{' '} - - IAM authentication - {' '} - in order to be used with Database Access for RDS. To enable, users - must have a rds_iam role: + Users must have an rds_iam role: - Database users must allow{' '} - - IAM authentication - {' '} - in order to be used with Database Access for RDS. Users must - have the RDS authentication plugin enabled: + Users must have the RDS authentication plugin enabled: = { kind: ResourceKind.Database, wrapper(component: React.ReactNode) { return {component}; }, + shouldPrompt(currentStep, resourceSpec) { + if (resourceSpec.dbMeta?.location === DatabaseLocation.Aws) { + // Allow user to bypass prompting on this step (Connect AWS Connect) + // on exit because users might need to change route to setup an + // integration. + if (currentStep === 0) { + return false; + } + } + return true; + }, views(resource) { let configureResourceViews; if (resource && resource.dbMeta) { @@ -44,20 +56,20 @@ export const DatabaseResource: ResourceViewConfig = { case DatabaseLocation.Aws: configureResourceViews = [ { - title: 'Register a Database', - component: CreateDatabase, - eventName: DiscoverEvent.DatabaseRegister, + title: 'Connect AWS Account', + component: ConnectAwsAccount, + eventName: DiscoverEvent.IntegrationAWSOIDCConnectEvent, + }, + { + title: 'Enroll RDS Database', + component: EnrollRdsDatabase, + eventName: DiscoverEvent.DatabaseRDSEnrollEvent, }, { title: 'Deploy Database Service', component: DownloadScript, eventName: DiscoverEvent.DeployService, }, - { - title: 'Configure IAM Policy', - component: IamPolicy, - eventName: DiscoverEvent.DatabaseConfigureIAMPolicy, - }, ]; break; diff --git a/web/packages/teleport/src/Discover/Database/util.ts b/web/packages/teleport/src/Discover/Database/util.ts index 6d9d8e3419035..5e71d3f9805b0 100644 --- a/web/packages/teleport/src/Discover/Database/util.ts +++ b/web/packages/teleport/src/Discover/Database/util.ts @@ -16,103 +16,68 @@ import type { AgentLabel } from 'teleport/services/agents'; -// makeLabelMaps makes a few lookup tables out of the label prop -// for easy lookup: -// - lookup table with label.name as key, and label.value as value -// - lookup table with label.value as key, and label.key as value -// - lookup table of flags with label.name as key, and booleans as value -// which serves to record seen labels. -export function makeLabelMaps(labels: AgentLabel[]) { - let labelKeysToMatchMap: Record = {}; - let labelValsToMatchMap: Record = {}; - let labelToMatchSeenMap: Record = {}; +export function matchLabels( + newDbLabels: AgentLabel[], + matcherLabels: Record +) { + // Sorting to match by asteriks sooner. + const entries = Object.entries({ ...matcherLabels }).sort(); - labels.forEach(label => { - labelKeysToMatchMap[label.name] = label.value; - labelToMatchSeenMap[label.name] = false; - labelValsToMatchMap[label.value] = label.name; - }); + if (!entries.length) { + return false; + } - return { labelKeysToMatchMap, labelValsToMatchMap, labelToMatchSeenMap }; -} + // Create a map for db labels for easy lookup. + let dbKeyMap = {}; + let dbValMap = {}; -// matchLabels will go through each `matcherlabels` and record matched labels. -// If all labels are matched (or all asteriks exists in `matcherLabels`), -// returns true. -// -// It will return false when: -// - there were no labels to match -// - `matcherLabel` contains a label that isn't seen in the `xxxToMatchMap` -export function matchLabels({ - hasLabelsToMatch, - matcherLabels, - labelKeysToMatchMap, - labelValsToMatchMap, - labelToMatchSeenMap, -}: { - hasLabelsToMatch: boolean; - matcherLabels: Record; - labelKeysToMatchMap: Record; - labelValsToMatchMap: Record; - labelToMatchSeenMap: Record; -}) { - const matchedLabelMap = { ...labelToMatchSeenMap }; - // Sorted to have asteriks be the first label key to test. - const entries = Object.entries({ ...matcherLabels }).sort(); - for (const [key, vals] of entries) { - // Check if the label contains asteriks, which means match all eg: - // a service with match all can pick up any database regardless of other labels + newDbLabels.forEach(label => { + dbKeyMap[label.name] = label.value; + dbValMap[label.value] = label.name; + }); + + // All matching labels must make a match with the new database labels. + let matched = true; + for (let i = 0; i < entries.length; i++) { + const [key, vals] = entries[i]; + // Check if this label set contains asteriks, which means match all. + // A service with match all can pick up any database regardless of other labels // or no labels. const foundAsterikAsValue = vals.includes('*'); if (key === '*' && foundAsterikAsValue) { return true; } - if (!hasLabelsToMatch) { - return false; + // If no newDbLabels labels were defined, there are no matches to make, + // which makes this service not a match. + if (!newDbLabels.length) { + matched = false; + break; } - // Start matching by value. + // Start matching by values. - // This means any key is fine, as long as value matches. - if (key === '*') { - let found = false; - vals.forEach(val => { - const key = labelValsToMatchMap[val]; - if (key) { - matchedLabelMap[key] = true; - found = true; - } - }); - if (found) { - continue; - } + // This means any key is fine, as long as a value matches. + if (key === '*' && vals.find(val => dbValMap[val])) { + continue; } // This means any value is fine, as long as a key matches. - // Note that db resource labels can't have duplicate keys - // (but db service can). - else if (foundAsterikAsValue && labelKeysToMatchMap[key]) { - matchedLabelMap[key] = true; + if (foundAsterikAsValue && dbKeyMap[key]) { continue; } - // Match against actual values of key and its value. - else { - const dbVal = labelKeysToMatchMap[key]; - if (dbVal && vals.find(val => val === dbVal)) { - matchedLabelMap[key] = true; - continue; - } + // Match against key and value. + const dbVal = dbKeyMap[key]; + if (dbVal && vals.find(val => val === dbVal)) { + continue; } - // At this point, the current label did not match any criteria, - // we can abort, since it takes only one mismatch to fail. - return false; - } + // No matches were found for this label set which + // means this service not a match. + matched = false; + break; + } // label set loop - return ( - hasLabelsToMatch && - Object.keys(matchedLabelMap).every(key => matchedLabelMap[key]) - ); + return matched; } diff --git a/web/packages/teleport/src/Discover/Database/utils.test.ts b/web/packages/teleport/src/Discover/Database/utils.test.ts new file mode 100644 index 0000000000000..a606758ecb811 --- /dev/null +++ b/web/packages/teleport/src/Discover/Database/utils.test.ts @@ -0,0 +1,113 @@ +/** + * Copyright 2023 Gravitational, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { matchLabels } from './util'; + +const newDbLabels = [ + { name: 'env', value: 'prod' }, + { name: 'os', value: 'mac' }, + { name: 'tag', value: 'v11.0.0' }, +]; + +const testCases = [ + { + name: 'match multiple by exact keys and values', + match: true, + matcherLabels: { + env: ['prod'], + os: ['mac'], + tag: ['v11.0.0'], + }, + }, + { + name: 'match by multivalues', + match: true, + matcherLabels: { + env: ['prod', 'staging'], + os: ['windows', 'mac', 'linux'], + tag: ['1', '2', '3', 'v11.0.0'], + }, + }, + { + name: 'match with one label set', + match: true, + matcherLabels: { + tag: ['v1', 'v11.0.0', 'v2'], + }, + }, + { + name: 'match by asteriks', + match: true, + matcherLabels: { + env: ['na'], + os: ['na'], + '*': ['*'], + }, + }, + { + name: 'match with a key and value asterik', + match: true, + matcherLabels: { + '*': ['prod', 'staging'], + os: ['*'], + }, + }, + { + name: 'match by asteriks with no db labels defined', + noDbLabels: true, + match: true, + matcherLabels: { '*': ['*'] }, + }, + { + name: 'no match with no db labels defined and no matcher labels', + noDbLabels: true, + match: false, + matcherLabels: {}, + }, + { + name: 'no match with no db labels with matcher labels', + noDbLabels: true, + match: false, + matcherLabels: { os: ['mac'] }, + }, + { + name: 'no match despite other matching labels', + match: false, + matcherLabels: { + '*': ['no-match'], // no match + env: ['prod'], + os: ['mac'], + tag: ['v11.0.0'], + }, + }, + { + name: 'no match with empty labels', + match: false, + matcherLabels: {}, + }, + { + name: 'no match with empty label values', + match: false, + matcherLabels: { os: [] }, + }, +]; + +describe('matchLabels()', () => { + test.each(testCases)('$name', ({ matcherLabels, match, noDbLabels }) => { + const isMatched = matchLabels(noDbLabels ? [] : newDbLabels, matcherLabels); + expect(isMatched).toEqual(match); + }); +}); diff --git a/web/packages/teleport/src/Discover/Discover.tsx b/web/packages/teleport/src/Discover/Discover.tsx index 691f51e6f18cb..4d22ac690130f 100644 --- a/web/packages/teleport/src/Discover/Discover.tsx +++ b/web/packages/teleport/src/Discover/Discover.tsx @@ -77,7 +77,7 @@ function DiscoverContent() { }} when={ viewConfig.shouldPrompt - ? viewConfig.shouldPrompt(currentStep) + ? viewConfig.shouldPrompt(currentStep, agentProps.resourceSpec) : true } /> diff --git a/web/packages/teleport/src/Discover/Navigation/StepItem.tsx b/web/packages/teleport/src/Discover/Navigation/StepItem.tsx index 7b041b0088cf3..c29520b685296 100644 --- a/web/packages/teleport/src/Discover/Navigation/StepItem.tsx +++ b/web/packages/teleport/src/Discover/Navigation/StepItem.tsx @@ -18,6 +18,8 @@ import React from 'react'; import styled from 'styled-components'; import { Flex } from 'design'; +import { icons } from 'teleport/Discover/SelectResource/icons'; + import { StepList } from './StepList'; import type { View } from 'teleport/Discover/flow'; @@ -51,7 +53,7 @@ export function StepItem(props: StepItemProps) { {getBulletIcon({ - Icon: props.selectedResource.Icon, + Icon: icons[props.selectedResource.icon], })} {props.selectedResource.name} @@ -155,6 +157,7 @@ const CheckedBullet = styled(Bullet)` :before { content: '✓'; + color: ${props => props.theme.colors.levels.popout}; } `; diff --git a/web/packages/teleport/src/Discover/SelectResource/SelectResource.tsx b/web/packages/teleport/src/Discover/SelectResource/SelectResource.tsx index d3ff189ee3397..0021af82aac04 100644 --- a/web/packages/teleport/src/Discover/SelectResource/SelectResource.tsx +++ b/web/packages/teleport/src/Discover/SelectResource/SelectResource.tsx @@ -35,6 +35,8 @@ import { RESOURCES, } from 'teleport/Discover/SelectResource/resources'; +import { icons } from './icons'; + import type { ResourceSpec } from './types'; import type { AddButtonResourceKind } from 'teleport/components/AgentButtonAdd/AgentButtonAdd'; @@ -160,7 +162,7 @@ export function SelectResource(props: SelectResourceProps) { )} - {r.Icon} + {icons[r.icon]} {pretitle && ( diff --git a/web/packages/teleport/src/Discover/SelectResource/__snapshots__/SelectResource.story.test.tsx.snap b/web/packages/teleport/src/Discover/SelectResource/__snapshots__/SelectResource.story.test.tsx.snap index 443e82a88fe4d..6f2a7c0466130 100644 --- a/web/packages/teleport/src/Discover/SelectResource/__snapshots__/SelectResource.story.test.tsx.snap +++ b/web/packages/teleport/src/Discover/SelectResource/__snapshots__/SelectResource.story.test.tsx.snap @@ -1192,6 +1192,47 @@ exports[`render with all access 1`] = ` + + + Guided + + + + + + + + Amazon Web Services (AWS) + + + Aurora PostgreSQL + + + + + + + Guided + + + + + + + + Amazon Web Services (AWS) + + + Aurora MySQL/MariaDB + + + + + + + Lacking Permissions + + + + + + + + Amazon Web Services (AWS) + + + Aurora PostgreSQL + + + + + + + Lacking Permissions + + + + + + + + Amazon Web Services (AWS) + + + Aurora MySQL/MariaDB + + + + + + + Lacking Permissions + + + + + + + + Amazon Web Services (AWS) + + + Aurora PostgreSQL + + + + + + + Lacking Permissions + + + + + + + + Amazon Web Services (AWS) + + + Aurora MySQL/MariaDB + + + + , Unknown: , }; + +export type ResourceIconName = keyof typeof icons; diff --git a/web/packages/teleport/src/Discover/SelectResource/resources.tsx b/web/packages/teleport/src/Discover/SelectResource/resources.tsx index 4236897602cdc..1e5b0a5d91f23 100644 --- a/web/packages/teleport/src/Discover/SelectResource/resources.tsx +++ b/web/packages/teleport/src/Discover/SelectResource/resources.tsx @@ -24,7 +24,6 @@ import { DATABASES_UNGUIDED_DOC, } from './databases'; import { ResourceSpec, DatabaseLocation, DatabaseEngine } from './types'; -import { icons } from './icons'; const baseServerKeywords = 'server node'; export const SERVERS: ResourceSpec[] = [ @@ -32,35 +31,35 @@ export const SERVERS: ResourceSpec[] = [ name: 'Ubuntu 14.04+', kind: ResourceKind.Server, keywords: baseServerKeywords + 'ubuntu', - Icon: icons.Linux, + icon: 'Linux', event: DiscoverEventResource.Server, }, { name: 'Debian 8+', kind: ResourceKind.Server, keywords: baseServerKeywords + 'debian', - Icon: icons.Linux, + icon: 'Linux', event: DiscoverEventResource.Server, }, { name: 'RHEL/CentOS 7+', kind: ResourceKind.Server, keywords: baseServerKeywords + 'rhel centos', - Icon: icons.Linux, + icon: 'Linux', event: DiscoverEventResource.Server, }, { name: 'Amazon Linux 2', kind: ResourceKind.Server, keywords: baseServerKeywords + 'amazon linux', - Icon: icons.Aws, + icon: 'Aws', event: DiscoverEventResource.Server, }, { name: 'macOS (Intel)', kind: ResourceKind.Server, keywords: baseServerKeywords + 'mac macos intel', - Icon: icons.Apple, + icon: 'Apple', event: DiscoverEventResource.Server, }, ]; @@ -70,7 +69,7 @@ export const APPLICATIONS: ResourceSpec[] = [ name: 'Application', kind: ResourceKind.Application, keywords: 'application', - Icon: icons.Application, + icon: 'Application', unguidedLink: 'https://goteleport.com/docs/application-access/getting-started/', event: DiscoverEventResource.ApplicationHttp, @@ -82,7 +81,7 @@ export const WINDOWS_DESKTOPS: ResourceSpec[] = [ name: 'Active Directory', kind: ResourceKind.Desktop, keywords: 'windows desktop active directory ad', - Icon: icons.Windows, + icon: 'Windows', event: DiscoverEventResource.WindowsDesktop, }, // { @@ -99,7 +98,7 @@ export const KUBERNETES: ResourceSpec[] = [ name: 'Kubernetes', kind: ResourceKind.Kubernetes, keywords: 'kubernetes cluster kubes', - Icon: icons.Kube, + icon: 'Kube', event: DiscoverEventResource.Kubernetes, }, ]; diff --git a/web/packages/teleport/src/Discover/SelectResource/types.ts b/web/packages/teleport/src/Discover/SelectResource/types.ts index ba1f284ff7f62..286f7de3921b6 100644 --- a/web/packages/teleport/src/Discover/SelectResource/types.ts +++ b/web/packages/teleport/src/Discover/SelectResource/types.ts @@ -17,6 +17,7 @@ import { ResourceKind } from '../Shared/ResourceKind'; import type { DiscoverEventResource } from 'teleport/services/userEvent'; +import type { ResourceIconName } from './icons'; export enum DatabaseLocation { Aws, @@ -52,7 +53,7 @@ export interface ResourceSpec { name: string; popular?: boolean; kind: ResourceKind; - Icon: React.ReactElement; + icon: ResourceIconName; // keywords are filter words that user may use to search for // this resource. keywords: string; diff --git a/web/packages/teleport/src/Discover/Server/SetupAccess/SetupAccess.tsx b/web/packages/teleport/src/Discover/Server/SetupAccess/SetupAccess.tsx index 8a802c1a191ec..06c617c07b275 100644 --- a/web/packages/teleport/src/Discover/Server/SetupAccess/SetupAccess.tsx +++ b/web/packages/teleport/src/Discover/Server/SetupAccess/SetupAccess.tsx @@ -72,7 +72,7 @@ export function SetupAccess(props: State) { const hasTraits = selectedLogins.length > 0; const canAddTraits = !props.isSsoUser && props.canEditUser; const headerSubtitle = - 'Select the OS users you will use to connect to server.'; + 'Select or create the OS users you will use to connect to server.'; return ( = (t: T) => View[]; export interface ResourceViewConfig { @@ -41,7 +43,7 @@ export interface ResourceViewConfig { // by "currentStep" param). // Not supplying a function is equivalent to always prompting // on exit or changing route. - shouldPrompt?: (currentStep: number) => boolean; + shouldPrompt?: (currentStep: number, resourceSpec: ResourceSpec) => boolean; } export interface View { diff --git a/web/packages/teleport/src/Discover/useDiscover.test.tsx b/web/packages/teleport/src/Discover/useDiscover.test.tsx index a1623a10f8196..8293d74e80452 100644 --- a/web/packages/teleport/src/Discover/useDiscover.test.tsx +++ b/web/packages/teleport/src/Discover/useDiscover.test.tsx @@ -292,6 +292,8 @@ describe('emitting events', () => { resource: DiscoverEventResource.Server, stepStatus: DiscoverEventStatus.Error, stepStatusError: 'some error message', + selectedResourcesCount: 0, + autoDiscoverResourcesCount: 0, }, }) ); diff --git a/web/packages/teleport/src/Discover/useDiscover.tsx b/web/packages/teleport/src/Discover/useDiscover.tsx index 875560ddf724a..ec7db069ef4e6 100644 --- a/web/packages/teleport/src/Discover/useDiscover.tsx +++ b/web/packages/teleport/src/Discover/useDiscover.tsx @@ -15,7 +15,7 @@ */ import React, { useContext, useState, useEffect, useCallback } from 'react'; -import { useHistory } from 'react-router'; +import { useHistory, useLocation } from 'react-router'; import { DiscoverEventStatus, @@ -66,6 +66,7 @@ type CustomEventInput = { eventName?: DiscoverEvent; eventResourceName?: DiscoverEventResource; autoDiscoverResourcesCount?: number; + selectedResourcesCount?: number; }; type DiscoverProviderProps = { @@ -73,18 +74,36 @@ type DiscoverProviderProps = { mockCtx?: DiscoverContextState; }; +// DiscoverUrlLocationState define fields to preserve state between +// react routes (eg. in RDS database flow, it is required of user +// to create a AWS OIDC integration which requires changing route +// and then coming back to resume the flow.) +export type DiscoverUrlLocationState = { + // discover contains the fields necessary to be able to resume + // the flow from where user left off. + discover: { + eventState: EventState; + resourceSpec: ResourceSpec; + currentStep: number; + }; + // integrationName is the name of the created integration + // resource name (eg: integration subkind "aws-oidc") + integrationName: string; +}; + const discoverContext = React.createContext(null); export function DiscoverProvider( props: React.PropsWithChildren ) { const history = useHistory(); + const location = useLocation(); const [currentStep, setCurrentStep] = useState(0); const [agentMeta, setAgentMeta] = useState(); const [resourceSpec, setResourceSpec] = useState(); const [viewConfig, setViewConfig] = useState(); - const [eventState, setEventState] = useState(); + const [eventState, setEventState] = useState({} as any); // indexedViews contains views of the selected resource where // each view has been assigned an index value. @@ -100,6 +119,7 @@ export function DiscoverProvider( id, resource: custom?.eventResourceName || resourceSpec?.event, autoDiscoverResourcesCount: custom?.autoDiscoverResourcesCount, + selectedResourcesCount: custom?.selectedResourcesCount, ...status, }, }); @@ -135,12 +155,14 @@ export function DiscoverProvider( }, [eventState, history.location.pathname, emitEvent]); useEffect(() => { - initEventState(); + if (location.state?.discover) { + resumeDiscoverFlow(); + } else { + initEventState(); + } }, []); function initEventState() { - // Generates a v4 UUID using a cryptographically secure - // random number. const id = crypto.randomUUID(); setEventState({ @@ -160,11 +182,46 @@ export function DiscoverProvider( }); } - // onSelectResources initializes all the required - // variables needed to start a guided flow. + // If a location.state.discover was provided, that means the user is + // coming back from another location to resume the flow. + // Users will resume at the step that is +1 from the step they left from. + // + // Example (only applies to AWS RDS & Aurora resources): + // A user can leave from route `web/discover/` + // to `web/integrations/enroll/` then + // come back to resume flow at `web/discover/` + // + // Resuming flow at `Enroll RDS Database` means the user has + // successfully finished the prior `Connect AWS Account` step, + // so we emit a success event for that step. + // + // The location.state.discover should contain all the state that allows + // the user to resume from where they left of. + function resumeDiscoverFlow() { + const { discover, integrationName } = location.state; + + updateAgentMeta({ integrationName } as DbMeta); + + startDiscoverFlow( + discover.resourceSpec, + discover.eventState, + discover.currentStep + 1 + ); + + emitEvent( + { stepStatus: DiscoverEventStatus.Success }, + { + eventName: discover.eventState.currEventName, + eventResourceName: discover.resourceSpec.event, + } + ); + } + + // onSelectResources inits states, starts flow, and + // emits events. function onSelectResource(resource: ResourceSpec) { - // We still want to emit an event if user clicked on - // unguided links to gather data on which unguided resource + // We still want to emit an event if user clicked on an + // unguided link to gather data on which unguided resource // is most popular. if (resource.unguidedLink) { emitEvent( @@ -177,6 +234,27 @@ export function DiscoverProvider( return; } + startDiscoverFlow(resource, eventState); + + // At this point it's considered the user has + // successfully selected a resource, so we send an event + // for it. + emitEvent( + { stepStatus: DiscoverEventStatus.Success }, + { + eventName: DiscoverEvent.ResourceSelection, + eventResourceName: resource.event, + } + ); + } + + // startDiscoverFlow sets all the required states + // that will begin the flow. + function startDiscoverFlow( + resource: ResourceSpec, + initEventState: EventState, + targetViewIndex = 0 + ) { // Process each view and assign each with an index number. const currCfg = viewConfigs.find(r => r.kind === resource.kind); let indexedViews = []; @@ -186,30 +264,22 @@ export function DiscoverProvider( indexedViews = addIndexToViews(currCfg.views); } - // Find the first view to update the event state. + // Find the target view to update the event state. const { eventName, manuallyEmitSuccessEvent } = findViewAtIndex( indexedViews, - currentStep - ); - // At this point it's considered the user has - // successfully selected a resource, so we send an event. - emitEvent( - { stepStatus: DiscoverEventStatus.Success }, - { - eventName: DiscoverEvent.ResourceSelection, - eventResourceName: resource.event, - } + targetViewIndex ); // Init all required states to start the flow. setEventState({ - ...eventState, + ...initEventState, currEventName: eventName, manuallyEmitSuccessEvent, }); setViewConfig(currCfg); setIndexedViews(indexedViews); setResourceSpec(resource); + setCurrentStep(targetViewIndex); } // nextStep takes the user to next screen and sends reporting events. @@ -247,7 +317,17 @@ export function DiscoverProvider( if (!numToIncrement) { emitEvent({ stepStatus: DiscoverEventStatus.Skipped }); } else if (!eventState.manuallyEmitSuccessEvent) { - emitEvent({ stepStatus: DiscoverEventStatus.Success }); + // TODO(lisa): Currently the RDS enroll screen only allows + // user to select one RDS database to enroll so we hard code + // it for now. + if (eventState.currEventName === DiscoverEvent.DatabaseRDSEnrollEvent) { + emitEvent( + { stepStatus: DiscoverEventStatus.Success }, + { selectedResourcesCount: 1 } + ); + } else { + emitEvent({ stepStatus: DiscoverEventStatus.Success }); + } } // Whenever a numToIncrement is > 1, it means some steps (after the current view) @@ -288,10 +368,13 @@ export function DiscoverProvider( } function emitErrorEvent(errorStr = '') { - emitEvent({ - stepStatus: DiscoverEventStatus.Error, - stepStatusError: errorStr, - }); + emitEvent( + { + stepStatus: DiscoverEventStatus.Error, + stepStatusError: errorStr, + }, + { autoDiscoverResourcesCount: 0, selectedResourcesCount: 0 } + ); } const value: DiscoverContextState = { @@ -342,6 +425,8 @@ export type NodeMeta = BaseMeta & { // DbMeta describes the fields for a db resource // that needs to be preserved throughout the flow. export type DbMeta = BaseMeta & { + // TODO(lisa): when we can enroll multiple RDS's, turn this into an array? + // The enroll event expects num count of enrolled RDS's, update accordingly. db: Database; integrationName?: string; }; diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/instructions/FifthStageInstructions.tsx b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/instructions/FifthStageInstructions.tsx index 35966bb6ba8b4..23f13b8cfd161 100644 --- a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/instructions/FifthStageInstructions.tsx +++ b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/instructions/FifthStageInstructions.tsx @@ -32,7 +32,10 @@ export function FifthStageInstructions(props: CommonInstructionsProps) { "Statement": [ { "Effect": "Allow", - "Action": "rds:DescribeDBInstances", + "Action": [ + "rds:DescribeDBInstances", + "rds:DescribeDBClusters" + ], "Resource": "*" } ] diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/instructions/Instructions.story.tsx b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/instructions/Instructions.story.tsx index 4125dae4d58c1..2aab67528b761 100644 --- a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/instructions/Instructions.story.tsx +++ b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/instructions/Instructions.story.tsx @@ -28,6 +28,8 @@ import { SuccessfullyAddedIntegrationDialog, } from './SeventhStageInstructions'; +import type { DiscoverUrlLocationState } from 'teleport/Discover/useDiscover'; + export default { title: 'Teleport/Integrations/Enroll/AwsOidc/Instructions', }; @@ -46,19 +48,15 @@ export const Step7 = () => ( export const ConfirmDialog = () => ( - + ); export const ConfirmDialogFromDiscover = () => ( - - + + ); diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/instructions/SecondStageInstructions.tsx b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/instructions/SecondStageInstructions.tsx index 324fdfdac500f..df09c181d307a 100644 --- a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/instructions/SecondStageInstructions.tsx +++ b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/instructions/SecondStageInstructions.tsx @@ -60,7 +60,7 @@ export function SecondStageInstructions(props: CommonInstructionsProps) { bash={false} lines={[ { - text: `https://${props.clusterPublicUri}`, + text: `https://${getClusterPublicUri(props.clusterPublicUri)}`, }, ]} /> @@ -92,6 +92,7 @@ export function SecondStageInstructions(props: CommonInstructionsProps) { <> setThumbprint(e.target.value)} value={thumbprint} @@ -110,3 +111,15 @@ export function SecondStageInstructions(props: CommonInstructionsProps) { ); } + +function getClusterPublicUri(uri: string) { + const uriParts = uri.split(':'); + const port = uriParts.length > 1 ? uriParts[1] : ''; + + // Strip 443 ports from uri. + if (port === '443') { + return uriParts[0]; + } + + return uri; +} diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/instructions/SeventhStageInstructions.tsx b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/instructions/SeventhStageInstructions.tsx index db9c2ec456f0a..d3cc7a3fbd1b5 100644 --- a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/instructions/SeventhStageInstructions.tsx +++ b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/instructions/SeventhStageInstructions.tsx @@ -37,12 +37,11 @@ import { integrationService, } from 'teleport/services/integrations'; import cfg from 'teleport/config'; +import { DiscoverUrlLocationState } from 'teleport/Discover/useDiscover'; import { InstructionsContainer } from './common'; export function SeventhStageInstructions() { - const location = useLocation<{ discoverEventId: string }>(); - const { attempt, setAttempt } = useAttempt(''); const [showConfirmBox, setShowConfirmBox] = useState(false); const [roleArn, setRoleArn] = useState(''); @@ -79,6 +78,7 @@ export function SeventhStageInstructions() { Copy the role ARN and paste it below setRoleArn(e.target.value)} value={roleArn} @@ -108,22 +108,19 @@ export function SeventhStageInstructions() { )} {showConfirmBox && ( - + )} ); } export function SuccessfullyAddedIntegrationDialog({ - discoverEventId, integrationName, }: { - discoverEventId: string; integrationName: string; }) { + const location = useLocation(); + return ( ({ maxWidth: '500px', width: '100%' })} @@ -140,16 +137,15 @@ export function SuccessfullyAddedIntegrationDialog({ - {discoverEventId ? ( + {location.state?.discover ? ( diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/instructions/SixthStageInstructions.tsx b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/instructions/SixthStageInstructions.tsx index 240dca368be3a..1b81159d2102b 100644 --- a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/instructions/SixthStageInstructions.tsx +++ b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/instructions/SixthStageInstructions.tsx @@ -28,14 +28,15 @@ import type { CommonInstructionsProps } from './common'; export function SixthStageInstructions(props: CommonInstructionsProps) { return ( - Close the "Create policy tab" + + Close the tab for "Create Policy" and go back to the tab for{' '} + Create Role + Refresh the list of policies and select the policy you just created - Search for the policy you just created and select it - Click Next: Tags and then Next: Review diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/instructions/ThirdStageInstructions.tsx b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/instructions/ThirdStageInstructions.tsx index d96c76576907c..2d3b3033c209d 100644 --- a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/instructions/ThirdStageInstructions.tsx +++ b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/instructions/ThirdStageInstructions.tsx @@ -38,7 +38,8 @@ export function ThirdStageInstructions(props: CommonInstructionsProps) { - Select Assign role and create a new role + Click Assign role and select{' '} + Create a new role diff --git a/web/packages/teleport/src/services/integrations/integrations.test.ts b/web/packages/teleport/src/services/integrations/integrations.test.ts index 0c665e745f6d1..f889056065685 100644 --- a/web/packages/teleport/src/services/integrations/integrations.test.ts +++ b/web/packages/teleport/src/services/integrations/integrations.test.ts @@ -123,8 +123,8 @@ test('fetchAwsDatabases response', async () => { engine: 'mysql', name: 'rds-2', uri: 'endpoint-2', - status: 'Available', labels: [], + status: undefined, accountId: undefined, resourceId: undefined, }, @@ -132,8 +132,8 @@ test('fetchAwsDatabases response', async () => { engine: 'mysql', name: 'rds-3', uri: 'endpoint-3', - status: 'Available', labels: [], + status: undefined, accountId: undefined, resourceId: undefined, }, @@ -200,9 +200,9 @@ const mockAwsDbs = [ protocol: 'postgres', name: 'rds-1', uri: 'endpoint-1', - status: 'Available', labels: [{ name: 'env', value: 'prod' }], aws: { + status: 'Available', account_id: 'account-id-1', rds: { resource_id: 'resource-id-1', @@ -214,7 +214,6 @@ const mockAwsDbs = [ protocol: 'mysql', name: 'rds-2', uri: 'endpoint-2', - status: 'Available', aws: {}, }, // Test without aws field. @@ -222,6 +221,5 @@ const mockAwsDbs = [ protocol: 'mysql', name: 'rds-3', uri: 'endpoint-3', - status: 'Available', }, ]; diff --git a/web/packages/teleport/src/services/integrations/integrations.ts b/web/packages/teleport/src/services/integrations/integrations.ts index 5f3e4bb29b643..98af8fab4b009 100644 --- a/web/packages/teleport/src/services/integrations/integrations.ts +++ b/web/packages/teleport/src/services/integrations/integrations.ts @@ -133,13 +133,13 @@ function makeIntegration(json: any): Integration { export function makeAwsDatabase(json: any): AwsRdsDatabase { json = json ?? {}; - const { aws, name, uri, status, labels, protocol } = json; + const { aws, name, uri, labels, protocol } = json; return { engine: protocol, name, uri, - status, + status: aws?.status, labels: labels ?? [], resourceId: aws?.rds?.resource_id, accountId: aws?.account_id, diff --git a/web/packages/teleport/src/services/integrations/types.ts b/web/packages/teleport/src/services/integrations/types.ts index 2657af5b98ea0..5a5b70502f238 100644 --- a/web/packages/teleport/src/services/integrations/types.ts +++ b/web/packages/teleport/src/services/integrations/types.ts @@ -192,7 +192,7 @@ export type AwsRdsDatabase = { // There is a lot of status states available so only a select few were // hard defined to use to determine the status color. // https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/accessing-monitoring.html - status: 'Available' | 'Failed' | 'Deleting'; + status: 'available' | 'failed' | 'deleting'; }; export type ListAwsRdsDatabaseResponse = { diff --git a/web/packages/teleport/src/services/user/makeAcl.ts b/web/packages/teleport/src/services/user/makeAcl.ts index d860e976eb2b8..9f15b8a91692d 100644 --- a/web/packages/teleport/src/services/user/makeAcl.ts +++ b/web/packages/teleport/src/services/user/makeAcl.ts @@ -31,10 +31,7 @@ export function makeAcl(json): Acl { const accessRequests = json.accessRequests || defaultAccess; const billing = json.billing || defaultAccess; const plugins = json.plugins || defaultAccess; - // TODO(lisa): requires backend changes to user context. - // Feature is off until all TODO related to integrations is done. - // const integrations = json.integrations || defaultAccessWithUse; - const integrations = defaultAccessWithUse; + const integrations = json.integrations || defaultAccessWithUse; const dbServers = json.dbServers || defaultAccess; const db = json.db || defaultAccess; const desktops = json.desktops || defaultAccess;