mirror of
https://github.com/gravitational/teleport
synced 2024-10-20 09:13:39 +00:00
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:
parent
c6529af658
commit
f6938613d2
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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' });
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}}
|
||||
|
|
32
web/packages/teleport/src/config.test.ts
Normal file
32
web/packages/teleport/src/config.test.ts
Normal 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}`
|
||||
);
|
||||
});
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in a new issue