({
emptyContent={
- No {title} found for this entity.
+ No {title} found
+ {title === 'flux controllers' ? '' : 'for this entity'}.
}
diff --git a/plugins/backstage-plugin-flux/src/components/FluxRuntimeCard/FluxRuntimeCard.test.tsx b/plugins/backstage-plugin-flux/src/components/FluxRuntimeCard/FluxRuntimeCard.test.tsx
new file mode 100644
index 0000000..a72a7fa
--- /dev/null
+++ b/plugins/backstage-plugin-flux/src/components/FluxRuntimeCard/FluxRuntimeCard.test.tsx
@@ -0,0 +1,272 @@
+import React from 'react';
+import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
+import { configApiRef } from '@backstage/core-plugin-api';
+import { ConfigReader } from '@backstage/core-app-api';
+import {
+ KubernetesApi,
+ kubernetesApiRef,
+ KubernetesAuthProvidersApi,
+ kubernetesAuthProvidersApiRef,
+} from '@backstage/plugin-kubernetes';
+import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
+import { FluxRuntimeCard } from './FluxRuntimeCard';
+import {
+ getDeploymentsPath,
+ NAMESPACES_PATH,
+} from '../../hooks/useGetDeployments';
+import { Namespace } from '../../objects';
+
+const makeTestFluxController = (
+ name: string,
+ namespace: string,
+ labels: { [name: string]: string },
+) => {
+ return {
+ apiVersion: 'meta.k8s.io/v1',
+ kind: 'PartialObjectMetadata',
+ metadata: {
+ name,
+ namespace,
+ uid: 'b062d329-538d-4bb3-b4df-b2ac4b06dba8',
+ resourceVersion: '1001263',
+ generation: 1,
+ creationTimestamp: '2023-10-19T16:34:14Z',
+ labels,
+ annotations: {
+ 'deployment.kubernetes.io/revision': '1',
+ },
+ },
+ };
+};
+
+class StubKubernetesClient implements KubernetesApi {
+ getObjectsByEntity = jest.fn();
+
+ async getClusters(): Promise<{ name: string; authProvider: string }[]> {
+ return [
+ { name: 'mock-cluster-1', authProvider: 'serviceAccount1' },
+ { name: 'mock-cluster-2', authProvider: 'serviceAccount2' },
+ ];
+ }
+
+ getWorkloadsByEntity = jest.fn();
+
+ getCustomObjectsByEntity = jest.fn();
+
+ async proxy({
+ clusterName,
+ init,
+ path,
+ }: {
+ clusterName?: string;
+ path: string;
+ init?: RequestInit | undefined;
+ }): Promise {
+ if (!init?.method && path === NAMESPACES_PATH) {
+ if (clusterName === 'mock-cluster-1') {
+ return {
+ ok: true,
+ json: () =>
+ Promise.resolve({
+ kind: 'NamespacesList',
+ apiVersion: 'meta.k8s.io/v1',
+ items: [
+ {
+ metadata: {
+ name: 'flux-system',
+ labels: {
+ 'app.kubernetes.io/instance': 'flux-system',
+ 'app.kubernetes.io/part-of': 'flux',
+ 'app.kubernetes.io/version': 'v2.0.0',
+ 'kubernetes.io/metadata.name': 'flux-system',
+ 'kustomize.toolkit.fluxcd.io/name': 'flux-system',
+ 'kustomize.toolkit.fluxcd.io/namespace': 'flux-system',
+ 'pod-security.kubernetes.io/warn': 'restricted',
+ 'pod-security.kubernetes.io/warn-version': 'latest',
+ },
+ uid: '1dcca7cb-c651-4a86-93b4-ecf440df2353',
+ resourceVersion: '1583',
+ creationTimestamp: '2023-10-19T16:34:12Z',
+ },
+ } as Namespace,
+ ],
+ }),
+ } as Response;
+ }
+ if (clusterName === 'mock-cluster-2') {
+ return {
+ ok: true,
+ json: () =>
+ Promise.resolve({
+ kind: 'NamespacesList',
+ apiVersion: 'meta.k8s.io/v1',
+ items: [
+ {
+ metadata: {
+ name: 'default',
+ uid: '1dcca7cb-c651-4a86-93b4-ecf440df2353',
+ resourceVersion: '1583',
+ creationTimestamp: '2023-10-19T16:34:12Z',
+ labels: {
+ 'app.kubernetes.io/instance': 'default',
+ 'app.kubernetes.io/part-of': 'flux',
+ 'app.kubernetes.io/version': 'v2.0.0',
+ 'kubernetes.io/metadata.name': 'default',
+ 'kustomize.toolkit.fluxcd.io/name': 'default',
+ 'kustomize.toolkit.fluxcd.io/namespace': 'default',
+ 'pod-security.kubernetes.io/warn': 'restricted',
+ 'pod-security.kubernetes.io/warn-version': 'latest',
+ },
+ },
+ } as Namespace,
+ ],
+ }),
+ } as Response;
+ }
+ }
+
+ if (!init?.method && path === getDeploymentsPath('flux-system')) {
+ return {
+ ok: true,
+ json: () =>
+ Promise.resolve({
+ kind: 'DeploymentList',
+ apiVersion: 'apps/v1',
+ items: [
+ makeTestFluxController('helm-controller', 'flux-system', {
+ 'app.kubernetes.io/component': 'helm-controller',
+ 'app.kubernetes.io/instance': 'flux-system',
+ 'app.kubernetes.io/part-of': 'flux',
+ 'app.kubernetes.io/version': 'v2.1.2',
+ 'control-plane': 'controller',
+ 'kustomize.toolkit.fluxcd.io/name': 'flux-system',
+ 'kustomize.toolkit.fluxcd.io/namespace': 'flux-system',
+ }),
+ makeTestFluxController(
+ 'image-automation-controller',
+ 'flux-system',
+ {
+ 'app.kubernetes.io/component': 'image-automation-controller',
+ 'app.kubernetes.io/instance': 'flux-system',
+ 'app.kubernetes.io/part-of': 'flux',
+ 'app.kubernetes.io/version': 'v2.1.2',
+ 'control-plane': 'controller',
+ 'kustomize.toolkit.fluxcd.io/name': 'flux-system',
+ 'kustomize.toolkit.fluxcd.io/namespace': 'flux-system',
+ },
+ ),
+ ],
+ }),
+ } as Response;
+ }
+
+ if (!init?.method && path === getDeploymentsPath('default')) {
+ return {
+ ok: true,
+ json: () =>
+ Promise.resolve({
+ kind: 'DeploymentList',
+ apiVersion: 'apps/v1',
+ items: [
+ makeTestFluxController('image-automation-controller', 'default', {
+ 'app.kubernetes.io/component': 'image-automation-controller',
+ 'app.kubernetes.io/instance': 'default',
+ 'app.kubernetes.io/part-of': 'flux',
+ 'app.kubernetes.io/version': 'v2.1.2',
+ 'control-plane': 'controller',
+ 'kustomize.toolkit.fluxcd.io/name': 'default',
+ 'kustomize.toolkit.fluxcd.io/namespace': 'default',
+ }),
+ ],
+ }),
+ } as Response;
+ }
+
+ return null;
+ }
+}
+
+class StubKubernetesAuthProvidersApi implements KubernetesAuthProvidersApi {
+ decorateRequestBodyForAuth(
+ _: string,
+ requestBody: KubernetesRequestBody,
+ ): Promise {
+ return Promise.resolve(requestBody);
+ }
+ getCredentials(_: string): Promise<{
+ token?: string;
+ }> {
+ return Promise.resolve({ token: 'mock-token' });
+ }
+}
+
+describe('', () => {
+ let Wrapper: React.ComponentType>;
+
+ beforeEach(() => {
+ Wrapper = ({ children }: { children?: React.ReactNode }) => (
+ {children}
+ );
+ });
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ describe('listing Flux Controllers per Cluster', () => {
+ it('shows for each cluster what flux controllers are running', async () => {
+ const result = await renderInTestApp(
+
+
+
+
+ ,
+ );
+
+ const { getByText } = result;
+
+ const testCases = [
+ {
+ cluster: 'mock-cluster-1',
+ namespace: 'flux-system',
+ version: 'v2.1.2',
+ availableComponents: [
+ 'helm-controller',
+ 'image-automation-controller',
+ ],
+ },
+ {
+ cluster: 'mock-cluster-2',
+ namespace: 'default',
+ version: 'v2.1.2',
+ availableComponents: ['image-automation-controller'],
+ },
+ ];
+
+ for (const testCase of testCases) {
+ const cell = getByText(testCase.cluster);
+ expect(cell).toBeInTheDocument();
+
+ const tr = cell.closest('tr');
+ expect(tr).toBeInTheDocument();
+ expect(tr).toHaveTextContent(testCase.namespace);
+ expect(tr).toHaveTextContent(testCase.version);
+ expect(tr).toHaveTextContent(testCase.availableComponents.join(', '));
+ }
+ });
+ });
+});
diff --git a/plugins/backstage-plugin-flux/src/components/FluxRuntimeCard/FluxRuntimeCard.tsx b/plugins/backstage-plugin-flux/src/components/FluxRuntimeCard/FluxRuntimeCard.tsx
new file mode 100644
index 0000000..fb319fe
--- /dev/null
+++ b/plugins/backstage-plugin-flux/src/components/FluxRuntimeCard/FluxRuntimeCard.tsx
@@ -0,0 +1,35 @@
+import React, { FC } from 'react';
+import { InfoCard } from '@backstage/core-components';
+import { WeaveGitOpsContext } from '../WeaveGitOpsContext';
+import { FluxRuntimeTable, defaultColumns } from './FluxRuntimeTable';
+import { useGetDeployments } from '../../hooks/useGetDeployments';
+
+const FluxRuntimePanel: FC<{ many?: boolean }> = ({ many }) => {
+ const { data, isLoading, error } = useGetDeployments();
+
+ if (error) {
+ return Error: {error.message}
;
+ }
+
+ return (
+
+
+
+ );
+};
+
+/**
+ * Render the Deployments in Flux Runtime.
+ *
+ * @public
+ */
+export const FluxRuntimeCard = ({ many = true }: { many?: boolean }) => (
+
+
+
+);
diff --git a/plugins/backstage-plugin-flux/src/components/FluxRuntimeCard/FluxRuntimeTable.tsx b/plugins/backstage-plugin-flux/src/components/FluxRuntimeCard/FluxRuntimeTable.tsx
new file mode 100644
index 0000000..6b16e57
--- /dev/null
+++ b/plugins/backstage-plugin-flux/src/components/FluxRuntimeCard/FluxRuntimeTable.tsx
@@ -0,0 +1,84 @@
+import React from 'react';
+import { TableColumn } from '@backstage/core-components';
+import {
+ idColumn,
+ filters,
+ clusterColumn,
+ namespaceColumn,
+ versionColumn,
+ Cluster,
+ availableComponentsColumn,
+ clusterNameFilteringColumn,
+} from '../helpers';
+import { FluxEntityTable } from '../FluxEntityTable';
+import { FluxControllerEnriched } from '../../objects';
+
+export const defaultColumns: TableColumn[] = [
+ clusterNameFilteringColumn(),
+ idColumn(),
+ clusterColumn(),
+ namespaceColumn(),
+ versionColumn(),
+ availableComponentsColumn(),
+];
+
+type Props = {
+ deployments: FluxControllerEnriched[];
+ isLoading: boolean;
+ columns: TableColumn[];
+ many?: boolean;
+};
+
+export const FluxRuntimeTable = ({
+ deployments,
+ isLoading,
+ columns,
+ many,
+}: Props) => {
+ let clusters: Cluster[] = [];
+ deployments.forEach(deployment => {
+ const cls = clusters.find(
+ cluster => cluster.clusterName === deployment.clusterName,
+ );
+ if (cls) {
+ cls.availableComponents = [
+ ...cls.availableComponents,
+ deployment.metadata.labels['app.kubernetes.io/component'],
+ ];
+ } else {
+ clusters = [
+ ...clusters,
+ {
+ clusterName: deployment.clusterName,
+ namespace: deployment.metadata.namespace,
+ version: deployment.metadata.labels['app.kubernetes.io/version'],
+ availableComponents: [
+ deployment.metadata.labels['app.kubernetes.io/component'],
+ ],
+ },
+ ];
+ }
+ });
+
+ const data = clusters.map(c => {
+ const { clusterName, namespace, version, availableComponents } = c;
+ return {
+ id: `${namespace}/${clusterName}`,
+ clusterName,
+ namespace,
+ version,
+ availableComponents,
+ } as Cluster & { id: string };
+ });
+
+ return (
+
+ );
+};
diff --git a/plugins/backstage-plugin-flux/src/components/FluxRuntimeCard/index.ts b/plugins/backstage-plugin-flux/src/components/FluxRuntimeCard/index.ts
new file mode 100644
index 0000000..62dbd54
--- /dev/null
+++ b/plugins/backstage-plugin-flux/src/components/FluxRuntimeCard/index.ts
@@ -0,0 +1 @@
+export { FluxRuntimeCard } from './FluxRuntimeCard';
diff --git a/plugins/backstage-plugin-flux/src/components/FluxRuntimePage/index.tsx b/plugins/backstage-plugin-flux/src/components/FluxRuntimePage/index.tsx
new file mode 100644
index 0000000..3ebdf40
--- /dev/null
+++ b/plugins/backstage-plugin-flux/src/components/FluxRuntimePage/index.tsx
@@ -0,0 +1,44 @@
+import { Content, Header, Page } from '@backstage/core-components';
+import { makeStyles } from '@material-ui/core';
+import React from 'react';
+import { FluxRuntimeCard } from '../FluxRuntimeCard';
+
+const useStyles = makeStyles(() => ({
+ overflowXScroll: {
+ overflowX: 'scroll',
+ },
+}));
+
+export interface FluxRuntimePageProps {
+ /**
+ * Title
+ */
+ title?: string;
+ /**
+ * Subtitle
+ */
+ subtitle?: string;
+ /**
+ * Page Title
+ */
+ pageTitle?: string;
+}
+
+/**
+ * Main Page of Flux Runtime
+ *
+ * @public
+ */
+export function FluxRuntimePage(props: FluxRuntimePageProps) {
+ const { title = 'Flux Runtime' } = props;
+ const classes = useStyles();
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/plugins/backstage-plugin-flux/src/components/helpers.tsx b/plugins/backstage-plugin-flux/src/components/helpers.tsx
index 09d3987..cc7e93b 100644
--- a/plugins/backstage-plugin-flux/src/components/helpers.tsx
+++ b/plugins/backstage-plugin-flux/src/components/helpers.tsx
@@ -30,11 +30,18 @@ import {
} from '../objects';
import Flex from './Flex';
import KubeStatusIndicator, { getIndicatorInfo } from './KubeStatusIndicator';
-import { helm, kubernetes, oci, git, imagepolicy } from '../images/icons';
+import { helm, kubernetes, oci, git, flux } from '../images/icons';
import { useToggleSuspendResource } from '../hooks/useToggleSuspendResource';
export type Source = GitRepository | OCIRepository | HelmRepository;
export type Deployment = HelmRelease | Kustomization;
+export type Cluster = {
+ clusterName: string;
+ namespace: string;
+ version: string;
+ availableComponents: string[];
+};
+
/**
* Calculate a Name label for a resource with the namespace/name and link to
* this in Weave GitOps if possible.
@@ -250,7 +257,7 @@ export const nameAndClusterName = ({
);
-export const idColumn = () => {
+export const idColumn = () => {
return {
title: 'Id',
field: 'id',
@@ -259,7 +266,9 @@ export const idColumn = () => {
};
// Added hidden column to allow checkbox filtering by clusterName
-export const clusterNameFilteringColumn = () => {
+export const clusterNameFilteringColumn = <
+ T extends FluxObject | Cluster,
+>() => {
return {
title: 'Cluster name',
hidden: true,
@@ -267,6 +276,40 @@ export const clusterNameFilteringColumn = () => {
} as TableColumn;
};
+export const clusterColumn = () => {
+ return {
+ title: 'Cluster',
+ render: resource => {resource?.clusterName},
+ ...sortAndFilterOptions(resource => resource?.clusterName),
+ } as TableColumn;
+};
+
+export const namespaceColumn = () => {
+ return {
+ title: 'Namespace',
+ render: resource => {resource?.namespace},
+ ...sortAndFilterOptions(resource => resource?.namespace),
+ } as TableColumn;
+};
+
+export const versionColumn = () => {
+ return {
+ title: 'Version',
+ render: resource => {resource?.version},
+ ...sortAndFilterOptions(resource => resource?.version),
+ } as TableColumn;
+};
+
+export const availableComponentsColumn = () => {
+ return {
+ title: 'Available Components',
+ render: resource => {resource?.availableComponents.join(', ')},
+ ...sortAndFilterOptions(resource =>
+ resource?.availableComponents.join(', '),
+ ),
+ } as TableColumn;
+};
+
export const nameAndClusterNameColumn = () => {
return {
title: 'Name',
@@ -378,7 +421,7 @@ export const getIconType = (type: string) => {
case 'OCIRepository':
return oci;
case 'ImagePolicy':
- return imagepolicy;
+ return flux;
default:
return null;
}
diff --git a/plugins/backstage-plugin-flux/src/components/index.ts b/plugins/backstage-plugin-flux/src/components/index.ts
index 1878814..eafe31e 100644
--- a/plugins/backstage-plugin-flux/src/components/index.ts
+++ b/plugins/backstage-plugin-flux/src/components/index.ts
@@ -3,3 +3,4 @@ export * from './EntityFluxHelmReleasesCard';
export * from './EntityFluxOCIRepositoriesCard';
export * from './EntityFluxKustomizationsCard';
export * from './EntityFluxHelmRepositoriesCard';
+export * from './FluxRuntimeCard';
diff --git a/plugins/backstage-plugin-flux/src/hooks/useGetDeployments.test.ts b/plugins/backstage-plugin-flux/src/hooks/useGetDeployments.test.ts
new file mode 100644
index 0000000..d25c605
--- /dev/null
+++ b/plugins/backstage-plugin-flux/src/hooks/useGetDeployments.test.ts
@@ -0,0 +1,118 @@
+import { kubernetesApiRef } from '@backstage/plugin-kubernetes';
+import {
+ NAMESPACES_PATH,
+ getDeploymentsList,
+ getDeploymentsPath,
+} from './useGetDeployments';
+import { FluxController, Namespace } from '../objects';
+
+function makeMockKubernetesApi() {
+ return {
+ getObjectsByEntity: jest.fn(),
+ getClusters: jest.fn(),
+ getWorkloadsByEntity: jest.fn(),
+ getCustomObjectsByEntity: jest.fn(),
+ proxy: jest.fn(),
+ } as jest.Mocked;
+}
+
+describe('getDeploymentsList', () => {
+ const namespace = {
+ metadata: {
+ name: 'flux-system',
+ uid: '1dcca7cb-c651-4a86-93b4-ecf440df2353',
+ resourceVersion: '1583',
+ creationTimestamp: '2023-10-19T16:34:12Z',
+ labels: {
+ 'app.kubernetes.io/instance': 'flux-system',
+ 'app.kubernetes.io/part-of': 'flux',
+ 'app.kubernetes.io/version': 'v2.0.0',
+ 'kubernetes.io/metadata.name': 'flux-system',
+ 'kustomize.toolkit.fluxcd.io/name': 'flux-system',
+ 'kustomize.toolkit.fluxcd.io/namespace': 'flux-system',
+ 'pod-security.kubernetes.io/warn': 'restricted',
+ 'pod-security.kubernetes.io/warn-version': 'latest',
+ },
+ },
+ } as Namespace;
+
+ const deployment = {
+ apiVersion: 'meta.k8s.io/v1',
+ kind: 'PartialObjectMetadata',
+ metadata: {
+ name: 'image-automation-controller',
+ namespace: 'flux-system',
+ uid: 'b062d329-538d-4bb3-b4df-b2ac4b06dba8',
+ resourceVersion: '1001263',
+ generation: 1,
+ creationTimestamp: '2023-10-19T16:34:14Z',
+ labels: {
+ 'app.kubernetes.io/component': 'image-automation-controller',
+ 'app.kubernetes.io/instance': 'flux-system',
+ 'app.kubernetes.io/part-of': 'flux',
+ 'app.kubernetes.io/version': 'v2.1.2',
+ 'control-plane': 'controller',
+ 'kustomize.toolkit.fluxcd.io/name': 'flux-system',
+ 'kustomize.toolkit.fluxcd.io/namespace': 'flux-system',
+ },
+ annotations: {
+ 'deployment.kubernetes.io/revision': '1',
+ },
+ },
+ } as FluxController;
+
+ it('should get a Deployments list', async () => {
+ const kubernetesApi = makeMockKubernetesApi();
+
+ kubernetesApi.getClusters.mockImplementation(async () => {
+ return [{ name: 'mock-cluster-1', authProvider: 'serviceAccount1' }];
+ });
+
+ kubernetesApi.proxy.mockImplementation(
+ async ({ init, path, clusterName }) => {
+ if (!init?.method && path === NAMESPACES_PATH) {
+ if (clusterName === 'mock-cluster-1') {
+ return {
+ ok: true,
+ json: () =>
+ Promise.resolve({
+ kind: 'NamespacesList',
+ apiVersion: 'meta.k8s.io/v1',
+ items: [namespace],
+ }),
+ } as Response;
+ }
+ }
+ if (!init?.method && path === getDeploymentsPath('flux-system')) {
+ return {
+ ok: true,
+ json: () =>
+ Promise.resolve({
+ kind: 'DeploymentList',
+ apiVersion: 'apps/v1',
+ items: [deployment],
+ }),
+ } as Response;
+ }
+ return {
+ ok: true,
+ json: () => Promise.resolve(),
+ } as Response;
+ },
+ );
+
+ await getDeploymentsList(kubernetesApi);
+
+ // Assert we tried to GET the namespaces that exist in the clusters
+ expect(kubernetesApi.proxy).toHaveBeenCalledWith({
+ clusterName: 'mock-cluster-1',
+ path: NAMESPACES_PATH,
+ });
+
+ // Assert we tried to GET the deployments for mock-cluster-1
+ expect(kubernetesApi.proxy).toHaveBeenCalledWith({
+ clusterName: 'mock-cluster-1',
+ path: getDeploymentsPath('flux-system'),
+ });
+ });
+});
diff --git a/plugins/backstage-plugin-flux/src/hooks/useGetDeployments.ts b/plugins/backstage-plugin-flux/src/hooks/useGetDeployments.ts
new file mode 100644
index 0000000..d679167
--- /dev/null
+++ b/plugins/backstage-plugin-flux/src/hooks/useGetDeployments.ts
@@ -0,0 +1,83 @@
+import { useApi } from '@backstage/core-plugin-api';
+import { KubernetesApi, kubernetesApiRef } from '@backstage/plugin-kubernetes';
+import { FluxController, FluxControllerEnriched, Namespace } from '../objects';
+import { useQuery } from 'react-query';
+import _ from 'lodash';
+
+export const NAMESPACES_PATH = `/api/v1/namespaces?labelSelector=app.kubernetes.io%2Fpart-of%3Dflux&limit=500`;
+export const getDeploymentsPath = (ns: string) =>
+ `/apis/apps/v1/namespaces/${ns}/deployments?labelSelector=app.kubernetes.io%2Fpart-of%3Dflux&limit=500`;
+
+export async function getDeploymentsList(kubernetesApi: KubernetesApi) {
+ const clusters = await kubernetesApi?.getClusters();
+
+ const namespacesListsProxyData = await Promise.all(
+ clusters?.map(async cluster => {
+ return {
+ clusterName: cluster.name,
+ proxy: await kubernetesApi.proxy({
+ clusterName: cluster.name,
+ path: NAMESPACES_PATH,
+ }),
+ };
+ }),
+ );
+
+ const namespacesLists = async () => {
+ let nsLists: { clusterName: string; namespaces: Namespace[] }[] = [];
+ for (const namespacesList of namespacesListsProxyData) {
+ const { clusterName } = namespacesList;
+ const namespaces = await namespacesList.proxy.json();
+ nsLists = [...nsLists, { clusterName, namespaces: namespaces.items }];
+ }
+ return _.uniqWith(nsLists, _.isEqual);
+ };
+
+ const namespacesList = await namespacesLists();
+
+ const deploymentsListsProxyData = await Promise.all(
+ namespacesList?.flatMap(nsList =>
+ nsList.namespaces.map(async ns => {
+ return {
+ clusterName: nsList.clusterName,
+ proxy: await kubernetesApi.proxy({
+ clusterName: nsList.clusterName,
+ path: getDeploymentsPath(ns.metadata.name),
+ }),
+ };
+ }),
+ ),
+ );
+
+ const deploymentsLists = async () => {
+ let items: FluxControllerEnriched[] = [];
+ for (const item of deploymentsListsProxyData) {
+ const { clusterName } = item;
+ const i = await item.proxy.json();
+ items = [
+ ...items,
+ ...i.items.map((fc: FluxController) => {
+ return { ...fc, clusterName };
+ }),
+ ];
+ }
+ return _.uniqWith(items, _.isEqual);
+ };
+
+ return await deploymentsLists();
+}
+
+/**
+ *
+ * @public
+ */
+export function useGetDeployments() {
+ const kubernetesApi = useApi(kubernetesApiRef);
+
+ const { isLoading, data, error } = useQuery(
+ 'deployments',
+ () => getDeploymentsList(kubernetesApi),
+ );
+
+ return { isLoading, data, error };
+}
diff --git a/plugins/backstage-plugin-flux/src/hooks/useWeaveFluxDeepLink.ts b/plugins/backstage-plugin-flux/src/hooks/useWeaveFluxDeepLink.ts
index 46845d7..1d8e780 100644
--- a/plugins/backstage-plugin-flux/src/hooks/useWeaveFluxDeepLink.ts
+++ b/plugins/backstage-plugin-flux/src/hooks/useWeaveFluxDeepLink.ts
@@ -1,5 +1,4 @@
import { configApiRef, useApi } from '@backstage/core-plugin-api';
-
import {
FluxObject,
GitRepository,
diff --git a/plugins/backstage-plugin-flux/src/images/icons.tsx b/plugins/backstage-plugin-flux/src/images/icons.tsx
index c802ed6..1892100 100644
--- a/plugins/backstage-plugin-flux/src/images/icons.tsx
+++ b/plugins/backstage-plugin-flux/src/images/icons.tsx
@@ -87,14 +87,17 @@ export const oci = (
);
-export const imagepolicy = (
+export const flux = (
);
+
+export const FluxIcon = () => flux;
diff --git a/plugins/backstage-plugin-flux/src/index.ts b/plugins/backstage-plugin-flux/src/index.ts
index 2341120..d64c411 100644
--- a/plugins/backstage-plugin-flux/src/index.ts
+++ b/plugins/backstage-plugin-flux/src/index.ts
@@ -8,4 +8,7 @@ export {
EntityFluxDeploymentsCard,
EntityFluxSourcesCard,
EntityFluxImagePoliciesCard,
+ FluxRuntimeCard,
+ FluxRuntimePage,
+ FluxIcon,
} from './plugin';
diff --git a/plugins/backstage-plugin-flux/src/objects.ts b/plugins/backstage-plugin-flux/src/objects.ts
index 4c58533..a593388 100644
--- a/plugins/backstage-plugin-flux/src/objects.ts
+++ b/plugins/backstage-plugin-flux/src/objects.ts
@@ -89,6 +89,34 @@ export interface ImgPolicy {
value?: string;
}
+export interface Namespace {
+ metadata: {
+ name: string;
+ labels: { [key: string]: string };
+ uid: string;
+ resourceVersion: string;
+ creationTimestamp: string;
+ };
+}
+
+export type FluxController = {
+ apiVersion: string;
+ kind: string;
+ metadata: {
+ name: string;
+ namespace: string;
+ uid: string;
+ resourceVersion: string;
+ generation: number;
+ creationTimestamp: string;
+ labels: { [key: string]: string };
+ annotations: { [key: string]: string };
+ };
+ clusterName?: string;
+};
+
+export type FluxControllerEnriched = FluxController & { clusterName: string };
+
export class FluxObject {
obj: any;
clusterName: string;
diff --git a/plugins/backstage-plugin-flux/src/plugin.ts b/plugins/backstage-plugin-flux/src/plugin.ts
index 6a71bec..85fc446 100644
--- a/plugins/backstage-plugin-flux/src/plugin.ts
+++ b/plugins/backstage-plugin-flux/src/plugin.ts
@@ -1,6 +1,7 @@
import {
createComponentExtension,
createPlugin,
+ createRoutableExtension,
} from '@backstage/core-plugin-api';
import { rootRouteRef } from './routes';
@@ -143,3 +144,43 @@ export const EntityFluxImagePoliciesCard = weaveworksFluxPlugin.provide(
},
}),
);
+
+/**
+ * Card used to show the state of Flux Runtime.
+ * @public
+ */
+export const FluxRuntimeCard = weaveworksFluxPlugin.provide(
+ createComponentExtension({
+ name: 'FluxRuntimeCard',
+ component: {
+ lazy: () =>
+ import('./components/FluxRuntimeCard').then(m => m.FluxRuntimeCard),
+ },
+ }),
+);
+
+/**
+ * Page used to show Flux Controllers / Deployments in Flux Runtime
+ * @public
+ */
+export const FluxRuntimePage = weaveworksFluxPlugin.provide(
+ createRoutableExtension({
+ name: 'FluxRuntimePage',
+ component: () =>
+ import('./components/FluxRuntimePage').then(m => m.FluxRuntimePage),
+ mountPoint: rootRouteRef,
+ }),
+);
+
+/**
+ * Export for Flux Icon to use in nav
+ * @public
+ */
+export const FluxIcon = weaveworksFluxPlugin.provide(
+ createComponentExtension({
+ name: 'FluxIcon',
+ component: {
+ lazy: () => import('./images/icons').then(m => m.FluxIcon),
+ },
+ }),
+);
diff --git a/tsconfig.json b/tsconfig.json
index ba3f901..e6f7038 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -4,7 +4,8 @@
"packages/*/src",
"plugins/*/src",
"plugins/*/dev",
- "plugins/*/migrations"
+ "plugins/*/migrations",
+ "plugins/backstage-plugin-flux/src/components/FluxRuntimePage"
],
"exclude": ["node_modules"],
"compilerOptions": {
diff --git a/yarn.lock b/yarn.lock
index cc38a85..96297b1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8097,9 +8097,9 @@
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
"@types/react-dom@*", "@types/react-dom@<18.0.0", "@types/react-dom@^17":
- version "17.0.20"
- resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.20.tgz#e0c8901469d732b36d8473b40b679ad899da1b53"
- integrity sha512-4pzIjSxDueZZ90F52mU3aPoogkHIoSIDG+oQ+wQK7Cy2B9S+MvOqY0uEA/qawKz381qrEDkvpwyt8Bm31I8sbA==
+ version "17.0.22"
+ resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.22.tgz#34317e08be27b33fa9e7cdb56125b22538261bad"
+ integrity sha512-wHt4gkdSMb4jPp1vc30MLJxoWGsZs88URfmt3FRXoOEYrrqK3I8IuZLE/uFBb4UT6MRfI0wXFu4DS7LS0kUC7Q==
dependencies:
"@types/react" "^17"