Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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