WebDiscover: Finish auto deploy screen (iam configure script) (#28621)

* Define the return type

* Add endpoint for config script

* Store the entire integration object instead of just the name

* Build the correct script string, renames, emit event

* Enable auto deploy as default

* Fix script endpoint and update story

* Add regex check, update story

* Touch ups, add test

* Address CR

* Remove sudo from bash command

* Make into ui friendly object
This commit is contained in:
Lisa Kim 2023-07-10 08:09:03 -07:00 committed by GitHub
parent c6529af658
commit f6938613d2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 464 additions and 75 deletions

View file

@ -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';
@ -48,6 +49,8 @@ import {
useDiscover,
} from '../../useDiscover';
type Option = BaseOption<Integration>;
export function ConnectAwsAccount() {
const { storeUser } = useTeleport();
const {
@ -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,
};
}
@ -149,7 +152,7 @@ export function ConnectAwsAccount() {
updateAgentMeta({
...(agentMeta as DbMeta),
integrationName: selectedAwsIntegration.value,
integration: selectedAwsIntegration.value,
});
nextStep();

View file

@ -31,6 +31,7 @@ import {
DiscoverProvider,
DiscoverContextState,
} from 'teleport/Discover/useDiscover';
import { IntegrationStatusCode } from 'teleport/services/integrations';
import { AutoDeploy } from './AutoDeploy';
@ -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,

View file

@ -0,0 +1,222 @@
/**
* 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 React from 'react';
import { MemoryRouter } from 'react-router';
import { render, screen, fireEvent, act } from 'design/utils/testing';
import { ContextProvider } from 'teleport';
import {
AwsRdsDatabase,
Integration,
IntegrationKind,
IntegrationStatusCode,
Regions,
integrationService,
} from 'teleport/services/integrations';
import { createTeleportContext } from 'teleport/mocks/contexts';
import cfg from 'teleport/config';
import TeleportContext from 'teleport/teleportContext';
import {
DbMeta,
DiscoverContextState,
DiscoverProvider,
} from 'teleport/Discover/useDiscover';
import {
DatabaseEngine,
DatabaseLocation,
} from 'teleport/Discover/SelectResource';
import { FeaturesContextProvider } from 'teleport/FeaturesContext';
import { PingTeleportProvider } from 'teleport/Discover/Shared/PingTeleportContext';
import { ResourceKind } from 'teleport/Discover/Shared';
import { SHOW_HINT_TIMEOUT } from 'teleport/Discover/Shared/useShowHint';
import { AutoDeploy } from './AutoDeploy';
const mockDbLabels = [{ name: 'env', value: 'prod' }];
const integrationName = 'aws-oidc-integration';
const region: Regions = 'us-east-2';
const awsoidcRoleArn = 'role-arn';
const mockAwsRdsDb: AwsRdsDatabase = {
engine: 'postgres',
name: 'rds-1',
uri: 'endpoint-1',
status: 'available',
labels: mockDbLabels,
accountId: 'account-id-1',
resourceId: 'resource-id-1',
region: region,
subnets: ['subnet1', 'subnet2'],
};
const mocKIntegration: Integration = {
kind: IntegrationKind.AwsOidc,
name: integrationName,
resourceType: 'integration',
spec: {
roleArn: `doncare/${awsoidcRoleArn}`,
},
statusCode: IntegrationStatusCode.Running,
};
describe('test AutoDeploy.tsx', () => {
jest.useFakeTimers();
const teleCtx = createTeleportContext();
const discoverCtx: DiscoverContextState = {
agentMeta: {
resourceName: 'db1',
integration: mocKIntegration,
selectedAwsRdsDb: mockAwsRdsDb,
agentMatcherLabels: mockDbLabels,
} as DbMeta,
currentStep: 0,
nextStep: jest.fn(x => x),
prevStep: () => null,
onSelectResource: () => null,
resourceSpec: {
dbMeta: {
location: DatabaseLocation.Aws,
engine: DatabaseEngine.AuroraMysql,
},
} as any,
viewConfig: null,
exitFlow: null,
indexedViews: [],
setResourceSpec: () => null,
updateAgentMeta: jest.fn(x => x),
emitErrorEvent: () => null,
emitEvent: () => null,
eventState: null,
};
beforeEach(() => {
jest.spyOn(integrationService, 'deployAwsOidcService').mockResolvedValue({
clusterArn: 'cluster-arn',
serviceArn: 'service-arn',
taskDefinitionArn: 'task-definition',
serviceDashboardUrl: 'dashboard-url',
});
jest.spyOn(teleCtx.databaseService, 'fetchDatabases').mockResolvedValue({
agents: [],
});
});
afterEach(() => {
jest.clearAllMocks();
});
test('init: labels are rendered, command is not rendered yet', () => {
renderAutoDeploy(teleCtx, discoverCtx);
expect(screen.getByText(/env: prod/i)).toBeInTheDocument();
expect(screen.queryByText(/copy\/paste/i)).not.toBeInTheDocument();
expect(screen.queryByText(/curl/i)).not.toBeInTheDocument();
});
test('clicking button renders command', () => {
renderAutoDeploy(teleCtx, discoverCtx);
fireEvent.click(screen.getByText(/generate command/i));
expect(screen.getByText(/copy\/paste/i)).toBeInTheDocument();
expect(
screen.getByText(
/integrationName=aws-oidc-integration&awsRegion=us-east-2&role=role-arn&taskRole=TeleportDatabaseAccess/i
)
).toBeInTheDocument();
});
test('invalid role name', () => {
renderAutoDeploy(teleCtx, discoverCtx);
expect(
screen.queryByText(/name can only contain/i)
).not.toBeInTheDocument();
// add invalid characters in role name
const inputEl = screen.getByPlaceholderText(/TeleportDatabaseAccess/i);
fireEvent.change(inputEl, { target: { value: 'invalidname!@#!$!%' } });
fireEvent.click(screen.getByText(/generate command/i));
expect(screen.getByText(/name can only contain/i)).toBeInTheDocument();
// change back to valid name
fireEvent.change(inputEl, { target: { value: 'llama' } });
expect(
screen.queryByText(/name can only contain/i)
).not.toBeInTheDocument();
});
test('deploy hint states', async () => {
renderAutoDeploy(teleCtx, discoverCtx);
fireEvent.click(screen.getByText(/Deploy Teleport Service/i));
// test initial loading state
await screen.findByText(
/Teleport is currently deploying a Database Service/i
);
// test waiting state
act(() => jest.advanceTimersByTime(SHOW_HINT_TIMEOUT + 1));
expect(
screen.getByText(
/We're still in the process of creating your Database Service/i
)
).toBeInTheDocument();
// test success state
jest.spyOn(teleCtx.databaseService, 'fetchDatabases').mockResolvedValue({
agents: [{} as any], // the result doesn't matter, just need size one array.
});
act(() => jest.advanceTimersByTime(TEST_PING_INTERVAL + 1));
await screen.findByText(/Successfully created/i);
});
});
const TEST_PING_INTERVAL = 1000 * 60 * 5; // 5 minutes
function renderAutoDeploy(
ctx: TeleportContext,
discoverCtx: DiscoverContextState
) {
return render(
<MemoryRouter
initialEntries={[
{ pathname: cfg.routes.discover, state: { entity: 'database' } },
]}
>
<ContextProvider ctx={ctx}>
<FeaturesContextProvider value={[]}>
<DiscoverProvider mockCtx={discoverCtx}>
<PingTeleportProvider
interval={TEST_PING_INTERVAL}
resourceKind={ResourceKind.Database}
>
<AutoDeploy />
</PingTeleportProvider>
</DiscoverProvider>
</FeaturesContextProvider>
</ContextProvider>
</MemoryRouter>
);
}

View file

@ -20,7 +20,6 @@ import { Box, ButtonSecondary, Link, Text } from 'design';
import * as Icons from 'design/Icon';
import FieldInput from 'shared/components/FieldInput';
import Validation, { Validator } from 'shared/components/Validation';
import { requiredField } from 'shared/components/Validation/rules';
import useAttempt from 'shared/hooks/useAttemptNext';
import { TextSelectCopyMulti } from 'teleport/components/TextSelectCopy';
@ -35,7 +34,12 @@ import {
integrationService,
} from 'teleport/services/integrations';
import { useDiscover, DbMeta } from 'teleport/Discover/useDiscover';
import { DiscoverEventStatus } from 'teleport/services/userEvent';
import {
DiscoverEventStatus,
DiscoverServiceDeployMethod,
DiscoverServiceDeployType,
} from 'teleport/services/userEvent';
import cfg from 'teleport/config';
import {
ActionButtons,
@ -69,7 +73,7 @@ export function AutoDeploy({ toggleDeployMethod }: DeployServiceProp) {
const [labels, setLabels] = useState<DiscoverLabel[]>([
{ name: '*', value: '*', isFixed: dbLabels.length === 0 },
]);
const agent = agentMeta as DbMeta;
const dbMeta = agentMeta as DbMeta;
useEffect(() => {
// Turn off error once user changes labels.
@ -78,7 +82,7 @@ export function AutoDeploy({ toggleDeployMethod }: DeployServiceProp) {
}
}, [labels]);
function handleDeploy(validator: Validator) {
function handleDeploy(validator) {
if (!validator.validate()) {
return;
}
@ -91,10 +95,10 @@ export function AutoDeploy({ toggleDeployMethod }: DeployServiceProp) {
setShowLabelMatchErr(false);
setAttempt({ status: 'processing' });
integrationService
.deployAwsOidcService(agent.integrationName, {
.deployAwsOidcService(dbMeta.integration?.name, {
deploymentMode: 'database-service',
region: agent.selectedAwsRdsDb?.region,
subnetIds: agent.selectedAwsRdsDb?.subnets,
region: dbMeta.selectedAwsRdsDb?.region,
subnetIds: dbMeta.selectedAwsRdsDb?.subnets,
taskRoleArn,
databaseAgentMatcherLabels: labels,
})
@ -114,9 +118,13 @@ export function AutoDeploy({ toggleDeployMethod }: DeployServiceProp) {
function handleOnProceed() {
nextStep(2); // skip the IAM policy view
emitEvent(
{ stepStatus: DiscoverEventStatus.Success }
// TODO(lisa) uncomment after backend handles this field
// { deployMethod: 'auto' }
{ stepStatus: DiscoverEventStatus.Success },
{
serviceDeploy: {
method: DiscoverServiceDeployMethod.Auto,
type: DiscoverServiceDeployType.AmazonEcs,
},
}
);
}
@ -148,7 +156,7 @@ export function AutoDeploy({ toggleDeployMethod }: DeployServiceProp) {
<Heading
toggleDeployMethod={abortDeploying}
togglerDisabled={isProcessing}
region={agent.selectedAwsRdsDb.region}
region={dbMeta.selectedAwsRdsDb.region}
/>
{/* step one */}
@ -156,6 +164,8 @@ export function AutoDeploy({ toggleDeployMethod }: DeployServiceProp) {
taskRoleArn={taskRoleArn}
setTaskRoleArn={setTaskRoleArn}
disabled={isProcessing}
dbMeta={dbMeta}
validator={validator}
/>
{/* step two */}
@ -169,7 +179,7 @@ export function AutoDeploy({ toggleDeployMethod }: DeployServiceProp) {
showLabelMatchErr={showLabelMatchErr}
dbLabels={dbLabels}
autoFocus={false}
region={agent.selectedAwsRdsDb?.region}
region={dbMeta.selectedAwsRdsDb?.region}
/>
</Box>
<ButtonSecondary
@ -227,10 +237,10 @@ const Heading = ({
<HeaderSubtitle>
Teleport needs a database service to be able to connect to your
database. Teleport can configure the permissions required to spin up an
ECS Fargate container (0.xxx vCPU, 1GB memory) in your Amazon account
with the ability to access databases in this region (
<Mark>{region}</Mark>). You will only need to do this once for all
databases per geographical region. <br />
ECS Fargate container (2vCPU, 4GB memory) in your Amazon account with
the ability to access databases in this region (<Mark>{region}</Mark>).
You will only need to do this once per geographical region.
<br />
<br />
Want to deploy a database service manually from one of your existing
servers?{' '}
@ -247,49 +257,84 @@ const CreateAccessRole = ({
taskRoleArn,
setTaskRoleArn,
disabled,
dbMeta,
validator,
}: {
taskRoleArn: string;
setTaskRoleArn(r: string): void;
disabled: boolean;
dbMeta: DbMeta;
validator: Validator;
}) => {
const [scriptUrl, setScriptUrl] = useState('');
const { integration, selectedAwsRdsDb } = dbMeta;
function generateAutoConfigScript() {
if (!validator.validate()) {
return;
}
const newScriptUrl = cfg.getDeployServiceIamConfigureScriptUrl({
integrationName: integration.name,
region: selectedAwsRdsDb.region,
// arn's are formatted as `don-care-about-this-part/role-arn`.
// We are splitting by slash and getting the last element.
awsOidcRoleArn: integration.spec.roleArn.split('/').pop(),
taskRoleArn,
});
setScriptUrl(newScriptUrl);
}
return (
<StyledBox mb={5}>
<Text bold>Step 1</Text>
<Text mb={2}>Create an Access Role for the Database Service</Text>
<Text mb={2}>
Name a Task Role ARN for this Database Service and generate a configure
command. This command will configure the required permissions in your
AWS account.
</Text>
<FieldInput
mb={4}
disabled={disabled}
rule={requiredField('Task Role ARN is required')}
rule={roleArnMatcher}
label="Name a Task Role ARN"
autoFocus
value={taskRoleArn}
placeholder="teleport"
width="400px"
placeholder="TeleportDatabaseAccess"
width="440px"
mr="3"
onChange={e => setTaskRoleArn(e.target.value)}
toolTipContent="Lorem ipsume dolores"
toolTipContent={`Amazon Resource Names (ARNs) uniquely identify AWS \
resources. In this case you will naming an IAM role that this \
deployed service will be using`}
/>
<Text mb={2}>
Then open{' '}
<Link
href="https://console.aws.amazon.com/cloudshell/home"
target="_blank"
>
Amazon CloudShell
</Link>{' '}
and copy/paste the following command to create an access role for your
database service:
</Text>
<Box mb={2}>
<TextSelectCopyMulti
// TODO(lisa): replace with actual script when ready
lines={[
{
text: 'sudo bash -c "$(curl -fsSL https://kenny-r-test.teleport.sh/scripts/40884566df6fbdb02411364e641f78b2/set-up-aws-role.sh)"',
},
]}
/>
</Box>
<ButtonSecondary mb={3} onClick={generateAutoConfigScript}>
{scriptUrl ? 'Regenerate Command' : 'Generate Command'}
</ButtonSecondary>
{scriptUrl && (
<>
<Text mb={2}>
Open{' '}
<Link
href="https://console.aws.amazon.com/cloudshell/home"
target="_blank"
>
Amazon CloudShell
</Link>{' '}
and copy/paste the following command:
</Text>
<Box mb={2}>
<TextSelectCopyMulti
lines={[
{
text: `bash -c "$(curl '${scriptUrl}')"`,
},
]}
/>
</Box>
</>
)}
</StyledBox>
);
};
@ -374,3 +419,21 @@ const StyledBox = styled(Box)`
padding: ${props => `${props.theme.space[3]}px`};
border-radius: ${props => `${props.theme.space[2]}px`};
`;
// ROLE_ARN_REGEX uses the same regex matcher used in the backend:
// https://github.com/gravitational/teleport/blob/2cba82cb332e769ebc8a658d32ff24ddda79daff/api/utils/aws/identifiers.go#L43
//
// Regex checks for alphanumerics and select few characters.
export const ROLE_ARN_REGEX = /^[\w+=,.@-]+$/;
const roleArnMatcher = value => () => {
const isValid = value.match(ROLE_ARN_REGEX);
if (!isValid) {
return {
valid: false,
message: 'name can only contain characters @ = , . + - and alphanumerics',
};
}
return {
valid: true,
};
};

View file

@ -40,7 +40,11 @@ import {
import { CommandBox } from 'teleport/Discover/Shared/CommandBox';
import { DbMeta, useDiscover } from 'teleport/Discover/useDiscover';
import { DatabaseLocation } from 'teleport/Discover/SelectResource';
import { DiscoverEventStatus } from 'teleport/services/userEvent';
import {
DiscoverEventStatus,
DiscoverServiceDeployMethod,
DiscoverServiceDeployType,
} from 'teleport/services/userEvent';
import {
ActionButtons,
@ -153,9 +157,13 @@ export function ManualDeploy(props: {
nextStep();
emitEvent(
{ stepStatus: DiscoverEventStatus.Success }
// TODO(lisa) uncomment after backend handles this field
// { deployMethod: 'manual' }
{ stepStatus: DiscoverEventStatus.Success },
{
serviceDeploy: {
method: DiscoverServiceDeployMethod.Manual,
type: DiscoverServiceDeployType.InstallScript,
},
}
);
}

View file

@ -21,6 +21,7 @@ import { render, screen, fireEvent } from 'design/utils/testing';
import { ContextProvider } from 'teleport';
import {
AwsRdsDatabase,
IntegrationStatusCode,
integrationService,
} from 'teleport/services/integrations';
import { createTeleportContext } from 'teleport/mocks/contexts';
@ -41,7 +42,17 @@ import { EnrollRdsDatabase } from './EnrollRdsDatabase';
describe('test EnrollRdsDatabase.tsx', () => {
const ctx = createTeleportContext();
const discoverCtx: DiscoverContextState = {
agentMeta: {} as any,
agentMeta: {
integration: {
kind: 'aws-oidc',
name: 'aws-oidc-integration',
resourceType: 'integration',
spec: {
roleArn: 'arn-123',
},
statusCode: IntegrationStatusCode.Running,
},
} as any,
currentStep: 0,
nextStep: jest.fn(x => x),
prevStep: () => null,

View file

@ -98,7 +98,7 @@ export function EnrollRdsDatabase() {
}
async function fetchDatabases(data: TableData) {
const integrationName = (agentMeta as DbMeta).integrationName;
const integrationName = (agentMeta as DbMeta).integration.name;
setTableData({ ...data, fetchStatus: 'loading' });
setFetchDbAttempt({ status: 'processing' });

View file

@ -26,6 +26,7 @@ import {
import { CreateDatabase } from 'teleport/Discover/Database/CreateDatabase';
import { SetupAccess } from 'teleport/Discover/Database/SetupAccess';
import { DeployService } from 'teleport/Discover/Database/DeployService';
import { ManualDeploy } from 'teleport/Discover/Database/DeployService/ManualDeploy';
import { MutualTls } from 'teleport/Discover/Database/MutualTls';
import { TestConnection } from 'teleport/Discover/Database/TestConnection';
@ -68,8 +69,9 @@ export const DatabaseResource: ResourceViewConfig<ResourceSpec> = {
},
{
title: 'Deploy Database Service',
component: ManualDeploy,
component: DeployService,
eventName: DiscoverEvent.DeployService,
manuallyEmitSuccessEvent: true,
},
{
title: 'Configure IAM Policy',

View file

@ -16,7 +16,7 @@
import { useEffect, useState } from 'react';
const SHOW_HINT_TIMEOUT = 1000 * 60 * 5; // 5 minutes
export const SHOW_HINT_TIMEOUT = 1000 * 60 * 5; // 5 minutes
export function useShowHint(enabled: boolean) {
const [showHint, setShowHint] = useState(false);

View file

@ -44,7 +44,10 @@ import type { Kube } from 'teleport/services/kube';
import type { Database } from 'teleport/services/databases';
import type { AgentLabel } from 'teleport/services/agents';
import type { ResourceSpec } from './SelectResource';
import type { AwsRdsDatabase } from 'teleport/services/integrations';
import type {
AwsRdsDatabase,
Integration,
} from 'teleport/services/integrations';
export interface DiscoverContextState<T = any> {
agentMeta: AgentMeta;
@ -97,9 +100,8 @@ export type DiscoverUrlLocationState = {
resourceSpec: ResourceSpec;
currentStep: number;
};
// integrationName is the name of the created integration
// resource name (eg: integration subkind "aws-oidc")
integrationName: string;
// integration is the created aws-oidc integration
integration: Integration;
};
const discoverContext = React.createContext<DiscoverContextState>(null);
@ -226,9 +228,9 @@ export function DiscoverProvider({
// 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;
const { discover, integration } = location.state;
updateAgentMeta({ integrationName } as DbMeta);
updateAgentMeta({ integration } as DbMeta);
startDiscoverFlow(
discover.resourceSpec,
@ -470,9 +472,9 @@ export type NodeMeta = BaseMeta & {
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;
selectedAwsRdsDb: AwsRdsDatabase;
db?: Database;
integration?: Integration;
selectedAwsRdsDb?: AwsRdsDatabase;
// serviceDeployedMethod flag will be undefined if user skipped
// deploying service (service already existed).
serviceDeployedMethod?: ServiceDeployMethod;

View file

@ -17,6 +17,12 @@
import React from 'react';
import { MemoryRouter } from 'react-router';
import {
Integration,
IntegrationKind,
IntegrationStatusCode,
} from 'teleport/services/integrations';
import { FirstStageInstructions } from './FirstStageInstructions';
import { SecondStageInstructions } from './SecondStageInstructions';
import { ThirdStageInstructions } from './ThirdStageInstructions';
@ -50,7 +56,7 @@ export const Step7 = () => (
export const ConfirmDialog = () => (
<MemoryRouter>
<SuccessfullyAddedIntegrationDialog
integrationName="some-integration-name"
integration={mockIntegration}
emitEvent={() => null}
/>
</MemoryRouter>
@ -61,7 +67,7 @@ export const ConfirmDialogFromDiscover = () => (
initialEntries={[{ state: { discover: {} } as DiscoverUrlLocationState }]}
>
<SuccessfullyAddedIntegrationDialog
integrationName="some-integration-name"
integration={mockIntegration}
emitEvent={() => null}
/>
</MemoryRouter>
@ -77,3 +83,13 @@ const props: CommonInstructionsProps = {
},
clusterPublicUri: 'gravitationalwashington.cloud.gravitional.io:4444',
};
const mockIntegration: Integration = {
kind: IntegrationKind.AwsOidc,
name: 'aws-oidc-integration',
resourceType: 'integration',
spec: {
roleArn: 'arn-123',
},
statusCode: IntegrationStatusCode.Running,
};

View file

@ -36,6 +36,7 @@ import {
} from 'shared/components/Validation/rules';
import {
Integration,
IntegrationKind,
integrationService,
} from 'teleport/services/integrations';
@ -51,7 +52,7 @@ export function SeventhStageInstructions(
props: PreviousStepProps & { emitEvent: EmitEvent }
) {
const { attempt, setAttempt } = useAttempt('');
const [showConfirmBox, setShowConfirmBox] = useState(false);
const [createdIntegration, setCreatedIntegration] = useState<Integration>();
const [roleArn, setRoleArn] = useState(props.awsOidc.roleArn);
const [name, setName] = useState(props.awsOidc.integrationName);
@ -67,7 +68,7 @@ export function SeventhStageInstructions(
subKind: IntegrationKind.AwsOidc,
awsoidc: { roleArn },
})
.then(() => setShowConfirmBox(true))
.then(setCreatedIntegration)
.catch((err: Error) =>
setAttempt({ status: 'failed', statusText: err.message })
);
@ -134,9 +135,9 @@ export function SeventhStageInstructions(
</>
)}
</Validation>
{showConfirmBox && (
{createdIntegration && (
<SuccessfullyAddedIntegrationDialog
integrationName={name}
integration={createdIntegration}
emitEvent={props.emitEvent}
/>
)}
@ -145,10 +146,10 @@ export function SeventhStageInstructions(
}
export function SuccessfullyAddedIntegrationDialog({
integrationName,
integration,
emitEvent,
}: {
integrationName: string;
integration: Integration;
emitEvent: EmitEvent;
}) {
const location = useLocation<DiscoverUrlLocationState>();
@ -174,7 +175,7 @@ export function SuccessfullyAddedIntegrationDialog({
</DialogHeader>
<DialogContent>
<Text textAlign="center">
AWS integration "{integrationName}" successfully added
AWS integration "{integration.name}" successfully added
</Text>
</DialogContent>
<DialogFooter css={{ margin: '0 auto' }}>
@ -185,7 +186,7 @@ export function SuccessfullyAddedIntegrationDialog({
to={{
pathname: cfg.routes.discover,
state: {
integrationName,
integration,
discover: location.state.discover,
},
}}

View file

@ -0,0 +1,32 @@
/**
* 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 cfg, { UrlDeployServiceIamConfigureScriptParams } from './config';
test('getDeployServiceIamConfigureScriptPath formatting', async () => {
const params: UrlDeployServiceIamConfigureScriptParams = {
integrationName: 'int-name',
region: 'us-east-1',
awsOidcRoleArn: 'oidc-arn',
taskRoleArn: 'task-arn',
};
const base =
'http://localhost/webapi/scripts/integrations/configure/deployservice-iam.sh?';
const expected = `integrationName=${'int-name'}&awsRegion=${'us-east-1'}&role=${'oidc-arn'}&taskRole=${'task-arn'}`;
expect(cfg.getDeployServiceIamConfigureScriptUrl(params)).toBe(
`${base}${expected}`
);
});

View file

@ -31,7 +31,7 @@ import type {
import type { SortType } from 'teleport/services/agents';
import type { RecordingType } from 'teleport/services/recordings';
import type { WebauthnAssertionResponse } from './services/auth';
import type { Regions } from './services/integrations';
import type { ParticipantMode } from 'teleport/services/session';
const cfg = {
@ -187,6 +187,9 @@ const cfg = {
nodeScriptPath: '/scripts/:token/install-node.sh',
appNodeScriptPath: '/scripts/:token/install-app.sh?name=:name&uri=:uri',
deployServiceIamConfigureScriptPath:
'/webapi/scripts/integrations/configure/deployservice-iam.sh?integrationName=:integrationName&awsRegion=:region&role=:awsOidcRoleArn&taskRole=:taskRoleArn',
mfaRequired: '/v1/webapi/sites/:clusterId/mfa/required',
mfaLoginBegin: '/v1/webapi/mfa/login/begin', // creates authnenticate challenge with user and password
mfaLoginFinish: '/v1/webapi/mfa/login/finishsession', // creates a web session
@ -347,6 +350,15 @@ const cfg = {
return cfg.baseUrl + generatePath(cfg.api.nodeScriptPath, { token });
},
getDeployServiceIamConfigureScriptUrl(
p: UrlDeployServiceIamConfigureScriptParams
) {
return (
cfg.baseUrl +
generatePath(cfg.api.deployServiceIamConfigureScriptPath, { ...p })
);
},
getDbScriptUrl(token: string) {
return cfg.baseUrl + generatePath(cfg.api.dbScriptPath, { token });
},
@ -842,4 +854,11 @@ export interface UrlIntegrationExecuteRequestParams {
action: 'aws-oidc/list_databases';
}
export interface UrlDeployServiceIamConfigureScriptParams {
integrationName: string;
region: Regions;
awsOidcRoleArn: string;
taskRoleArn: string;
}
export default cfg;

View file

@ -46,8 +46,8 @@ export const integrationService = {
});
},
createIntegration(req: IntegrationCreateRequest): Promise<void> {
return api.post(cfg.getIntegrationsUrl(), req);
createIntegration(req: IntegrationCreateRequest): Promise<Integration> {
return api.post(cfg.getIntegrationsUrl(), req).then(makeIntegration);
},
updateIntegration(