Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v13] WebDiscover: Enable auto deploy and skip IAM policy screen on condition #29978

Merged
merged 2 commits into from Aug 3, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -27,13 +27,14 @@ import {
} from 'design';
import FieldSelect from 'shared/components/FieldSelect';
import useAttempt from 'shared/hooks/useAttemptNext';
import { Option } from 'shared/components/Select';
import { Option as BaseOption } from 'shared/components/Select';
import Validation, { Validator } from 'shared/components/Validation';
import { requiredField } from 'shared/components/Validation/rules';
import TextEditor from 'shared/components/TextEditor';

import cfg from 'teleport/config';
import {
Integration,
IntegrationKind,
integrationService,
} from 'teleport/services/integrations';
Expand All @@ -48,6 +49,8 @@ import {
useDiscover,
} from '../../useDiscover';

type Option = BaseOption<Integration>;

export function ConnectAwsAccount() {
const { storeUser } = useTeleport();
const {
Expand Down Expand Up @@ -86,7 +89,7 @@ export function ConnectAwsAccount() {
const options = res.items.map(i => {
if (i.kind === 'aws-oidc') {
return {
value: i.name,
value: i,
label: i.name,
};
}
Expand Down Expand Up @@ -149,7 +152,7 @@ export function ConnectAwsAccount() {

updateAgentMeta({
...(agentMeta as DbMeta),
integrationName: selectedAwsIntegration.value,
integration: selectedAwsIntegration.value,
});

nextStep();
Expand Down
Expand Up @@ -32,15 +32,17 @@ import {
DatabaseEngine,
DatabaseLocation,
} from 'teleport/Discover/SelectResource';
import {
IamPolicyStatus,
CreateDatabaseRequest,
} from 'teleport/services/databases';

import {
useCreateDatabase,
findActiveDatabaseSvc,
WAITING_TIMEOUT,
} from './useCreateDatabase';

import type { CreateDatabaseRequest } from 'teleport/services/databases';

const dbLabels = [
{ name: 'env', value: 'prod' },
{ name: 'os', value: 'mac' },
Expand Down Expand Up @@ -312,7 +314,7 @@ describe('registering new databases, mainly error checking', () => {
jest.clearAllMocks();
});

test('with matching service, activates polling', async () => {
test('polling until result returns (non aws)', async () => {
const { result } = renderHook(() => useCreateDatabase(), {
wrapper,
});
Expand Down Expand Up @@ -342,6 +344,96 @@ describe('registering new databases, mainly error checking', () => {
expect(discoverCtx.nextStep).toHaveBeenCalledWith(2);
});

test('continue polling when poll result returns with iamPolicyStatus field set to "pending"', async () => {
jest.spyOn(teleCtx.databaseService, 'fetchDatabases').mockResolvedValue({
agents: [
{
name: 'new-db',
aws: { iamPolicyStatus: IamPolicyStatus.Pending },
} as any,
],
});
const { result } = renderHook(() => useCreateDatabase(), {
wrapper,
});

await act(async () => {
result.current.registerDatabase(newDatabaseReq);
});
expect(teleCtx.databaseService.createDatabase).toHaveBeenCalledTimes(1);
expect(teleCtx.databaseService.fetchDatabaseServices).toHaveBeenCalledTimes(
1
);

// The first result will not have the aws marker we are looking for.
// Polling should continue.
await act(async () => jest.advanceTimersByTime(3000));
expect(teleCtx.databaseService.fetchDatabases).toHaveBeenCalledTimes(1);
expect(discoverCtx.updateAgentMeta).not.toHaveBeenCalled();

// Set the marker we are looking for in the next api reply.
jest.clearAllMocks();
jest.spyOn(teleCtx.databaseService, 'fetchDatabases').mockResolvedValue({
agents: [
{
name: 'new-db',
aws: { iamPolicyStatus: IamPolicyStatus.Success },
} as any,
],
});

// The second poll result has the marker that should cancel polling.
await act(async () => jest.advanceTimersByTime(3000));
expect(teleCtx.databaseService.fetchDatabases).toHaveBeenCalledTimes(1);
expect(discoverCtx.updateAgentMeta).toHaveBeenCalledWith({
resourceName: 'db-name',
db: {
name: 'new-db',
aws: { iamPolicyStatus: IamPolicyStatus.Success },
},
serviceDeployedMethod: 'skipped',
});

result.current.nextStep();
// Skips both deploy service AND IAM policy step.
expect(discoverCtx.nextStep).toHaveBeenCalledWith(3);
});

test('stops polling when poll result returns with iamPolicyStatus field set to "unspecified"', async () => {
jest.spyOn(teleCtx.databaseService, 'fetchDatabases').mockResolvedValue({
agents: [
{
name: 'new-db',
aws: { iamPolicyStatus: IamPolicyStatus.Unspecified },
} as any,
],
});
const { result } = renderHook(() => useCreateDatabase(), {
wrapper,
});

await act(async () => {
result.current.registerDatabase(newDatabaseReq);
});
expect(teleCtx.databaseService.createDatabase).toHaveBeenCalledTimes(1);
expect(teleCtx.databaseService.fetchDatabaseServices).toHaveBeenCalledTimes(
1
);

await act(async () => jest.advanceTimersByTime(3000));
expect(teleCtx.databaseService.fetchDatabases).toHaveBeenCalledTimes(1);
expect(discoverCtx.updateAgentMeta).toHaveBeenCalledWith({
resourceName: 'db-name',
db: {
name: 'new-db',
aws: { iamPolicyStatus: IamPolicyStatus.Unspecified },
},
});

result.current.nextStep();
expect(discoverCtx.nextStep).toHaveBeenCalledWith(2);
});

test('when there are no services, skips polling', async () => {
jest
.spyOn(teleCtx.databaseService, 'fetchDatabaseServices')
Expand Down
Expand Up @@ -23,6 +23,8 @@ 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 { DatabaseLocation } from 'teleport/Discover/SelectResource';
import { IamPolicyStatus } from 'teleport/services/databases';

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

Expand Down Expand Up @@ -67,6 +69,8 @@ export function useCreateDatabase() {
// backend error or network failure)
const [createdDb, setCreatedDb] = useState<CreateDatabaseRequest>();

const isAws = resourceSpec.dbMeta.location === DatabaseLocation.Aws;

const dbPollingResult = usePoll<DatabaseResource>(
signal => fetchDatabaseServer(signal),
pollActive, // does not poll on init, since the value is false.
Expand Down Expand Up @@ -115,11 +119,17 @@ export function useCreateDatabase() {
resourceName: createdDb.name,
agentMatcherLabels: dbPollingResult.labels,
db: dbPollingResult,
serviceDeployedMethod:
dbPollingResult.aws?.iamPolicyStatus === IamPolicyStatus.Success
? 'skipped'
: undefined, // User has to deploy a service (can be auto or manual)
});

setAttempt({ status: 'success' });
}, [dbPollingResult]);

// fetchDatabaseServer is the callback that is run every interval by the poller.
// The poller will stop polling once a result returns (a dbServer).
function fetchDatabaseServer(signal: AbortSignal) {
const request = {
search: createdDb.name,
Expand All @@ -129,8 +139,21 @@ export function useCreateDatabase() {
.fetchDatabases(clusterId, request, signal)
.then(res => {
if (res.agents.length) {
return res.agents[0];
const dbServer = res.agents[0];
if (
!isAws || // If not AWS, then we return the first thing we get back.
// If AWS and aws.iamPolicyStatus is undefined or non-pending,
// return the dbServer.
dbServer.aws?.iamPolicyStatus !== IamPolicyStatus.Pending
) {
return dbServer;
}
}
// Returning nothing here will continue the polling.
// Either no result came back back yet or
// a result did come back but we are waiting for a specific
// marker to appear in the result. Specifically for AWS dbs,
// we wait for a non-pending flag to appear.
return null;
});
}
Expand Down Expand Up @@ -285,6 +308,21 @@ export function useCreateDatabase() {
emitErrorEvent(`${preErrMsg}${message}`);
}

function handleNextStep() {
if (dbPollingResult) {
if (
isAws &&
dbPollingResult.aws?.iamPolicyStatus === IamPolicyStatus.Success
) {
// Skips the deploy db service step AND setting up IAM policy step.
return nextStep(3);
}
// Skips the deploy database service step.
return nextStep(2);
}
nextStep(); // Goes to deploy database service step.
}

const access = ctx.storeUser.getDatabaseAccess();
return {
createdDb,
Expand All @@ -298,9 +336,7 @@ export function useCreateDatabase() {
dbLocation: resourceSpec.dbMeta.location,
isDbCreateErr,
prevStep,
// If there was a result from database polling, then
// allow user to skip the next step.
nextStep: dbPollingResult ? () => nextStep(2) : () => nextStep(),
nextStep: handleNextStep,
};
}

Expand Down
Expand Up @@ -31,6 +31,7 @@ import {
DiscoverProvider,
DiscoverContextState,
} from 'teleport/Discover/useDiscover';
import { IntegrationStatusCode } from 'teleport/services/integrations';

import { AutoDeploy } from './AutoDeploy';

Expand Down Expand Up @@ -69,6 +70,15 @@ const Provider = props => {
agentMatcherLabels: [],
db: {} as any,
selectedAwsRdsDb: { region: 'us-east-1' } as any,
integration: {
kind: 'aws-oidc',
name: 'integration/aws-oidc',
resourceType: 'integration',
spec: {
roleArn: 'arn-123',
},
statusCode: IntegrationStatusCode.Running,
},
...props.agentMeta,
},
currentStep: 0,
Expand Down