diff --git a/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.story.tsx b/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.story.tsx index 5184782287825..dc6ed96bcbe2d 100644 --- a/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.story.tsx +++ b/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.story.tsx @@ -151,7 +151,10 @@ const ec2InstancesResponse = [ kind: 'node', clusterId: 'cluster', hostname: 'ec2-hostname-1', - tags: [{ name: 'instance', value: 'ec2-1' }], + tags: [ + { name: 'teleport.dev/instance-id', value: 'instance-ec2-1' }, + { name: 'Name', value: 'My EC2 Box 1' }, + ], addr: 'ec2.1.com', tunnel: false, subKind: 'openssh-ec2-ice', @@ -170,7 +173,10 @@ const ec2InstancesResponse = [ kind: 'node', clusterId: 'cluster', hostname: 'ec2-hostname-2', - tags: [{ name: 'instance', value: 'ec2-2' }], + tags: [ + { name: 'teleport.dev/instance-id', value: 'instance-ec2-2' }, + { name: 'Name', value: 'My EC2 Box 2' }, + ], addr: 'ec2.2.com', tunnel: false, subKind: 'openssh-ec2-ice', @@ -189,7 +195,10 @@ const ec2InstancesResponse = [ kind: 'node', clusterId: 'cluster', hostname: 'ec2-hostname-3', - tags: [{ name: 'instance', value: 'ec2-3' }], + tags: [ + { name: 'teleport.dev/instance-id', value: 'instance-ec2-3' }, + { name: 'Name', value: 'My EC2 Box 3' }, + ], addr: 'ec2.3.com', tunnel: false, subKind: 'openssh-ec2-ice', @@ -208,7 +217,10 @@ const ec2InstancesResponse = [ kind: 'node', clusterId: 'cluster', hostname: 'ec2-hostname-4', - tags: [{ name: 'instance', value: 'ec2-4' }], + tags: [ + { name: 'teleport.dev/instance-id', value: 'instance-ec2-4' }, + { name: 'Name', value: 'My EC2 Box 4' }, + ], addr: 'ec2.4.com', tunnel: false, subKind: 'openssh-ec2-ice', @@ -228,8 +240,8 @@ const ec2InstancesResponse = [ clusterId: 'cluster', hostname: 'ec2-hostname-5', tags: [ - { name: 'instance', value: 'ec2-5' }, - { name: 'Name', value: 'My EC2 Box' }, + { name: 'teleport.dev/instance-id', value: 'instance-ec2-5' }, + { name: 'Name', value: 'My EC2 Box 5' }, ], addr: 'ec2.5.com', tunnel: false, diff --git a/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.test.tsx b/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.test.tsx new file mode 100644 index 0000000000000..e572a4641a3e3 --- /dev/null +++ b/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.test.tsx @@ -0,0 +1,324 @@ +/** + * Copyright 2023 Gravitational, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { MemoryRouter } from 'react-router'; +import { render, screen, fireEvent } from 'design/utils/testing'; + +import { ContextProvider } from 'teleport'; +import { + IntegrationKind, + IntegrationStatusCode, + integrationService, +} from 'teleport/services/integrations'; +import { createTeleportContext } from 'teleport/mocks/contexts'; +import cfg from 'teleport/config'; +import TeleportContext from 'teleport/teleportContext'; +import { + DiscoverContextState, + DiscoverProvider, +} from 'teleport/Discover/useDiscover'; +import { FeaturesContextProvider } from 'teleport/FeaturesContext'; +import { Node } from 'teleport/services/nodes'; + +import { EnrollEc2Instance } from './EnrollEc2Instance'; + +describe('test EnrollEc2Instance.tsx', () => { + 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, + }; + + beforeEach(() => { + jest.spyOn(ctx.nodeService, 'fetchNodes').mockResolvedValue({ agents: [] }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('a cloudshell script should be shown if there is an aws permissions error', async () => { + jest + .spyOn(integrationService, 'fetchAwsEc2Instances') + .mockRejectedValue( + new Error('StatusCode: 403, RequestID: operation error') + ); + // Prevent noise in the test output caused by the error. + jest.spyOn(console, 'error').mockImplementation(); + + renderEc2Instances(ctx, discoverCtx); + + // Selects a region + const regionSelectorElement = screen.getByLabelText(/aws region/i); + fireEvent.focus(regionSelectorElement); + fireEvent.keyDown(regionSelectorElement, { key: 'ArrowDown', keyCode: 40 }); + fireEvent.click(screen.getByText('us-west-1')); + + // Wait for results to be listed. + await screen.findAllByText( + /We were unable to list your EC2 instances. Run the command below/i + ); + + expect(integrationService.fetchAwsEc2Instances).toHaveBeenCalledTimes(1); + expect(ctx.nodeService.fetchNodes).not.toHaveBeenCalled(); + }); + + test('an instance that is already enrolled should be disabled', async () => { + jest + .spyOn(integrationService, 'fetchAwsEc2Instances') + .mockResolvedValue({ instances: mockEc2Instances }); + + jest + .spyOn(ctx.nodeService, 'fetchNodes') + .mockResolvedValue({ agents: mockFetchedNodes }); + + renderEc2Instances(ctx, discoverCtx); + + // Selects a region + const regionSelectorElement = screen.getByLabelText(/aws region/i); + fireEvent.focus(regionSelectorElement); + fireEvent.keyDown(regionSelectorElement, { key: 'ArrowDown', keyCode: 40 }); + fireEvent.click(screen.getByText('us-west-1')); + + // Wait for results to be listed. + await screen.findAllByText(/My EC2 Box 1/i); + + expect(integrationService.fetchAwsEc2Instances).toHaveBeenCalledTimes(1); + expect(ctx.nodeService.fetchNodes).toHaveBeenCalledTimes(1); + + // Get the disabled table rows. + const disabledRowElements = screen + .getAllByTitle( + 'This EC2 instance is already enrolled and is a part of this cluster' + ) + // Only select the radio elements, this is to prevent duplicates since every + // column in the row will have the title we're querying for. + .filter(el => el.innerHTML.includes('type="radio"')) + // Get the row that the radio element is in. + .map(el => el.closest('tr')); + + // Expect the disabled row to be EC2 Box 2. + expect(disabledRowElements[0].innerHTML).toContain('My EC2 Box 2'); + // There should only be one disabled row. + expect(disabledRowElements).toHaveLength(1); + }); + + test('there should be no disabled rows if the fetchNodes response is empty', async () => { + jest + .spyOn(integrationService, 'fetchAwsEc2Instances') + .mockResolvedValue({ instances: mockEc2Instances }); + + renderEc2Instances(ctx, discoverCtx); + + // Selects a region + const regionSelectorElement = screen.getByLabelText(/aws region/i); + fireEvent.focus(regionSelectorElement); + fireEvent.keyDown(regionSelectorElement, { key: 'ArrowDown', keyCode: 40 }); + fireEvent.click(screen.getByText('us-west-1')); + + // Wait for results to be listed. + await screen.findAllByText(/My EC2 Box 1/i); + + expect(integrationService.fetchAwsEc2Instances).toHaveBeenCalledTimes(1); + expect(ctx.nodeService.fetchNodes).toHaveBeenCalledTimes(1); + + // There should be no disabled rows. + expect( + screen.queryAllByTitle( + 'This EC2 instance is already enrolled and is a part of this cluster' + )[0] + ).toBeUndefined(); + }); +}); + +function renderEc2Instances( + ctx: TeleportContext, + discoverCtx: DiscoverContextState +) { + return render( + + + + + + + + + + ); +} + +const mockEc2Instances: Node[] = [ + { + id: 'ec2-instance-1', + kind: 'node', + clusterId: 'cluster', + hostname: 'ec2-hostname-1', + labels: [ + { name: 'teleport.dev/instance-id', value: 'instance-ec2-1' }, + { name: 'Name', value: 'My EC2 Box 1' }, + ], + addr: 'ec2.1.com', + tunnel: false, + subKind: 'openssh-ec2-ice', + sshLogins: ['test'], + awsMetadata: { + 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', + labels: [ + { name: 'teleport.dev/instance-id', value: 'instance-ec2-2' }, + { name: 'Name', value: 'My EC2 Box 2' }, + ], + addr: 'ec2.2.com', + tunnel: false, + subKind: 'openssh-ec2-ice', + sshLogins: ['test'], + awsMetadata: { + 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', + labels: [ + { name: 'teleport.dev/instance-id', value: 'instance-ec2-3' }, + { name: 'Name', value: 'My EC2 Box 3' }, + ], + addr: 'ec2.3.com', + tunnel: false, + subKind: 'openssh-ec2-ice', + sshLogins: ['test'], + awsMetadata: { + 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', + labels: [ + { name: 'teleport.dev/instance-id', value: 'instance-ec2-4' }, + { name: 'Name', value: 'My EC2 Box 4' }, + ], + addr: 'ec2.4.com', + tunnel: false, + subKind: 'openssh-ec2-ice', + sshLogins: ['test'], + awsMetadata: { + accountId: 'test-account', + instanceId: 'instance-ec2-4', + region: 'us-west-1', + vpcId: 'test', + integration: 'test', + subnetId: 'test', + }, + }, + { + id: 'ec2-instance-5', + kind: 'node', + clusterId: 'cluster', + hostname: 'ec2-hostname-5', + labels: [ + { name: 'teleport.dev/instance-id', value: 'instance-ec2-5' }, + { name: 'Name', value: 'My EC2 Box 5' }, + ], + addr: 'ec2.5.com', + tunnel: false, + subKind: 'openssh-ec2-ice', + sshLogins: ['test'], + awsMetadata: { + accountId: 'test-account', + instanceId: 'instance-ec2-5', + region: 'us-west-1', + vpcId: 'test', + integration: 'test', + subnetId: 'test', + }, + }, +]; + +const mockFetchedNodes: Node[] = [ + { + id: 'ec2-instance-2', + kind: 'node', + clusterId: 'cluster', + hostname: 'ec2-hostname-2', + labels: [ + { name: 'teleport.dev/instance-id', value: 'instance-ec2-2' }, + { name: 'Name', value: 'My EC2 Box 2' }, + ], + addr: 'ec2.2.com', + tunnel: false, + subKind: 'openssh-ec2-ice', + sshLogins: ['test'], + }, +]; diff --git a/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx b/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx index 07687a4625612..2e81b7686e4c2 100644 --- a/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx +++ b/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx @@ -21,6 +21,7 @@ import useAttempt from 'shared/hooks/useAttemptNext'; import { getErrMessage } from 'shared/utils/errorType'; +import useTeleport from 'teleport/useTeleport'; import cfg from 'teleport/config'; import { NodeMeta, useDiscover } from 'teleport/Discover/useDiscover'; import { @@ -29,7 +30,7 @@ import { integrationService, } from 'teleport/services/integrations'; import { AwsRegionSelector } from 'teleport/Discover/Shared/AwsRegionSelector'; -import NodeService, { Node } from 'teleport/services/nodes'; +import { Node } from 'teleport/services/nodes'; import { DiscoverEvent, @@ -64,7 +65,7 @@ const emptyTableData: TableData = { export function EnrollEc2Instance() { const { agentMeta, emitErrorEvent, nextStep, updateAgentMeta, emitEvent } = useDiscover(); - const nodeService = new NodeService(); + const { nodeService } = useTeleport(); const [currRegion, setCurrRegion] = useState(); const [existingEice, setExistingEice] = @@ -141,9 +142,14 @@ export function EnrollEc2Instance() { ); const ec2InstancesLookupByInstanceId: Record = {}; - fetchedNodes.forEach( - d => (ec2InstancesLookupByInstanceId[d.awsMetadata.instanceId] = d) - ); + fetchedNodes.forEach(d => { + // Extract the instanceId of the fetched node from its label. + const instanceId = d.labels.find( + label => label.name === 'teleport.dev/instance-id' + )?.value; + + ec2InstancesLookupByInstanceId[instanceId] = d; + }); // Check for already existing EC2 instances. const checkedEc2Instances: CheckedEc2Instance[] = fetchedEc2Instances.map(