diff --git a/.changeset/slow-dodos-remember.md b/.changeset/slow-dodos-remember.md
new file mode 100644
index 0000000000..fcc49e3f45
--- /dev/null
+++ b/.changeset/slow-dodos-remember.md
@@ -0,0 +1,8 @@
+---
+'@backstage/plugin-kubernetes-backend': patch
+'@backstage/plugin-kubernetes-cluster': patch
+'@backstage/plugin-kubernetes-react': patch
+'@backstage/plugin-kubernetes': patch
+---
+
+Add Kubernetes cluster plugin. Viewing Kubernetes clusters as an Admin from Backstage
diff --git a/packages/app/package.json b/packages/app/package.json
index e23da6c8dc..6e06d2ca03 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -52,6 +52,7 @@
"@backstage/plugin-jenkins": "workspace:^",
"@backstage/plugin-kafka": "workspace:^",
"@backstage/plugin-kubernetes": "workspace:^",
+ "@backstage/plugin-kubernetes-cluster": "workspace:^",
"@backstage/plugin-lighthouse": "workspace:^",
"@backstage/plugin-linguist": "workspace:^",
"@backstage/plugin-linguist-common": "workspace:^",
diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx
index ae78d0b771..81af3b91e7 100644
--- a/packages/app/src/components/catalog/EntityPage.tsx
+++ b/packages/app/src/components/catalog/EntityPage.tsx
@@ -103,6 +103,10 @@ import {
} from '@backstage/plugin-jenkins';
import { EntityKafkaContent } from '@backstage/plugin-kafka';
import { EntityKubernetesContent } from '@backstage/plugin-kubernetes';
+import {
+ isKubernetesClusterAvailable,
+ EntityKubernetesClusterContent,
+} from '@backstage/plugin-kubernetes-cluster';
import {
EntityLastLighthouseAuditCard,
EntityLighthouseContent,
@@ -898,6 +902,13 @@ const resourcePage = (
+
+
+
Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+///
+
+import { Entity } from '@backstage/catalog-model';
+import { default as React_2 } from 'react';
+
+// @public
+export const EntityKubernetesClusterContent: (
+ props: EntityKubernetesClusterContentProps,
+) => JSX.Element;
+
+// @public
+export type EntityKubernetesClusterContentProps = {};
+
+// @public (undocumented)
+export const isKubernetesClusterAvailable: (entity: Entity) => boolean;
+
+// @public (undocumented)
+export const Router: () => React_2.JSX.Element;
+```
diff --git a/plugins/kubernetes-cluster/catalog-info.yaml b/plugins/kubernetes-cluster/catalog-info.yaml
new file mode 100644
index 0000000000..3da5102e46
--- /dev/null
+++ b/plugins/kubernetes-cluster/catalog-info.yaml
@@ -0,0 +1,10 @@
+apiVersion: backstage.io/v1alpha1
+kind: Component
+metadata:
+ name: backstage-plugin-kubernetes-cluster
+ title: '@backstage/plugin-kubernetes-cluster'
+ description: A Backstage plugin that integrates towards Kubernetes clusters
+spec:
+ lifecycle: experimental
+ type: backstage-frontend-plugin
+ owner: kubernetes-maintainers
diff --git a/plugins/kubernetes-cluster/package.json b/plugins/kubernetes-cluster/package.json
new file mode 100644
index 0000000000..2e1c8d8309
--- /dev/null
+++ b/plugins/kubernetes-cluster/package.json
@@ -0,0 +1,80 @@
+{
+ "name": "@backstage/plugin-kubernetes-cluster",
+ "description": "A Backstage plugin that shows details of Kubernetes clusters",
+ "version": "0.0.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.esm.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "frontend-plugin"
+ },
+ "homepage": "https://backstage.io",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/backstage/backstage",
+ "directory": "plugins/kubernetes-cluster"
+ },
+ "keywords": [
+ "backstage",
+ "kubernetes"
+ ],
+ "sideEffects": false,
+ "scripts": {
+ "build": "backstage-cli package build",
+ "start": "backstage-cli package start",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack",
+ "clean": "backstage-cli package clean"
+ },
+ "dependencies": {
+ "@backstage/catalog-model": "workspace:^",
+ "@backstage/config": "workspace:^",
+ "@backstage/core-components": "workspace:^",
+ "@backstage/core-plugin-api": "workspace:^",
+ "@backstage/errors": "workspace:^",
+ "@backstage/plugin-catalog-react": "workspace:^",
+ "@backstage/plugin-kubernetes-common": "workspace:^",
+ "@backstage/plugin-kubernetes-react": "workspace:^",
+ "@backstage/theme": "workspace:^",
+ "@kubernetes-models/apimachinery": "^1.1.0",
+ "@kubernetes-models/base": "^4.0.1",
+ "@material-ui/core": "^4.12.2",
+ "@material-ui/icons": "^4.9.1",
+ "@material-ui/lab": "4.0.0-alpha.61",
+ "@types/react": "^16.13.1 || ^17.0.0",
+ "cronstrue": "^2.2.0",
+ "js-yaml": "^4.0.0",
+ "kubernetes-models": "^4.1.0",
+ "lodash": "^4.17.21",
+ "luxon": "^3.0.0",
+ "react-use": "^17.2.4"
+ },
+ "peerDependencies": {
+ "react": "^16.13.1 || ^17.0.0",
+ "react-dom": "^16.13.1 || ^17.0.0",
+ "react-router-dom": "6.0.0-beta.0 || ^6.3.0"
+ },
+ "devDependencies": {
+ "@backstage/cli": "workspace:^",
+ "@backstage/core-app-api": "workspace:^",
+ "@backstage/dev-utils": "workspace:^",
+ "@backstage/test-utils": "workspace:^",
+ "@testing-library/dom": "^8.0.0",
+ "@testing-library/jest-dom": "^5.10.1",
+ "@testing-library/react": "^12.1.3",
+ "@testing-library/react-hooks": "^8.0.0",
+ "@testing-library/user-event": "^14.0.0",
+ "@types/node": "^16.11.26",
+ "msw": "^1.0.0"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/plugins/kubernetes-cluster/src/Router.tsx b/plugins/kubernetes-cluster/src/Router.tsx
new file mode 100644
index 0000000000..86563439ce
--- /dev/null
+++ b/plugins/kubernetes-cluster/src/Router.tsx
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2020 The Backstage Authors
+ *
+ * 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 { Entity } from '@backstage/catalog-model';
+import { useEntity } from '@backstage/plugin-catalog-react';
+import { Route, Routes } from 'react-router-dom';
+import { MissingAnnotationEmptyState } from '@backstage/core-components';
+import { ANNOTATION_KUBERNETES_API_SERVER } from '@backstage/plugin-kubernetes-common';
+import { KubernetesClusterContent } from './components/KubernetesClusterContent';
+
+/**
+ *
+ *
+ * @public
+ */
+export const isKubernetesClusterAvailable = (entity: Entity) =>
+ Boolean(entity.metadata.annotations?.[ANNOTATION_KUBERNETES_API_SERVER]);
+
+/**
+ *
+ *
+ * @public
+ */
+export const Router = () => {
+ const { entity } = useEntity();
+
+ if (isKubernetesClusterAvailable(entity)) {
+ return (
+
+ } />
+
+ );
+ }
+
+ return (
+ <>
+
+ >
+ );
+};
diff --git a/plugins/kubernetes-cluster/src/components/ApiResources/ApiResources.test.tsx b/plugins/kubernetes-cluster/src/components/ApiResources/ApiResources.test.tsx
new file mode 100644
index 0000000000..601ba54a6d
--- /dev/null
+++ b/plugins/kubernetes-cluster/src/components/ApiResources/ApiResources.test.tsx
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2020 The Backstage Authors
+ *
+ * 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 { renderInTestApp } from '@backstage/test-utils';
+import { ApiResources } from './ApiResources';
+import '@testing-library/jest-dom';
+
+jest.mock('@backstage/plugin-catalog-react', () => ({
+ useEntity: () => {
+ return {
+ entity: {
+ metadata: {
+ name: 'some-cluster',
+ },
+ },
+ };
+ },
+}));
+
+jest.mock('./useApiResources', () => ({
+ useApiResources: jest.fn().mockReturnValue({
+ loading: false,
+ value: {
+ groups: [
+ {
+ name: 'some-apiVersion',
+ preferredVersion: {
+ groupVersion: 'some-group-version',
+ },
+ },
+ ],
+ },
+ }),
+}));
+
+describe('ApiResources', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+ it('displays ApiResources', async () => {
+ const { getByText } = await renderInTestApp();
+
+ // Title
+ expect(getByText('Name')).toBeInTheDocument();
+ expect(getByText('Preferred Version')).toBeInTheDocument();
+
+ // Row 1
+ expect(getByText('some-apiVersion')).toBeInTheDocument();
+ expect(getByText('some-group-version')).toBeInTheDocument();
+ });
+});
diff --git a/plugins/kubernetes-cluster/src/components/ApiResources/ApiResources.tsx b/plugins/kubernetes-cluster/src/components/ApiResources/ApiResources.tsx
new file mode 100644
index 0000000000..f6e8bc65ce
--- /dev/null
+++ b/plugins/kubernetes-cluster/src/components/ApiResources/ApiResources.tsx
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * 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 { useApiResources } from './useApiResources';
+import React, { useCallback, useEffect } from 'react';
+import { useEntity } from '@backstage/plugin-catalog-react';
+import { Table, TableColumn } from '@backstage/core-components';
+import { IAPIGroup } from '@kubernetes-models/apimachinery/apis/meta/v1';
+import { useKubernetesClusterError } from '../KubernetesClusterErrorContext/KubernetesClusterErrorContext';
+import { makeStyles } from '@material-ui/core';
+
+const useStyles = makeStyles(theme => ({
+ empty: {
+ padding: theme.spacing(2),
+ display: 'flex',
+ justifyContent: 'center',
+ },
+}));
+
+const defaultColumns: TableColumn[] = [
+ {
+ title: 'Name',
+ highlight: true,
+ render: (apiGroup: IAPIGroup) => {
+ return apiGroup.name;
+ },
+ },
+ {
+ title: 'Preferred Version',
+ highlight: true,
+ render: (apiGroup: IAPIGroup) => {
+ return apiGroup.preferredVersion?.groupVersion;
+ },
+ },
+];
+
+export const ApiResources = () => {
+ const classes = useStyles();
+ const { entity } = useEntity();
+ const { setError } = useKubernetesClusterError();
+ const setErrorCallback = useCallback(setError, [setError]);
+ const { value, error, loading } = useApiResources({
+ clusterName: entity.metadata.name,
+ });
+
+ useEffect(() => {
+ if (error) {
+ setErrorCallback(error.message);
+ }
+ }, [error, setErrorCallback]);
+
+ return (
+
+ {error !== undefined
+ ? 'Error loading API Resources'
+ : 'No API Resources found'}
+
+ }
+ columns={defaultColumns}
+ />
+ );
+};
diff --git a/plugins/kubernetes-cluster/src/components/ApiResources/useApiResources.ts b/plugins/kubernetes-cluster/src/components/ApiResources/useApiResources.ts
new file mode 100644
index 0000000000..b28b0b0b67
--- /dev/null
+++ b/plugins/kubernetes-cluster/src/components/ApiResources/useApiResources.ts
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * 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 useAsync from 'react-use/lib/useAsync';
+
+import { useApi } from '@backstage/core-plugin-api';
+import { kubernetesApiRef } from '@backstage/plugin-kubernetes-react';
+import { IAPIGroupList } from '@kubernetes-models/apimachinery/apis/meta/v1';
+
+/**
+ * Arguments for useApiResources
+ *
+ * @public
+ */
+export interface ApiResourcesOptions {
+ clusterName: string;
+}
+
+/**
+ * Retrieves the logs for the given pod
+ *
+ * @public
+ */
+export const useApiResources = ({ clusterName }: ApiResourcesOptions) => {
+ const kubernetesApi = useApi(kubernetesApiRef);
+ return useAsync(async () => {
+ return await kubernetesApi
+ .proxy({
+ clusterName,
+ path: '/apis',
+ })
+ .then(r => {
+ return r.json() as Promise;
+ });
+ }, [clusterName]);
+};
diff --git a/plugins/kubernetes-cluster/src/components/ClusterOverview/ClusterOverview.test.tsx b/plugins/kubernetes-cluster/src/components/ClusterOverview/ClusterOverview.test.tsx
new file mode 100644
index 0000000000..6398093a74
--- /dev/null
+++ b/plugins/kubernetes-cluster/src/components/ClusterOverview/ClusterOverview.test.tsx
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2020 The Backstage Authors
+ *
+ * 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 { renderInTestApp } from '@backstage/test-utils';
+import { ClusterOverview } from './ClusterOverview';
+import '@testing-library/jest-dom';
+
+jest.mock('@backstage/plugin-catalog-react', () => ({
+ useEntity: () => {
+ return {
+ entity: {
+ metadata: {
+ name: 'some-cluster',
+ },
+ },
+ };
+ },
+}));
+
+jest.mock('./useCluster', () => ({
+ useCluster: jest.fn().mockReturnValue({
+ loading: false,
+ value: {
+ name: 'some-cluster',
+ authProvider: 'google',
+ },
+ }),
+}));
+
+describe('ClusterOverview', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+ it('displays ClusterOverview', async () => {
+ const { getByText, queryAllByText } = await renderInTestApp(
+ ,
+ );
+
+ expect(getByText('Name')).toBeInTheDocument();
+ expect(getByText('some-cluster')).toBeInTheDocument();
+ expect(getByText('Backstage Auth Provider')).toBeInTheDocument();
+ expect(getByText('google')).toBeInTheDocument();
+ expect(getByText('OIDC Token Provider')).toBeInTheDocument();
+ expect(getByText('Dashboard Link')).toBeInTheDocument();
+ expect(queryAllByText('N/A')).toHaveLength(2);
+ });
+});
diff --git a/plugins/kubernetes-cluster/src/components/ClusterOverview/ClusterOverview.tsx b/plugins/kubernetes-cluster/src/components/ClusterOverview/ClusterOverview.tsx
new file mode 100644
index 0000000000..a4d4ffc40c
--- /dev/null
+++ b/plugins/kubernetes-cluster/src/components/ClusterOverview/ClusterOverview.tsx
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * 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, { useCallback, useEffect } from 'react';
+import { InfoCard, StructuredMetadataTable } from '@backstage/core-components';
+import { useEntity } from '@backstage/plugin-catalog-react';
+import { useCluster } from './useCluster';
+import { Skeleton } from '@material-ui/lab';
+import { Theme, createStyles, makeStyles } from '@material-ui/core';
+import { useKubernetesClusterError } from '../KubernetesClusterErrorContext/KubernetesClusterErrorContext';
+
+const useStyles = makeStyles((_theme: Theme) =>
+ createStyles({
+ root: {
+ height: '100%',
+ },
+ }),
+);
+
+export const ClusterOverview = () => {
+ const classes = useStyles();
+ const { entity } = useEntity();
+ const { value, loading, error } = useCluster({
+ clusterName: entity.metadata.name,
+ });
+ const { setError } = useKubernetesClusterError();
+ const setErrorCallback = useCallback(setError, [setError]);
+ useEffect(() => {
+ if (error) {
+ setErrorCallback(error.message);
+ }
+ }, [error, setErrorCallback]);
+
+ return (
+
+ {!value && loading && (
+ <>
+
+ >
+ )}
+ {value && (
+
+ )}
+
+ );
+};
diff --git a/plugins/kubernetes-cluster/src/components/ClusterOverview/index.ts b/plugins/kubernetes-cluster/src/components/ClusterOverview/index.ts
new file mode 100644
index 0000000000..5a46125f3e
--- /dev/null
+++ b/plugins/kubernetes-cluster/src/components/ClusterOverview/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * 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 { ClusterOverview } from './ClusterOverview';
diff --git a/plugins/kubernetes-cluster/src/components/ClusterOverview/useCluster.ts b/plugins/kubernetes-cluster/src/components/ClusterOverview/useCluster.ts
new file mode 100644
index 0000000000..05106cdc5b
--- /dev/null
+++ b/plugins/kubernetes-cluster/src/components/ClusterOverview/useCluster.ts
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * 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 useAsync from 'react-use/lib/useAsync';
+
+import { useApi } from '@backstage/core-plugin-api';
+import { kubernetesApiRef } from '@backstage/plugin-kubernetes-react';
+
+/**
+ * Arguments for useApiResources
+ *
+ * @public
+ */
+export interface UseClusterOptions {
+ clusterName: string;
+}
+
+/**
+ * Retrieves the logs for the given pod
+ *
+ * @public
+ */
+export const useCluster = ({ clusterName }: UseClusterOptions) => {
+ const kubernetesApi = useApi(kubernetesApiRef);
+ return useAsync(async () => {
+ return await kubernetesApi.getCluster(clusterName);
+ }, [clusterName]);
+};
diff --git a/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx b/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx
new file mode 100644
index 0000000000..665ca2acdd
--- /dev/null
+++ b/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * 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 { ApiResources } from '../ApiResources/ApiResources';
+import { Grid, Typography } from '@material-ui/core';
+import { Nodes } from '../Nodes/Nodes';
+import { ClusterOverview } from '../ClusterOverview';
+import {
+ KubernetesClusterErrorProvider,
+ useKubernetesClusterError,
+} from '../KubernetesClusterErrorContext/KubernetesClusterErrorContext';
+import { WarningPanel } from '@backstage/core-components';
+
+const ContentGrid = () => {
+ const { error } = useKubernetesClusterError();
+ return (
+ <>
+
+ {error && (
+
+
+ {error}
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+/**
+ *
+ *
+ * @public
+ */
+export const KubernetesClusterContent = () => {
+ return (
+
+
+
+ );
+};
diff --git a/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/index.ts b/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/index.ts
new file mode 100644
index 0000000000..2f5850c10c
--- /dev/null
+++ b/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * 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 { KubernetesClusterContent } from './KubernetesClusterContent';
diff --git a/plugins/kubernetes-cluster/src/components/KubernetesClusterErrorContext/KubernetesClusterErrorContext.tsx b/plugins/kubernetes-cluster/src/components/KubernetesClusterErrorContext/KubernetesClusterErrorContext.tsx
new file mode 100644
index 0000000000..049a7b541d
--- /dev/null
+++ b/plugins/kubernetes-cluster/src/components/KubernetesClusterErrorContext/KubernetesClusterErrorContext.tsx
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * 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, { useCallback, useContext, useState } from 'react';
+
+export interface ErrorContext {
+ error?: string;
+ setError: (message: string) => void;
+}
+
+export const KubernetesClusterErrorContext = React.createContext({
+ setError: (_: string) => {},
+});
+
+export interface KubernetesClusterErrorProviderProps {
+ children: React.ReactNode;
+}
+
+export const KubernetesClusterErrorProvider = ({
+ children,
+}: KubernetesClusterErrorProviderProps) => {
+ const [error, setError] = useState(undefined);
+
+ const contextValue: ErrorContext = {
+ error,
+ setError: useCallback((message: string) => setError(message), []),
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useKubernetesClusterError = () => {
+ return useContext(KubernetesClusterErrorContext);
+};
diff --git a/plugins/kubernetes-cluster/src/components/Nodes/Nodes.test.tsx b/plugins/kubernetes-cluster/src/components/Nodes/Nodes.test.tsx
new file mode 100644
index 0000000000..f48fdfbcef
--- /dev/null
+++ b/plugins/kubernetes-cluster/src/components/Nodes/Nodes.test.tsx
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2020 The Backstage Authors
+ *
+ * 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 { renderInTestApp } from '@backstage/test-utils';
+import { Nodes } from './Nodes';
+import '@testing-library/jest-dom';
+
+jest.mock('@backstage/plugin-catalog-react', () => ({
+ useEntity: () => {
+ return {
+ entity: {
+ metadata: {
+ name: 'some-cluster',
+ },
+ },
+ };
+ },
+}));
+
+jest.mock('./useNodes', () => ({
+ useNodes: jest.fn().mockReturnValue({
+ loading: false,
+ value: {
+ items: [
+ {
+ metadata: {
+ name: 'some-node-name',
+ },
+ status: {
+ conditions: [
+ {
+ type: 'Ready',
+ status: 'True',
+ },
+ ],
+ nodeInfo: {
+ operatingSystem: 'linux',
+ architecture: 'ARM',
+ },
+ },
+ },
+ {
+ metadata: {
+ name: 'some-node-bad-name',
+ },
+ spec: {
+ unschedulable: true,
+ },
+ status: {
+ conditions: [
+ {
+ type: 'Ready',
+ status: 'False',
+ },
+ ],
+ nodeInfo: {
+ operatingSystem: 'windows',
+ architecture: 'x86',
+ },
+ },
+ },
+ ],
+ },
+ }),
+}));
+
+describe('Nodes', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+ it('displays nodes table - ready schedulable node', async () => {
+ const { getByText } = await renderInTestApp();
+
+ expect(getByText('Nodes')).toBeInTheDocument();
+
+ // Titles
+ expect(getByText('Name')).toBeInTheDocument();
+ expect(getByText('Schedulable')).toBeInTheDocument();
+ expect(getByText('Status')).toBeInTheDocument();
+ expect(getByText('OS')).toBeInTheDocument();
+
+ // Row 1
+ expect(getByText('some-node-name')).toBeInTheDocument();
+ expect(getByText('✅')).toBeInTheDocument();
+ expect(getByText('Ready')).toBeInTheDocument();
+ expect(getByText('linux (ARM)')).toBeInTheDocument();
+
+ // Row 2
+ expect(getByText('some-node-bad-name')).toBeInTheDocument();
+ expect(getByText('❌')).toBeInTheDocument();
+ expect(getByText('Not Ready')).toBeInTheDocument();
+ expect(getByText('windows (x86)')).toBeInTheDocument();
+ });
+});
diff --git a/plugins/kubernetes-cluster/src/components/Nodes/Nodes.tsx b/plugins/kubernetes-cluster/src/components/Nodes/Nodes.tsx
new file mode 100644
index 0000000000..e7f6e957ae
--- /dev/null
+++ b/plugins/kubernetes-cluster/src/components/Nodes/Nodes.tsx
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * 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 { useNodes } from './useNodes';
+import React, { useCallback, useEffect } from 'react';
+import { useEntity } from '@backstage/plugin-catalog-react';
+import {
+ StructuredMetadataTable,
+ Table,
+ TableColumn,
+} from '@backstage/core-components';
+import { INode } from 'kubernetes-models/v1';
+import { Grid, Typography, makeStyles } from '@material-ui/core';
+import { useKubernetesClusterError } from '../KubernetesClusterErrorContext/KubernetesClusterErrorContext';
+import { KubernetesDrawer } from '@backstage/plugin-kubernetes-react';
+
+const useStyles = makeStyles(theme => ({
+ empty: {
+ padding: theme.spacing(2),
+ display: 'flex',
+ justifyContent: 'center',
+ },
+}));
+
+const defaultColumns: TableColumn[] = [
+ {
+ title: 'Name',
+ highlight: true,
+ width: 'auto',
+ render: (node: INode) => {
+ return (
+
+
+
+ Node Info
+
+
+
+ Addresses
+ {
+ accum[next.type] = next.address;
+ return accum;
+ }, {} as any) ?? {}
+ }
+ />
+
+
+ Taints
+ {
+ accum[`${next.effect}`] = `${next.key} (${next.value})`;
+ return accum;
+ }, {} as any) ?? {}
+ }
+ />
+
+
+
+ );
+ },
+ },
+ {
+ title: 'Schedulable',
+ align: 'center',
+ width: 'auto',
+ render: (node: INode) => {
+ if (node.spec?.unschedulable) {
+ return '❌';
+ }
+ return '✅';
+ },
+ },
+ {
+ title: 'Status',
+ width: 'auto',
+ render: (node: INode) => {
+ // TODO add an icon
+ const readyCondition = node.status?.conditions?.find(c => {
+ return c.type === 'Ready';
+ });
+ if (!readyCondition) {
+ return 'Unknown';
+ }
+ return readyCondition.status === 'True' ? 'Ready' : 'Not Ready';
+ },
+ },
+ {
+ title: 'OS',
+ width: 'auto',
+ render: (node: INode) => {
+ return `${node.status?.nodeInfo?.operatingSystem ?? 'unknown'} (${
+ node.status?.nodeInfo?.architecture ?? '?'
+ })`;
+ },
+ },
+];
+
+export const Nodes = () => {
+ const classes = useStyles();
+ const { entity } = useEntity();
+ const { value, error, loading } = useNodes({
+ clusterName: entity.metadata.name,
+ });
+ const { setError } = useKubernetesClusterError();
+ const setErrorCallback = useCallback(setError, [setError]);
+ useEffect(() => {
+ if (error) {
+ setErrorCallback(error.message);
+ }
+ }, [error, setErrorCallback]);
+
+ return (
+
+ {error !== undefined ? 'Error loading nodes' : 'No nodes found'}
+
+ }
+ columns={defaultColumns}
+ />
+ );
+};
diff --git a/plugins/kubernetes-cluster/src/components/Nodes/useNodes.ts b/plugins/kubernetes-cluster/src/components/Nodes/useNodes.ts
new file mode 100644
index 0000000000..1feaff3bdb
--- /dev/null
+++ b/plugins/kubernetes-cluster/src/components/Nodes/useNodes.ts
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * 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 useAsync from 'react-use/lib/useAsync';
+
+import { useApi } from '@backstage/core-plugin-api';
+import { kubernetesApiRef } from '@backstage/plugin-kubernetes-react';
+import { NodeList } from 'kubernetes-models/v1';
+
+/**
+ * Arguments for useApiResources
+ *
+ * @public
+ */
+export interface useNodesOptions {
+ clusterName: string;
+}
+
+/**
+ * Retrieves nodes for a cluster
+ *
+ * @public
+ */
+export const useNodes = ({ clusterName }: useNodesOptions) => {
+ const kubernetesApi = useApi(kubernetesApiRef);
+ return useAsync(async () => {
+ return await kubernetesApi
+ .proxy({
+ clusterName,
+ path: '/api/v1/nodes?limit=500',
+ })
+ .then(r => {
+ return r.json() as Promise;
+ });
+ }, [clusterName]);
+};
diff --git a/plugins/kubernetes-cluster/src/index.ts b/plugins/kubernetes-cluster/src/index.ts
new file mode 100644
index 0000000000..6171b151e4
--- /dev/null
+++ b/plugins/kubernetes-cluster/src/index.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * 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.
+ */
+
+/**
+ * A Backstage plugin that integrates towards Kubernetes
+ *
+ * @packageDocumentation
+ */
+
+export {
+ EntityKubernetesClusterContent,
+ type EntityKubernetesClusterContentProps,
+} from './plugin';
+export { Router, isKubernetesClusterAvailable } from './Router';
diff --git a/plugins/kubernetes-cluster/src/plugin.ts b/plugins/kubernetes-cluster/src/plugin.ts
new file mode 100644
index 0000000000..5cd9b2287c
--- /dev/null
+++ b/plugins/kubernetes-cluster/src/plugin.ts
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2020 The Backstage Authors
+ *
+ * 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 {
+ createPlugin,
+ createRouteRef,
+ createRoutableExtension,
+} from '@backstage/core-plugin-api';
+
+export const rootCatalogKubernetesClusterRouteRef = createRouteRef({
+ id: 'kubernetes-cluster',
+});
+
+export const kubernetesClusterPlugin = createPlugin({
+ id: 'kubernetes-cluster',
+ apis: [],
+ routes: {
+ entityContent: rootCatalogKubernetesClusterRouteRef,
+ },
+});
+
+/**
+ * Props of EntityKubernetesContent
+ *
+ * @public
+ */
+export type EntityKubernetesClusterContentProps = {};
+
+/**
+ * Props of EntityKubernetesContent
+ *
+ * @public
+ */
+export const EntityKubernetesClusterContent: (
+ props: EntityKubernetesClusterContentProps,
+) => JSX.Element = kubernetesClusterPlugin.provide(
+ createRoutableExtension({
+ name: 'EntityKubernetesClusterContent',
+ component: () => import('./Router').then(m => m.Router),
+ mountPoint: rootCatalogKubernetesClusterRouteRef,
+ }),
+);
diff --git a/plugins/kubernetes-react/api-report.md b/plugins/kubernetes-react/api-report.md
index e182524426..bb8e4923f6 100644
--- a/plugins/kubernetes-react/api-report.md
+++ b/plugins/kubernetes-react/api-report.md
@@ -251,12 +251,22 @@ export type JobsAccordionsProps = {
// @public (undocumented)
export interface KubernetesApi {
+ // (undocumented)
+ getCluster(clusterName: string): Promise<
+ | {
+ name: string;
+ authProvider: string;
+ oidcTokenProvider?: string;
+ dashboardUrl?: string;
+ }
+ | undefined
+ >;
// (undocumented)
getClusters(): Promise<
{
name: string;
authProvider: string;
- oidcTokenProvider?: string | undefined;
+ oidcTokenProvider?: string;
}[]
>;
// (undocumented)
@@ -338,6 +348,12 @@ export class KubernetesBackendClient implements KubernetesApi {
kubernetesAuthProvidersApi: KubernetesAuthProvidersApi;
});
// (undocumented)
+ getCluster(clusterName: string): Promise<{
+ name: string;
+ authProvider: string;
+ oidcTokenProvider?: string;
+ }>;
+ // (undocumented)
getClusters(): Promise<
{
name: string;
diff --git a/plugins/kubernetes-react/src/api/KubernetesBackendClient.ts b/plugins/kubernetes-react/src/api/KubernetesBackendClient.ts
index d4e11c43da..a769eb088d 100644
--- a/plugins/kubernetes-react/src/api/KubernetesBackendClient.ts
+++ b/plugins/kubernetes-react/src/api/KubernetesBackendClient.ts
@@ -75,7 +75,7 @@ export class KubernetesBackendClient implements KubernetesApi {
return this.handleResponse(response);
}
- private async getCluster(clusterName: string): Promise<{
+ public async getCluster(clusterName: string): Promise<{
name: string;
authProvider: string;
oidcTokenProvider?: string;
diff --git a/plugins/kubernetes-react/src/api/types.ts b/plugins/kubernetes-react/src/api/types.ts
index c0f97ffd41..28cf2dde55 100644
--- a/plugins/kubernetes-react/src/api/types.ts
+++ b/plugins/kubernetes-react/src/api/types.ts
@@ -42,9 +42,18 @@ export interface KubernetesApi {
{
name: string;
authProvider: string;
- oidcTokenProvider?: string | undefined;
+ oidcTokenProvider?: string;
}[]
>;
+ getCluster(clusterName: string): Promise<
+ | {
+ name: string;
+ authProvider: string;
+ oidcTokenProvider?: string;
+ dashboardUrl?: string;
+ }
+ | undefined
+ >;
getWorkloadsByEntity(
request: WorkloadsByEntityRequest,
): Promise;
diff --git a/plugins/kubernetes/dev/index.tsx b/plugins/kubernetes/dev/index.tsx
index 520f8802a9..fbc666ae13 100644
--- a/plugins/kubernetes/dev/index.tsx
+++ b/plugins/kubernetes/dev/index.tsx
@@ -116,6 +116,12 @@ class MockKubernetesClient implements KubernetesApi {
return [{ name: 'mock-cluster', authProvider: 'serviceAccount' }];
}
+ async getCluster(
+ _clusterName: string,
+ ): Promise<{ name: string; authProvider: string }> {
+ return { name: 'mock-cluster', authProvider: 'serviceAccount' };
+ }
+
async proxy(_options: { clusterName: String; path: String }): Promise {
return {
kind: 'Namespace',
diff --git a/yarn.lock b/yarn.lock
index 2c48e783e0..38af62fcd1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7643,6 +7643,49 @@ __metadata:
languageName: unknown
linkType: soft
+"@backstage/plugin-kubernetes-cluster@workspace:^, @backstage/plugin-kubernetes-cluster@workspace:plugins/kubernetes-cluster":
+ version: 0.0.0-use.local
+ resolution: "@backstage/plugin-kubernetes-cluster@workspace:plugins/kubernetes-cluster"
+ dependencies:
+ "@backstage/catalog-model": "workspace:^"
+ "@backstage/cli": "workspace:^"
+ "@backstage/config": "workspace:^"
+ "@backstage/core-app-api": "workspace:^"
+ "@backstage/core-components": "workspace:^"
+ "@backstage/core-plugin-api": "workspace:^"
+ "@backstage/dev-utils": "workspace:^"
+ "@backstage/errors": "workspace:^"
+ "@backstage/plugin-catalog-react": "workspace:^"
+ "@backstage/plugin-kubernetes-common": "workspace:^"
+ "@backstage/plugin-kubernetes-react": "workspace:^"
+ "@backstage/test-utils": "workspace:^"
+ "@backstage/theme": "workspace:^"
+ "@kubernetes-models/apimachinery": ^1.1.0
+ "@kubernetes-models/base": ^4.0.1
+ "@material-ui/core": ^4.12.2
+ "@material-ui/icons": ^4.9.1
+ "@material-ui/lab": 4.0.0-alpha.61
+ "@testing-library/dom": ^8.0.0
+ "@testing-library/jest-dom": ^5.10.1
+ "@testing-library/react": ^12.1.3
+ "@testing-library/react-hooks": ^8.0.0
+ "@testing-library/user-event": ^14.0.0
+ "@types/node": ^16.11.26
+ "@types/react": ^16.13.1 || ^17.0.0
+ cronstrue: ^2.2.0
+ js-yaml: ^4.0.0
+ kubernetes-models: ^4.1.0
+ lodash: ^4.17.21
+ luxon: ^3.0.0
+ msw: ^1.0.0
+ react-use: ^17.2.4
+ peerDependencies:
+ react: ^16.13.1 || ^17.0.0
+ react-dom: ^16.13.1 || ^17.0.0
+ react-router-dom: 6.0.0-beta.0 || ^6.3.0
+ languageName: unknown
+ linkType: soft
+
"@backstage/plugin-kubernetes-common@workspace:^, @backstage/plugin-kubernetes-common@workspace:plugins/kubernetes-common":
version: 0.0.0-use.local
resolution: "@backstage/plugin-kubernetes-common@workspace:plugins/kubernetes-common"
@@ -17989,10 +18032,10 @@ __metadata:
languageName: node
linkType: hard
-"@types/node@npm:^16.9.2":
- version: 16.18.53
- resolution: "@types/node@npm:16.18.53"
- checksum: 26c05cde59664360c22e0dda70776ca6f1b35f0b94e4f84d2c21e2afa2e69ac3a2c99bbb57b43405f81df1b2598f6d707ccc7c4c31865f90e45c4625d8400518
+"@types/node@npm:^16.11.26, @types/node@npm:^16.9.2":
+ version: 16.18.54
+ resolution: "@types/node@npm:16.18.54"
+ checksum: 208e8fc64f605e9cd55ab5e620a0fd019d8fe5629e3e3c5de869a149b731ab0fac5720c516dccc0ecc834ac27df754723dfe6554551663f016ba5096ea8851df
languageName: node
linkType: hard
@@ -25544,6 +25587,7 @@ __metadata:
"@backstage/plugin-jenkins": "workspace:^"
"@backstage/plugin-kafka": "workspace:^"
"@backstage/plugin-kubernetes": "workspace:^"
+ "@backstage/plugin-kubernetes-cluster": "workspace:^"
"@backstage/plugin-lighthouse": "workspace:^"
"@backstage/plugin-linguist": "workspace:^"
"@backstage/plugin-linguist-common": "workspace:^"