feat: Kubernetes cluster plugin (#18760)
* refactor: k8s plugins Signed-off-by: Matthew Clarke <mclarke@spotify.com> * feat: add kubernetes cluster plugin Signed-off-by: Matthew Clarke <mclarke@spotify.com> * fix: yarn fix Signed-off-by: Matthew Clarke <mclarke@spotify.com> * fix: tsc Signed-off-by: Matthew Clarke <mclarke@spotify.com> * fix: types Signed-off-by: Matthew Clarke <mclarke@spotify.com> * chore: missing changeset Signed-off-by: Matthew Clarke <mclarke@spotify.com> * chore: update api report Signed-off-by: Matthew Clarke <mclarke@spotify.com> --------- Signed-off-by: Matthew Clarke <mclarke@spotify.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,24 @@
|
||||
## API Report File for "@backstage/plugin-kubernetes-cluster"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
/// <reference types="react" />
|
||||
|
||||
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;
|
||||
```
|
||||
@@ -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
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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 (
|
||||
<Routes>
|
||||
<Route path="/" element={<KubernetesClusterContent />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<MissingAnnotationEmptyState
|
||||
annotation={ANNOTATION_KUBERNETES_API_SERVER}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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(<ApiResources />);
|
||||
|
||||
// Title
|
||||
expect(getByText('Name')).toBeInTheDocument();
|
||||
expect(getByText('Preferred Version')).toBeInTheDocument();
|
||||
|
||||
// Row 1
|
||||
expect(getByText('some-apiVersion')).toBeInTheDocument();
|
||||
expect(getByText('some-group-version')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -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<IAPIGroup>[] = [
|
||||
{
|
||||
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 (
|
||||
<Table
|
||||
title="API Resources"
|
||||
options={{ paging: true, search: false, emptyRowsWhenPaging: false }}
|
||||
isLoading={!value && loading}
|
||||
data={value?.groups ?? []}
|
||||
emptyContent={
|
||||
<div className={classes.empty}>
|
||||
{error !== undefined
|
||||
? 'Error loading API Resources'
|
||||
: 'No API Resources found'}
|
||||
</div>
|
||||
}
|
||||
columns={defaultColumns}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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<IAPIGroupList>;
|
||||
});
|
||||
}, [clusterName]);
|
||||
};
|
||||
@@ -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(
|
||||
<ClusterOverview />,
|
||||
);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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 (
|
||||
<InfoCard title="Cluster Overview" className={classes.root}>
|
||||
{!value && loading && (
|
||||
<>
|
||||
<Skeleton height="35rem" />
|
||||
</>
|
||||
)}
|
||||
{value && (
|
||||
<StructuredMetadataTable
|
||||
metadata={{
|
||||
name: value.name,
|
||||
'Backstage auth provider': value.authProvider,
|
||||
'OIDC Token Provider': value.oidcTokenProvider ?? 'N/A',
|
||||
'Dashboard Link': value.dashboardUrl ?? 'N/A',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</InfoCard>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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]);
|
||||
};
|
||||
+64
@@ -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 (
|
||||
<>
|
||||
<Grid container>
|
||||
{error && (
|
||||
<Grid item xs={12}>
|
||||
<WarningPanel title="Error loading Kubernetes Cluster Plugin">
|
||||
<Typography>{error}</Typography>
|
||||
</WarningPanel>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={6}>
|
||||
<ClusterOverview />
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<ApiResources />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Nodes />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const KubernetesClusterContent = () => {
|
||||
return (
|
||||
<KubernetesClusterErrorProvider>
|
||||
<ContentGrid />
|
||||
</KubernetesClusterErrorProvider>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
+50
@@ -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<ErrorContext>({
|
||||
setError: (_: string) => {},
|
||||
});
|
||||
|
||||
export interface KubernetesClusterErrorProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const KubernetesClusterErrorProvider = ({
|
||||
children,
|
||||
}: KubernetesClusterErrorProviderProps) => {
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
|
||||
const contextValue: ErrorContext = {
|
||||
error,
|
||||
setError: useCallback((message: string) => setError(message), []),
|
||||
};
|
||||
|
||||
return (
|
||||
<KubernetesClusterErrorContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</KubernetesClusterErrorContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useKubernetesClusterError = () => {
|
||||
return useContext(KubernetesClusterErrorContext);
|
||||
};
|
||||
@@ -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(<Nodes />);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -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<INode>[] = [
|
||||
{
|
||||
title: 'Name',
|
||||
highlight: true,
|
||||
width: 'auto',
|
||||
render: (node: INode) => {
|
||||
return (
|
||||
<KubernetesDrawer
|
||||
kubernetesObject={node}
|
||||
label={node.metadata?.name ?? 'unknown-node'}
|
||||
>
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h5">Node Info</Typography>
|
||||
<StructuredMetadataTable metadata={node.status?.nodeInfo ?? {}} />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h5">Addresses</Typography>
|
||||
<StructuredMetadataTable
|
||||
metadata={
|
||||
node.status?.addresses?.reduce((accum, next) => {
|
||||
accum[next.type] = next.address;
|
||||
return accum;
|
||||
}, {} as any) ?? {}
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h5">Taints</Typography>
|
||||
<StructuredMetadataTable
|
||||
metadata={
|
||||
node.spec?.taints?.reduce((accum, next) => {
|
||||
accum[`${next.effect}`] = `${next.key} (${next.value})`;
|
||||
return accum;
|
||||
}, {} as any) ?? {}
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</KubernetesDrawer>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<Table
|
||||
title="Nodes"
|
||||
options={{ paging: true, search: false, emptyRowsWhenPaging: false }}
|
||||
isLoading={!value && loading}
|
||||
data={value?.items ?? []}
|
||||
emptyContent={
|
||||
<div className={classes.empty}>
|
||||
{error !== undefined ? 'Error loading nodes' : 'No nodes found'}
|
||||
</div>
|
||||
}
|
||||
columns={defaultColumns}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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<NodeList>;
|
||||
});
|
||||
}, [clusterName]);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<ObjectsByEntityResponse>;
|
||||
|
||||
@@ -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<any> {
|
||||
return {
|
||||
kind: 'Namespace',
|
||||
|
||||
Reference in New Issue
Block a user