From cf18b6f59dcc62e900282509827849254694c920 Mon Sep 17 00:00:00 2001
From: Michelle Bergquist <michelle.bergquist@goteleport.com>
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 && (
         <AwsOidcHeader integration={integration} resource={resourceKind} />
       )}
-      {resourceKind == AwsResource.ec2 && <Ec2 />}
-      {resourceKind == AwsResource.eks && <Eks />}
+      {resourceKind == AwsResource.ec2 && <Rules />}
+      {resourceKind == AwsResource.eks && <Rules />}
       {resourceKind == AwsResource.rds && <Rds />}
     </FeatureBox>
   );
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 <http://www.gnu.org/licenses/>.
- */
-
-import React from 'react';
-
-import Table, { LabelCell } from 'design/DataTable';
-
-export function Ec2() {
-  return (
-    <Table
-      data={[]}
-      columns={[
-        {
-          key: 'region',
-          headerText: 'Region',
-          isSortable: true,
-        },
-        {
-          key: 'labels',
-          headerText: 'Labels',
-          isSortable: true,
-          onSort: (a, b) => {
-            const aStr = a.labels.toString();
-            const bStr = b.labels.toString();
-
-            if (aStr < bStr) {
-              return -1;
-            }
-            if (aStr > bStr) {
-              return 1;
-            }
-
-            return 0;
-          },
-          render: ({ labels }) => <LabelCell data={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 <http://www.gnu.org/licenses/>.
- */
-
-import React from 'react';
-import Table, { LabelCell } from 'design/DataTable';
-
-export function Eks() {
-  return (
-    <Table
-      data={[]}
-      columns={[
-        {
-          key: 'region',
-          headerText: 'Region',
-          isSortable: true,
-        },
-        {
-          key: 'labels',
-          headerText: 'Labels',
-          isSortable: true,
-          onSort: (a, b) => {
-            const aStr = a.labels.toString();
-            const bStr = b.labels.toString();
-
-            if (aStr < bStr) {
-              return -1;
-            }
-            if (aStr > bStr) {
-              return 1;
-            }
-
-            return 0;
-          },
-          render: ({ labels }) => <LabelCell data={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 <http://www.gnu.org/licenses/>.
+ */
+
+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(
+    <MemoryRouter
+      initialEntries={[
+        `/web/integrations/status/aws-oidc/some-name/resources/eks`,
+      ]}
+    >
+      <Rules />
+    </MemoryRouter>
+  );
+
+  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> = {}
+): 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 <http://www.gnu.org/licenses/>.
  */
 
-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 <Indicator />;
+  }
+
+  if (attempt.status == 'error') {
+    return <Danger>{attempt.statusText}</Danger>;
+  }
+
+  if (!attempt.data) {
+    return null;
+  }
+
   return (
     <Table
-      data={[]}
+      data={attempt.data.rules}
       columns={[
-        {
-          key: 'name',
-          headerText: 'Integration Name',
-          isSortable: true,
-        },
         {
           key: 'region',
           headerText: 'Region',
           isSortable: true,
         },
         {
-          key: 'tags',
-          headerText: 'Tags',
+          key: 'labelMatcher',
+          headerText: getResourceTerm(resourceKind),
           isSortable: true,
           onSort: (a, b) => {
-            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 }) => <LabelCell data={tags} />,
+          render: ({ labelMatcher }) => (
+            <LabelCell data={labelMatcher.map(l => `${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<IntegrationDiscoveryRules> {
+    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.<Matcher>.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.