From cf18b6f59dcc62e900282509827849254694c920 Mon Sep 17 00:00:00 2001 From: Michelle Bergquist Date: Mon, 9 Dec 2024 12:15:45 -0700 Subject: [PATCH] Load data in rules tables --- .../status/AwsOidc/Details/Details.tsx | 7 +- .../status/AwsOidc/Details/Ec2.tsx | 57 --------- .../status/AwsOidc/Details/Eks.tsx | 56 --------- .../status/AwsOidc/Details/Rules.test.tsx | 111 ++++++++++++++++++ .../status/AwsOidc/Details/Rules.tsx | 70 +++++++++-- web/packages/teleport/src/config.ts | 12 ++ .../integrations/integrations.test.ts | 52 +++++++- .../src/services/integrations/integrations.ts | 17 +++ .../src/services/integrations/types.ts | 25 ++++ 9 files changed, 276 insertions(+), 131 deletions(-) delete mode 100644 web/packages/teleport/src/Integrations/status/AwsOidc/Details/Ec2.tsx delete mode 100644 web/packages/teleport/src/Integrations/status/AwsOidc/Details/Eks.tsx create mode 100644 web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rules.test.tsx diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Details.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Details.tsx index dd999cd7d92e2..a294fc20fa7b9 100644 --- a/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Details.tsx +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Details.tsx @@ -26,8 +26,7 @@ import { useAwsOidcStatus } from 'teleport/Integrations/status/AwsOidc/useAwsOid import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard'; import { IntegrationKind } from 'teleport/services/integrations'; import { Rds } from 'teleport/Integrations/status/AwsOidc/Details/Rds'; -import { Ec2 } from 'teleport/Integrations/status/AwsOidc/Details/Ec2'; -import { Eks } from 'teleport/Integrations/status/AwsOidc/Details/Eks'; +import { Rules } from 'teleport/Integrations/status/AwsOidc/Details/Rules'; export function Details() { const { resourceKind } = useParams<{ @@ -43,8 +42,8 @@ export function Details() { {integration && ( )} - {resourceKind == AwsResource.ec2 && } - {resourceKind == AwsResource.eks && } + {resourceKind == AwsResource.ec2 && } + {resourceKind == AwsResource.eks && } {resourceKind == AwsResource.rds && } ); diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Ec2.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Ec2.tsx deleted file mode 100644 index 8d91e43a12eca..0000000000000 --- a/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Ec2.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Teleport - * Copyright (C) 2024 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import React from 'react'; - -import Table, { LabelCell } from 'design/DataTable'; - -export function Ec2() { - return ( - { - const aStr = a.labels.toString(); - const bStr = b.labels.toString(); - - if (aStr < bStr) { - return -1; - } - if (aStr > bStr) { - return 1; - } - - return 0; - }, - render: ({ labels }) => , - }, - ]} - emptyText="EC2 details coming soon" - isSearchable - /> - ); -} diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Eks.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Eks.tsx deleted file mode 100644 index db5b34372c4e0..0000000000000 --- a/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Eks.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Teleport - * Copyright (C) 2024 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import React from 'react'; -import Table, { LabelCell } from 'design/DataTable'; - -export function Eks() { - return ( -
{ - const aStr = a.labels.toString(); - const bStr = b.labels.toString(); - - if (aStr < bStr) { - return -1; - } - if (aStr > bStr) { - return 1; - } - - return 0; - }, - render: ({ labels }) => , - }, - ]} - emptyText="EKS details coming soon" - isSearchable - /> - ); -} diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rules.test.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rules.test.tsx new file mode 100644 index 0000000000000..36fcc9ac7f1e0 --- /dev/null +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rules.test.tsx @@ -0,0 +1,111 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { render, screen, waitFor } from 'design/utils/testing'; +import React from 'react'; + +import { MemoryRouter } from 'react-router'; + +import { within } from '@testing-library/react'; + +import { Rules } from 'teleport/Integrations/status/AwsOidc/Details/Rules'; +import { + IntegrationDiscoveryRule, + integrationService, +} from 'teleport/services/integrations'; + +test('renders region & labels from response', async () => { + jest.spyOn(integrationService, 'fetchIntegrationRules').mockResolvedValue({ + rules: [ + makeIntegrationDiscoveryRule({ + region: 'us-west-2', + labelMatcher: [ + { name: 'env', value: 'prod' }, + { name: 'key', value: '123' }, + ], + }), + makeIntegrationDiscoveryRule({ + region: 'us-east-2', + labelMatcher: [{ name: 'env', value: 'stage' }], + }), + makeIntegrationDiscoveryRule({ + region: 'us-west-1', + labelMatcher: [{ name: 'env', value: 'test' }], + }), + makeIntegrationDiscoveryRule({ + region: 'us-east-1', + labelMatcher: [{ name: 'env', value: 'dev' }], + }), + ], + nextKey: '', + }); + render( + + + + ); + + await waitFor(() => { + expect(screen.getByText('env:prod')).toBeInTheDocument(); + }); + + expect(getTableCellContents()).toEqual({ + header: ['Region', 'Labels'], + rows: [ + ['us-west-1', 'env:test'], + ['us-east-1', 'env:dev'], + ['us-west-2', 'env:prodkey:123'], + ['us-east-2', 'env:stage'], + ], + }); + + jest.clearAllMocks(); +}); + +function makeIntegrationDiscoveryRule( + overrides: Partial = {} +): IntegrationDiscoveryRule { + return Object.assign( + { + resourceType: '', + region: '', + labelMatcher: [], + discoveryConfig: '', + lastSync: 0, + }, + overrides + ); +} + +function getTableCellContents() { + const [header, ...rows] = screen.getAllByRole('row'); + return { + header: within(header) + .getAllByRole('columnheader') + .map(cell => cell.textContent), + rows: rows.map(row => + within(row) + .getAllByRole('cell') + .map(cell => cell.textContent) + ), + }; +} diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rules.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rules.tsx index 9f00e38573f3f..f053fde53899f 100644 --- a/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rules.tsx +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rules.tsx @@ -16,32 +16,65 @@ * along with this program. If not, see . */ -import React from 'react'; +import React, { useEffect } from 'react'; import Table, { LabelCell } from 'design/DataTable'; +import { useParams } from 'react-router'; + +import { useAsync } from 'shared/hooks/useAsync'; +import { Indicator } from 'design'; +import { Danger } from 'design/Alert'; + +import { + IntegrationKind, + integrationService, +} from 'teleport/services/integrations'; +import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard'; + export function Rules() { + const { name, resourceKind } = useParams<{ + type: IntegrationKind; + name: string; + resourceKind: AwsResource; + }>(); + + const [attempt, fetchRules] = useAsync(() => + integrationService.fetchIntegrationRules(name, resourceKind) + ); + + useEffect(() => { + fetchRules(); + }, []); + + if (attempt.status == 'processing') { + return ; + } + + if (attempt.status == 'error') { + return {attempt.statusText}; + } + + if (!attempt.data) { + return null; + } + return (
{ - const aStr = a.tags.toString(); - const bStr = b.tags.toString(); + const aStr = a.labelMatcher.toString(); + const bStr = b.labelMatcher.toString(); if (aStr < bStr) { return -1; @@ -52,11 +85,22 @@ export function Rules() { return 0; }, - render: ({ tags }) => , + render: ({ labelMatcher }) => ( + `${l.name}:${l.value}`)} /> + ), }, ]} - emptyText="Rules details coming soon" + emptyText={`No ${resourceKind} data`} isSearchable /> ); } + +function getResourceTerm(resource: AwsResource): string { + switch (resource) { + case AwsResource.rds: + return 'Tags'; + default: + return 'Labels'; + } +} diff --git a/web/packages/teleport/src/config.ts b/web/packages/teleport/src/config.ts index 75e0a6cc54e9e..558327ce87d93 100644 --- a/web/packages/teleport/src/config.ts +++ b/web/packages/teleport/src/config.ts @@ -327,6 +327,9 @@ const cfg = { integrationsPath: '/v1/webapi/sites/:clusterId/integrations/:name?', integrationStatsPath: '/v1/webapi/sites/:clusterId/integrations/:name/stats', + integrationRulesPath: + '/v1/webapi/sites/:clusterId/integrations/:name/discoveryrules?resourceType=:resourceType', + thumbprintPath: '/v1/webapi/thumbprint', pingAwsOidcIntegrationPath: '/v1/webapi/sites/:clusterId/integrations/aws-oidc/:name/ping', @@ -994,6 +997,15 @@ const cfg = { }); }, + getIntegrationRulesUrl(name: string, resourceType: AwsResource) { + const clusterId = cfg.proxyCluster; + return generatePath(cfg.api.integrationRulesPath, { + clusterId, + name, + resourceType, + }); + }, + getPingAwsOidcIntegrationUrl({ integrationName, clusterId, diff --git a/web/packages/teleport/src/services/integrations/integrations.test.ts b/web/packages/teleport/src/services/integrations/integrations.test.ts index fb917c96a6b97..a799dd90246c8 100644 --- a/web/packages/teleport/src/services/integrations/integrations.test.ts +++ b/web/packages/teleport/src/services/integrations/integrations.test.ts @@ -19,8 +19,10 @@ import api from 'teleport/services/api'; import cfg from 'teleport/config'; +import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard'; + import { integrationService } from './integrations'; -import { IntegrationStatusCode, IntegrationAudience } from './types'; +import { IntegrationAudience, IntegrationStatusCode } from './types'; test('fetch a single integration: fetchIntegration()', async () => { // test a valid response @@ -226,6 +228,54 @@ describe('fetchAwsDatabases() request body formatting', () => { ); }); +test('fetch integration rules: fetchIntegrationRules()', async () => { + // test a valid response + jest.spyOn(api, 'get').mockResolvedValue({ + rules: [ + { + resourceType: 'eks', + region: 'us-west-2', + labelMatcher: [{ name: 'env', value: 'dev' }], + discoveryConfig: 'cfg', + lastSync: 1733782634, + }, + ], + nextKey: 'some-key', + }); + + let response = await integrationService.fetchIntegrationRules( + 'name', + AwsResource.eks + ); + expect(api.get).toHaveBeenCalledWith( + cfg.getIntegrationRulesUrl('name', AwsResource.eks) + ); + expect(response).toEqual({ + nextKey: 'some-key', + rules: [ + { + resourceType: 'eks', + region: 'us-west-2', + labelMatcher: [{ name: 'env', value: 'dev' }], + discoveryConfig: 'cfg', + lastSync: 1733782634, + }, + ], + }); + + // test null response + jest.spyOn(api, 'get').mockResolvedValue(null); + + response = await integrationService.fetchIntegrationRules( + 'name', + AwsResource.eks + ); + expect(response).toEqual({ + nextKey: undefined, + rules: [], + }); +}); + const nonAwsOidcIntegration = { name: 'non-aws-oidc-integration', subKind: 'abc', diff --git a/web/packages/teleport/src/services/integrations/integrations.ts b/web/packages/teleport/src/services/integrations/integrations.ts index 214b07f39efc1..8bc4de43a3a23 100644 --- a/web/packages/teleport/src/services/integrations/integrations.ts +++ b/web/packages/teleport/src/services/integrations/integrations.ts @@ -19,6 +19,8 @@ import api from 'teleport/services/api'; import cfg from 'teleport/config'; +import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard'; + import makeNode from '../nodes/makeNode'; import auth from '../auth/auth'; import { App } from '../apps'; @@ -60,6 +62,7 @@ import { AwsOidcPingResponse, AwsOidcPingRequest, IntegrationWithSummary, + IntegrationDiscoveryRules, } from './types'; export const integrationService = { @@ -420,6 +423,20 @@ export const integrationService = { return resp; }); }, + + fetchIntegrationRules( + name: string, + resourceType: AwsResource + ): Promise { + return api + .get(cfg.getIntegrationRulesUrl(name, resourceType)) + .then(resp => { + return { + rules: resp?.rules || [], + nextKey: resp?.nextKey, + }; + }); + }, }; export function makeIntegrations(json: any): Integration[] { diff --git a/web/packages/teleport/src/services/integrations/types.ts b/web/packages/teleport/src/services/integrations/types.ts index ff8f4347b985c..d63cac5c85a96 100644 --- a/web/packages/teleport/src/services/integrations/types.ts +++ b/web/packages/teleport/src/services/integrations/types.ts @@ -298,6 +298,31 @@ export type IntegrationWithSummary = { awseks: ResourceTypeSummary; }; +// IntegrationDiscoveryRules contains the list of discovery rules for a given Integration. +export type IntegrationDiscoveryRules = { + // rules is the list of integration rules. + rules: IntegrationDiscoveryRule[]; + // nextKey is the position to resume listing rules. + nextKey: string; +}; + +// IntegrationDiscoveryRule describes a discovery rule associated with an integration. +export type IntegrationDiscoveryRule = { + // resourceType indicates the type of resource that this rule targets. + // This is the same value that is set in DiscoveryConfig.AWS..Types + // Example: ec2, rds, eks + resourceType: string; + // region where this rule applies to. + region: string; + // labelMatcher is the set of labels that are used to filter the resources before trying to auto-enroll them. + labelMatcher: Label[]; + // discoveryConfig is the name of the DiscoveryConfig that created this rule. + discoveryConfig: string; + // lastSync contains the time when this rule was used. + // If empty, it indicates that the rule is not being used. + lastSync: number; +}; + // ResourceTypeSummary contains the summary of the enrollment rules and found resources by the integration. export type ResourceTypeSummary = { // rulesCount is the number of enrollment rules that are using this integration.