Skip to content
Merged
11 changes: 11 additions & 0 deletions web/packages/shared/utils/errorType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,14 @@ import { privateKeyEnablingPolicies } from 'shared/services';
export function isPrivateKeyRequiredError(err: Error) {
return privateKeyEnablingPolicies.some(p => err.message.includes(p));
}

// getErrMessage first checks if the error is of type Error
// before attempting to access the error message field.
// Used with try catch blocks, where the error caught
// may not necessary be of type Error.
export function getErrMessage(err: unknown) {
let message = 'something went wrong';
if (err instanceof Error) message = err.message;

return message;
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const props: State = {
attempt: { status: '' },
clearAttempt: () => null,
registerDatabase: () => null,
fetchDatabaseServers: () => null,
canCreateDatabase: true,
pollTimeout: Date.now() + 30000,
dbEngine: DatabaseEngine.Postgres,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export function CreateDatabaseDialog({
<>
<Text mb={5}>
<Icons.Warning ml={1} mr={2} color="error.main" />
Register Failed: {attempt.statusText}
{attempt.statusText}
</Text>
<Flex>
<ButtonPrimary mr={3} width="50%" onClick={retry}>
Expand Down Expand Up @@ -104,7 +104,7 @@ export function CreateDatabaseDialog({
return (
<Dialog disableEscapeKeyDown={false} open={true}>
<DialogContent
width="400px"
width="460px"
alignItems="center"
mb={0}
textAlign="center"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
import { useEffect, useState } from 'react';

import useAttempt from 'shared/hooks/useAttemptNext';
import { getErrMessage } from 'shared/utils/errorType';

import useTeleport from 'teleport/useTeleport';
import { useDiscover } from 'teleport/Discover/useDiscover';
import { usePoll } from 'teleport/Discover/Shared/usePoll';
import { compareByString } from 'teleport/lib/util';
import { ApiError } from 'teleport/services/api/parseError';

import { matchLabels } from '../util';

Expand Down Expand Up @@ -133,6 +135,14 @@ export function useCreateDatabase() {
});
}

function fetchDatabaseServers(query: string, limit: number) {
const request = {
query,
limit,
};
return ctx.databaseService.fetchDatabases(clusterId, request);
}

async function registerDatabase(db: CreateDatabaseRequest, newDb = false) {
// Set the timeout now, because this entire registering process
// should take less than WAITING_TIMEOUT.
Expand All @@ -146,6 +156,13 @@ export function useCreateDatabase() {
await ctx.databaseService.createDatabase(clusterId, db);
setCreatedDb(db);
} catch (err) {
// Check if the error is a result of an existing database.
if (err instanceof ApiError) {
if (err.response.status === 409) {
const isAwsRds = Boolean(db.awsRds && db.awsRds.accountId);
return attemptDbServerQueryAndBuildErrMsg(db.name, isAwsRds);
}
}
handleRequestError(err, 'failed to create database: ');
setIsDbCreateErr(true);
return;
Expand Down Expand Up @@ -192,6 +209,45 @@ export function useCreateDatabase() {
setPollActive(true);
}

// attemptDbServerQueryAndBuildErrMsg tests if the duplicated `dbName`
// (determined by an error returned from the initial register db attempt)
// is already a part of the cluster by querying for its db server.
// This is an attempt to provide accurate actionable steps for the
// user.
async function attemptDbServerQueryAndBuildErrMsg(
dbName: string,
isAwsRds = false
) {
const preErrMsg = 'failed to register database: ';
const nonAwsMsg = `use a different name and try again`;
const awsMsg = `change (or define) the value of the \
tag "teleport.dev/database_name" on the RDS instance and try again`;

try {
await ctx.databaseService.fetchDatabase(clusterId, dbName);
let message = `a database with the name "${dbName}" is already \
a part of this cluster, ${isAwsRds ? awsMsg : nonAwsMsg}`;
handleRequestError(new Error(message), preErrMsg);
} catch (e) {
// No database server were found for the database name.
if (e instanceof ApiError) {
if (e.response.status === 404) {
let message = `a database with the name "${dbName}" already exists \
but there are no database servers for it, you can remove this \
database using the command, “tctl rm db/${dbName}”, or ${
isAwsRds ? awsMsg : nonAwsMsg
}`;
handleRequestError(new Error(message), preErrMsg);
}
return;
}

// Display other errors as is.
handleRequestError(e, preErrMsg);
}
setIsDbCreateErr(true);
}

function requiresDbUpdate(db: CreateDatabaseRequest) {
if (!createdDb) {
return false;
Expand Down Expand Up @@ -223,8 +279,7 @@ export function useCreateDatabase() {
}

function handleRequestError(err: Error, preErrMsg = '') {
let message = 'something went wrong';
if (err instanceof Error) message = err.message;
const message = getErrMessage(err);
setAttempt({ status: 'failed', statusText: `${preErrMsg}${message}` });
emitErrorEvent(`${preErrMsg}${message}`);
}
Expand All @@ -235,6 +290,7 @@ export function useCreateDatabase() {
attempt,
clearAttempt,
registerDatabase,
fetchDatabaseServers,
canCreateDatabase: access.create,
pollTimeout,
dbEngine: resourceSpec.dbMeta.engine,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@

import React from 'react';

import { AwsRdsDatabase } from 'teleport/services/integrations';

import { AwsRegionSelector } from './AwsRegionSelector';
import { DatabaseList } from './RdsDatabaseList';
import { CheckedAwsRdsDatabase } from './EnrollRdsDatabase';

export default {
title: 'Teleport/Discover/Database/EnrollRds',
Expand Down Expand Up @@ -85,7 +84,7 @@ export const RdsDatabaseListLoading = () => (
/>
);

const fixtures: AwsRdsDatabase[] = [
const fixtures: CheckedAwsRdsDatabase[] = [
{
name: 'postgres-name',
engine: 'postgres',
Expand All @@ -103,6 +102,7 @@ const fixtures: AwsRdsDatabase[] = [
status: 'available',
accountId: '',
resourceId: '',
dbServerExists: true,
},
{
name: 'alpaca',
Expand Down Expand Up @@ -137,6 +137,7 @@ const fixtures: AwsRdsDatabase[] = [
status: 'Unknown' as any,
accountId: '',
resourceId: '',
dbServerExists: true,
},
{
name: 'llama',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,17 @@ import { FetchStatus } from 'design/DataTable/types';
import { Danger } from 'design/Alert';

import useAttempt from 'shared/hooks/useAttemptNext';
import { getErrMessage } from 'shared/utils/errorType';

import { DbMeta, useDiscover } from 'teleport/Discover/useDiscover';
import {
AwsRdsDatabase,
ListAwsRdsDatabaseResponse,
RdsEngineIdentifier,
Regions,
integrationService,
} from 'teleport/services/integrations';
import { DatabaseEngine } from 'teleport/Discover/SelectResource';
import { Database } from 'teleport/services/databases';

import { ActionButtons, Header } from '../../Shared';

Expand All @@ -40,7 +41,7 @@ import { AwsRegionSelector } from './AwsRegionSelector';
import { DatabaseList } from './RdsDatabaseList';

type TableData = {
items: ListAwsRdsDatabaseResponse['databases'];
items: CheckedAwsRdsDatabase[];
fetchStatus: FetchStatus;
startKey?: string;
currRegion?: Regions;
Expand All @@ -52,6 +53,14 @@ const emptyTableData: TableData = {
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 EnrollRdsDatabase() {
const {
createdDb,
Expand All @@ -60,6 +69,7 @@ export function EnrollRdsDatabase() {
attempt: registerAttempt,
clearAttempt: clearRegisterAttempt,
nextStep,
fetchDatabaseServers,
} = useCreateDatabase();

const { agentMeta, resourceSpec, emitErrorEvent } = useDiscover();
Expand All @@ -71,7 +81,7 @@ export function EnrollRdsDatabase() {
startKey: '',
fetchStatus: 'disabled',
});
const [selectedDb, setSelectedDb] = useState<AwsRdsDatabase>();
const [selectedDb, setSelectedDb] = useState<CheckedAwsRdsDatabase>();

function fetchDatabasesWithNewRegion(region: Regions) {
// Clear table when fetching with new region.
Expand All @@ -87,36 +97,69 @@ export function EnrollRdsDatabase() {
fetchDatabases({ ...tableData, startKey: '', items: [] });
}

function fetchDatabases(data: TableData) {
async function fetchDatabases(data: TableData) {
const integrationName = (agentMeta as DbMeta).integrationName;

setTableData({ ...data, fetchStatus: 'loading' });
setFetchDbAttempt({ status: 'processing' });

integrationService
.fetchAwsRdsDatabases(
integrationName,
getRdsEngineIdentifier(resourceSpec.dbMeta?.engine),
{
region: data.currRegion,
nextToken: data.startKey,
try {
const { databases: fetchedRdsDbs, nextToken } =
await integrationService.fetchAwsRdsDatabases(
integrationName,
getRdsEngineIdentifier(resourceSpec.dbMeta?.engine),
{
region: data.currRegion,
nextToken: data.startKey,
}
);

// 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<string, Database> = {};
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,
};
}
)
.then(resp => {
setFetchDbAttempt({ status: 'success' });
setTableData({
currRegion: data.currRegion,
startKey: resp.nextToken,
fetchStatus: resp.nextToken ? '' : 'disabled',
// concat each page fetch.
items: [...data.items, ...resp.databases],
});
})
.catch((err: Error) => {
setFetchDbAttempt({ status: 'failed', statusText: err.message });
setTableData(data); // fallback to previous data
emitErrorEvent(`failed to fetch aws rds list: ${err.message}`);
return rds;
});

setFetchDbAttempt({ status: 'success' });
setTableData({
currRegion: data.currRegion,
startKey: nextToken,
fetchStatus: nextToken ? '' : 'disabled',
// concat each page fetch.
items: [...data.items, ...checkedRdsDbs],
});
} catch (err) {
const errMsg = getErrMessage(err);
setFetchDbAttempt({ status: 'failed', statusText: errMsg });
setTableData(data); // fallback to previous data
emitErrorEvent(`database fetch error: ${errMsg}`);
}
}

function clear() {
Expand Down
Loading