mirror of
https://github.com/gravitational/teleport
synced 2024-10-19 16:53:57 +00:00
add eice discover flow (#32202)
This commit is contained in:
parent
c2f470fe66
commit
10a1f2d1d1
|
@ -339,12 +339,12 @@ type desktopIsActive struct {
|
|||
|
||||
// createNodeRequest contains the required information to create a Node.
|
||||
type createNodeRequest struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
SubKind string `json:"subKind,omitempty"`
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
Addr string `json:"addr,omitempty"`
|
||||
Labels []ui.Label `json:"labels,omitempty"`
|
||||
AWSInfo *types.AWSInfo `json:"aws,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
SubKind string `json:"subKind,omitempty"`
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
Addr string `json:"addr,omitempty"`
|
||||
Labels []ui.Label `json:"labels,omitempty"`
|
||||
AWSInfo *ui.AWSMetadata `json:"aws,omitempty"`
|
||||
}
|
||||
|
||||
func (r *createNodeRequest) checkAndSetDefaults() error {
|
||||
|
@ -402,7 +402,14 @@ func (h *Handler) handleNodeCreate(w http.ResponseWriter, r *http.Request, p htt
|
|||
Hostname: req.Hostname,
|
||||
Addr: req.Addr,
|
||||
CloudMetadata: &types.CloudMetadata{
|
||||
AWS: req.AWSInfo,
|
||||
AWS: &types.AWSInfo{
|
||||
AccountID: req.AWSInfo.AccountID,
|
||||
InstanceID: req.AWSInfo.InstanceID,
|
||||
Region: req.AWSInfo.Region,
|
||||
VPCID: req.AWSInfo.VPCID,
|
||||
Integration: req.AWSInfo.Integration,
|
||||
SubnetID: req.AWSInfo.SubnetID,
|
||||
},
|
||||
},
|
||||
},
|
||||
labels,
|
||||
|
|
|
@ -45,7 +45,7 @@ func TestCreateNode(t *testing.T) {
|
|||
Hostname: "myhostname",
|
||||
Addr: "172.31.1.1:22",
|
||||
Labels: []ui.Label{},
|
||||
AWSInfo: &types.AWSInfo{
|
||||
AWSInfo: &ui.AWSMetadata{
|
||||
AccountID: "123456789012",
|
||||
InstanceID: "i-123",
|
||||
Region: "us-east-1",
|
||||
|
@ -155,7 +155,14 @@ func TestCreateNode(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, node.GetName(), tt.req.Name)
|
||||
require.Equal(t, node.GetCloudMetadata().AWS, tt.req.AWSInfo)
|
||||
require.Equal(t, node.GetAWSInfo(), &types.AWSInfo{
|
||||
AccountID: tt.req.AWSInfo.AccountID,
|
||||
InstanceID: tt.req.AWSInfo.InstanceID,
|
||||
Region: tt.req.AWSInfo.Region,
|
||||
VPCID: tt.req.AWSInfo.VPCID,
|
||||
Integration: tt.req.AWSInfo.Integration,
|
||||
SubnetID: tt.req.AWSInfo.SubnetID,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -134,6 +134,7 @@ func MakeServer(clusterName string, server types.Server, accessChecker services.
|
|||
Region: awsMetadata.Region,
|
||||
Integration: awsMetadata.Integration,
|
||||
SubnetID: awsMetadata.SubnetID,
|
||||
VPCID: awsMetadata.VPCID,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,11 +16,16 @@
|
|||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Flex, Box, Label as Pill } from 'design';
|
||||
import Table, { Cell as TableCell } from 'design/DataTable';
|
||||
import { Flex, Box } from 'design';
|
||||
import Table from 'design/DataTable';
|
||||
import { FetchStatus } from 'design/DataTable/types';
|
||||
|
||||
import { Label } from 'teleport/types';
|
||||
import {
|
||||
DisableableCell as Cell,
|
||||
RadioCell,
|
||||
Labels,
|
||||
labelMatcher,
|
||||
} from 'teleport/Discover/Shared';
|
||||
|
||||
import { CheckedAwsRdsDatabase } from './EnrollRdsDatabase';
|
||||
|
||||
|
@ -32,6 +37,8 @@ type Props = {
|
|||
selectedDatabase?: CheckedAwsRdsDatabase;
|
||||
};
|
||||
|
||||
const disabledText = `This RDS database is already enrolled and is a part of this cluster`;
|
||||
|
||||
export const DatabaseList = ({
|
||||
items = [],
|
||||
fetchStatus = '',
|
||||
|
@ -51,12 +58,14 @@ export const DatabaseList = ({
|
|||
item.name === selectedDatabase?.name &&
|
||||
item.engine === selectedDatabase?.engine;
|
||||
return (
|
||||
<RadioCell
|
||||
<RadioCell<CheckedAwsRdsDatabase>
|
||||
disabledText={disabledText}
|
||||
item={item}
|
||||
key={`${item.name}${item.resourceId}`}
|
||||
isChecked={isChecked}
|
||||
onChange={onSelectDatabase}
|
||||
disabled={item.dbServerExists}
|
||||
value={item.name}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
@ -65,21 +74,25 @@ export const DatabaseList = ({
|
|||
key: 'name',
|
||||
headerText: 'Name',
|
||||
render: ({ name, dbServerExists }) => (
|
||||
<Cell disabled={dbServerExists}>{name}</Cell>
|
||||
<Cell disabledText={disabledText} disabled={dbServerExists}>
|
||||
{name}
|
||||
</Cell>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'engine',
|
||||
headerText: 'Engine',
|
||||
render: ({ engine, dbServerExists }) => (
|
||||
<Cell disabled={dbServerExists}>{engine}</Cell>
|
||||
<Cell disabledText={disabledText} disabled={dbServerExists}>
|
||||
{engine}
|
||||
</Cell>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'labels',
|
||||
headerText: 'Labels',
|
||||
render: ({ labels, dbServerExists }) => (
|
||||
<Cell disabled={dbServerExists}>
|
||||
<Cell disabledText={disabledText} disabled={dbServerExists}>
|
||||
<Labels labels={labels} />
|
||||
</Cell>
|
||||
),
|
||||
|
@ -103,7 +116,7 @@ const StatusCell = ({ item }: { item: CheckedAwsRdsDatabase }) => {
|
|||
const status = getStatus(item);
|
||||
|
||||
return (
|
||||
<Cell disabled={item.dbServerExists}>
|
||||
<Cell disabledText={disabledText} disabled={item.dbServerExists}>
|
||||
<Flex alignItems="center">
|
||||
<StatusLight status={status} />
|
||||
{item.status}
|
||||
|
@ -112,42 +125,6 @@ const StatusCell = ({ item }: { item: CheckedAwsRdsDatabase }) => {
|
|||
);
|
||||
};
|
||||
|
||||
function RadioCell({
|
||||
item,
|
||||
isChecked,
|
||||
onChange,
|
||||
disabled,
|
||||
}: {
|
||||
item: CheckedAwsRdsDatabase;
|
||||
isChecked: boolean;
|
||||
onChange(selectedItem: CheckedAwsRdsDatabase): void;
|
||||
disabled: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Cell width="20px" disabled={disabled}>
|
||||
<Flex alignItems="center" my={2} justifyContent="center">
|
||||
<input
|
||||
css={`
|
||||
margin: 0 ${props => props.theme.space[2]}px 0 0;
|
||||
accent-color: ${props => props.theme.colors.brand.accent};
|
||||
cursor: pointer;
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
`}
|
||||
type="radio"
|
||||
name={item.name}
|
||||
checked={isChecked}
|
||||
onChange={() => onChange(item)}
|
||||
value={item.name}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Flex>
|
||||
</Cell>
|
||||
);
|
||||
}
|
||||
|
||||
enum Status {
|
||||
Success,
|
||||
Warning,
|
||||
|
@ -185,61 +162,3 @@ const StatusLight = styled(Box)`
|
|||
return theme.colors.grey[300]; // Unknown
|
||||
}};
|
||||
`;
|
||||
|
||||
const Labels = ({ labels }: { labels: Label[] }) => {
|
||||
const $labels = labels.map((label, index) => {
|
||||
const labelText = `${label.name}: ${label.value}`;
|
||||
|
||||
return (
|
||||
<Pill key={`${label.name}${label.value}${index}`} mr="1" kind="secondary">
|
||||
{labelText}
|
||||
</Pill>
|
||||
);
|
||||
});
|
||||
|
||||
return <Flex flexWrap="wrap">{$labels}</Flex>;
|
||||
};
|
||||
|
||||
// labelMatcher allows user to client search by labels in the format
|
||||
// 1) `key: value` or
|
||||
// 2) `key:value` or
|
||||
// 3) `key` or `value`
|
||||
function labelMatcher(
|
||||
targetValue: any,
|
||||
searchValue: string,
|
||||
propName: keyof CheckedAwsRdsDatabase & string
|
||||
) {
|
||||
if (propName === 'labels') {
|
||||
return targetValue.some((label: Label) => {
|
||||
const convertedKey = label.name.toLocaleUpperCase();
|
||||
const convertedVal = label.value.toLocaleUpperCase();
|
||||
const formattedWords = [
|
||||
`${convertedKey}:${convertedVal}`,
|
||||
`${convertedKey}: ${convertedVal}`,
|
||||
];
|
||||
return formattedWords.some(w => w.includes(searchValue));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const Cell: React.FC<{ disabled: boolean; width?: string }> = ({
|
||||
disabled,
|
||||
width,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<TableCell
|
||||
width={width}
|
||||
title={
|
||||
disabled
|
||||
? 'this RDS database is already enrolled and is a part of this cluster'
|
||||
: null
|
||||
}
|
||||
css={`
|
||||
opacity: ${disabled ? '0.5' : '1'};
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</TableCell>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { ResourceKind, Finished } from 'teleport/Discover/Shared';
|
||||
import { AwsAccount, ResourceKind, Finished } from 'teleport/Discover/Shared';
|
||||
import { ResourceViewConfig } from 'teleport/Discover/flow';
|
||||
import { DatabaseWrapper } from 'teleport/Discover/Database/DatabaseWrapper';
|
||||
import {
|
||||
|
@ -31,7 +31,6 @@ import { ManualDeploy } from 'teleport/Discover/Database/DeployService/ManualDep
|
|||
import { MutualTls } from 'teleport/Discover/Database/MutualTls';
|
||||
import { TestConnection } from 'teleport/Discover/Database/TestConnection';
|
||||
import { DiscoverEvent } from 'teleport/services/userEvent';
|
||||
import { ConnectAwsAccount } from 'teleport/Discover/Database/ConnectAwsAccount';
|
||||
import { EnrollRdsDatabase } from 'teleport/Discover/Database/EnrollRdsDatabase';
|
||||
import { IamPolicy } from 'teleport/Discover/Database/IamPolicy';
|
||||
|
||||
|
@ -59,7 +58,7 @@ export const DatabaseResource: ResourceViewConfig<ResourceSpec> = {
|
|||
configureResourceViews = [
|
||||
{
|
||||
title: 'Connect AWS Account',
|
||||
component: ConnectAwsAccount,
|
||||
component: AwsAccount,
|
||||
eventName: DiscoverEvent.IntegrationAWSOIDCConnectEvent,
|
||||
},
|
||||
{
|
||||
|
|
|
@ -268,6 +268,47 @@ exports[`render with URL loc state set to "server" 1`] = `
|
|||
<div
|
||||
class="c10"
|
||||
>
|
||||
<div
|
||||
class="c11"
|
||||
data-testid="4"
|
||||
>
|
||||
<div
|
||||
class="c12"
|
||||
>
|
||||
Guided
|
||||
</div>
|
||||
<div
|
||||
class="c13"
|
||||
>
|
||||
<div
|
||||
class="c14"
|
||||
width="24px"
|
||||
>
|
||||
<img
|
||||
class="c15"
|
||||
height="24px"
|
||||
src="file_stub"
|
||||
width="23.9px"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c16"
|
||||
>
|
||||
<div
|
||||
class="c17"
|
||||
color="text.slightlyMuted"
|
||||
font-size="12px"
|
||||
>
|
||||
Amazon Web Services (AWS)
|
||||
</div>
|
||||
<div
|
||||
class="c18"
|
||||
>
|
||||
EC2 Instance
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c11"
|
||||
data-testid="4"
|
||||
|
@ -933,6 +974,47 @@ exports[`render with all access 1`] = `
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c7"
|
||||
data-testid="4"
|
||||
>
|
||||
<div
|
||||
class="c8"
|
||||
>
|
||||
Guided
|
||||
</div>
|
||||
<div
|
||||
class="c9"
|
||||
>
|
||||
<div
|
||||
class="c10"
|
||||
width="24px"
|
||||
>
|
||||
<img
|
||||
class="c11"
|
||||
height="24px"
|
||||
src="file_stub"
|
||||
width="23.9px"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c12"
|
||||
>
|
||||
<div
|
||||
class="c14"
|
||||
color="text.slightlyMuted"
|
||||
font-size="12px"
|
||||
>
|
||||
Amazon Web Services (AWS)
|
||||
</div>
|
||||
<div
|
||||
class="c13"
|
||||
>
|
||||
EC2 Instance
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c7"
|
||||
data-testid="3"
|
||||
|
@ -3097,6 +3179,48 @@ exports[`render with no access 1`] = `
|
|||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<div
|
||||
class="c7"
|
||||
data-testid="4"
|
||||
>
|
||||
<div
|
||||
class="c8"
|
||||
data-testid="tooltip"
|
||||
>
|
||||
Lacking Permissions
|
||||
</div>
|
||||
<div
|
||||
class="c9"
|
||||
>
|
||||
<div
|
||||
class="c10"
|
||||
width="24px"
|
||||
>
|
||||
<img
|
||||
class="c11"
|
||||
height="24px"
|
||||
src="file_stub"
|
||||
width="23.9px"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c12"
|
||||
>
|
||||
<div
|
||||
class="c13"
|
||||
color="text.slightlyMuted"
|
||||
font-size="12px"
|
||||
>
|
||||
Amazon Web Services (AWS)
|
||||
</div>
|
||||
<div
|
||||
class="c14"
|
||||
>
|
||||
EC2 Instance
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
class="c15 c7"
|
||||
data-testid="1"
|
||||
|
@ -4436,6 +4560,47 @@ exports[`render with partial access 1`] = `
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c7"
|
||||
data-testid="4"
|
||||
>
|
||||
<div
|
||||
class="c8"
|
||||
>
|
||||
Guided
|
||||
</div>
|
||||
<div
|
||||
class="c9"
|
||||
>
|
||||
<div
|
||||
class="c10"
|
||||
width="24px"
|
||||
>
|
||||
<img
|
||||
class="c11"
|
||||
height="24px"
|
||||
src="file_stub"
|
||||
width="23.9px"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c12"
|
||||
>
|
||||
<div
|
||||
class="c14"
|
||||
color="text.slightlyMuted"
|
||||
font-size="12px"
|
||||
>
|
||||
Amazon Web Services (AWS)
|
||||
</div>
|
||||
<div
|
||||
class="c13"
|
||||
>
|
||||
EC2 Instance
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c7"
|
||||
data-testid="3"
|
||||
|
|
|
@ -26,7 +26,12 @@ import {
|
|||
DATABASES_UNGUIDED,
|
||||
DATABASES_UNGUIDED_DOC,
|
||||
} from './databases';
|
||||
import { ResourceSpec, DatabaseLocation, DatabaseEngine } from './types';
|
||||
import {
|
||||
ResourceSpec,
|
||||
DatabaseLocation,
|
||||
DatabaseEngine,
|
||||
ServerLocation,
|
||||
} from './types';
|
||||
import { SAML_APPLICATIONS } from './resourcesE';
|
||||
|
||||
const baseServerKeywords = 'server node';
|
||||
|
@ -71,6 +76,15 @@ export const SERVERS: ResourceSpec[] = [
|
|||
event: DiscoverEventResource.Server,
|
||||
platform: Platform.PLATFORM_MACINTOSH,
|
||||
},
|
||||
{
|
||||
name: 'EC2 Instance',
|
||||
kind: ResourceKind.Server,
|
||||
keywords:
|
||||
baseServerKeywords + 'ec2 instance connect endpoint aws amazon eice',
|
||||
icon: 'Aws',
|
||||
event: null, // TODO rudream (ADD EVENTS FOR EICE FLOW)
|
||||
nodeMeta: { location: ServerLocation.Aws },
|
||||
},
|
||||
];
|
||||
|
||||
export const APPLICATIONS: ResourceSpec[] = [
|
||||
|
@ -155,6 +169,9 @@ export function getResourcePretitle(r: ResourceSpec) {
|
|||
case ResourceKind.Desktop:
|
||||
return 'Windows Desktop';
|
||||
case ResourceKind.Server:
|
||||
if (r.nodeMeta?.location === ServerLocation.Aws) {
|
||||
return 'Amazon Web Services (AWS)';
|
||||
}
|
||||
return 'Server';
|
||||
}
|
||||
|
||||
|
|
|
@ -53,8 +53,13 @@ export enum DatabaseEngine {
|
|||
Doc,
|
||||
}
|
||||
|
||||
export enum ServerLocation {
|
||||
Aws,
|
||||
}
|
||||
|
||||
export interface ResourceSpec {
|
||||
dbMeta?: { location: DatabaseLocation; engine: DatabaseEngine };
|
||||
nodeMeta?: { location: ServerLocation };
|
||||
name: string;
|
||||
popular?: boolean;
|
||||
kind: ResourceKind;
|
||||
|
|
|
@ -0,0 +1,467 @@
|
|||
/**
|
||||
* 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 { initialize, mswLoader } from 'msw-storybook-addon';
|
||||
import { rest } from 'msw';
|
||||
|
||||
import { Info } from 'design/Alert';
|
||||
|
||||
import { ContextProvider } from 'teleport';
|
||||
import cfg from 'teleport/config';
|
||||
import { createTeleportContext } from 'teleport/mocks/contexts';
|
||||
import {
|
||||
DiscoverProvider,
|
||||
DiscoverContextState,
|
||||
NodeMeta,
|
||||
} from 'teleport/Discover/useDiscover';
|
||||
import {
|
||||
IntegrationKind,
|
||||
IntegrationStatusCode,
|
||||
} from 'teleport/services/integrations';
|
||||
|
||||
import { CreateEc2Ice } from './CreateEc2Ice';
|
||||
|
||||
export default {
|
||||
title: 'Teleport/Discover/Server/EC2/CreateEICE',
|
||||
loaders: [mswLoader],
|
||||
};
|
||||
|
||||
initialize();
|
||||
|
||||
export const ListSecurityGroupsLoading = () => <Component />;
|
||||
|
||||
ListSecurityGroupsLoading.parameters = {
|
||||
msw: {
|
||||
handlers: [
|
||||
rest.post(cfg.getListSecurityGroupsUrl('test-oidc'), (req, res, ctx) =>
|
||||
res(ctx.delay('infinite'))
|
||||
),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const ListSecurityGroupsFail = () => <Component />;
|
||||
|
||||
ListSecurityGroupsFail.parameters = {
|
||||
msw: {
|
||||
handlers: [
|
||||
rest.post(cfg.getListSecurityGroupsUrl('test-oidc'), (req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(403),
|
||||
ctx.json({
|
||||
message: 'some error when trying to list security groups',
|
||||
})
|
||||
)
|
||||
),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const DeployEiceFail = () => (
|
||||
<>
|
||||
<Info width="1000px">To trigger this Story's state, click on "Next."</Info>
|
||||
<Component />
|
||||
</>
|
||||
);
|
||||
|
||||
DeployEiceFail.parameters = {
|
||||
msw: {
|
||||
handlers: [
|
||||
rest.post(cfg.getListSecurityGroupsUrl('test-oidc'), (req, res, ctx) =>
|
||||
res(ctx.json({ securityGroups: securityGroupsResponse }))
|
||||
),
|
||||
rest.post(
|
||||
cfg.getDeployEc2InstanceConnectEndpointUrl('test-oidc'),
|
||||
(req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(403),
|
||||
ctx.json({
|
||||
message: 'some error when trying to initiate the deployment',
|
||||
})
|
||||
)
|
||||
),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const CreatingInProgress = () => (
|
||||
<>
|
||||
<Info width="1000px">To trigger this Story's state, click on "Next."</Info>
|
||||
<Component />
|
||||
</>
|
||||
);
|
||||
|
||||
CreatingInProgress.parameters = {
|
||||
msw: {
|
||||
handlers: [
|
||||
rest.post(cfg.getListSecurityGroupsUrl('test-oidc'), (req, res, ctx) =>
|
||||
res(ctx.json({ securityGroups: securityGroupsResponse }))
|
||||
),
|
||||
rest.post(
|
||||
cfg.getListEc2InstanceConnectEndpointsUrl('test-oidc'),
|
||||
(req, res, ctx) =>
|
||||
res(
|
||||
ctx.json({
|
||||
ec2Ices: [
|
||||
{
|
||||
name: 'test-eice',
|
||||
state: 'create-in-progress',
|
||||
stateMessage: '',
|
||||
dashboardLink: 'goteleport.com',
|
||||
subnetId: 'test-subnetid',
|
||||
},
|
||||
],
|
||||
nextToken: '',
|
||||
})
|
||||
)
|
||||
),
|
||||
rest.post(
|
||||
cfg.getDeployEc2InstanceConnectEndpointUrl('test-oidc'),
|
||||
(req, res, ctx) => res(ctx.json({ name: 'test-eice' }))
|
||||
),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const CreatingFailed = () => (
|
||||
<>
|
||||
{' '}
|
||||
<Info width="1000px">
|
||||
To trigger this Story's state, click on "Next" and wait 10 seconds.
|
||||
</Info>
|
||||
<Component />
|
||||
</>
|
||||
);
|
||||
|
||||
CreatingFailed.parameters = {
|
||||
msw: {
|
||||
handlers: [
|
||||
rest.post(cfg.getListSecurityGroupsUrl('test-oidc'), (req, res, ctx) =>
|
||||
res(ctx.json({ securityGroups: securityGroupsResponse }))
|
||||
),
|
||||
rest.post(
|
||||
cfg.getListEc2InstanceConnectEndpointsUrl('test-oidc'),
|
||||
(req, res, ctx) =>
|
||||
res(
|
||||
ctx.json({
|
||||
ec2Ices: [
|
||||
{
|
||||
name: 'test-eice',
|
||||
state: 'create-failed',
|
||||
stateMessage: '',
|
||||
dashboardLink: 'goteleport.com',
|
||||
subnetId: 'test-subnetid',
|
||||
},
|
||||
],
|
||||
nextToken: '',
|
||||
})
|
||||
)
|
||||
),
|
||||
rest.post(
|
||||
cfg.getDeployEc2InstanceConnectEndpointUrl('test-oidc'),
|
||||
(req, res, ctx) => res(ctx.json({ name: 'test-eice' }))
|
||||
),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const CreatingComplete = () => (
|
||||
<>
|
||||
<Info width="1000px">
|
||||
To trigger this Story's state, click on "Next" and wait 10 seconds.
|
||||
</Info>
|
||||
<Component />
|
||||
</>
|
||||
);
|
||||
|
||||
CreatingComplete.parameters = {
|
||||
msw: {
|
||||
handlers: [
|
||||
rest.post(cfg.getListSecurityGroupsUrl('test-oidc'), (req, res, ctx) =>
|
||||
res(ctx.json({ securityGroups: securityGroupsResponse }))
|
||||
),
|
||||
rest.post(
|
||||
cfg.getDeployEc2InstanceConnectEndpointUrl('test-oidc'),
|
||||
(req, res, ctx) => res(ctx.json({ name: 'test-eice' }))
|
||||
),
|
||||
rest.post(
|
||||
cfg.getListEc2InstanceConnectEndpointsUrl('test-oidc'),
|
||||
(req, res, ctx) =>
|
||||
res(
|
||||
ctx.json({
|
||||
ec2Ices: [
|
||||
{
|
||||
name: 'test-eice',
|
||||
state: 'create-complete',
|
||||
stateMessage: '',
|
||||
dashboardLink: 'goteleport.com',
|
||||
subnetId: 'test-subnetid',
|
||||
},
|
||||
],
|
||||
nextToken: '',
|
||||
})
|
||||
)
|
||||
),
|
||||
rest.post(cfg.getClusterNodesUrlNoParams('localhost'), (req, res, ctx) =>
|
||||
res(
|
||||
ctx.delay(2000), // delay by 2 seconds
|
||||
ctx.json({
|
||||
id: 'ec2-instance-1',
|
||||
kind: 'node',
|
||||
clusterId: 'cluster',
|
||||
hostname: 'ec2-hostname-1',
|
||||
labels: [{ name: 'instance', value: 'ec2-1' }],
|
||||
addr: 'ec2.1.com',
|
||||
tunnel: false,
|
||||
subKind: 'openssh-ec2-ice',
|
||||
sshLogins: ['test'],
|
||||
aws: {
|
||||
accountId: 'test-account',
|
||||
instanceId: 'instance-ec2-1',
|
||||
region: 'us-east-1',
|
||||
vpcId: 'test',
|
||||
integration: 'test',
|
||||
subnetId: 'test',
|
||||
},
|
||||
})
|
||||
)
|
||||
),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const Component = () => {
|
||||
const ctx = createTeleportContext();
|
||||
const discoverCtx: DiscoverContextState = {
|
||||
agentMeta: {
|
||||
resourceName: 'node-name',
|
||||
agentMatcherLabels: [],
|
||||
db: {} as any,
|
||||
selectedAwsRdsDb: {} as any,
|
||||
node: {
|
||||
kind: 'node',
|
||||
subKind: 'openssh-ec2-ice',
|
||||
id: 'test-node',
|
||||
hostname: 'test-node-hostname',
|
||||
clusterId: 'localhost',
|
||||
labels: [],
|
||||
addr: 'test',
|
||||
tunnel: false,
|
||||
sshLogins: [],
|
||||
awsMetadata: {
|
||||
accountId: 'test-account',
|
||||
integration: 'test-oidc',
|
||||
instanceId: 'i-test',
|
||||
subnetId: 'test',
|
||||
vpcId: 'test-vpc',
|
||||
region: 'us-east-1',
|
||||
},
|
||||
},
|
||||
integration: {
|
||||
kind: IntegrationKind.AwsOidc,
|
||||
name: 'test-oidc',
|
||||
resourceType: 'integration',
|
||||
spec: {
|
||||
roleArn: 'arn-123',
|
||||
},
|
||||
statusCode: IntegrationStatusCode.Running,
|
||||
},
|
||||
} as NodeMeta,
|
||||
updateAgentMeta: agentMeta => {
|
||||
discoverCtx.agentMeta = agentMeta;
|
||||
},
|
||||
currentStep: 0,
|
||||
nextStep: () => null,
|
||||
prevStep: () => null,
|
||||
onSelectResource: () => null,
|
||||
resourceSpec: {} as any,
|
||||
exitFlow: () => null,
|
||||
viewConfig: null,
|
||||
indexedViews: [],
|
||||
setResourceSpec: () => null,
|
||||
emitErrorEvent: () => null,
|
||||
emitEvent: () => null,
|
||||
eventState: null,
|
||||
};
|
||||
|
||||
cfg.proxyCluster = 'localhost';
|
||||
return (
|
||||
<MemoryRouter
|
||||
initialEntries={[
|
||||
{ pathname: cfg.routes.discover, state: { entity: 'server' } },
|
||||
]}
|
||||
>
|
||||
<ContextProvider ctx={ctx}>
|
||||
<DiscoverProvider mockCtx={discoverCtx}>
|
||||
<CreateEc2Ice />
|
||||
</DiscoverProvider>
|
||||
</ContextProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
|
||||
const securityGroupsResponse = [
|
||||
{
|
||||
name: 'security-group-1',
|
||||
id: 'sg-1',
|
||||
description: 'this is security group 1',
|
||||
inboundRules: [
|
||||
{
|
||||
ipProtocol: 'tcp',
|
||||
fromPort: '0',
|
||||
toPort: '0',
|
||||
cidrs: [{ cidr: '0.0.0.0/0', description: 'Everything' }],
|
||||
},
|
||||
{
|
||||
ipProtocol: 'tcp',
|
||||
fromPort: '443',
|
||||
toPort: '443',
|
||||
cidrs: [{ cidr: '0.0.0.0/0', description: 'Everything' }],
|
||||
},
|
||||
{
|
||||
ipProtocol: 'tcp',
|
||||
fromPort: '2000',
|
||||
toPort: '5000',
|
||||
cidrs: [
|
||||
{ cidr: '192.168.1.0/24', description: 'Subnet Mask 255.255.255.0' },
|
||||
],
|
||||
},
|
||||
],
|
||||
outboundRules: [
|
||||
{
|
||||
ipProtocol: 'tcp',
|
||||
fromPort: '0',
|
||||
toPort: '0',
|
||||
cidrs: [{ cidr: '0.0.0.0/0', description: 'Everything' }],
|
||||
},
|
||||
{
|
||||
ipProtocol: 'tcp',
|
||||
fromPort: '22',
|
||||
toPort: '22',
|
||||
cidrs: [{ cidr: '0.0.0.0/0', description: 'Everything' }],
|
||||
},
|
||||
{
|
||||
ipProtocol: 'tcp',
|
||||
fromPort: '2000',
|
||||
toPort: '5000',
|
||||
cidrs: [
|
||||
{ cidr: '10.0.0.0/16', description: 'Subnet Mask 255.255.0.0"' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'security-group-2',
|
||||
id: 'sg-2',
|
||||
description: 'this is security group 2',
|
||||
inboundRules: [
|
||||
{
|
||||
ipProtocol: 'tcp',
|
||||
fromPort: '0',
|
||||
toPort: '0',
|
||||
cidrs: [{ cidr: '0.0.0.0/0', description: 'Everything' }],
|
||||
},
|
||||
{
|
||||
ipProtocol: 'tcp',
|
||||
fromPort: '443',
|
||||
toPort: '443',
|
||||
cidrs: [{ cidr: '0.0.0.0/0', description: 'Everything' }],
|
||||
},
|
||||
{
|
||||
ipProtocol: 'tcp',
|
||||
fromPort: '2000',
|
||||
toPort: '5000',
|
||||
cidrs: [
|
||||
{ cidr: '192.168.1.0/24', description: 'Subnet Mask 255.255.255.0' },
|
||||
],
|
||||
},
|
||||
],
|
||||
outboundRules: [
|
||||
{
|
||||
ipProtocol: 'tcp',
|
||||
fromPort: '0',
|
||||
toPort: '0',
|
||||
cidrs: [{ cidr: '0.0.0.0/0', description: 'Everything' }],
|
||||
},
|
||||
{
|
||||
ipProtocol: 'tcp',
|
||||
fromPort: '22',
|
||||
toPort: '22',
|
||||
cidrs: [{ cidr: '0.0.0.0/0', description: 'Everything' }],
|
||||
},
|
||||
{
|
||||
ipProtocol: 'tcp',
|
||||
fromPort: '2000',
|
||||
toPort: '5000',
|
||||
cidrs: [
|
||||
{ cidr: '10.0.0.0/16', description: 'Subnet Mask 255.255.0.0"' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'security-group-3',
|
||||
id: 'sg-3',
|
||||
description: 'this is security group 3',
|
||||
inboundRules: [
|
||||
{
|
||||
ipProtocol: 'tcp',
|
||||
fromPort: '0',
|
||||
toPort: '0',
|
||||
cidrs: [{ cidr: '0.0.0.0/0', description: 'Everything' }],
|
||||
},
|
||||
{
|
||||
ipProtocol: 'tcp',
|
||||
fromPort: '443',
|
||||
toPort: '443',
|
||||
cidrs: [{ cidr: '0.0.0.0/0', description: 'Everything' }],
|
||||
},
|
||||
{
|
||||
ipProtocol: 'tcp',
|
||||
fromPort: '2000',
|
||||
toPort: '5000',
|
||||
cidrs: [
|
||||
{ cidr: '192.168.1.0/24', description: 'Subnet Mask 255.255.255.0' },
|
||||
],
|
||||
},
|
||||
],
|
||||
outboundRules: [
|
||||
{
|
||||
ipProtocol: 'tcp',
|
||||
fromPort: '0',
|
||||
toPort: '0',
|
||||
cidrs: [{ cidr: '0.0.0.0/0', description: 'Everything' }],
|
||||
},
|
||||
{
|
||||
ipProtocol: 'tcp',
|
||||
fromPort: '22',
|
||||
toPort: '22',
|
||||
cidrs: [{ cidr: '0.0.0.0/0', description: 'Everything' }],
|
||||
},
|
||||
{
|
||||
ipProtocol: 'tcp',
|
||||
fromPort: '2000',
|
||||
toPort: '5000',
|
||||
cidrs: [
|
||||
{ cidr: '10.0.0.0/16', description: 'Subnet Mask 255.255.0.0"' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
|
@ -0,0 +1,192 @@
|
|||
/**
|
||||
* 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, { useState, useEffect } from 'react';
|
||||
|
||||
import { Box, Indicator, Text, Flex } from 'design';
|
||||
import { Danger } from 'design/Alert';
|
||||
import { FetchStatus } from 'design/DataTable/types';
|
||||
|
||||
import useAttempt from 'shared/hooks/useAttemptNext';
|
||||
import { getErrMessage } from 'shared/utils/errorType';
|
||||
|
||||
import {
|
||||
SecurityGroup,
|
||||
integrationService,
|
||||
} from 'teleport/services/integrations';
|
||||
import { NodeMeta, useDiscover } from 'teleport/Discover/useDiscover';
|
||||
import {
|
||||
ActionButtons,
|
||||
Header,
|
||||
SecurityGroupPicker,
|
||||
} from 'teleport/Discover/Shared';
|
||||
|
||||
import { CreateEc2IceDialog } from './CreateEc2IceDialog';
|
||||
|
||||
type TableData = {
|
||||
items: SecurityGroup[];
|
||||
nextToken?: string;
|
||||
fetchStatus: FetchStatus;
|
||||
};
|
||||
|
||||
export function CreateEc2Ice() {
|
||||
const [showCreatingDialog, setShowCreatingDialog] = useState(false);
|
||||
const [selectedSecurityGroups, setSelectedSecurityGroups] = useState<
|
||||
string[]
|
||||
>([]);
|
||||
const [tableData, setTableData] = useState<TableData>({
|
||||
items: [],
|
||||
nextToken: '',
|
||||
fetchStatus: 'disabled',
|
||||
});
|
||||
|
||||
function onSelectSecurityGroup(
|
||||
sg: SecurityGroup,
|
||||
e: React.ChangeEvent<HTMLInputElement>
|
||||
) {
|
||||
if (e.target.checked) {
|
||||
return setSelectedSecurityGroups([...selectedSecurityGroups, sg.id]);
|
||||
} else {
|
||||
setSelectedSecurityGroups(
|
||||
selectedSecurityGroups.filter(id => id !== sg.id)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchSecurityGroups();
|
||||
}, []);
|
||||
|
||||
const {
|
||||
attempt: fetchSecurityGroupsAttempt,
|
||||
setAttempt: setFetchSecurityGroupsAttempt,
|
||||
} = useAttempt('');
|
||||
|
||||
const { attempt: deployEc2IceAttempt, setAttempt: setDeployEc2IceAttempt } =
|
||||
useAttempt('');
|
||||
|
||||
const { emitErrorEvent, agentMeta, prevStep, nextStep } = useDiscover();
|
||||
|
||||
async function fetchSecurityGroups() {
|
||||
const integration = (agentMeta as NodeMeta).integration;
|
||||
|
||||
setFetchSecurityGroupsAttempt({ status: 'processing' });
|
||||
try {
|
||||
const { securityGroups, nextToken } =
|
||||
await integrationService.fetchSecurityGroups(integration.name, {
|
||||
vpcId: (agentMeta as NodeMeta).node.awsMetadata.vpcId,
|
||||
region: (agentMeta as NodeMeta).node.awsMetadata.region,
|
||||
nextToken: tableData.nextToken,
|
||||
});
|
||||
|
||||
setFetchSecurityGroupsAttempt({ status: 'success' });
|
||||
setTableData({
|
||||
nextToken: nextToken,
|
||||
fetchStatus: nextToken ? '' : 'disabled',
|
||||
items: [...tableData.items, ...securityGroups],
|
||||
});
|
||||
} catch (err) {
|
||||
const errMsg = getErrMessage(err);
|
||||
setFetchSecurityGroupsAttempt({ status: 'failed', statusText: errMsg });
|
||||
emitErrorEvent(`fetch security groups error: ${errMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function deployEc2InstanceConnectEndpoint() {
|
||||
const integration = (agentMeta as NodeMeta).integration;
|
||||
|
||||
setDeployEc2IceAttempt({ status: 'processing' });
|
||||
setShowCreatingDialog(true);
|
||||
try {
|
||||
await integrationService.deployAwsEc2InstanceConnectEndpoint(
|
||||
integration.name,
|
||||
{
|
||||
region: (agentMeta as NodeMeta).node.awsMetadata.region,
|
||||
subnetId: (agentMeta as NodeMeta).node.awsMetadata.subnetId,
|
||||
...(selectedSecurityGroups.length && {
|
||||
securityGroupIds: selectedSecurityGroups,
|
||||
}),
|
||||
}
|
||||
);
|
||||
// Capture event for deploying EICE.
|
||||
// emitEvent(null); TODO rudream (ADD EVENTS FOR EICE FLOW)
|
||||
} catch (err) {
|
||||
const errMsg = getErrMessage(err);
|
||||
setShowCreatingDialog(false);
|
||||
setDeployEc2IceAttempt({ status: 'failed', statusText: errMsg });
|
||||
emitErrorEvent(
|
||||
`ec2 instance connect endpoint deploying failed: ${errMsg}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function handleOnProceed() {
|
||||
deployEc2InstanceConnectEndpoint();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box maxWidth="800px">
|
||||
<Header>Create an EC2 Instance Connect Endpoint</Header>
|
||||
<Box width="800px">
|
||||
{deployEc2IceAttempt.status === 'failed' && (
|
||||
<Danger>{deployEc2IceAttempt.statusText}</Danger>
|
||||
)}
|
||||
<Text mb={1} typography="h4">
|
||||
Select AWS Security Groups to assign to the new EC2 Instance Connect
|
||||
Endpoint:
|
||||
</Text>
|
||||
<Text mb={2}>
|
||||
The security groups you pick should allow outbound connectivity for
|
||||
the agent to be able to dial Teleport clusters. If you don't select
|
||||
any security groups, the default one for the VPC will be used.
|
||||
</Text>
|
||||
{fetchSecurityGroupsAttempt.status === 'failed' && (
|
||||
<Danger>{fetchSecurityGroupsAttempt.statusText}</Danger>
|
||||
)}
|
||||
{fetchSecurityGroupsAttempt.status === 'processing' && (
|
||||
<Flex width="352px" justifyContent="center" mt={3}>
|
||||
<Indicator />
|
||||
</Flex>
|
||||
)}
|
||||
{fetchSecurityGroupsAttempt.status === 'success' && (
|
||||
<Box width="1000px">
|
||||
<SecurityGroupPicker
|
||||
items={tableData.items}
|
||||
attempt={fetchSecurityGroupsAttempt}
|
||||
fetchNextPage={() => fetchSecurityGroups()}
|
||||
fetchStatus={tableData.fetchStatus}
|
||||
onSelectSecurityGroup={onSelectSecurityGroup}
|
||||
selectedSecurityGroups={selectedSecurityGroups}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<ActionButtons
|
||||
onPrev={prevStep}
|
||||
onProceed={() => handleOnProceed()}
|
||||
disableProceed={deployEc2IceAttempt.status === 'processing'}
|
||||
/>
|
||||
</Box>
|
||||
{showCreatingDialog && (
|
||||
<CreateEc2IceDialog
|
||||
nextStep={nextStep}
|
||||
retry={() => deployEc2InstanceConnectEndpoint()}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,308 @@
|
|||
/**
|
||||
* 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, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Text,
|
||||
Flex,
|
||||
AnimatedProgressBar,
|
||||
ButtonPrimary,
|
||||
Link,
|
||||
Box,
|
||||
} from 'design';
|
||||
import * as Icons from 'design/Icon';
|
||||
import Dialog, { DialogContent } from 'design/DialogConfirmation';
|
||||
|
||||
import { getErrMessage } from 'shared/utils/errorType';
|
||||
|
||||
import useAttempt, { Attempt } from 'shared/hooks/useAttemptNext';
|
||||
|
||||
import cfg from 'teleport/config';
|
||||
|
||||
import {
|
||||
Ec2InstanceConnectEndpoint,
|
||||
integrationService,
|
||||
} from 'teleport/services/integrations';
|
||||
import NodeService from 'teleport/services/nodes';
|
||||
import { TextIcon } from 'teleport/Discover/Shared';
|
||||
import { NodeMeta, useDiscover } from 'teleport/Discover/useDiscover';
|
||||
import { usePoll } from 'teleport/Discover/Shared/usePoll';
|
||||
|
||||
export function CreateEc2IceDialog({
|
||||
nextStep,
|
||||
retry,
|
||||
existingEice,
|
||||
}: {
|
||||
nextStep: () => void;
|
||||
retry?: () => void;
|
||||
existingEice?: Ec2InstanceConnectEndpoint;
|
||||
}) {
|
||||
// If the EICE already exists from the previous step and is create-complete, we don't need to do any polling for the EICE.
|
||||
const [isPollingActive, setIsPollingActive] = useState(
|
||||
existingEice?.state !== 'create-complete'
|
||||
);
|
||||
|
||||
const { emitErrorEvent, updateAgentMeta, agentMeta } = useDiscover();
|
||||
const typedAgentMeta = agentMeta as NodeMeta;
|
||||
|
||||
const nodeService = new NodeService();
|
||||
|
||||
const { attempt: fetchEc2IceAttempt, setAttempt: setFetchEc2IceAttempt } =
|
||||
useAttempt('');
|
||||
const { attempt: createNodeAttempt, setAttempt: setCreateNodeAttempt } =
|
||||
useAttempt('');
|
||||
|
||||
// When the EICE's state is 'create-complete', create the node.
|
||||
useEffect(() => {
|
||||
if (typedAgentMeta.ec2Ice?.state === 'create-complete') {
|
||||
createNode();
|
||||
}
|
||||
}, [typedAgentMeta.ec2Ice]);
|
||||
|
||||
let ec2Ice = usePoll<Ec2InstanceConnectEndpoint>(
|
||||
() =>
|
||||
fetchEc2InstanceConnectEndpoint().then(e => {
|
||||
if (e?.state === 'create-complete') {
|
||||
setIsPollingActive(false);
|
||||
updateAgentMeta({
|
||||
...typedAgentMeta,
|
||||
ec2Ice: e,
|
||||
});
|
||||
}
|
||||
return e;
|
||||
}),
|
||||
isPollingActive,
|
||||
10000 // poll every 10 seconds
|
||||
);
|
||||
|
||||
// If the EICE already existed from the previous step and was create-complete, we set
|
||||
// `ec2Ice` to it.
|
||||
if (existingEice?.state === 'create-complete') {
|
||||
ec2Ice = existingEice;
|
||||
}
|
||||
|
||||
async function fetchEc2InstanceConnectEndpoint() {
|
||||
const integration = typedAgentMeta.integration;
|
||||
|
||||
setFetchEc2IceAttempt({ status: 'processing' });
|
||||
try {
|
||||
const { endpoints: fetchedEc2Ices } =
|
||||
await integrationService.fetchAwsEc2InstanceConnectEndpoints(
|
||||
integration.name,
|
||||
{
|
||||
region: typedAgentMeta.node.awsMetadata.region,
|
||||
vpcId: typedAgentMeta.node.awsMetadata.vpcId,
|
||||
}
|
||||
);
|
||||
|
||||
setFetchEc2IceAttempt({ status: 'success' });
|
||||
|
||||
const createCompleteEice = fetchedEc2Ices.find(
|
||||
e => e.state === 'create-complete'
|
||||
);
|
||||
if (createCompleteEice) {
|
||||
return createCompleteEice;
|
||||
}
|
||||
|
||||
const createInProgressEice = fetchedEc2Ices.find(
|
||||
e => e.state === 'create-in-progress'
|
||||
);
|
||||
if (createInProgressEice) {
|
||||
return createInProgressEice;
|
||||
}
|
||||
|
||||
const createFailedEice = fetchedEc2Ices.find(
|
||||
e => e.state === 'create-failed'
|
||||
);
|
||||
if (createFailedEice) {
|
||||
return createFailedEice;
|
||||
}
|
||||
} catch (err) {
|
||||
const errMsg = getErrMessage(err);
|
||||
setFetchEc2IceAttempt({ status: 'failed', statusText: errMsg });
|
||||
setIsPollingActive(false);
|
||||
emitErrorEvent(`ec2 instance connect endpoint fetch error: ${errMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function createNode() {
|
||||
setCreateNodeAttempt({ status: 'processing' });
|
||||
try {
|
||||
const node = await nodeService.createNode(cfg.proxyCluster, {
|
||||
hostname: typedAgentMeta.node.hostname,
|
||||
addr: typedAgentMeta.node.addr,
|
||||
labels: typedAgentMeta.node.labels,
|
||||
aws: typedAgentMeta.node.awsMetadata,
|
||||
name: typedAgentMeta.node.id,
|
||||
subKind: 'openssh-ec2-ice',
|
||||
});
|
||||
|
||||
updateAgentMeta({
|
||||
...typedAgentMeta,
|
||||
node,
|
||||
resourceName: node.id,
|
||||
});
|
||||
setCreateNodeAttempt({ status: 'success' });
|
||||
} catch (err) {
|
||||
const errMsg = getErrMessage(err);
|
||||
setCreateNodeAttempt({ status: 'failed', statusText: errMsg });
|
||||
setIsPollingActive(false);
|
||||
emitErrorEvent(`error creating teleport node: ${errMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
let content: JSX.Element;
|
||||
if (
|
||||
fetchEc2IceAttempt.status === 'failed' ||
|
||||
createNodeAttempt.status === 'failed'
|
||||
) {
|
||||
content = (
|
||||
<>
|
||||
<Flex mb={5} alignItems="center">
|
||||
{' '}
|
||||
<Icons.Warning size="large" ml={1} mr={2} color="error.main" />
|
||||
<Text>
|
||||
{fetchEc2IceAttempt.status === 'failed'
|
||||
? fetchEc2IceAttempt.statusText
|
||||
: createNodeAttempt.statusText}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex>
|
||||
{!!retry && (
|
||||
<ButtonPrimary mr={3} width="50%" onClick={retry}>
|
||||
Retry
|
||||
</ButtonPrimary>
|
||||
)}
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
if (ec2Ice?.state === 'create-failed') {
|
||||
content = (
|
||||
<>
|
||||
<AnimatedProgressBar mb={1} />
|
||||
<TextIcon mt={2} mb={3}>
|
||||
<Icons.Warning size="large" ml={1} mr={2} color="warning.main" />
|
||||
<Box
|
||||
css={`
|
||||
text-align: center;
|
||||
`}
|
||||
>
|
||||
We couldn't create the EC2 Instance Connect Endpoint.
|
||||
<br />
|
||||
Please visit your{' '}
|
||||
<Link
|
||||
color="text.main"
|
||||
href={ec2Ice?.dashboardLink}
|
||||
target="_blank"
|
||||
>
|
||||
dashboard{' '}
|
||||
</Link>
|
||||
to troubleshoot.
|
||||
<br />
|
||||
We'll keep looking for the endpoint until it becomes available.
|
||||
</Box>
|
||||
</TextIcon>
|
||||
<ButtonPrimary width="100%" disabled>
|
||||
Next
|
||||
</ButtonPrimary>
|
||||
</>
|
||||
);
|
||||
} else if (
|
||||
ec2Ice?.state === 'create-complete' &&
|
||||
createNodeAttempt.status === 'success'
|
||||
) {
|
||||
content = (
|
||||
<>
|
||||
{/* Don't show this message if the EICE had already been deployed before this step. */}
|
||||
{!(existingEice?.state === 'create-complete') && (
|
||||
<Text
|
||||
mb={2}
|
||||
style={{ display: 'flex', textAlign: 'left', width: '100%' }}
|
||||
>
|
||||
<Icons.Check size="small" ml={1} mr={2} color="success" />
|
||||
The EC2 Instance Connect Endpoint was successfully deployed.
|
||||
</Text>
|
||||
)}
|
||||
<Text
|
||||
mb={5}
|
||||
style={{ display: 'flex', textAlign: 'left', width: '100%' }}
|
||||
>
|
||||
<Icons.Check size="small" ml={1} mr={2} color="success" />
|
||||
The EC2 instance [{typedAgentMeta?.node.awsMetadata.instanceId}] has
|
||||
been added to Teleport.
|
||||
</Text>
|
||||
<ButtonPrimary width="100%" onClick={() => nextStep()}>
|
||||
Next
|
||||
</ButtonPrimary>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<>
|
||||
<AnimatedProgressBar mb={1} />
|
||||
<TextIcon
|
||||
mt={2}
|
||||
mb={3}
|
||||
css={`
|
||||
white-space: pre;
|
||||
`}
|
||||
>
|
||||
<Icons.Clock size="medium" />
|
||||
This may take a few minutes..
|
||||
</TextIcon>
|
||||
<ButtonPrimary width="100%" disabled>
|
||||
Next
|
||||
</ButtonPrimary>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let title = 'Creating EC2 Instance Connect Endpoint';
|
||||
|
||||
if (ec2Ice?.state === 'create-complete') {
|
||||
if (createNodeAttempt.status === 'success') {
|
||||
title = 'Created Teleport Node';
|
||||
} else {
|
||||
title = 'Creating Teleport Node';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog disableEscapeKeyDown={false} open={true}>
|
||||
<DialogContent
|
||||
width="460px"
|
||||
alignItems="center"
|
||||
mb={0}
|
||||
textAlign="center"
|
||||
>
|
||||
<Text bold caps mb={4}>
|
||||
{title}
|
||||
</Text>
|
||||
{content}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export type CreateEc2IceDialogProps = {
|
||||
ec2Ice: Ec2InstanceConnectEndpoint;
|
||||
fetchEc2IceAttempt: Attempt;
|
||||
createNodeAttempt: Attempt;
|
||||
retry: () => void;
|
||||
next: () => void;
|
||||
};
|
|
@ -0,0 +1,193 @@
|
|||
/**
|
||||
* 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, { useState, useEffect } from 'react';
|
||||
import { Box, Text } from 'design';
|
||||
import Table from 'design/DataTable';
|
||||
import { Danger } from 'design/Alert';
|
||||
import { FetchStatus } from 'design/DataTable/types';
|
||||
import { Attempt } from 'shared/hooks/useAttemptNext';
|
||||
|
||||
import cfg from 'teleport/config';
|
||||
|
||||
import { TextSelectCopyMulti } from 'teleport/components/TextSelectCopy';
|
||||
import { CommandBox } from 'teleport/Discover/Shared/CommandBox';
|
||||
import {
|
||||
RadioCell,
|
||||
DisableableCell as Cell,
|
||||
Labels,
|
||||
labelMatcher,
|
||||
} from 'teleport/Discover/Shared';
|
||||
|
||||
import { NodeMeta, useDiscover } from 'teleport/Discover/useDiscover';
|
||||
import { Regions } from 'teleport/services/integrations';
|
||||
|
||||
import { CheckedEc2Instance } from './EnrollEc2Instance';
|
||||
|
||||
type Props = {
|
||||
attempt: Attempt;
|
||||
items: CheckedEc2Instance[];
|
||||
fetchStatus: FetchStatus;
|
||||
fetchNextPage(): void;
|
||||
onSelectInstance(item: CheckedEc2Instance): void;
|
||||
selectedInstance?: CheckedEc2Instance;
|
||||
region: Regions;
|
||||
};
|
||||
|
||||
export const Ec2InstanceList = ({
|
||||
attempt,
|
||||
items = [],
|
||||
fetchStatus = '',
|
||||
fetchNextPage,
|
||||
onSelectInstance,
|
||||
selectedInstance,
|
||||
region,
|
||||
}: Props) => {
|
||||
const [scriptUrl, setScriptUrl] = useState('');
|
||||
const hasError = attempt.status === 'failed';
|
||||
const { agentMeta } = useDiscover();
|
||||
|
||||
const showConfigureScript =
|
||||
hasError &&
|
||||
attempt.statusText.includes('StatusCode: 403, RequestID:') &&
|
||||
attempt.statusText.includes('operation error');
|
||||
|
||||
// Regenerate the script any time the region changes.
|
||||
useEffect(() => {
|
||||
if (region) {
|
||||
generateAutoConfigScript();
|
||||
}
|
||||
}, [region]);
|
||||
|
||||
function generateAutoConfigScript() {
|
||||
const newScriptUrl = cfg.getEc2InstanceConnectIAMConfigureScriptUrl({
|
||||
region: region,
|
||||
|
||||
// arn's are formatted as `don-care-about-this-part/role-arn`.
|
||||
// We are splitting by slash and getting the last element.
|
||||
awsOidcRoleArn: (agentMeta as NodeMeta).integration.spec.roleArn
|
||||
.split('/')
|
||||
.pop(),
|
||||
});
|
||||
|
||||
setScriptUrl(newScriptUrl);
|
||||
}
|
||||
|
||||
const disabledText = `This EC2 instance is already enrolled and is a part of this cluster`;
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasError && !showConfigureScript && (
|
||||
<Danger>{attempt.statusText}</Danger>
|
||||
)}
|
||||
{!hasError && (
|
||||
<Table
|
||||
data={items}
|
||||
columns={[
|
||||
{
|
||||
altKey: 'radio-select',
|
||||
headerText: 'Select',
|
||||
render: item => {
|
||||
const isChecked =
|
||||
item.awsMetadata.instanceId ===
|
||||
selectedInstance?.awsMetadata.instanceId;
|
||||
return (
|
||||
<RadioCell<CheckedEc2Instance>
|
||||
item={item}
|
||||
key={item.awsMetadata.instanceId}
|
||||
isChecked={isChecked}
|
||||
onChange={onSelectInstance}
|
||||
disabled={item.ec2InstanceExists}
|
||||
value={item.awsMetadata.instanceId}
|
||||
disabledText={disabledText}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'hostname',
|
||||
headerText: 'Hostname',
|
||||
render: ({ hostname, ec2InstanceExists }) => (
|
||||
<Cell disabledText={disabledText} disabled={ec2InstanceExists}>
|
||||
{hostname}
|
||||
</Cell>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'addr',
|
||||
headerText: 'Address',
|
||||
render: ({ addr, ec2InstanceExists }) => (
|
||||
<Cell disabledText={disabledText} disabled={ec2InstanceExists}>
|
||||
{addr}
|
||||
</Cell>
|
||||
),
|
||||
},
|
||||
{
|
||||
altKey: 'instanceId',
|
||||
headerText: 'AWS Instance ID',
|
||||
render: ({ awsMetadata, ec2InstanceExists }) => (
|
||||
<Cell disabledText={disabledText} disabled={ec2InstanceExists}>
|
||||
<Text
|
||||
css={`
|
||||
text-wrap: nowrap;
|
||||
`}
|
||||
>
|
||||
{awsMetadata.instanceId}
|
||||
</Text>
|
||||
</Cell>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'labels',
|
||||
headerText: 'Labels',
|
||||
render: ({ labels, ec2InstanceExists }) => (
|
||||
<Cell disabledText={disabledText} disabled={ec2InstanceExists}>
|
||||
<Labels labels={labels} />
|
||||
</Cell>
|
||||
),
|
||||
},
|
||||
]}
|
||||
emptyText="No Results"
|
||||
pagination={{ pageSize: 10 }}
|
||||
customSearchMatchers={[labelMatcher]}
|
||||
fetching={{ onFetchMore: fetchNextPage, fetchStatus }}
|
||||
isSearchable
|
||||
/>
|
||||
)}
|
||||
{showConfigureScript && (
|
||||
<Box mt={4}>
|
||||
<CommandBox
|
||||
header={
|
||||
<>
|
||||
<Text bold>Configure your AWS IAM permissions</Text>
|
||||
<Text typography="subtitle1" mb={3}>
|
||||
We were unable to list your EC2 instances. Run the command
|
||||
below on your AWS CloudShell to configure your IAM
|
||||
permissions. Then press the refresh button above.
|
||||
</Text>
|
||||
</>
|
||||
}
|
||||
hasTtl={false}
|
||||
>
|
||||
<TextSelectCopyMulti
|
||||
lines={[{ text: `bash -c "$(curl '${scriptUrl}')"` }]}
|
||||
/>
|
||||
</CommandBox>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,225 @@
|
|||
/**
|
||||
* 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 { initialize, mswLoader } from 'msw-storybook-addon';
|
||||
import { rest } from 'msw';
|
||||
|
||||
import { ContextProvider } from 'teleport';
|
||||
import cfg from 'teleport/config';
|
||||
import { createTeleportContext } from 'teleport/mocks/contexts';
|
||||
import {
|
||||
DiscoverProvider,
|
||||
DiscoverContextState,
|
||||
} from 'teleport/Discover/useDiscover';
|
||||
import {
|
||||
IntegrationKind,
|
||||
IntegrationStatusCode,
|
||||
} from 'teleport/services/integrations';
|
||||
|
||||
import { EnrollEc2Instance } from './EnrollEc2Instance';
|
||||
|
||||
export default {
|
||||
title: 'Teleport/Discover/Server/EC2/InstanceList',
|
||||
loaders: [mswLoader],
|
||||
};
|
||||
|
||||
initialize();
|
||||
|
||||
export const InstanceList = () => <Component />;
|
||||
|
||||
InstanceList.parameters = {
|
||||
msw: {
|
||||
handlers: [
|
||||
rest.post(cfg.getListEc2InstancesUrl('test-oidc'), (req, res, ctx) =>
|
||||
res(ctx.json({ servers: ec2InstancesResponse }))
|
||||
),
|
||||
rest.get(cfg.getClusterNodesUrl('localhost'), (req, res, ctx) =>
|
||||
res(ctx.json({ items: [ec2InstancesResponse[2]] }))
|
||||
),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const InstanceListLoading = () => <Component />;
|
||||
|
||||
InstanceListLoading.parameters = {
|
||||
msw: {
|
||||
handlers: [
|
||||
rest.post(cfg.getListEc2InstancesUrl('test-oidc'), (req, res, ctx) =>
|
||||
res(ctx.delay('infinite'))
|
||||
),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const WithAwsPermissionsError = () => <Component />;
|
||||
|
||||
WithAwsPermissionsError.parameters = {
|
||||
msw: {
|
||||
handlers: [
|
||||
rest.post(cfg.getListEc2InstancesUrl('test-oidc'), (req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(403),
|
||||
ctx.json({ message: 'StatusCode: 403, RequestID: operation error' })
|
||||
)
|
||||
),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const WithOtherError = () => <Component />;
|
||||
|
||||
WithOtherError.parameters = {
|
||||
msw: {
|
||||
handlers: [
|
||||
rest.post(cfg.getListEc2InstancesUrl('test-oidc'), (req, res, ctx) =>
|
||||
res(ctx.status(404))
|
||||
),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const Component = () => {
|
||||
const ctx = createTeleportContext();
|
||||
const discoverCtx: DiscoverContextState = {
|
||||
agentMeta: {
|
||||
resourceName: 'node-name',
|
||||
agentMatcherLabels: [],
|
||||
db: {} as any,
|
||||
selectedAwsRdsDb: {} as any,
|
||||
node: {} as any,
|
||||
integration: {
|
||||
kind: IntegrationKind.AwsOidc,
|
||||
name: 'test-oidc',
|
||||
resourceType: 'integration',
|
||||
spec: {
|
||||
roleArn: 'arn-123',
|
||||
},
|
||||
statusCode: IntegrationStatusCode.Running,
|
||||
},
|
||||
},
|
||||
currentStep: 0,
|
||||
nextStep: () => null,
|
||||
prevStep: () => null,
|
||||
onSelectResource: () => null,
|
||||
resourceSpec: {} as any,
|
||||
exitFlow: () => null,
|
||||
viewConfig: null,
|
||||
indexedViews: [],
|
||||
setResourceSpec: () => null,
|
||||
updateAgentMeta: () => null,
|
||||
emitErrorEvent: () => null,
|
||||
emitEvent: () => null,
|
||||
eventState: null,
|
||||
};
|
||||
|
||||
cfg.proxyCluster = 'localhost';
|
||||
return (
|
||||
<MemoryRouter
|
||||
initialEntries={[
|
||||
{ pathname: cfg.routes.discover, state: { entity: 'server' } },
|
||||
]}
|
||||
>
|
||||
<ContextProvider ctx={ctx}>
|
||||
<DiscoverProvider mockCtx={discoverCtx}>
|
||||
<EnrollEc2Instance />
|
||||
</DiscoverProvider>
|
||||
</ContextProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
|
||||
const ec2InstancesResponse = [
|
||||
{
|
||||
id: 'ec2-instance-1',
|
||||
kind: 'node',
|
||||
clusterId: 'cluster',
|
||||
hostname: 'ec2-hostname-1',
|
||||
tags: [{ name: 'instance', value: 'ec2-1' }],
|
||||
addr: 'ec2.1.com',
|
||||
tunnel: false,
|
||||
subKind: 'openssh-ec2-ice',
|
||||
sshLogins: ['test'],
|
||||
aws: {
|
||||
accountId: 'test-account',
|
||||
instanceId: 'instance-ec2-1',
|
||||
region: 'us-west-1',
|
||||
vpcId: 'test',
|
||||
integration: 'test',
|
||||
subnetId: 'test',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'ec2-instance-2',
|
||||
kind: 'node',
|
||||
clusterId: 'cluster',
|
||||
hostname: 'ec2-hostname-2',
|
||||
tags: [{ name: 'instance', value: 'ec2-2' }],
|
||||
addr: 'ec2.2.com',
|
||||
tunnel: false,
|
||||
subKind: 'openssh-ec2-ice',
|
||||
sshLogins: ['test'],
|
||||
aws: {
|
||||
accountId: 'test-account',
|
||||
instanceId: 'instance-ec2-2',
|
||||
region: 'us-west-1',
|
||||
vpcId: 'test',
|
||||
integration: 'test',
|
||||
subnetId: 'test',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'ec2-instance-3',
|
||||
kind: 'node',
|
||||
clusterId: 'cluster',
|
||||
hostname: 'ec2-hostname-3',
|
||||
tags: [{ name: 'instance', value: 'ec2-3' }],
|
||||
addr: 'ec2.3.com',
|
||||
tunnel: false,
|
||||
subKind: 'openssh-ec2-ice',
|
||||
sshLogins: ['test'],
|
||||
aws: {
|
||||
accountId: 'test-account',
|
||||
instanceId: 'instance-ec2-3',
|
||||
region: 'us-west-1',
|
||||
vpcId: 'test',
|
||||
integration: 'test',
|
||||
subnetId: 'test',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'ec2-instance-4',
|
||||
kind: 'node',
|
||||
clusterId: 'cluster',
|
||||
hostname: 'ec2-hostname-4',
|
||||
tags: [{ name: 'instance', value: 'ec2-4' }],
|
||||
addr: 'ec2.4.com',
|
||||
tunnel: false,
|
||||
subKind: 'openssh-ec2-ice',
|
||||
sshLogins: ['test'],
|
||||
aws: {
|
||||
accountId: 'test-account',
|
||||
instanceId: 'instance-ec2-4',
|
||||
region: 'us-west-1',
|
||||
vpcId: 'test',
|
||||
integration: 'test',
|
||||
subnetId: 'test',
|
||||
},
|
||||
},
|
||||
];
|
|
@ -0,0 +1,272 @@
|
|||
/**
|
||||
* 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, { useState } from 'react';
|
||||
import { Box, Text } from 'design';
|
||||
import { FetchStatus } from 'design/DataTable/types';
|
||||
import useAttempt from 'shared/hooks/useAttemptNext';
|
||||
|
||||
import { getErrMessage } from 'shared/utils/errorType';
|
||||
|
||||
import cfg from 'teleport/config';
|
||||
import { NodeMeta, useDiscover } from 'teleport/Discover/useDiscover';
|
||||
import {
|
||||
Ec2InstanceConnectEndpoint,
|
||||
Regions,
|
||||
integrationService,
|
||||
} from 'teleport/services/integrations';
|
||||
import { AwsRegionSelector } from 'teleport/Discover/Shared/AwsRegionSelector';
|
||||
import NodeService, { Node } from 'teleport/services/nodes';
|
||||
|
||||
import { ActionButtons, Header } from '../../Shared';
|
||||
|
||||
import { CreateEc2IceDialog } from '../CreateEc2Ice/CreateEc2IceDialog';
|
||||
|
||||
import { Ec2InstanceList } from './Ec2InstanceList';
|
||||
|
||||
// CheckedEc2Instance is a type to describe that an EC2 instance
|
||||
// has been checked to determine whether or not it is already enrolled in the cluster.
|
||||
export type CheckedEc2Instance = Node & {
|
||||
ec2InstanceExists?: boolean;
|
||||
};
|
||||
|
||||
type TableData = {
|
||||
items: CheckedEc2Instance[];
|
||||
fetchStatus: FetchStatus;
|
||||
nextToken?: string;
|
||||
currRegion?: Regions;
|
||||
};
|
||||
|
||||
const emptyTableData: TableData = {
|
||||
items: [],
|
||||
fetchStatus: 'disabled',
|
||||
nextToken: '',
|
||||
};
|
||||
|
||||
export function EnrollEc2Instance() {
|
||||
const { agentMeta, emitErrorEvent, nextStep, updateAgentMeta } =
|
||||
useDiscover();
|
||||
const nodeService = new NodeService();
|
||||
|
||||
const [currRegion, setCurrRegion] = useState<Regions>();
|
||||
const [existingEice, setExistingEice] =
|
||||
useState<Ec2InstanceConnectEndpoint>();
|
||||
const [selectedInstance, setSelectedInstance] =
|
||||
useState<CheckedEc2Instance>();
|
||||
|
||||
const [tableData, setTableData] = useState<TableData>({
|
||||
items: [],
|
||||
nextToken: '',
|
||||
fetchStatus: 'disabled',
|
||||
});
|
||||
|
||||
const {
|
||||
attempt: fetchEc2InstancesAttempt,
|
||||
setAttempt: setFetchEc2InstancesAttempt,
|
||||
} = useAttempt('');
|
||||
|
||||
const { attempt: fetchEc2IceAttempt, setAttempt: setFetchEc2IceAttempt } =
|
||||
useAttempt('');
|
||||
|
||||
function fetchEc2InstancesWithNewRegion(region: Regions) {
|
||||
if (region) {
|
||||
setCurrRegion(region);
|
||||
fetchEc2Instances({ ...emptyTableData, currRegion: region });
|
||||
}
|
||||
}
|
||||
|
||||
function fetchNextPage() {
|
||||
fetchEc2Instances({ ...tableData });
|
||||
}
|
||||
|
||||
function refreshEc2Instances() {
|
||||
// When refreshing, start the table back at page 1.
|
||||
fetchEc2Instances({ ...tableData, nextToken: '', items: [], currRegion });
|
||||
}
|
||||
|
||||
async function fetchEc2Instances(data: TableData) {
|
||||
const integrationName = (agentMeta as NodeMeta).integration.name;
|
||||
|
||||
setTableData({ ...data, fetchStatus: 'loading' });
|
||||
setFetchEc2InstancesAttempt({ status: 'processing' });
|
||||
|
||||
try {
|
||||
const { instances: fetchedEc2Instances, nextToken } =
|
||||
await integrationService.fetchAwsEc2Instances(integrationName, {
|
||||
region: data.currRegion,
|
||||
nextToken: data.nextToken,
|
||||
});
|
||||
|
||||
// Abort if there were no EC2 instances for the selected region.
|
||||
if (fetchedEc2Instances.length <= 0) {
|
||||
setFetchEc2InstancesAttempt({ status: 'success' });
|
||||
setTableData({ ...data, fetchStatus: 'disabled' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if fetched EC2 instances are already in the cluster
|
||||
// so that they can be disabled in the table.
|
||||
|
||||
// Builds the predicate string that will query for
|
||||
// all the fetched EC2 instances by searching by the AWS instance ID label.
|
||||
const instanceIdPredicateQueries: string[] = fetchedEc2Instances.map(
|
||||
d =>
|
||||
`labels["teleport.dev/instance-id"] == "${d.awsMetadata.instanceId}"`
|
||||
);
|
||||
const fullPredicateQuery = instanceIdPredicateQueries.join(' || ');
|
||||
const { agents: fetchedNodes } = await nodeService.fetchNodes(
|
||||
cfg.proxyCluster,
|
||||
{
|
||||
query: fullPredicateQuery,
|
||||
limit: fetchedEc2Instances.length,
|
||||
}
|
||||
);
|
||||
|
||||
const ec2InstancesLookupByInstanceId: Record<string, Node> = {};
|
||||
fetchedNodes.forEach(
|
||||
d => (ec2InstancesLookupByInstanceId[d.awsMetadata.instanceId] = d)
|
||||
);
|
||||
|
||||
// Check for already existing EC2 instances.
|
||||
const checkedEc2Instances: CheckedEc2Instance[] = fetchedEc2Instances.map(
|
||||
ec2 => {
|
||||
const instance =
|
||||
ec2InstancesLookupByInstanceId[ec2.awsMetadata.instanceId];
|
||||
if (instance) {
|
||||
return {
|
||||
...ec2,
|
||||
ec2InstanceExists: true,
|
||||
};
|
||||
}
|
||||
return ec2;
|
||||
}
|
||||
);
|
||||
|
||||
setFetchEc2InstancesAttempt({ status: 'success' });
|
||||
setTableData({
|
||||
currRegion,
|
||||
nextToken,
|
||||
fetchStatus: nextToken ? '' : 'disabled',
|
||||
items: [...data.items, ...checkedEc2Instances],
|
||||
});
|
||||
} catch (err) {
|
||||
const errMsg = getErrMessage(err);
|
||||
setTableData(data);
|
||||
setFetchEc2InstancesAttempt({ status: 'failed', statusText: errMsg });
|
||||
emitErrorEvent(`ec2 instance fetch error: ${errMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchEc2InstanceConnectEndpoints() {
|
||||
const integrationName = (agentMeta as NodeMeta).integration.name;
|
||||
|
||||
setFetchEc2IceAttempt({ status: 'processing' });
|
||||
try {
|
||||
const { endpoints: fetchedEc2Ices } =
|
||||
await integrationService.fetchAwsEc2InstanceConnectEndpoints(
|
||||
integrationName,
|
||||
{
|
||||
region: selectedInstance.awsMetadata.region,
|
||||
vpcId: selectedInstance.awsMetadata.vpcId,
|
||||
}
|
||||
);
|
||||
setFetchEc2IceAttempt({ status: 'success' });
|
||||
return fetchedEc2Ices;
|
||||
} catch (err) {
|
||||
const errMsg = getErrMessage(err);
|
||||
setFetchEc2InstancesAttempt({ status: 'failed', statusText: errMsg });
|
||||
emitErrorEvent(`ec2 instance connect endpoint fetch error: ${errMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
function clear() {
|
||||
setFetchEc2InstancesAttempt({ status: '' });
|
||||
setTableData(emptyTableData);
|
||||
setSelectedInstance(null);
|
||||
}
|
||||
|
||||
function handleOnProceed() {
|
||||
fetchEc2InstanceConnectEndpoints().then(ec2Ices => {
|
||||
const createCompleteEice = ec2Ices.find(
|
||||
e => e.state === 'create-complete'
|
||||
);
|
||||
const createInProgressEice = ec2Ices.find(
|
||||
e => e.state === 'create-in-progress'
|
||||
);
|
||||
|
||||
// If we find existing EICE's that are either create-complete or create-in-progress, we skip the step where we create the EICE.
|
||||
|
||||
// We first check for any EICE's that are create-complete, if we find one, the dialog will go straight to creating the node.
|
||||
// If we don't find any, we check if there are any that are create-in-progress, if we find one, the dialog will wait until
|
||||
// it's create-complete and then create the node.
|
||||
if (createCompleteEice || createInProgressEice) {
|
||||
setExistingEice(createCompleteEice || createInProgressEice);
|
||||
updateAgentMeta({
|
||||
...(agentMeta as NodeMeta),
|
||||
node: selectedInstance,
|
||||
ec2Ice: createCompleteEice || createInProgressEice,
|
||||
});
|
||||
// If we find neither, then we go to the next step to create the EICE.
|
||||
} else {
|
||||
updateAgentMeta({
|
||||
...(agentMeta as NodeMeta),
|
||||
node: selectedInstance,
|
||||
});
|
||||
nextStep();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Box maxWidth="1000px">
|
||||
<Header>Enroll an EC2 instance</Header>
|
||||
<Text mt={4}>
|
||||
Select the AWS Region you would like to see EC2 instances for:
|
||||
</Text>
|
||||
<AwsRegionSelector
|
||||
onFetch={fetchEc2InstancesWithNewRegion}
|
||||
onRefresh={refreshEc2Instances}
|
||||
clear={clear}
|
||||
disableSelector={fetchEc2InstancesAttempt.status === 'processing'}
|
||||
/>
|
||||
{currRegion && (
|
||||
<Ec2InstanceList
|
||||
attempt={fetchEc2InstancesAttempt}
|
||||
items={tableData.items}
|
||||
fetchStatus={tableData.fetchStatus}
|
||||
selectedInstance={selectedInstance}
|
||||
onSelectInstance={setSelectedInstance}
|
||||
fetchNextPage={fetchNextPage}
|
||||
region={currRegion}
|
||||
/>
|
||||
)}
|
||||
{existingEice && (
|
||||
<CreateEc2IceDialog
|
||||
nextStep={() => nextStep(2)}
|
||||
existingEice={existingEice}
|
||||
/>
|
||||
)}
|
||||
<ActionButtons
|
||||
onProceed={handleOnProceed}
|
||||
disableProceed={
|
||||
fetchEc2InstancesAttempt.status === 'processing' ||
|
||||
fetchEc2IceAttempt.status === 'processing' ||
|
||||
!selectedInstance
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -20,38 +20,85 @@ import { ResourceViewConfig } from 'teleport/Discover/flow';
|
|||
import { DownloadScript } from 'teleport/Discover/Server/DownloadScript';
|
||||
import { SetupAccess } from 'teleport/Discover/Server/SetupAccess';
|
||||
import { TestConnection } from 'teleport/Discover/Server/TestConnection';
|
||||
import { ResourceKind, Finished } from 'teleport/Discover/Shared';
|
||||
import { AwsAccount, ResourceKind, Finished } from 'teleport/Discover/Shared';
|
||||
import { DiscoverEvent } from 'teleport/services/userEvent';
|
||||
|
||||
import { ResourceSpec, ServerLocation } from '../SelectResource';
|
||||
|
||||
import { EnrollEc2Instance } from './EnrollEc2Instance/EnrollEc2Instance';
|
||||
import { CreateEc2Ice } from './CreateEc2Ice/CreateEc2Ice';
|
||||
|
||||
import { ServerWrapper } from './ServerWrapper';
|
||||
|
||||
export const ServerResource: ResourceViewConfig = {
|
||||
export const ServerResource: ResourceViewConfig<ResourceSpec> = {
|
||||
kind: ResourceKind.Server,
|
||||
wrapper: (component: React.ReactNode) => (
|
||||
<ServerWrapper>{component}</ServerWrapper>
|
||||
),
|
||||
views: [
|
||||
{
|
||||
title: 'Configure Resource',
|
||||
component: DownloadScript,
|
||||
eventName: DiscoverEvent.DeployService,
|
||||
},
|
||||
{
|
||||
title: 'Set Up Access',
|
||||
component: SetupAccess,
|
||||
eventName: DiscoverEvent.PrincipalsConfigure,
|
||||
},
|
||||
{
|
||||
title: 'Test Connection',
|
||||
component: TestConnection,
|
||||
eventName: DiscoverEvent.TestConnection,
|
||||
manuallyEmitSuccessEvent: true,
|
||||
},
|
||||
{
|
||||
title: 'Finished',
|
||||
component: Finished,
|
||||
hide: true,
|
||||
eventName: DiscoverEvent.Completed,
|
||||
},
|
||||
],
|
||||
shouldPrompt(currentStep, resourceSpec) {
|
||||
if (resourceSpec?.nodeMeta?.location === ServerLocation.Aws) {
|
||||
// Allow user to bypass prompting on this step (Connect AWS Connect)
|
||||
// on exit because users might need to change route to setup an
|
||||
// integration.
|
||||
if (currentStep === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
views(resource) {
|
||||
let configureResourceViews;
|
||||
if (resource && resource.nodeMeta?.location === ServerLocation.Aws) {
|
||||
configureResourceViews = [
|
||||
{
|
||||
title: 'Connect AWS Account',
|
||||
component: AwsAccount,
|
||||
eventName: DiscoverEvent.IntegrationAWSOIDCConnectEvent,
|
||||
},
|
||||
{
|
||||
title: 'Enroll EC2 Instance',
|
||||
component: EnrollEc2Instance,
|
||||
// eventName: null, TODO rudream (ADD EVENTS FOR EICE FLOW)
|
||||
},
|
||||
{
|
||||
title: 'Create EC2 Instance Connect Endpoint',
|
||||
component: CreateEc2Ice,
|
||||
// eventName: null, TODO rudream (ADD EVENTS FOR EICE FLOW)
|
||||
},
|
||||
];
|
||||
} else {
|
||||
configureResourceViews = [
|
||||
{
|
||||
title: 'Configure Resource',
|
||||
component: DownloadScript,
|
||||
eventName: DiscoverEvent.DeployService,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
title: 'Configure Resource',
|
||||
views: configureResourceViews,
|
||||
},
|
||||
{
|
||||
title: 'Set Up Access',
|
||||
component: SetupAccess,
|
||||
eventName: DiscoverEvent.PrincipalsConfigure,
|
||||
},
|
||||
{
|
||||
title: 'Test Connection',
|
||||
component: TestConnection,
|
||||
eventName: DiscoverEvent.TestConnection,
|
||||
manuallyEmitSuccessEvent: true,
|
||||
},
|
||||
{
|
||||
title: 'Finished',
|
||||
component: Finished,
|
||||
hide: true,
|
||||
eventName: DiscoverEvent.Completed,
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* 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 { Cell } from 'design/DataTable';
|
||||
|
||||
export const DisableableCell: React.FC<{
|
||||
disabledText: string;
|
||||
disabled: boolean;
|
||||
width?: string;
|
||||
}> = ({ disabledText, disabled, width, children }) => {
|
||||
return (
|
||||
<Cell
|
||||
width={width}
|
||||
title={disabled ? disabledText : null}
|
||||
css={`
|
||||
opacity: ${disabled ? '0.5' : '1'};
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</Cell>
|
||||
);
|
||||
};
|
57
web/packages/teleport/src/Discover/Shared/Aws/Labels.tsx
Normal file
57
web/packages/teleport/src/Discover/Shared/Aws/Labels.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* 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 { Flex, Label as Pill } from 'design';
|
||||
|
||||
import { Label } from 'teleport/types';
|
||||
|
||||
export const Labels = ({ labels }: { labels: Label[] }) => {
|
||||
const $labels = labels.map((label, index) => {
|
||||
const labelText = `${label.name}: ${label.value}`;
|
||||
|
||||
return (
|
||||
<Pill key={`${label.name}${label.value}${index}`} mr="1" kind="secondary">
|
||||
{labelText}
|
||||
</Pill>
|
||||
);
|
||||
});
|
||||
|
||||
return <Flex flexWrap="wrap">{$labels}</Flex>;
|
||||
};
|
||||
|
||||
// labelMatcher allows user to client search by labels in the format
|
||||
// 1) `key: value` or
|
||||
// 2) `key:value` or
|
||||
// 3) `key` or `value`
|
||||
export function labelMatcher<T>(
|
||||
targetValue: any,
|
||||
searchValue: string,
|
||||
propName: keyof T & string
|
||||
) {
|
||||
if (propName === 'labels') {
|
||||
return targetValue.some((label: Label) => {
|
||||
const convertedKey = label.name.toLocaleUpperCase();
|
||||
const convertedVal = label.value.toLocaleUpperCase();
|
||||
const formattedWords = [
|
||||
`${convertedKey}:${convertedVal}`,
|
||||
`${convertedKey}: ${convertedVal}`,
|
||||
];
|
||||
return formattedWords.some(w => w.includes(searchValue));
|
||||
});
|
||||
}
|
||||
}
|
66
web/packages/teleport/src/Discover/Shared/Aws/RadioCell.tsx
Normal file
66
web/packages/teleport/src/Discover/Shared/Aws/RadioCell.tsx
Normal file
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* 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 { Flex } from 'design';
|
||||
|
||||
import { DisableableCell } from './DisableableCell';
|
||||
|
||||
export function RadioCell<T>({
|
||||
item,
|
||||
value,
|
||||
isChecked,
|
||||
onChange,
|
||||
disabled,
|
||||
disabledText,
|
||||
}: {
|
||||
item: T;
|
||||
value: string;
|
||||
isChecked: boolean;
|
||||
onChange(selectedItem: T): void;
|
||||
disabled: boolean;
|
||||
disabledText: string;
|
||||
}) {
|
||||
return (
|
||||
<DisableableCell
|
||||
width="20px"
|
||||
disabled={disabled}
|
||||
disabledText={disabledText}
|
||||
>
|
||||
<Flex alignItems="center" my={2} justifyContent="center">
|
||||
<input
|
||||
// eslint-disable-next-line react/no-unknown-property
|
||||
css={`
|
||||
margin: 0 ${props => props.theme.space[2]}px 0 0;
|
||||
accent-color: ${props => props.theme.colors.brand.accent};
|
||||
cursor: pointer;
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
`}
|
||||
type="radio"
|
||||
name={value}
|
||||
checked={isChecked}
|
||||
onChange={() => onChange(item)}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Flex>
|
||||
</DisableableCell>
|
||||
);
|
||||
}
|
19
web/packages/teleport/src/Discover/Shared/Aws/index.ts
Normal file
19
web/packages/teleport/src/Discover/Shared/Aws/index.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { DisableableCell } from './DisableableCell';
|
||||
export { Labels, labelMatcher } from './Labels';
|
||||
export { RadioCell } from './RadioCell';
|
|
@ -38,20 +38,25 @@ import {
|
|||
IntegrationKind,
|
||||
integrationService,
|
||||
} from 'teleport/services/integrations';
|
||||
import { integrationRWEAndDbCU } from 'teleport/Discover/yamlTemplates';
|
||||
import {
|
||||
integrationRWE,
|
||||
integrationRWEAndNodeRWE,
|
||||
integrationRWEAndDbCU,
|
||||
} from 'teleport/Discover/yamlTemplates';
|
||||
import useTeleport from 'teleport/useTeleport';
|
||||
|
||||
import { ActionButtons, HeaderSubtitle, Header } from '../../Shared';
|
||||
|
||||
import {
|
||||
DbMeta,
|
||||
DiscoverUrlLocationState,
|
||||
useDiscover,
|
||||
} from '../../useDiscover';
|
||||
ActionButtons,
|
||||
HeaderSubtitle,
|
||||
Header,
|
||||
ResourceKind,
|
||||
} from '../../Shared';
|
||||
|
||||
import { DiscoverUrlLocationState, useDiscover } from '../../useDiscover';
|
||||
|
||||
type Option = BaseOption<Integration>;
|
||||
|
||||
export function ConnectAwsAccount() {
|
||||
export function AwsAccount() {
|
||||
const { storeUser } = useTeleport();
|
||||
const {
|
||||
prevStep,
|
||||
|
@ -61,16 +66,32 @@ export function ConnectAwsAccount() {
|
|||
eventState,
|
||||
resourceSpec,
|
||||
currentStep,
|
||||
viewConfig,
|
||||
} = useDiscover();
|
||||
|
||||
const integrationAccess = storeUser.getIntegrationsAccess();
|
||||
const databaseAccess = storeUser.getDatabaseAccess();
|
||||
const hasAccess =
|
||||
integrationAccess.create &&
|
||||
integrationAccess.list &&
|
||||
// Required access after integrating:
|
||||
integrationAccess.use && // required to list AWS RDS db's
|
||||
databaseAccess.create; // required to enroll AWS RDS db
|
||||
|
||||
let roleTemplate = integrationRWE;
|
||||
let hasAccess =
|
||||
integrationAccess.create && integrationAccess.list && integrationAccess.use;
|
||||
|
||||
// Ensure required permissions based on which flow this is in.
|
||||
if (viewConfig.kind === ResourceKind.Database) {
|
||||
roleTemplate = integrationRWEAndDbCU;
|
||||
const databaseAccess = storeUser.getDatabaseAccess();
|
||||
hasAccess = hasAccess && databaseAccess.create; // required to enroll AWS RDS db
|
||||
}
|
||||
if (viewConfig.kind === ResourceKind.Server) {
|
||||
roleTemplate = integrationRWEAndNodeRWE;
|
||||
const nodesAccess = storeUser.getNodeAccess();
|
||||
hasAccess =
|
||||
hasAccess &&
|
||||
nodesAccess.create &&
|
||||
nodesAccess.edit &&
|
||||
nodesAccess.list &&
|
||||
nodesAccess.read; // Needed for TestConnection flow
|
||||
}
|
||||
|
||||
const { attempt, run } = useAttempt(hasAccess ? 'processing' : '');
|
||||
|
||||
const [awsIntegrations, setAwsIntegrations] = useState<Option[]>([]);
|
||||
|
@ -114,7 +135,7 @@ export function ConnectAwsAccount() {
|
|||
<TextEditor
|
||||
readOnly={true}
|
||||
bg="levels.deep"
|
||||
data={[{ content: integrationRWEAndDbCU, type: 'yaml' }]}
|
||||
data={[{ content: roleTemplate, type: 'yaml' }]}
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
@ -151,7 +172,7 @@ export function ConnectAwsAccount() {
|
|||
}
|
||||
|
||||
updateAgentMeta({
|
||||
...(agentMeta as DbMeta),
|
||||
...agentMeta,
|
||||
integration: selectedAwsIntegration.value,
|
||||
});
|
||||
|
|
@ -14,4 +14,4 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { ConnectAwsAccount } from './ConnectAwsAccount';
|
||||
export { AwsAccount } from './AwsAccount';
|
|
@ -28,16 +28,23 @@ const Container = styled(Box)`
|
|||
|
||||
interface CommandBoxProps {
|
||||
header?: React.ReactNode;
|
||||
// hasTtl when true means that the command has an expiry TTL, otherwise the command
|
||||
// is valid forever.
|
||||
hasTtl?: boolean;
|
||||
}
|
||||
|
||||
export function CommandBox(props: React.PropsWithChildren<CommandBoxProps>) {
|
||||
export function CommandBox({
|
||||
header,
|
||||
children,
|
||||
hasTtl = true,
|
||||
}: React.PropsWithChildren<CommandBoxProps>) {
|
||||
return (
|
||||
<Container p={3} borderRadius={3} mb={3}>
|
||||
{props.header || <Text bold>Command</Text>}
|
||||
{header || <Text bold>Command</Text>}
|
||||
<Box mt={3} mb={3}>
|
||||
{props.children}
|
||||
{children}
|
||||
</Box>
|
||||
This script is valid for 4 hours.
|
||||
{hasTtl && `This script is valid for 4 hours.`}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,177 @@
|
|||
/**
|
||||
* 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, { useState } from 'react';
|
||||
|
||||
import { Flex, Link } from 'design';
|
||||
import Table, { Cell } from 'design/DataTable';
|
||||
import { Danger } from 'design/Alert';
|
||||
import { CheckboxInput } from 'design/Checkbox';
|
||||
import { FetchStatus } from 'design/DataTable/types';
|
||||
|
||||
import { Attempt } from 'shared/hooks/useAttemptNext';
|
||||
|
||||
import { SecurityGroup } from 'teleport/services/integrations';
|
||||
|
||||
import { SecurityGroupRulesDialog } from './SecurityGroupRulesDialog';
|
||||
|
||||
type Props = {
|
||||
attempt: Attempt;
|
||||
items: SecurityGroup[];
|
||||
fetchStatus: FetchStatus;
|
||||
fetchNextPage(): void;
|
||||
onSelectSecurityGroup: (
|
||||
sg: SecurityGroup,
|
||||
e: React.ChangeEvent<HTMLInputElement>
|
||||
) => void;
|
||||
selectedSecurityGroups: string[];
|
||||
};
|
||||
|
||||
export type ViewRulesSelection = {
|
||||
sg: SecurityGroup;
|
||||
ruleType: 'inbound' | 'outbound';
|
||||
};
|
||||
|
||||
export const SecurityGroupPicker = ({
|
||||
attempt,
|
||||
items = [],
|
||||
fetchStatus = '',
|
||||
fetchNextPage,
|
||||
onSelectSecurityGroup,
|
||||
selectedSecurityGroups,
|
||||
}: Props) => {
|
||||
const [viewRulesSelection, setViewRulesSelection] =
|
||||
useState<ViewRulesSelection>();
|
||||
|
||||
function onCloseRulesDialog() {
|
||||
setViewRulesSelection(null);
|
||||
}
|
||||
|
||||
if (attempt.status === 'failed') {
|
||||
return <Danger>{attempt.statusText}</Danger>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table
|
||||
data={items}
|
||||
columns={[
|
||||
{
|
||||
altKey: 'checkbox-select',
|
||||
headerText: 'Select',
|
||||
render: item => {
|
||||
const isChecked = selectedSecurityGroups.includes(item.id);
|
||||
return (
|
||||
<CheckboxCell
|
||||
item={item}
|
||||
key={item.id}
|
||||
isChecked={isChecked}
|
||||
onChange={onSelectSecurityGroup}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
headerText: 'Name',
|
||||
},
|
||||
{
|
||||
key: 'id',
|
||||
headerText: 'ID',
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
headerText: 'Description',
|
||||
},
|
||||
{
|
||||
altKey: 'inboundRules',
|
||||
headerText: 'Inbound Rules',
|
||||
render: sg => {
|
||||
return (
|
||||
<Cell>
|
||||
<Link
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() =>
|
||||
setViewRulesSelection({ sg, ruleType: 'inbound' })
|
||||
}
|
||||
>
|
||||
View ({sg.inboundRules.length})
|
||||
</Link>
|
||||
</Cell>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
altKey: 'outboundRules',
|
||||
headerText: 'Outbound Rules',
|
||||
render: sg => {
|
||||
return (
|
||||
<Cell>
|
||||
<Link
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() =>
|
||||
setViewRulesSelection({ sg, ruleType: 'outbound' })
|
||||
}
|
||||
>
|
||||
View ({sg.outboundRules.length})
|
||||
</Link>
|
||||
</Cell>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
emptyText="No Security Groups Found"
|
||||
pagination={{ pageSize: 5 }}
|
||||
fetching={{ onFetchMore: fetchNextPage, fetchStatus }}
|
||||
isSearchable
|
||||
/>
|
||||
{viewRulesSelection && (
|
||||
<SecurityGroupRulesDialog
|
||||
viewRulesSelection={viewRulesSelection}
|
||||
onClose={onCloseRulesDialog}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function CheckboxCell({
|
||||
item,
|
||||
isChecked,
|
||||
onChange,
|
||||
}: {
|
||||
item: SecurityGroup;
|
||||
isChecked: boolean;
|
||||
onChange(
|
||||
selectedItem: SecurityGroup,
|
||||
e: React.ChangeEvent<HTMLInputElement>
|
||||
): void;
|
||||
}) {
|
||||
return (
|
||||
<Cell width="20px">
|
||||
<Flex alignItems="center" my={2} justifyContent="center">
|
||||
<CheckboxInput
|
||||
type="checkbox"
|
||||
id={item.id}
|
||||
onChange={e => {
|
||||
onChange(item, e);
|
||||
}}
|
||||
checked={isChecked}
|
||||
/>
|
||||
</Flex>
|
||||
</Cell>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
/**
|
||||
* 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 styled from 'styled-components';
|
||||
|
||||
import { Text, ButtonSecondary } from 'design';
|
||||
import Table, { Cell } from 'design/DataTable';
|
||||
import Dialog, { DialogContent, DialogFooter } from 'design/DialogConfirmation';
|
||||
|
||||
import { ViewRulesSelection } from './SecurityGroupPicker';
|
||||
|
||||
export function SecurityGroupRulesDialog({
|
||||
viewRulesSelection,
|
||||
onClose,
|
||||
}: {
|
||||
viewRulesSelection: ViewRulesSelection;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { ruleType, sg } = viewRulesSelection;
|
||||
const data = ruleType === 'inbound' ? sg.inboundRules : sg.outboundRules;
|
||||
|
||||
return (
|
||||
<Dialog disableEscapeKeyDown={false} open={true}>
|
||||
<DialogContent
|
||||
width="600px"
|
||||
alignItems="center"
|
||||
mb={0}
|
||||
textAlign="center"
|
||||
>
|
||||
<Text mb={4} typography="h4">
|
||||
{ruleType === 'inbound' ? 'Inbound' : 'Outbound'} Rules for [{sg.name}
|
||||
]
|
||||
</Text>
|
||||
<StyledTable
|
||||
data={data}
|
||||
columns={[
|
||||
{
|
||||
key: 'ipProtocol',
|
||||
headerText: 'Type',
|
||||
},
|
||||
{
|
||||
altKey: 'portRange',
|
||||
headerText: 'Port Range',
|
||||
render: ({ fromPort, toPort }) => {
|
||||
// If they are the same, only show one number.
|
||||
const portRange =
|
||||
fromPort === toPort ? fromPort : `${fromPort} - ${toPort}`;
|
||||
return <Cell>{portRange}</Cell>;
|
||||
},
|
||||
},
|
||||
{
|
||||
altKey: 'source',
|
||||
headerText: 'Source',
|
||||
render: ({ cidrs }) => {
|
||||
// The AWS API returns an array, however it appears it's not actually possible to have multiple CIDR's for a single rule.
|
||||
// As a fallback we just display the first one.
|
||||
const cidr = cidrs[0];
|
||||
if (cidr) {
|
||||
return <Cell>{cidr.cidr}</Cell>;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
{
|
||||
altKey: 'description',
|
||||
headerText: 'Description',
|
||||
render: ({ cidrs }) => {
|
||||
const cidr = cidrs[0];
|
||||
if (cidr) {
|
||||
return <Cell>{cidr.description}</Cell>;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
]}
|
||||
emptyText="No Rules Found"
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogFooter
|
||||
css={`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
`}
|
||||
>
|
||||
<ButtonSecondary mt={3} onClick={onClose}>
|
||||
Close
|
||||
</ButtonSecondary>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledTable = styled(Table)`
|
||||
& > tbody > tr > td {
|
||||
vertical-align: middle;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
& > thead > tr > th {
|
||||
background: ${props => props.theme.colors.spotBackground[1]};
|
||||
}
|
||||
|
||||
border-radius: 8px;
|
||||
box-shadow: ${props => props.theme.boxShadow[0]};
|
||||
overflow: hidden;
|
||||
` as typeof Table;
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { SecurityGroupPicker } from './SecurityGroupPicker';
|
||||
export type { ViewRulesSelection } from './SecurityGroupPicker';
|
|
@ -30,5 +30,9 @@ export {
|
|||
} from './ConnectionDiagnostic';
|
||||
export { useShowHint } from './useShowHint';
|
||||
export { StepBox } from './StepBox';
|
||||
export { SecurityGroupPicker } from './SecurityGroupPicker';
|
||||
export type { ViewRulesSelection } from './SecurityGroupPicker';
|
||||
export { AwsAccount } from './AwsAccount';
|
||||
export { DisableableCell, Labels, labelMatcher, RadioCell } from './Aws';
|
||||
|
||||
export type { DiscoverLabel } from './LabelsCreater';
|
||||
|
|
|
@ -46,6 +46,7 @@ import type { ResourceLabel } from 'teleport/services/agents';
|
|||
import type { ResourceSpec } from './SelectResource';
|
||||
import type {
|
||||
AwsRdsDatabase,
|
||||
Ec2InstanceConnectEndpoint,
|
||||
Integration,
|
||||
} from 'teleport/services/integrations';
|
||||
|
||||
|
@ -465,6 +466,8 @@ type BaseMeta = {
|
|||
// that needs to be preserved throughout the flow.
|
||||
export type NodeMeta = BaseMeta & {
|
||||
node: Node;
|
||||
integration?: Integration;
|
||||
ec2Ice?: Ec2InstanceConnectEndpoint;
|
||||
};
|
||||
|
||||
// DbMeta describes the fields for a db resource
|
||||
|
|
|
@ -22,7 +22,9 @@ import kubeAccessRO from './kubeAccessRO.yaml?raw';
|
|||
import dbAccessRW from './dbAccessRW.yaml?raw';
|
||||
import dbAccessRO from './dbAccessRO.yaml?raw';
|
||||
import dbCU from './dbCU.yaml?raw';
|
||||
import integrationRWE from './integrationRWE.yaml?raw';
|
||||
import integrationRWEAndDbCU from './integrationRWEAndDbCU.yaml?raw';
|
||||
import integrationRWEAndNodeRWE from './integrationRWEAndNodeRWE.yaml?raw';
|
||||
|
||||
export {
|
||||
nodeAccessRO,
|
||||
|
@ -33,5 +35,7 @@ export {
|
|||
dbAccessRO,
|
||||
dbAccessRW,
|
||||
dbCU,
|
||||
integrationRWE,
|
||||
integrationRWEAndDbCU,
|
||||
integrationRWEAndNodeRWE,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
kind: role
|
||||
spec:
|
||||
allow:
|
||||
rules:
|
||||
- resources:
|
||||
- integration
|
||||
verbs:
|
||||
- list
|
||||
- create
|
||||
- use
|
|
@ -0,0 +1,17 @@
|
|||
kind: role
|
||||
spec:
|
||||
allow:
|
||||
rules:
|
||||
- resources:
|
||||
- integration
|
||||
verbs:
|
||||
- list
|
||||
- create
|
||||
- use
|
||||
- resources:
|
||||
- node
|
||||
verbs:
|
||||
- create
|
||||
- update
|
||||
- list
|
||||
- read
|
|
@ -191,7 +191,7 @@ export function SuccessfullyAddedIntegrationDialog({
|
|||
},
|
||||
}}
|
||||
>
|
||||
Begin RDS Enrollment
|
||||
Begin AWS Resource Enrollment
|
||||
</ButtonPrimary>
|
||||
) : (
|
||||
<Flex gap="3">
|
||||
|
|
|
@ -252,7 +252,7 @@ const cfg = {
|
|||
ec2InstanceConnectIAMConfigureScriptPath:
|
||||
'/v1/webapi/scripts/integrations/configure/eice-iam.sh?awsRegion=:region&role=:awsOidcRoleArn',
|
||||
ec2InstanceConnectDeployPath:
|
||||
'/v1/webapi/sites/:site/integrations/aws-oidc/:name/deployec2ice',
|
||||
'/v1/webapi/sites/:clusterId/integrations/aws-oidc/:name/deployec2ice',
|
||||
|
||||
userGroupsListPath:
|
||||
'/v1/webapi/sites/:clusterId/user-groups?searchAsRoles=:searchAsRoles?&limit=:limit?&startKey=:startKey?&query=:query?&search=:search?&sort=:sort?',
|
||||
|
|
|
@ -159,7 +159,7 @@ export const integrationService = {
|
|||
return api
|
||||
.post(cfg.getListEc2InstanceConnectEndpointsUrl(integrationName), req)
|
||||
.then(json => {
|
||||
const endpoints = json?.ec2InstanceConnectEndpoints ?? [];
|
||||
const endpoints = json?.ec2Ices ?? [];
|
||||
|
||||
return {
|
||||
endpoints: endpoints.map(makeEc2InstanceConnectEndpoint),
|
||||
|
|
|
@ -281,13 +281,7 @@ export type ListEc2InstanceConnectEndpointsResponse = {
|
|||
export type Ec2InstanceConnectEndpoint = {
|
||||
name: string;
|
||||
// state is the current state of the EC2 Instance Connect Endpoint.
|
||||
state:
|
||||
| 'create-in-progress'
|
||||
| 'create-complete'
|
||||
| 'create-failed'
|
||||
| 'delete-in-progress'
|
||||
| 'delete-complete'
|
||||
| 'delete-failed';
|
||||
state: Ec2InstanceConnectEndpointState;
|
||||
// stateMessage is an optional message describing the state of the EICE, such as an error message.
|
||||
stateMessage?: string;
|
||||
// dashboardLink is a URL to AWS Console where the user can see the EC2 Instance Connect Endpoint.
|
||||
|
@ -296,6 +290,14 @@ export type Ec2InstanceConnectEndpoint = {
|
|||
subnetId: string;
|
||||
};
|
||||
|
||||
export type Ec2InstanceConnectEndpointState =
|
||||
| 'create-in-progress'
|
||||
| 'create-complete'
|
||||
| 'create-failed'
|
||||
| 'delete-in-progress'
|
||||
| 'delete-complete'
|
||||
| 'delete-failed';
|
||||
|
||||
export type DeployEc2InstanceConnectEndpointRequest = {
|
||||
region: Regions;
|
||||
// subnetID is the subnet id for the EC2 Instance Connect Endpoint.
|
||||
|
@ -312,6 +314,7 @@ export type DeployEc2InstanceConnectEndpointResponse = {
|
|||
export type ListAwsSecurityGroupsRequest = {
|
||||
// VPCID is the VPC to filter Security Groups.
|
||||
vpcId: string;
|
||||
region: Regions;
|
||||
nextToken?: string;
|
||||
};
|
||||
|
||||
|
|
|
@ -15,6 +15,8 @@ limitations under the License.
|
|||
*/
|
||||
import { ResourceLabel } from 'teleport/services/agents';
|
||||
|
||||
import { Regions } from '../integrations';
|
||||
|
||||
export interface Node {
|
||||
kind: 'node';
|
||||
id: string;
|
||||
|
@ -36,7 +38,7 @@ export interface BashCommand {
|
|||
export type AwsMetadata = {
|
||||
accountId: string;
|
||||
instanceId: string;
|
||||
region: string;
|
||||
region: Regions;
|
||||
vpcId: string;
|
||||
integration: string;
|
||||
subnetId: string;
|
||||
|
|
Loading…
Reference in a new issue