refactor: Refactors the Kubernetes plugin in line with ADR11 (#19446)
* refactor: k8s plugins Signed-off-by: Matthew Clarke <mclarke@spotify.com> * chore: move more files Signed-off-by: Matthew Clarke <mclarke@spotify.com> * chore: fix Signed-off-by: Matthew Clarke <mclarke@spotify.com> * fix: tsc Signed-off-by: Matthew Clarke <mclarke@spotify.com> * chore: changeset Signed-off-by: Matthew Clarke <mclarke@spotify.com> * chore: api report Signed-off-by: Matthew Clarke <mclarke@spotify.com> * fix: catalog Signed-off-by: Matthew Clarke <mclarke@spotify.com> * fix: types Signed-off-by: Matthew Clarke <mclarke@spotify.com> * fix: update lock Signed-off-by: Matthew Clarke <mclarke@spotify.com> --------- Signed-off-by: Matthew Clarke <mclarke@spotify.com>
This commit is contained in:
+10
-4
@@ -18,13 +18,19 @@ import React from 'react';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { KubernetesContent } from './KubernetesContent';
|
||||
import { useKubernetesObjects } from '../hooks';
|
||||
import { useKubernetesObjects } from '@backstage/plugin-kubernetes-react';
|
||||
import * as oneDeployment from './__fixtures__/1-deployments.json';
|
||||
import * as twoDeployments from './__fixtures__/2-deployments.json';
|
||||
|
||||
jest.mock('../hooks');
|
||||
import * as oneDeployment from '../__fixtures__/1-deployments.json';
|
||||
import * as twoDeployments from '../__fixtures__/2-deployments.json';
|
||||
jest.mock('@backstage/plugin-kubernetes-react', () => ({
|
||||
...jest.requireActual('@backstage/plugin-kubernetes-react'),
|
||||
useKubernetesObjects: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('KubernetesContent', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('render empty response', async () => {
|
||||
(useKubernetesObjects as any).mockReturnValue({
|
||||
kubernetesObjects: {
|
||||
+22
-19
@@ -17,14 +17,23 @@
|
||||
import React from 'react';
|
||||
import { Grid, Typography } from '@material-ui/core';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { ErrorPanel } from './ErrorPanel';
|
||||
import { ErrorReporting } from './ErrorReporting';
|
||||
import { DetectedError, detectErrors } from '../error-detection';
|
||||
import { Cluster } from './Cluster';
|
||||
import EmptyStateImage from '../assets/emptystate.svg';
|
||||
import { useKubernetesObjects } from '../hooks';
|
||||
import { Content, Page, Progress } from '@backstage/core-components';
|
||||
import { DetectedErrorsContext } from '../hooks/useMatchingErrors';
|
||||
import {
|
||||
ErrorPanel,
|
||||
ErrorReporting,
|
||||
Cluster,
|
||||
useKubernetesObjects,
|
||||
DetectedErrorsContext,
|
||||
} from '@backstage/plugin-kubernetes-react';
|
||||
import {
|
||||
DetectedError,
|
||||
detectErrors,
|
||||
} from '@backstage/plugin-kubernetes-common';
|
||||
import {
|
||||
Content,
|
||||
EmptyState,
|
||||
Page,
|
||||
Progress,
|
||||
} from '@backstage/core-components';
|
||||
|
||||
type KubernetesContentProps = {
|
||||
entity: Entity;
|
||||
@@ -98,17 +107,11 @@ export const KubernetesContent = ({
|
||||
alignItems="center"
|
||||
spacing={2}
|
||||
>
|
||||
<Grid item xs={4}>
|
||||
<Typography variant="h5">
|
||||
No resources on any known clusters for{' '}
|
||||
{entity.metadata.name}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<img
|
||||
src={EmptyStateImage}
|
||||
alt="EmptyState"
|
||||
data-testid="emptyStateImg"
|
||||
<Grid item xs={8}>
|
||||
<EmptyState
|
||||
missing="data"
|
||||
title="No Kubernetes resources"
|
||||
description={`No resources on any known clusters for ${entity.metadata.name}`}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
@@ -18,7 +18,7 @@ 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 { KubernetesContent } from './components/KubernetesContent';
|
||||
import { KubernetesContent } from './KubernetesContent';
|
||||
import { Button } from '@material-ui/core';
|
||||
import { MissingAnnotationEmptyState } from '@backstage/core-components';
|
||||
|
||||
|
||||
@@ -1,563 +0,0 @@
|
||||
/*
|
||||
* 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 { KubernetesAuthProvidersApi } from '../kubernetes-auth-provider';
|
||||
import { KubernetesBackendClient } from './KubernetesBackendClient';
|
||||
import { rest } from 'msw';
|
||||
import { UrlPatternDiscovery } from '@backstage/core-app-api';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { setupRequestMockHandlers } from '@backstage/test-utils';
|
||||
import {
|
||||
CustomObjectsByEntityRequest,
|
||||
KubernetesRequestBody,
|
||||
ObjectsByEntityResponse,
|
||||
WorkloadsByEntityRequest,
|
||||
} from '@backstage/plugin-kubernetes-common';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
|
||||
describe('KubernetesBackendClient', () => {
|
||||
let backendClient: KubernetesBackendClient;
|
||||
const kubernetesAuthProvidersApi: jest.Mocked<KubernetesAuthProvidersApi> = {
|
||||
decorateRequestBodyForAuth: jest.fn(),
|
||||
getCredentials: jest.fn(),
|
||||
};
|
||||
let mockResponse: ObjectsByEntityResponse;
|
||||
const worker = setupServer();
|
||||
setupRequestMockHandlers(worker);
|
||||
|
||||
const identityApi = {
|
||||
getCredentials: jest.fn(),
|
||||
getProfileInfo: jest.fn(),
|
||||
getBackstageIdentity: jest.fn(),
|
||||
signOut: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
backendClient = new KubernetesBackendClient({
|
||||
discoveryApi: UrlPatternDiscovery.compile(
|
||||
'http://localhost:1234/api/{{ pluginId }}',
|
||||
),
|
||||
identityApi,
|
||||
kubernetesAuthProvidersApi,
|
||||
});
|
||||
mockResponse = {
|
||||
items: [
|
||||
{
|
||||
cluster: {
|
||||
name: 'cluster-a',
|
||||
},
|
||||
resources: [{ type: 'pods', resources: [] }],
|
||||
podMetrics: [
|
||||
{
|
||||
pod: {},
|
||||
cpu: { currentUsage: 8, requestTotal: 2, limitTotal: 1 },
|
||||
memory: { currentUsage: 8, requestTotal: 2, limitTotal: 1 },
|
||||
containers: [
|
||||
{
|
||||
container: 'test',
|
||||
cpuUsage: { currentUsage: 8, requestTotal: 2, limitTotal: 1 },
|
||||
memoryUsage: {
|
||||
currentUsage: 8,
|
||||
requestTotal: 2,
|
||||
limitTotal: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
errors: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
it('hits the /clusters API', async () => {
|
||||
identityApi.getCredentials.mockResolvedValue({ token: 'idToken' });
|
||||
worker.use(
|
||||
rest.get('http://localhost:1234/api/kubernetes/clusters', (_, res, ctx) =>
|
||||
res(ctx.json({ items: [{ name: 'cluster-a', authProvider: 'aws' }] })),
|
||||
),
|
||||
);
|
||||
|
||||
const clusters = await backendClient.getClusters();
|
||||
|
||||
expect(clusters).toStrictEqual([
|
||||
{ name: 'cluster-a', authProvider: 'aws' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('/clusters API throws a 404 Error', async () => {
|
||||
identityApi.getCredentials.mockResolvedValue({ token: 'idToken' });
|
||||
worker.use(
|
||||
rest.get('http://localhost:1234/api/kubernetes/clusters', (_, res, ctx) =>
|
||||
res(ctx.status(404)),
|
||||
),
|
||||
);
|
||||
|
||||
await expect(backendClient.getClusters()).rejects.toThrow(
|
||||
'Could not find the Kubernetes Backend (HTTP 404). Make sure the plugin has been fully installed.',
|
||||
);
|
||||
});
|
||||
|
||||
it('/clusters API throws a 500 Error', async () => {
|
||||
identityApi.getCredentials.mockResolvedValue({ token: 'idToken' });
|
||||
worker.use(
|
||||
rest.get('http://localhost:1234/api/kubernetes/clusters', (_, res, ctx) =>
|
||||
res(ctx.status(500)),
|
||||
),
|
||||
);
|
||||
|
||||
await expect(backendClient.getClusters()).rejects.toThrow(
|
||||
'Request failed with 500 Internal Server Error, ',
|
||||
);
|
||||
});
|
||||
|
||||
it('hits the /resources/custom/query API', async () => {
|
||||
identityApi.getCredentials.mockResolvedValue({ token: 'idToken' });
|
||||
worker.use(
|
||||
rest.post(
|
||||
'http://localhost:1234/api/kubernetes/resources/custom/query',
|
||||
(_, res, ctx) => res(ctx.json(mockResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
const request: CustomObjectsByEntityRequest = {
|
||||
auth: {},
|
||||
customResources: [
|
||||
{
|
||||
group: 'test-group',
|
||||
apiVersion: 'v1',
|
||||
plural: 'none',
|
||||
},
|
||||
],
|
||||
entity: {
|
||||
apiVersion: 'v1',
|
||||
kind: 'pod',
|
||||
metadata: {
|
||||
name: 'test-name',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const customObject: ObjectsByEntityResponse =
|
||||
await backendClient.getCustomObjectsByEntity(request);
|
||||
|
||||
expect(customObject).toStrictEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('/resources/custom/query API throws a 404 error', async () => {
|
||||
identityApi.getCredentials.mockResolvedValue({ token: 'idToken' });
|
||||
worker.use(
|
||||
rest.post(
|
||||
'http://localhost:1234/api/kubernetes/resources/custom/query',
|
||||
(_, res, ctx) => res(ctx.status(404)),
|
||||
),
|
||||
);
|
||||
|
||||
const request: CustomObjectsByEntityRequest = {
|
||||
auth: {},
|
||||
customResources: [
|
||||
{
|
||||
group: 'test-group',
|
||||
apiVersion: 'v1',
|
||||
plural: 'none',
|
||||
},
|
||||
],
|
||||
entity: {
|
||||
apiVersion: 'v1',
|
||||
kind: 'pod',
|
||||
metadata: {
|
||||
name: 'test-name',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = backendClient.getCustomObjectsByEntity(request);
|
||||
|
||||
await expect(response).rejects.toThrow(
|
||||
'Could not find the Kubernetes Backend (HTTP 404). Make sure the plugin has been fully installed.',
|
||||
);
|
||||
});
|
||||
|
||||
it('/resources/custom/query API throws a 500 error', async () => {
|
||||
identityApi.getCredentials.mockResolvedValue({ token: 'idToken' });
|
||||
worker.use(
|
||||
rest.post(
|
||||
'http://localhost:1234/api/kubernetes/resources/custom/query',
|
||||
(_, res, ctx) => res(ctx.status(500)),
|
||||
),
|
||||
);
|
||||
|
||||
const request: CustomObjectsByEntityRequest = {
|
||||
auth: {},
|
||||
customResources: [
|
||||
{
|
||||
group: 'test-group',
|
||||
apiVersion: 'v1',
|
||||
plural: 'none',
|
||||
},
|
||||
],
|
||||
entity: {
|
||||
apiVersion: 'v1',
|
||||
kind: 'pod',
|
||||
metadata: {
|
||||
name: 'test-name',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = backendClient.getCustomObjectsByEntity(request);
|
||||
|
||||
await expect(response).rejects.toThrow(
|
||||
'Request failed with 500 Internal Server Error, ',
|
||||
);
|
||||
});
|
||||
|
||||
it('hits the /services/{entityName} API', async () => {
|
||||
identityApi.getCredentials.mockResolvedValue({ token: 'idToken' });
|
||||
worker.use(
|
||||
rest.post(
|
||||
'http://localhost:1234/api/kubernetes/services/test-name',
|
||||
(_, res, ctx) => res(ctx.json(mockResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
const request: KubernetesRequestBody = {
|
||||
entity: {
|
||||
apiVersion: 'v1',
|
||||
kind: 'pod',
|
||||
metadata: {
|
||||
name: 'test-name',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const entityObject: ObjectsByEntityResponse =
|
||||
await backendClient.getObjectsByEntity(request);
|
||||
|
||||
expect(entityObject).toStrictEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('services/{entityName} API throws a 404 error', async () => {
|
||||
identityApi.getCredentials.mockResolvedValue({ token: 'idToken' });
|
||||
worker.use(
|
||||
rest.post(
|
||||
'http://localhost:1234/api/kubernetes/services/test-name',
|
||||
(_, res, ctx) => res(ctx.status(404)),
|
||||
),
|
||||
);
|
||||
|
||||
const request: KubernetesRequestBody = {
|
||||
entity: {
|
||||
apiVersion: 'v1',
|
||||
kind: 'pod',
|
||||
metadata: {
|
||||
name: 'test-name',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = backendClient.getObjectsByEntity(request);
|
||||
|
||||
await expect(response).rejects.toThrow(
|
||||
'Could not find the Kubernetes Backend (HTTP 404). Make sure the plugin has been fully installed.',
|
||||
);
|
||||
});
|
||||
|
||||
it('services/{entityName} API throws a 500 error', async () => {
|
||||
identityApi.getCredentials.mockResolvedValue({ token: 'idToken' });
|
||||
worker.use(
|
||||
rest.post(
|
||||
'http://localhost:1234/api/kubernetes/services/test-name',
|
||||
(_, res, ctx) => res(ctx.status(500)),
|
||||
),
|
||||
);
|
||||
|
||||
const request: KubernetesRequestBody = {
|
||||
entity: {
|
||||
apiVersion: 'v1',
|
||||
kind: 'pod',
|
||||
metadata: {
|
||||
name: 'test-name',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = backendClient.getObjectsByEntity(request);
|
||||
|
||||
await expect(response).rejects.toThrow(
|
||||
'Request failed with 500 Internal Server Error, ',
|
||||
);
|
||||
});
|
||||
|
||||
it('hits the /resources/workloads/query API', async () => {
|
||||
identityApi.getCredentials.mockResolvedValue({ token: 'idToken' });
|
||||
worker.use(
|
||||
rest.post(
|
||||
'http://localhost:1234/api/kubernetes/resources/workloads/query',
|
||||
(_, res, ctx) => res(ctx.json(mockResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
const request: WorkloadsByEntityRequest = {
|
||||
auth: {},
|
||||
entity: {
|
||||
apiVersion: 'v1',
|
||||
kind: 'pod',
|
||||
metadata: {
|
||||
name: 'test-name',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response: ObjectsByEntityResponse =
|
||||
await backendClient.getWorkloadsByEntity(request);
|
||||
|
||||
expect(response).toStrictEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('/resources/workloads/query API throws a 404 error', async () => {
|
||||
identityApi.getCredentials.mockResolvedValue({ token: 'idToken' });
|
||||
worker.use(
|
||||
rest.post(
|
||||
'http://localhost:1234/api/kubernetes/resources/workloads/query',
|
||||
(_, res, ctx) => res(ctx.status(404)),
|
||||
),
|
||||
);
|
||||
|
||||
const request: WorkloadsByEntityRequest = {
|
||||
auth: {},
|
||||
entity: {
|
||||
apiVersion: 'v1',
|
||||
kind: 'pod',
|
||||
metadata: {
|
||||
name: 'test-name',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = backendClient.getWorkloadsByEntity(request);
|
||||
|
||||
await expect(response).rejects.toThrow(
|
||||
'Could not find the Kubernetes Backend (HTTP 404). Make sure the plugin has been fully installed.',
|
||||
);
|
||||
});
|
||||
|
||||
it('/resources/workloads/query API throws a 500 error', async () => {
|
||||
identityApi.getCredentials.mockResolvedValue({ token: 'idToken' });
|
||||
worker.use(
|
||||
rest.post(
|
||||
'http://localhost:1234/api/kubernetes/resources/workloads/query',
|
||||
(_, res, ctx) => res(ctx.status(500)),
|
||||
),
|
||||
);
|
||||
|
||||
const request: WorkloadsByEntityRequest = {
|
||||
auth: {},
|
||||
entity: {
|
||||
apiVersion: 'v1',
|
||||
kind: 'pod',
|
||||
metadata: {
|
||||
name: 'test-name',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = backendClient.getWorkloadsByEntity(request);
|
||||
|
||||
await expect(response).rejects.toThrow(
|
||||
'Request failed with 500 Internal Server Error, ',
|
||||
);
|
||||
});
|
||||
|
||||
describe('proxy', () => {
|
||||
beforeEach(() => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'http://localhost:1234/api/kubernetes/clusters',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.json({ items: [{ name: 'cluster-a', authProvider: 'aws' }] }),
|
||||
),
|
||||
),
|
||||
);
|
||||
identityApi.getCredentials.mockResolvedValue({ token: 'idToken' });
|
||||
});
|
||||
|
||||
it('hits the /proxy API', async () => {
|
||||
kubernetesAuthProvidersApi.getCredentials.mockResolvedValue({
|
||||
token: 'k8-token',
|
||||
});
|
||||
const nsResponse = {
|
||||
kind: 'Namespace',
|
||||
apiVersion: 'v1',
|
||||
metadata: {
|
||||
name: 'new-ns',
|
||||
},
|
||||
};
|
||||
worker.use(
|
||||
rest.get(
|
||||
'http://localhost:1234/api/kubernetes/proxy/api/v1/namespaces',
|
||||
(req, res, ctx) =>
|
||||
res(
|
||||
req.headers.get('Backstage-Kubernetes-Authorization') ===
|
||||
'Bearer k8-token'
|
||||
? ctx.json(nsResponse)
|
||||
: ctx.status(403),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const request = {
|
||||
clusterName: 'cluster-a',
|
||||
path: '/api/v1/namespaces',
|
||||
};
|
||||
|
||||
const response = await backendClient.proxy(request);
|
||||
|
||||
await expect(response.json()).resolves.toStrictEqual(nsResponse);
|
||||
});
|
||||
|
||||
it('hits /proxy api when signed in as a guest', async () => {
|
||||
// when a user is signed in as a guest the result of the getCredentials() method resolves to the {} value.
|
||||
identityApi.getCredentials.mockResolvedValue({});
|
||||
kubernetesAuthProvidersApi.getCredentials.mockResolvedValue({
|
||||
token: 'k8-token',
|
||||
});
|
||||
const nsResponse = {
|
||||
kind: 'Namespace',
|
||||
apiVersion: 'v1',
|
||||
metadata: {
|
||||
name: 'new-ns',
|
||||
},
|
||||
};
|
||||
worker.use(
|
||||
rest.get(
|
||||
'http://localhost:1234/api/kubernetes/proxy/api/v1/namespaces',
|
||||
(req, res, ctx) =>
|
||||
res(
|
||||
req.headers.get('Backstage-Kubernetes-Authorization') ===
|
||||
'Bearer k8-token'
|
||||
? ctx.json(nsResponse)
|
||||
: ctx.status(403),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const request = {
|
||||
clusterName: 'cluster-a',
|
||||
path: '/api/v1/namespaces',
|
||||
};
|
||||
|
||||
const response = await backendClient.proxy(request);
|
||||
await expect(response.json()).resolves.toStrictEqual(nsResponse);
|
||||
});
|
||||
|
||||
it('/proxy API throws a 404 error', async () => {
|
||||
kubernetesAuthProvidersApi.getCredentials.mockResolvedValue({
|
||||
token: 'k8-token',
|
||||
});
|
||||
worker.use(
|
||||
rest.get(
|
||||
'http://localhost:1234/api/kubernetes/proxy/api/v1/namespaces',
|
||||
(_, res, ctx) => res(ctx.status(404)),
|
||||
),
|
||||
);
|
||||
|
||||
const request = {
|
||||
clusterName: 'cluster-a',
|
||||
path: '/api/v1/namespaces',
|
||||
};
|
||||
|
||||
const response = await backendClient.proxy(request);
|
||||
|
||||
expect(response.status).toEqual(404);
|
||||
});
|
||||
|
||||
it('throws a ERROR_NOT_FOUND if the cluster in the request is not found', async () => {
|
||||
const request = {
|
||||
clusterName: 'cluster-b',
|
||||
path: '/api/v1/namespaces',
|
||||
};
|
||||
|
||||
await expect(backendClient.proxy(request)).rejects.toThrow(NotFoundError);
|
||||
});
|
||||
|
||||
it('responds with an 403 error when invalid k8 token is provided', async () => {
|
||||
kubernetesAuthProvidersApi.getCredentials.mockResolvedValue({
|
||||
token: 'wrong-token',
|
||||
});
|
||||
|
||||
const nsResponse = {
|
||||
kind: 'Namespace',
|
||||
apiVersion: 'v1',
|
||||
metadata: {
|
||||
name: 'new-ns',
|
||||
},
|
||||
};
|
||||
worker.use(
|
||||
rest.get(
|
||||
'http://localhost:1234/api/kubernetes/proxy/api/v1/namespaces',
|
||||
(req, res, ctx) =>
|
||||
res(
|
||||
req.headers.get('Backstage-Kubernetes-Authorization') ===
|
||||
'Bearer k8-token'
|
||||
? ctx.json(nsResponse)
|
||||
: ctx.status(403),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const request = {
|
||||
clusterName: 'cluster-a',
|
||||
path: '/api/v1/namespaces',
|
||||
};
|
||||
|
||||
const response = await backendClient.proxy(request);
|
||||
expect(response.status).toEqual(403);
|
||||
});
|
||||
|
||||
it('skips authorization header when auth provider returns no creds', async () => {
|
||||
const nsResponse = {
|
||||
kind: 'Namespace',
|
||||
apiVersion: 'v1',
|
||||
metadata: {
|
||||
name: 'new-ns',
|
||||
},
|
||||
};
|
||||
worker.use(
|
||||
rest.get(
|
||||
'http://localhost:1234/api/kubernetes/proxy/api/v1/namespaces/new-ns',
|
||||
(req, res, ctx) =>
|
||||
res(
|
||||
req.headers.get('Backstage-Kubernetes-Authorization')
|
||||
? ctx.status(403)
|
||||
: ctx.json(nsResponse),
|
||||
),
|
||||
),
|
||||
);
|
||||
kubernetesAuthProvidersApi.getCredentials.mockResolvedValue({});
|
||||
|
||||
const response = await backendClient.proxy({
|
||||
clusterName: 'cluster-a',
|
||||
path: '/api/v1/namespaces/new-ns',
|
||||
});
|
||||
|
||||
await expect(response.json()).resolves.toStrictEqual(nsResponse);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,161 +0,0 @@
|
||||
/*
|
||||
* 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 { KubernetesApi } from './types';
|
||||
import {
|
||||
KubernetesRequestBody,
|
||||
ObjectsByEntityResponse,
|
||||
WorkloadsByEntityRequest,
|
||||
CustomObjectsByEntityRequest,
|
||||
} from '@backstage/plugin-kubernetes-common';
|
||||
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
|
||||
import { stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { KubernetesAuthProvidersApi } from '../kubernetes-auth-provider';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
|
||||
export class KubernetesBackendClient implements KubernetesApi {
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
private readonly identityApi: IdentityApi;
|
||||
private readonly kubernetesAuthProvidersApi: KubernetesAuthProvidersApi;
|
||||
|
||||
constructor(options: {
|
||||
discoveryApi: DiscoveryApi;
|
||||
identityApi: IdentityApi;
|
||||
kubernetesAuthProvidersApi: KubernetesAuthProvidersApi;
|
||||
}) {
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
this.identityApi = options.identityApi;
|
||||
this.kubernetesAuthProvidersApi = options.kubernetesAuthProvidersApi;
|
||||
}
|
||||
|
||||
private async handleResponse(response: Response): Promise<any> {
|
||||
if (!response.ok) {
|
||||
const payload = await response.text();
|
||||
let message;
|
||||
switch (response.status) {
|
||||
case 404:
|
||||
message =
|
||||
'Could not find the Kubernetes Backend (HTTP 404). Make sure the plugin has been fully installed.';
|
||||
break;
|
||||
default:
|
||||
message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
private async postRequired(path: string, requestBody: any): Promise<any> {
|
||||
const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}${path}`;
|
||||
const { token: idToken } = await this.identityApi.getCredentials();
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(idToken && { Authorization: `Bearer ${idToken}` }),
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
return this.handleResponse(response);
|
||||
}
|
||||
|
||||
private async getCluster(
|
||||
clusterName: string,
|
||||
): Promise<{ name: string; authProvider: string }> {
|
||||
const cluster = await this.getClusters().then(clusters =>
|
||||
clusters.find(c => c.name === clusterName),
|
||||
);
|
||||
if (!cluster) {
|
||||
throw new NotFoundError(`Cluster ${clusterName} not found`);
|
||||
}
|
||||
|
||||
return cluster;
|
||||
}
|
||||
|
||||
private async getCredentials(
|
||||
authProvider: string,
|
||||
): Promise<{ token?: string }> {
|
||||
return await this.kubernetesAuthProvidersApi.getCredentials(authProvider);
|
||||
}
|
||||
|
||||
async getObjectsByEntity(
|
||||
requestBody: KubernetesRequestBody,
|
||||
): Promise<ObjectsByEntityResponse> {
|
||||
return await this.postRequired(
|
||||
`/services/${requestBody.entity.metadata.name}`,
|
||||
requestBody,
|
||||
);
|
||||
}
|
||||
|
||||
async getWorkloadsByEntity(
|
||||
request: WorkloadsByEntityRequest,
|
||||
): Promise<ObjectsByEntityResponse> {
|
||||
return await this.postRequired('/resources/workloads/query', {
|
||||
auth: request.auth,
|
||||
entityRef: stringifyEntityRef(request.entity),
|
||||
});
|
||||
}
|
||||
|
||||
async getCustomObjectsByEntity(
|
||||
request: CustomObjectsByEntityRequest,
|
||||
): Promise<ObjectsByEntityResponse> {
|
||||
return await this.postRequired(`/resources/custom/query`, {
|
||||
entityRef: stringifyEntityRef(request.entity),
|
||||
auth: request.auth,
|
||||
customResources: request.customResources,
|
||||
});
|
||||
}
|
||||
|
||||
async getClusters(): Promise<{ name: string; authProvider: string }[]> {
|
||||
const { token: idToken } = await this.identityApi.getCredentials();
|
||||
const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}/clusters`;
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
...(idToken && { Authorization: `Bearer ${idToken}` }),
|
||||
},
|
||||
});
|
||||
|
||||
return (await this.handleResponse(response)).items;
|
||||
}
|
||||
|
||||
async proxy(options: {
|
||||
clusterName: string;
|
||||
path: string;
|
||||
init?: RequestInit;
|
||||
}): Promise<Response> {
|
||||
const { authProvider } = await this.getCluster(options.clusterName);
|
||||
const { token: k8sToken } = await this.getCredentials(authProvider);
|
||||
const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}/proxy${
|
||||
options.path
|
||||
}`;
|
||||
const identityResponse = await this.identityApi.getCredentials();
|
||||
const headers = {
|
||||
...options.init?.headers,
|
||||
[`Backstage-Kubernetes-Cluster`]: options.clusterName,
|
||||
...(k8sToken && {
|
||||
[`Backstage-Kubernetes-Authorization`]: `Bearer ${k8sToken}`,
|
||||
}),
|
||||
...(identityResponse.token && {
|
||||
Authorization: `Bearer ${identityResponse.token}`,
|
||||
}),
|
||||
};
|
||||
|
||||
return await fetch(url, { ...options.init, headers });
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
/*
|
||||
* 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 { DateTime } from 'luxon';
|
||||
import { KubernetesProxyClient } from './KubernetesProxyClient';
|
||||
|
||||
describe('KubernetesProxyClient', () => {
|
||||
let proxy: KubernetesProxyClient;
|
||||
const callProxyMock = jest.fn();
|
||||
const oneHourAgo = DateTime.now().minus({ hours: 1 }).toISO();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
proxy = new KubernetesProxyClient({
|
||||
kubernetesApi: {
|
||||
proxy: callProxyMock,
|
||||
} as any,
|
||||
});
|
||||
});
|
||||
it('/logs returns log text', async () => {
|
||||
const request = {
|
||||
podName: 'some-pod',
|
||||
namespace: 'some-namespace',
|
||||
clusterName: 'some-cluster',
|
||||
containerName: 'some-container',
|
||||
};
|
||||
|
||||
callProxyMock.mockResolvedValue({
|
||||
text: jest.fn().mockResolvedValue('Hello World'),
|
||||
ok: true,
|
||||
});
|
||||
|
||||
const response = await proxy.getPodLogs(request);
|
||||
await expect(response).toStrictEqual({ text: 'Hello World' });
|
||||
expect(callProxyMock).toHaveBeenCalledWith({
|
||||
clusterName: 'some-cluster',
|
||||
init: {
|
||||
method: 'GET',
|
||||
},
|
||||
path: '/api/v1/namespaces/some-namespace/pods/some-pod/log?container=some-container',
|
||||
});
|
||||
});
|
||||
it('/logs returns log text - crash logs', async () => {
|
||||
const request = {
|
||||
podName: 'some-pod',
|
||||
namespace: 'some-namespace',
|
||||
clusterName: 'some-cluster',
|
||||
containerName: 'some-container',
|
||||
previous: true,
|
||||
};
|
||||
|
||||
callProxyMock.mockResolvedValue({
|
||||
text: jest.fn().mockResolvedValue('Hello World'),
|
||||
ok: true,
|
||||
});
|
||||
|
||||
const response = await proxy.getPodLogs(request);
|
||||
await expect(response).toStrictEqual({ text: 'Hello World' });
|
||||
expect(callProxyMock).toHaveBeenCalledWith({
|
||||
clusterName: 'some-cluster',
|
||||
init: {
|
||||
method: 'GET',
|
||||
},
|
||||
path: '/api/v1/namespaces/some-namespace/pods/some-pod/log?container=some-container&previous=',
|
||||
});
|
||||
});
|
||||
it('/getEventsByInvolvedObjectName returns events', async () => {
|
||||
const request = {
|
||||
clusterName: 'some-cluster',
|
||||
involvedObjectName: 'some-object',
|
||||
namespace: 'some-namespace',
|
||||
};
|
||||
const events = [
|
||||
{
|
||||
type: 'Warning',
|
||||
message: 'uh oh',
|
||||
reason: 'something happened',
|
||||
count: 23,
|
||||
metadata: {
|
||||
creationTimestamp: oneHourAgo,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'Info',
|
||||
message: 'hello there',
|
||||
reason: 'something happened',
|
||||
count: 52,
|
||||
metadata: {
|
||||
creationTimestamp: oneHourAgo,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
callProxyMock.mockResolvedValue({
|
||||
json: jest.fn().mockResolvedValue({
|
||||
items: events,
|
||||
}),
|
||||
ok: true,
|
||||
});
|
||||
|
||||
const response = await proxy.getEventsByInvolvedObjectName(request);
|
||||
await expect(response).toStrictEqual(events);
|
||||
expect(callProxyMock).toHaveBeenCalledWith({
|
||||
clusterName: 'some-cluster',
|
||||
init: {
|
||||
method: 'GET',
|
||||
},
|
||||
path: '/api/v1/namespaces/some-namespace/events?fieldSelector=involvedObject.name=some-object',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,113 +0,0 @@
|
||||
/*
|
||||
* 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 { KubernetesApi } from './types';
|
||||
import { Event } from 'kubernetes-models/v1';
|
||||
|
||||
/**
|
||||
* A client for common requests through the proxy endpoint of the kubernetes backend plugin.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class KubernetesProxyClient {
|
||||
private readonly kubernetesApi: KubernetesApi;
|
||||
|
||||
constructor(options: { kubernetesApi: KubernetesApi }) {
|
||||
this.kubernetesApi = options.kubernetesApi;
|
||||
}
|
||||
|
||||
private async handleText(response: Response): Promise<string> {
|
||||
if (!response.ok) {
|
||||
const payload = await response.text();
|
||||
let message;
|
||||
switch (response.status) {
|
||||
default:
|
||||
message = `Proxy request failed with ${response.status} ${response.statusText}, ${payload}`;
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return await response.text();
|
||||
}
|
||||
|
||||
private async handleJson(response: Response): Promise<any> {
|
||||
if (!response.ok) {
|
||||
const payload = await response.text();
|
||||
let message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;
|
||||
switch (response.status) {
|
||||
case 404:
|
||||
message = `Proxy request failed with ${response.status} ${response.statusText}, ${payload}`;
|
||||
break;
|
||||
default:
|
||||
message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async getEventsByInvolvedObjectName({
|
||||
clusterName,
|
||||
involvedObjectName,
|
||||
namespace,
|
||||
}: {
|
||||
clusterName: string;
|
||||
involvedObjectName: string;
|
||||
namespace: string;
|
||||
}): Promise<Event[]> {
|
||||
return await this.kubernetesApi
|
||||
.proxy({
|
||||
clusterName,
|
||||
path: `/api/v1/namespaces/${namespace}/events?fieldSelector=involvedObject.name=${involvedObjectName}`,
|
||||
init: {
|
||||
method: 'GET',
|
||||
},
|
||||
})
|
||||
.then(response => this.handleJson(response))
|
||||
.then(eventList => eventList.items);
|
||||
}
|
||||
|
||||
async getPodLogs({
|
||||
podName,
|
||||
namespace,
|
||||
clusterName,
|
||||
containerName,
|
||||
previous,
|
||||
}: {
|
||||
podName: string;
|
||||
namespace: string;
|
||||
clusterName: string;
|
||||
containerName: string;
|
||||
previous?: boolean;
|
||||
}): Promise<{ text: string }> {
|
||||
const params = new URLSearchParams({
|
||||
container: containerName,
|
||||
});
|
||||
if (previous) {
|
||||
params.append('previous', '');
|
||||
}
|
||||
return await this.kubernetesApi
|
||||
.proxy({
|
||||
clusterName: clusterName,
|
||||
path: `/api/v1/namespaces/${namespace}/pods/${podName}/log?${params.toString()}`,
|
||||
init: {
|
||||
method: 'GET',
|
||||
},
|
||||
})
|
||||
.then(response => this.handleText(response))
|
||||
.then(text => ({ text }));
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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 { kubernetesApiRef, kubernetesProxyApiRef } from './types';
|
||||
export type { KubernetesApi, KubernetesProxyApi } from './types';
|
||||
export { KubernetesBackendClient } from './KubernetesBackendClient';
|
||||
export { KubernetesProxyClient } from './KubernetesProxyClient';
|
||||
@@ -1,71 +0,0 @@
|
||||
/*
|
||||
* 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 {
|
||||
KubernetesRequestBody,
|
||||
ObjectsByEntityResponse,
|
||||
WorkloadsByEntityRequest,
|
||||
CustomObjectsByEntityRequest,
|
||||
} from '@backstage/plugin-kubernetes-common';
|
||||
import { createApiRef } from '@backstage/core-plugin-api';
|
||||
import { Event } from 'kubernetes-models/v1';
|
||||
|
||||
export const kubernetesApiRef = createApiRef<KubernetesApi>({
|
||||
id: 'plugin.kubernetes.service',
|
||||
});
|
||||
|
||||
export const kubernetesProxyApiRef = createApiRef<KubernetesProxyApi>({
|
||||
id: 'plugin.kubernetes.proxy-service',
|
||||
});
|
||||
|
||||
export interface KubernetesApi {
|
||||
getObjectsByEntity(
|
||||
requestBody: KubernetesRequestBody,
|
||||
): Promise<ObjectsByEntityResponse>;
|
||||
getClusters(): Promise<
|
||||
{
|
||||
name: string;
|
||||
authProvider: string;
|
||||
oidcTokenProvider?: string | undefined;
|
||||
}[]
|
||||
>;
|
||||
getWorkloadsByEntity(
|
||||
request: WorkloadsByEntityRequest,
|
||||
): Promise<ObjectsByEntityResponse>;
|
||||
getCustomObjectsByEntity(
|
||||
request: CustomObjectsByEntityRequest,
|
||||
): Promise<ObjectsByEntityResponse>;
|
||||
proxy(options: {
|
||||
clusterName: string;
|
||||
path: string;
|
||||
init?: RequestInit;
|
||||
}): Promise<Response>;
|
||||
}
|
||||
|
||||
export interface KubernetesProxyApi {
|
||||
getPodLogs(request: {
|
||||
podName: string;
|
||||
namespace: string;
|
||||
clusterName: string;
|
||||
containerName: string;
|
||||
previous?: boolean;
|
||||
}): Promise<{ text: string }>;
|
||||
getEventsByInvolvedObjectName(request: {
|
||||
clusterName: string;
|
||||
involvedObjectName: string;
|
||||
namespace: string;
|
||||
}): Promise<Event[]>;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="282" height="173" fill="none" viewBox="0 0 282 173"><path fill="#000" fill-opacity=".05" fill-rule="evenodd" d="M16.4571 45.1637C11.0514 46.1711 7.48574 51.3699 8.49306 56.7756C9.50039 62.1814 14.6992 65.747 20.105 64.7397L27.5528 63.3518C25.4791 65.5835 24.4525 68.7347 25.0535 71.9596C26.0608 77.3653 31.2596 80.931 36.6654 79.9236L89.691 70.0427C89.7016 70.1067 89.7129 70.1708 89.7249 70.2349C90.3258 73.4598 92.4185 76.0298 95.1569 77.3647L91.9031 77.971C86.4974 78.9784 82.9318 84.1772 83.9391 89.583C84.9464 94.9887 90.1452 98.5543 95.551 97.547L250.098 68.7482C255.504 67.7409 259.069 62.5421 258.062 57.1363C257.461 53.9114 255.368 51.3414 252.63 50.0065L257.835 49.0366C263.241 48.0292 266.807 42.8304 265.799 37.4247C264.792 32.0189 259.593 28.4533 254.187 29.4606L161.492 46.7338C161.481 46.6697 161.47 46.6056 161.458 46.5415C160.857 43.3166 158.764 40.7466 156.026 39.4117L165.025 37.7347C170.431 36.7274 173.997 31.5286 172.989 26.1228C171.982 20.7171 166.783 17.1514 161.378 18.1588L16.4571 45.1637ZM24.3031 122.54C23.2958 117.134 26.8614 111.936 32.2672 110.928L190.856 81.3762C196.262 80.3688 201.461 83.9345 202.468 89.3402C203.476 94.746 199.91 99.9448 194.504 100.952L189.963 101.798C190.493 102.057 190.999 102.362 191.474 102.708L246.43 92.4677C251.835 91.4604 257.034 95.026 258.041 100.432C258.642 103.657 257.616 106.808 255.542 109.04L256.649 108.833C262.055 107.826 267.253 111.392 268.261 116.797C269.268 122.203 265.702 127.402 260.297 128.409L95.5591 159.107C90.1534 160.114 84.9545 156.549 83.9472 151.143C82.9399 145.737 86.5055 140.538 91.9113 139.531L103.94 137.29C103.41 137.031 102.904 136.726 102.429 136.38L29.1002 150.044C23.6944 151.051 18.4956 147.486 17.4882 142.08C16.4809 136.674 20.0465 131.475 25.4523 130.468L29.7352 129.67C26.9967 128.335 24.904 125.765 24.3031 122.54Z" clip-rule="evenodd"/><circle cx="188" cy="55" r="6" fill="#69DDC7"/><circle cx="91" cy="92" r="6" fill="#69DDC7"/><path fill="#69DDC7" d="M121 114L95.5 88L86.5 96L121 130L192.5 59L183.5 51L121 114Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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 { screen } from '@testing-library/react';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { Cluster } from './Cluster';
|
||||
|
||||
jest.mock('../../hooks');
|
||||
import * as oneDeployment from '../../__fixtures__/1-deployments.json';
|
||||
|
||||
describe('Cluster', () => {
|
||||
it('render 1 cluster', async () => {
|
||||
await renderInTestApp(
|
||||
<Cluster
|
||||
{...({
|
||||
clusterObjects: {
|
||||
cluster: {
|
||||
name: 'cluster-1',
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
type: 'deployments',
|
||||
resources: oneDeployment.deployments,
|
||||
},
|
||||
{
|
||||
type: 'replicasets',
|
||||
resources: oneDeployment.replicaSets,
|
||||
},
|
||||
{
|
||||
type: 'pods',
|
||||
resources: oneDeployment.pods,
|
||||
},
|
||||
],
|
||||
podMetrics: [],
|
||||
errors: [],
|
||||
},
|
||||
podsWithErrors: new Set<string>(),
|
||||
} as any)}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('cluster-1')).toBeInTheDocument();
|
||||
expect(screen.getByText('10 pods')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,170 +0,0 @@
|
||||
/*
|
||||
* 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 {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Grid,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import {
|
||||
ClientPodStatus,
|
||||
ClusterObjects,
|
||||
} from '@backstage/plugin-kubernetes-common';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import { DeploymentsAccordions } from '../DeploymentsAccordions';
|
||||
import { StatefulSetsAccordions } from '../StatefulSetsAccordions';
|
||||
import { groupResponses } from '../../utils/response';
|
||||
import { IngressesAccordions } from '../IngressesAccordions';
|
||||
import { ServicesAccordions } from '../ServicesAccordions';
|
||||
import { CronJobsAccordions } from '../CronJobsAccordions';
|
||||
import { CustomResources } from '../CustomResources';
|
||||
import {
|
||||
ClusterContext,
|
||||
GroupedResponsesContext,
|
||||
PodNamesWithErrorsContext,
|
||||
} from '../../hooks';
|
||||
|
||||
import { StatusError, StatusOK } from '@backstage/core-components';
|
||||
import { PodMetricsContext } from '../../hooks/usePodMetrics';
|
||||
|
||||
type ClusterSummaryProps = {
|
||||
clusterName: string;
|
||||
totalNumberOfPods: number;
|
||||
numberOfPodsWithErrors: number;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const ClusterSummary = ({
|
||||
clusterName,
|
||||
totalNumberOfPods,
|
||||
numberOfPodsWithErrors,
|
||||
}: ClusterSummaryProps) => {
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="flex-start"
|
||||
spacing={0}
|
||||
>
|
||||
<Grid
|
||||
xs={6}
|
||||
item
|
||||
container
|
||||
direction="column"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
spacing={0}
|
||||
>
|
||||
<Grid item xs>
|
||||
<Typography variant="body1">{clusterName}</Typography>
|
||||
<Typography color="textSecondary" variant="subtitle1">
|
||||
Cluster
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid
|
||||
item
|
||||
container
|
||||
xs={3}
|
||||
direction="column"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-end"
|
||||
spacing={0}
|
||||
>
|
||||
<Grid item>
|
||||
<StatusOK>{totalNumberOfPods} pods</StatusOK>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
{numberOfPodsWithErrors > 0 ? (
|
||||
<StatusError>{numberOfPodsWithErrors} pods with errors</StatusError>
|
||||
) : (
|
||||
<StatusOK>No pods with errors</StatusOK>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
type ClusterProps = {
|
||||
clusterObjects: ClusterObjects;
|
||||
podsWithErrors: Set<string>;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => {
|
||||
const groupedResponses = groupResponses(clusterObjects.resources);
|
||||
|
||||
const podMetricsMap = new Map<string, ClientPodStatus[]>();
|
||||
podMetricsMap.set(clusterObjects.cluster.name, clusterObjects.podMetrics);
|
||||
return (
|
||||
<ClusterContext.Provider value={clusterObjects.cluster}>
|
||||
<GroupedResponsesContext.Provider value={groupedResponses}>
|
||||
<PodMetricsContext.Provider value={podMetricsMap}>
|
||||
<PodNamesWithErrorsContext.Provider value={podsWithErrors}>
|
||||
<Accordion TransitionProps={{ unmountOnExit: true }}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<ClusterSummary
|
||||
clusterName={clusterObjects.cluster.name}
|
||||
totalNumberOfPods={groupedResponses.pods.length}
|
||||
numberOfPodsWithErrors={podsWithErrors.size}
|
||||
/>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Grid container direction="column">
|
||||
{groupedResponses.customResources.length > 0 ? (
|
||||
<Grid item>
|
||||
<CustomResources />
|
||||
</Grid>
|
||||
) : undefined}
|
||||
{groupedResponses.deployments.length > 0 ? (
|
||||
<Grid item>
|
||||
<DeploymentsAccordions />
|
||||
</Grid>
|
||||
) : undefined}
|
||||
{groupedResponses.statefulsets.length > 0 ? (
|
||||
<Grid item>
|
||||
<StatefulSetsAccordions />
|
||||
</Grid>
|
||||
) : undefined}
|
||||
{groupedResponses.ingresses.length > 0 ? (
|
||||
<Grid item>
|
||||
<IngressesAccordions />
|
||||
</Grid>
|
||||
) : undefined}
|
||||
{groupedResponses.services.length > 0 ? (
|
||||
<Grid item>
|
||||
<ServicesAccordions />
|
||||
</Grid>
|
||||
) : undefined}
|
||||
{groupedResponses.cronJobs.length > 0 ? (
|
||||
<Grid item>
|
||||
<CronJobsAccordions />
|
||||
</Grid>
|
||||
) : undefined}
|
||||
</Grid>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</PodNamesWithErrorsContext.Provider>
|
||||
</PodMetricsContext.Provider>
|
||||
</GroupedResponsesContext.Provider>
|
||||
</ClusterContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
export { Cluster } from './Cluster';
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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 { screen } from '@testing-library/react';
|
||||
import { CronJobsAccordions } from './CronJobsAccordions';
|
||||
import * as oneCronJobsFixture from '../../__fixtures__/1-cronjobs.json';
|
||||
import * as twoCronJobsFixture from '../../__fixtures__/2-cronjobs.json';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { kubernetesProviders } from '../../hooks/test-utils';
|
||||
|
||||
describe('CronJobsAccordions', () => {
|
||||
it('should render 1 active cronjobs', async () => {
|
||||
const wrapper = kubernetesProviders(oneCronJobsFixture, new Set<string>());
|
||||
|
||||
await renderInTestApp(wrapper(<CronJobsAccordions />));
|
||||
|
||||
expect(screen.getByText('dice-roller-cronjob')).toBeInTheDocument();
|
||||
expect(screen.getByText('CronJob')).toBeInTheDocument();
|
||||
expect(screen.getByText('namespace: default')).toBeInTheDocument();
|
||||
expect(screen.getByText('Active')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render 1 suspended cronjobs', async () => {
|
||||
const wrapper = kubernetesProviders(twoCronJobsFixture, new Set<string>());
|
||||
|
||||
await renderInTestApp(wrapper(<CronJobsAccordions />));
|
||||
|
||||
expect(screen.getByText('dice-roller-cronjob')).toBeInTheDocument();
|
||||
expect(screen.getByText('CronJob')).toBeInTheDocument();
|
||||
expect(screen.getByText('namespace: default')).toBeInTheDocument();
|
||||
expect(screen.getByText('Suspended')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,126 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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, { useContext } from 'react';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Grid,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import { V1CronJob, V1Job } from '@kubernetes/client-node';
|
||||
import { JobsAccordions } from '../JobsAccordions';
|
||||
import { CronJobDrawer } from './CronJobsDrawer';
|
||||
import { getOwnedResources } from '../../utils/owner';
|
||||
import { GroupedResponsesContext } from '../../hooks';
|
||||
import { StatusError, StatusOK } from '@backstage/core-components';
|
||||
import { humanizeCron } from '../../utils/crons';
|
||||
|
||||
type CronJobsAccordionsProps = {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
type CronJobAccordionProps = {
|
||||
cronJob: V1CronJob;
|
||||
ownedJobs: V1Job[];
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
type CronJobSummaryProps = {
|
||||
cronJob: V1CronJob;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const CronJobSummary = ({ cronJob }: CronJobSummaryProps) => {
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
spacing={0}
|
||||
>
|
||||
<Grid xs={6} item>
|
||||
<CronJobDrawer cronJob={cronJob} />
|
||||
</Grid>
|
||||
<Grid
|
||||
item
|
||||
container
|
||||
xs={6}
|
||||
direction="column"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-end"
|
||||
spacing={0}
|
||||
>
|
||||
<Grid item>
|
||||
{cronJob.spec?.suspend ? (
|
||||
<StatusError>Suspended</StatusError>
|
||||
) : (
|
||||
<StatusOK>Active</StatusOK>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant="body1">
|
||||
Schedule:{' '}
|
||||
{cronJob.spec?.schedule
|
||||
? `${cronJob.spec.schedule} (${humanizeCron(
|
||||
cronJob.spec.schedule,
|
||||
)})`
|
||||
: 'N/A'}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
const CronJobAccordion = ({ cronJob, ownedJobs }: CronJobAccordionProps) => {
|
||||
return (
|
||||
<Accordion TransitionProps={{ unmountOnExit: true }} variant="outlined">
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<CronJobSummary cronJob={cronJob} />
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<JobsAccordions jobs={ownedJobs.reverse()} />
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
export const CronJobsAccordions = ({}: CronJobsAccordionsProps) => {
|
||||
const groupedResponses = useContext(GroupedResponsesContext);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
{groupedResponses.cronJobs.map((cronJob, i) => (
|
||||
<Grid container item key={i} xs>
|
||||
<Grid item xs>
|
||||
<CronJobAccordion
|
||||
ownedJobs={getOwnedResources(cronJob, groupedResponses.jobs)}
|
||||
cronJob={cronJob}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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 * as oneCronJobsFixture from '../../__fixtures__/1-cronjobs.json';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { CronJobDrawer } from './CronJobsDrawer';
|
||||
|
||||
describe('CronJobDrawer', () => {
|
||||
it('should render cronJob drawer', async () => {
|
||||
const { getByText, getAllByText } = await renderInTestApp(
|
||||
<CronJobDrawer
|
||||
cronJob={(oneCronJobsFixture as any).cronJobs[0]}
|
||||
expanded
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getAllByText('dice-roller-cronjob')).toHaveLength(2);
|
||||
expect(getAllByText('CronJob')).toHaveLength(2);
|
||||
expect(getByText('YAML')).toBeInTheDocument();
|
||||
expect(getByText('Schedule')).toBeInTheDocument();
|
||||
expect(getByText('30 5 * * *')).toBeInTheDocument();
|
||||
expect(getByText('Starting Deadline Seconds')).toBeInTheDocument();
|
||||
expect(getByText('Last Schedule Time')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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 { V1CronJob } from '@kubernetes/client-node';
|
||||
import { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer';
|
||||
import { Typography, Grid, Chip } from '@material-ui/core';
|
||||
|
||||
export const CronJobDrawer = ({
|
||||
cronJob,
|
||||
expanded,
|
||||
}: {
|
||||
cronJob: V1CronJob;
|
||||
expanded?: boolean;
|
||||
}) => {
|
||||
const namespace = cronJob.metadata?.namespace;
|
||||
return (
|
||||
<KubernetesStructuredMetadataTableDrawer
|
||||
object={cronJob}
|
||||
expanded={expanded}
|
||||
kind="CronJob"
|
||||
renderObject={(cronJobObj: V1CronJob) => ({
|
||||
schedule: cronJobObj.spec?.schedule ?? '???',
|
||||
startingDeadlineSeconds:
|
||||
cronJobObj.spec?.startingDeadlineSeconds ?? '???',
|
||||
concurrencyPolicy: cronJobObj.spec?.concurrencyPolicy ?? '???',
|
||||
lastScheduleTime: cronJobObj.status?.lastScheduleTime ?? '???',
|
||||
})}
|
||||
>
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
spacing={0}
|
||||
>
|
||||
<Grid item>
|
||||
<Typography variant="body1">
|
||||
{cronJob.metadata?.name ?? 'unknown object'}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography color="textSecondary" variant="subtitle1">
|
||||
CronJob
|
||||
</Typography>
|
||||
</Grid>
|
||||
{namespace && (
|
||||
<Grid item>
|
||||
<Chip size="small" label={`namespace: ${namespace}`} />
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</KubernetesStructuredMetadataTableDrawer>
|
||||
);
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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 { CronJobsAccordions } from './CronJobsAccordions';
|
||||
@@ -1,102 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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 { screen } from '@testing-library/react';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { kubernetesProviders } from '../../../hooks/test-utils';
|
||||
import * as rollout from './__fixtures__/rollout.json';
|
||||
import * as pausedRollout from './__fixtures__/paused-rollout.json';
|
||||
import * as abortedRollout from './__fixtures__/aborted-rollout.json';
|
||||
import * as groupedResources from './__fixtures__/grouped-resources.json';
|
||||
import { RolloutAccordions } from './Rollout';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
|
||||
describe('Rollout', () => {
|
||||
it('should render RolloutAccordion', async () => {
|
||||
const wrapper = kubernetesProviders(groupedResources, new Set([]));
|
||||
|
||||
await renderInTestApp(
|
||||
wrapper(<RolloutAccordions rollouts={[rollout] as any} />),
|
||||
);
|
||||
|
||||
expect(screen.getByText('dice-roller')).toBeInTheDocument();
|
||||
expect(screen.getByText('Rollout')).toBeInTheDocument();
|
||||
expect(screen.getByText('2 pods')).toBeInTheDocument();
|
||||
expect(screen.getByText('No pods with errors')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Paused')).toBeNull();
|
||||
});
|
||||
|
||||
it('should render RolloutAccordion with error', async () => {
|
||||
const wrapper = kubernetesProviders(
|
||||
groupedResources,
|
||||
new Set(['dice-roller-6c8646bfd-2m5hv']),
|
||||
);
|
||||
|
||||
await renderInTestApp(
|
||||
wrapper(<RolloutAccordions rollouts={[rollout] as any} />),
|
||||
);
|
||||
|
||||
expect(screen.getByText('dice-roller')).toBeInTheDocument();
|
||||
expect(screen.getByText('Rollout')).toBeInTheDocument();
|
||||
expect(screen.getByText('2 pods')).toBeInTheDocument();
|
||||
expect(screen.getByText('1 pod with errors')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Paused')).toBeNull();
|
||||
});
|
||||
|
||||
it('should render Paused Rollout with pause text', async () => {
|
||||
const wrapper = kubernetesProviders(groupedResources, new Set([]));
|
||||
|
||||
(pausedRollout.status.pauseConditions[0].startTime as any) =
|
||||
DateTime.local()
|
||||
// millis * secs * mins = 45 mins
|
||||
.minus(Duration.fromMillis(1000 * 60 * 45));
|
||||
|
||||
await renderInTestApp(
|
||||
wrapper(<RolloutAccordions rollouts={[pausedRollout] as any} />),
|
||||
);
|
||||
|
||||
expect(screen.getByText('dice-roller')).toBeInTheDocument();
|
||||
expect(screen.getByText('Rollout')).toBeInTheDocument();
|
||||
expect(screen.getByText('2 pods')).toBeInTheDocument();
|
||||
expect(screen.getByText('No pods with errors')).toBeInTheDocument();
|
||||
expect(screen.getByText('Paused (45 minutes ago)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render aborted Rollout with aborted text', async () => {
|
||||
const wrapper = kubernetesProviders(groupedResources, new Set([]));
|
||||
|
||||
await renderInTestApp(
|
||||
wrapper(
|
||||
<RolloutAccordions
|
||||
defaultExpanded
|
||||
rollouts={[abortedRollout] as any}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(screen.getByText('dice-roller')).toBeInTheDocument();
|
||||
expect(screen.getByText('Rollout')).toBeInTheDocument();
|
||||
expect(screen.getByText('2 pods')).toBeInTheDocument();
|
||||
expect(screen.getByText('No pods with errors')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Paused')).toBeNull();
|
||||
expect(screen.getByText('Rollout status')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('Aborted')).toHaveLength(2);
|
||||
expect(
|
||||
screen.getByText('some metric related failure message'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,289 +0,0 @@
|
||||
/*
|
||||
* 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, { useContext } from 'react';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Grid,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import { V1Pod, V1HorizontalPodAutoscaler } from '@kubernetes/client-node';
|
||||
import { PodsTable } from '../../Pods';
|
||||
import { HorizontalPodAutoscalerDrawer } from '../../HorizontalPodAutoscalers';
|
||||
import { RolloutDrawer } from './RolloutDrawer';
|
||||
import PauseIcon from '@material-ui/icons/Pause';
|
||||
import ErrorOutlineIcon from '@material-ui/icons/ErrorOutline';
|
||||
import { DateTime } from 'luxon';
|
||||
import { StepsProgress } from './StepsProgress';
|
||||
import {
|
||||
PodNamesWithErrorsContext,
|
||||
GroupedResponsesContext,
|
||||
} from '../../../hooks';
|
||||
import {
|
||||
getMatchingHpa,
|
||||
getOwnedPodsThroughReplicaSets,
|
||||
} from '../../../utils/owner';
|
||||
import { StatusError, StatusOK } from '@backstage/core-components';
|
||||
import { READY_COLUMNS, RESOURCE_COLUMNS } from '../../Pods/PodsTable';
|
||||
|
||||
type RolloutAccordionsProps = {
|
||||
rollouts: any[];
|
||||
defaultExpanded?: boolean;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
type RolloutAccordionProps = {
|
||||
rollout: any;
|
||||
ownedPods: V1Pod[];
|
||||
defaultExpanded?: boolean;
|
||||
matchingHpa?: V1HorizontalPodAutoscaler;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
type RolloutSummaryProps = {
|
||||
rollout: any;
|
||||
numberOfCurrentPods: number;
|
||||
numberOfPodsWithErrors: number;
|
||||
hpa?: V1HorizontalPodAutoscaler;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const AbortedTitle = (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<ErrorOutlineIcon />
|
||||
<Typography variant="subtitle1">Aborted</Typography>
|
||||
</div>
|
||||
);
|
||||
|
||||
const findAbortedMessage = (rollout: any): string | undefined =>
|
||||
rollout.status?.conditions?.find(
|
||||
(c: any) =>
|
||||
c.type === 'Progressing' &&
|
||||
c.status === 'False' &&
|
||||
c.reason === 'RolloutAborted',
|
||||
)?.message;
|
||||
|
||||
const RolloutSummary = ({
|
||||
rollout,
|
||||
numberOfCurrentPods,
|
||||
numberOfPodsWithErrors,
|
||||
hpa,
|
||||
}: RolloutSummaryProps) => {
|
||||
const pauseTime: string | undefined = rollout.status?.pauseConditions?.find(
|
||||
(p: any) => p.reason === 'CanaryPauseStep',
|
||||
)?.startTime;
|
||||
const abortedMessage = findAbortedMessage(rollout);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
spacing={0}
|
||||
>
|
||||
<Grid xs={6} item>
|
||||
<RolloutDrawer rollout={rollout} />
|
||||
</Grid>
|
||||
{hpa && (
|
||||
<Grid item xs={3}>
|
||||
<HorizontalPodAutoscalerDrawer hpa={hpa}>
|
||||
<Grid
|
||||
item
|
||||
container
|
||||
direction="column"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
spacing={0}
|
||||
>
|
||||
<Grid item>
|
||||
<Typography variant="subtitle2">
|
||||
min replicas {hpa.spec?.minReplicas ?? '?'} / max replicas{' '}
|
||||
{hpa.spec?.maxReplicas ?? '?'}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant="subtitle2">
|
||||
current CPU usage:{' '}
|
||||
{hpa.status?.currentCPUUtilizationPercentage ?? '?'}%
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant="subtitle2">
|
||||
target CPU usage:{' '}
|
||||
{hpa.spec?.targetCPUUtilizationPercentage ?? '?'}%
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</HorizontalPodAutoscalerDrawer>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid
|
||||
item
|
||||
container
|
||||
xs={3}
|
||||
direction="column"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-end"
|
||||
spacing={0}
|
||||
>
|
||||
<Grid item>
|
||||
<StatusOK>{numberOfCurrentPods} pods</StatusOK>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
{numberOfPodsWithErrors > 0 ? (
|
||||
<StatusError>
|
||||
{numberOfPodsWithErrors} pod
|
||||
{numberOfPodsWithErrors > 1 ? 's' : ''} with errors
|
||||
</StatusError>
|
||||
) : (
|
||||
<StatusOK>No pods with errors</StatusOK>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
{pauseTime && (
|
||||
<Grid item xs={3}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<PauseIcon />
|
||||
<Typography variant="subtitle1">
|
||||
Paused ({DateTime.fromISO(pauseTime).toRelative({ locale: 'en' })}
|
||||
)
|
||||
</Typography>
|
||||
</div>
|
||||
</Grid>
|
||||
)}
|
||||
{abortedMessage && (
|
||||
<Grid item xs={3}>
|
||||
{AbortedTitle}
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
const RolloutAccordion = ({
|
||||
rollout,
|
||||
ownedPods,
|
||||
matchingHpa,
|
||||
defaultExpanded,
|
||||
}: RolloutAccordionProps) => {
|
||||
const podNamesWithErrors = useContext(PodNamesWithErrorsContext);
|
||||
|
||||
const podsWithErrors = ownedPods.filter(p =>
|
||||
podNamesWithErrors.has(p.metadata?.name ?? ''),
|
||||
);
|
||||
|
||||
const currentStepIndex = rollout.status?.currentStepIndex ?? 0;
|
||||
const abortedMessage = findAbortedMessage(rollout);
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
defaultExpanded={defaultExpanded}
|
||||
TransitionProps={{ unmountOnExit: true }}
|
||||
variant="outlined"
|
||||
>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<RolloutSummary
|
||||
rollout={rollout}
|
||||
numberOfCurrentPods={ownedPods.length}
|
||||
numberOfPodsWithErrors={podsWithErrors.length}
|
||||
hpa={matchingHpa}
|
||||
/>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<div style={{ width: '100%' }}>
|
||||
<div>
|
||||
<Typography variant="h6">Rollout status</Typography>
|
||||
</div>
|
||||
<div style={{ margin: '1rem' }}>
|
||||
{abortedMessage && (
|
||||
<>
|
||||
{AbortedTitle}
|
||||
<Typography variant="subtitle2">{abortedMessage}</Typography>
|
||||
</>
|
||||
)}
|
||||
<StepsProgress
|
||||
aborted={abortedMessage !== undefined}
|
||||
steps={rollout.spec?.strategy?.canary?.steps ?? []}
|
||||
currentStepIndex={currentStepIndex}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<PodsTable
|
||||
pods={ownedPods}
|
||||
extraColumns={[READY_COLUMNS, RESOURCE_COLUMNS]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
export const RolloutAccordions = ({
|
||||
rollouts,
|
||||
defaultExpanded = false,
|
||||
}: RolloutAccordionsProps) => {
|
||||
const groupedResponses = useContext(GroupedResponsesContext);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
{rollouts.map((rollout, i) => (
|
||||
<Grid container item key={i} xs>
|
||||
<Grid item xs>
|
||||
<RolloutAccordion
|
||||
defaultExpanded={defaultExpanded}
|
||||
matchingHpa={getMatchingHpa(
|
||||
{
|
||||
name: rollout.metadata?.name,
|
||||
namespace: rollout.metadata?.namespace,
|
||||
kind: 'rollout',
|
||||
},
|
||||
groupedResponses.horizontalPodAutoscalers,
|
||||
)}
|
||||
ownedPods={getOwnedPodsThroughReplicaSets(
|
||||
rollout,
|
||||
groupedResponses.replicaSets,
|
||||
groupedResponses.pods,
|
||||
)}
|
||||
rollout={rollout}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
* 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 { KubernetesStructuredMetadataTableDrawer } from '../../KubernetesDrawer';
|
||||
import { Typography, Grid } from '@material-ui/core';
|
||||
|
||||
export const RolloutDrawer = ({
|
||||
rollout,
|
||||
expanded,
|
||||
}: {
|
||||
rollout: any;
|
||||
expanded?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<KubernetesStructuredMetadataTableDrawer
|
||||
object={rollout}
|
||||
expanded={expanded}
|
||||
kind="Rollout"
|
||||
renderObject={() => ({})}
|
||||
>
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
spacing={0}
|
||||
>
|
||||
<Grid item>
|
||||
<Typography variant="body1">
|
||||
{rollout.metadata?.name ?? 'unknown object'}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography color="textSecondary" variant="subtitle1">
|
||||
Rollout
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</KubernetesStructuredMetadataTableDrawer>
|
||||
);
|
||||
};
|
||||
-127
@@ -1,127 +0,0 @@
|
||||
/*
|
||||
* 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 { screen } from '@testing-library/react';
|
||||
import pauseSteps from './__fixtures__/pause-steps';
|
||||
import setWeightSteps from './__fixtures__/setweight-steps';
|
||||
import analysisSteps from './__fixtures__/analysis-steps';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { StepsProgress } from './StepsProgress';
|
||||
|
||||
describe('StepsProgress', () => {
|
||||
it('should render Pause step text', async () => {
|
||||
await renderInTestApp(
|
||||
<StepsProgress currentStepIndex={0} aborted={false} steps={pauseSteps} />,
|
||||
);
|
||||
|
||||
expect(screen.getByText('pause for 1h')).toBeInTheDocument();
|
||||
expect(screen.getByText('infinite pause')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render SetWeight step text', async () => {
|
||||
await renderInTestApp(
|
||||
<StepsProgress
|
||||
currentStepIndex={0}
|
||||
aborted={false}
|
||||
steps={setWeightSteps}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('setWeight 10%')).toBeInTheDocument();
|
||||
expect(screen.getByText('setWeight 95%')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render Analysis step text', async () => {
|
||||
await renderInTestApp(
|
||||
<StepsProgress
|
||||
currentStepIndex={0}
|
||||
aborted={false}
|
||||
steps={analysisSteps}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getAllByText('analysis templates:')).toHaveLength(2);
|
||||
expect(screen.getByText('always-pass')).toBeInTheDocument();
|
||||
expect(screen.getByText('always-fail')).toBeInTheDocument();
|
||||
expect(screen.getByText('req-rate (cluster scoped)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render 3 different steps', async () => {
|
||||
await renderInTestApp(
|
||||
<StepsProgress
|
||||
currentStepIndex={0}
|
||||
aborted={false}
|
||||
steps={[setWeightSteps[0], pauseSteps[0], analysisSteps[0]]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('setWeight 10%')).toBeInTheDocument();
|
||||
expect(screen.getByText('pause for 1h')).toBeInTheDocument();
|
||||
expect(screen.getByText('analysis templates:')).toBeInTheDocument();
|
||||
expect(screen.getByText('always-pass')).toBeInTheDocument();
|
||||
expect(screen.getByText('Canary promoted')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('current step is highlighted, previous steps are ticked', async () => {
|
||||
await renderInTestApp(
|
||||
<StepsProgress
|
||||
currentStepIndex={1}
|
||||
aborted={false}
|
||||
steps={[setWeightSteps[0], pauseSteps[0], analysisSteps[0]]}
|
||||
/>,
|
||||
);
|
||||
|
||||
// It is ticked, so it's not visible
|
||||
expect(screen.queryByText('1')).toBeNull();
|
||||
// The current step
|
||||
expect(screen.getByText('2')).toBeInTheDocument();
|
||||
// The future step
|
||||
expect(screen.getByText('3')).toBeInTheDocument();
|
||||
// The canary promoted step should always be added at the end
|
||||
expect(screen.getByText('4')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('aborted canary has all steps grey', async () => {
|
||||
await renderInTestApp(
|
||||
<StepsProgress
|
||||
currentStepIndex={2}
|
||||
aborted
|
||||
steps={[setWeightSteps[0], pauseSteps[0], analysisSteps[0]]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('1')).toBeInTheDocument();
|
||||
expect(screen.getByText('2')).toBeInTheDocument();
|
||||
expect(screen.getByText('3')).toBeInTheDocument();
|
||||
expect(screen.getByText('4')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('promoted canary has all steps ticked', async () => {
|
||||
await renderInTestApp(
|
||||
<StepsProgress
|
||||
currentStepIndex={3}
|
||||
aborted={false}
|
||||
steps={[setWeightSteps[0], pauseSteps[0], analysisSteps[0]]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('1')).toBeNull();
|
||||
expect(screen.queryByText('2')).toBeNull();
|
||||
expect(screen.queryByText('3')).toBeNull();
|
||||
expect(screen.queryByText('4')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,97 +0,0 @@
|
||||
/*
|
||||
* 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 { Step, StepLabel, Stepper } from '@material-ui/core';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import {
|
||||
ArgoRolloutCanaryStep,
|
||||
SetWeightStep,
|
||||
PauseStep,
|
||||
AnalysisStep,
|
||||
} from './types';
|
||||
|
||||
interface StepsProgressProps {
|
||||
currentStepIndex: number;
|
||||
aborted: boolean;
|
||||
steps: ArgoRolloutCanaryStep[];
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const isSetWeightStep = (step: ArgoRolloutCanaryStep): step is SetWeightStep =>
|
||||
step.hasOwnProperty('setWeight');
|
||||
|
||||
const isPauseStep = (step: ArgoRolloutCanaryStep): step is PauseStep =>
|
||||
step.hasOwnProperty('pause');
|
||||
|
||||
const isAnalysisStep = (step: ArgoRolloutCanaryStep): step is AnalysisStep =>
|
||||
step.hasOwnProperty('analysis');
|
||||
|
||||
const createLabelForStep = (step: ArgoRolloutCanaryStep): React.ReactNode => {
|
||||
if (isSetWeightStep(step)) {
|
||||
return `setWeight ${step.setWeight}%`;
|
||||
} else if (isPauseStep(step)) {
|
||||
return step.pause.duration === undefined
|
||||
? 'infinite pause'
|
||||
: `pause for ${step.pause.duration}`;
|
||||
} else if (isAnalysisStep(step)) {
|
||||
return (
|
||||
<div>
|
||||
<Typography paragraph>analysis templates:</Typography>
|
||||
{step.analysis.templates.map((t, i) => (
|
||||
<Typography paragraph key={i}>{`${t.templateName}${
|
||||
t.clusterScope ? ' (cluster scoped)' : ''
|
||||
}`}</Typography>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return 'unknown step';
|
||||
};
|
||||
|
||||
export const StepsProgress = ({
|
||||
currentStepIndex,
|
||||
aborted,
|
||||
steps,
|
||||
}: StepsProgressProps) => {
|
||||
// If the activeStep is greater/equal to the number of steps
|
||||
// Then the canary is being promoted
|
||||
// Increase the step index to mark the 'canary promoted' step as done also
|
||||
const activeStepIndex =
|
||||
currentStepIndex >= steps.length ? currentStepIndex + 1 : currentStepIndex;
|
||||
|
||||
/*
|
||||
* When the Rollout is aborted set the active step to -1
|
||||
* otherwise it appears to always be on the first step
|
||||
*/
|
||||
return (
|
||||
<Stepper activeStep={aborted ? -1 : activeStepIndex} alternativeLabel>
|
||||
{steps
|
||||
.map((step, i) => (
|
||||
<Step key={i}>
|
||||
<StepLabel data-testid={`step-${i}`}>
|
||||
{createLabelForStep(step)}
|
||||
</StepLabel>
|
||||
</Step>
|
||||
))
|
||||
.concat(
|
||||
<Step key="-1">
|
||||
<StepLabel data-testid="step--1">Canary promoted</StepLabel>
|
||||
</Step>,
|
||||
)}
|
||||
</Stepper>
|
||||
);
|
||||
};
|
||||
-129
@@ -1,129 +0,0 @@
|
||||
{
|
||||
"apiVersion": "argoproj.io/v1alpha1",
|
||||
"kind": "Rollout",
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"argoproj.io/v1alpha1\",\"kind\":\"Rollout\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\"},\"name\":\"dice-roller\",\"namespace\":\"default\"},\"spec\":{\"minReadySeconds\":30,\"replicas\":4,\"selector\":{\"matchLabels\":{\"app\":\"dice-roller\",\"backstage.io/kubernetes-id\":\"dice-roller\"}},\"strategy\":{\"canary\":{\"maxSurge\":\"25%\",\"maxUnavailable\":0,\"steps\":[{\"setWeight\":10},{\"analysis\":{\"templates\":[{\"templateName\":\"always-pass\"}]}},{\"pause\":{\"duration\":\"1m\"}},{\"setWeight\":20},{\"pause\":{\"duration\":\"1m\"}},{\"analysis\":{\"templates\":[{\"templateName\":\"always-pass\"}]}}]}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"dice-roller\",\"backstage.io/kubernetes-id\":\"dice-roller\",\"start\":\"1234\"}},\"spec\":{\"containers\":[{\"image\":\"nginx:1.15.4\",\"name\":\"nginx\",\"ports\":[{\"containerPort\":80}]}]}}}}\n",
|
||||
"rollout.argoproj.io/revision": "4"
|
||||
},
|
||||
"creationTimestamp": "2021-03-08T10:38:23Z",
|
||||
"generation": 12,
|
||||
"labels": {
|
||||
"backstage.io/kubernetes-id": "dice-roller"
|
||||
},
|
||||
"name": "dice-roller",
|
||||
"namespace": "default",
|
||||
"resourceVersion": "3336911046",
|
||||
"selfLink": "/apis/argoproj.io/v1alpha1/namespaces/default/rollouts/dice-roller",
|
||||
"uid": "8552f08d-32e8-4f95-a43f-8524763eeg60"
|
||||
},
|
||||
"spec": {
|
||||
"minReadySeconds": 30,
|
||||
"replicas": 4,
|
||||
"selector": {
|
||||
"matchLabels": {
|
||||
"app": "dice-roller",
|
||||
"backstage.io/kubernetes-id": "dice-roller"
|
||||
}
|
||||
},
|
||||
"strategy": {
|
||||
"canary": {
|
||||
"maxSurge": "25%",
|
||||
"maxUnavailable": 0,
|
||||
"steps": [
|
||||
{
|
||||
"setWeight": 10
|
||||
},
|
||||
{
|
||||
"analysis": {
|
||||
"templates": [
|
||||
{
|
||||
"templateName": "always-pass"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"pause": {
|
||||
"duration": "1h"
|
||||
}
|
||||
},
|
||||
{
|
||||
"setWeight": 20
|
||||
},
|
||||
{
|
||||
"pause": {
|
||||
"duration": "1m"
|
||||
}
|
||||
},
|
||||
{
|
||||
"analysis": {
|
||||
"templates": [
|
||||
{
|
||||
"templateName": "always-pass"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"creationTimestamp": null,
|
||||
"labels": {
|
||||
"app": "dice-roller",
|
||||
"backstage.io/kubernetes-id": "dice-roller",
|
||||
"start": "1236"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"image": "nginx:1.15.4",
|
||||
"name": "nginx",
|
||||
"ports": [
|
||||
{
|
||||
"containerPort": 80
|
||||
}
|
||||
],
|
||||
"resources": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"HPAReplicas": 4,
|
||||
"availableReplicas": 4,
|
||||
"blueGreen": {},
|
||||
"canary": {},
|
||||
"conditions": [
|
||||
{
|
||||
"lastTransitionTime": "2021-03-08T10:39:31Z",
|
||||
"lastUpdateTime": "2021-03-08T10:39:31Z",
|
||||
"message": "Rollout has minimum availability",
|
||||
"reason": "AvailableReason",
|
||||
"status": "True",
|
||||
"type": "Available"
|
||||
},
|
||||
{
|
||||
"lastTransitionTime": "2021-03-09T16:11:14Z",
|
||||
"lastUpdateTime": "2021-03-09T16:13:09Z",
|
||||
"message": "some metric related failure message",
|
||||
"reason": "RolloutAborted",
|
||||
"status": "False",
|
||||
"type": "Progressing"
|
||||
}
|
||||
],
|
||||
"currentPodHash": "546c476497",
|
||||
"currentStepHash": "5b48bb87dc",
|
||||
"currentStepIndex": 0,
|
||||
"observedGeneration": "12",
|
||||
"readyReplicas": 4,
|
||||
"replicas": 4,
|
||||
"selector": "app=dice-roller,backstage.io/kubernetes-id=dice-roller",
|
||||
"stableRS": "546c476497",
|
||||
"updatedReplicas": 4
|
||||
}
|
||||
}
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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 { AnalysisStep } from '../types';
|
||||
|
||||
export const steps: AnalysisStep[] = [
|
||||
{
|
||||
analysis: {
|
||||
templates: [
|
||||
{
|
||||
templateName: 'always-pass',
|
||||
},
|
||||
{
|
||||
templateName: 'always-fail',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
analysis: {
|
||||
templates: [
|
||||
{
|
||||
templateName: 'req-rate',
|
||||
clusterScope: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default steps;
|
||||
-576
@@ -1,576 +0,0 @@
|
||||
{
|
||||
"pods": [
|
||||
{
|
||||
"metadata": {
|
||||
"creationTimestamp": "2020-09-25T09:58:50.000Z",
|
||||
"generateName": "dice-roller-6c8646bfd-",
|
||||
"labels": {
|
||||
"app": "dice-roller",
|
||||
"backstage.io/kubernetes-id": "dice-roller",
|
||||
"pod-template-hash": "6c8646bfd"
|
||||
},
|
||||
"managedFields": [
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"fieldsType": "FieldsV1",
|
||||
"fieldsV1": {
|
||||
"f:metadata": {
|
||||
"f:generateName": {},
|
||||
"f:labels": {
|
||||
".": {},
|
||||
"f:app": {},
|
||||
"f:backstage.io/kubernetes-id": {},
|
||||
"f:pod-template-hash": {}
|
||||
},
|
||||
"f:ownerReferences": {
|
||||
".": {},
|
||||
"k:{\"uid\":\"5126c354-4310-4e23-a9e4-c9b87cb69792\"}": {
|
||||
".": {},
|
||||
"f:apiVersion": {},
|
||||
"f:blockOwnerDeletion": {},
|
||||
"f:controller": {},
|
||||
"f:kind": {},
|
||||
"f:name": {},
|
||||
"f:uid": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"f:spec": {
|
||||
"f:containers": {
|
||||
"k:{\"name\":\"nginx\"}": {
|
||||
".": {},
|
||||
"f:image": {},
|
||||
"f:imagePullPolicy": {},
|
||||
"f:name": {},
|
||||
"f:ports": {
|
||||
".": {},
|
||||
"k:{\"containerPort\":80,\"protocol\":\"TCP\"}": {
|
||||
".": {},
|
||||
"f:containerPort": {},
|
||||
"f:protocol": {}
|
||||
}
|
||||
},
|
||||
"f:resources": {},
|
||||
"f:terminationMessagePath": {},
|
||||
"f:terminationMessagePolicy": {}
|
||||
}
|
||||
},
|
||||
"f:dnsPolicy": {},
|
||||
"f:enableServiceLinks": {},
|
||||
"f:restartPolicy": {},
|
||||
"f:schedulerName": {},
|
||||
"f:securityContext": {},
|
||||
"f:terminationGracePeriodSeconds": {}
|
||||
}
|
||||
},
|
||||
"manager": "kube-controller-manager",
|
||||
"operation": "Update",
|
||||
"time": "2020-09-25T09:58:50.000Z"
|
||||
},
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"fieldsType": "FieldsV1",
|
||||
"fieldsV1": {
|
||||
"f:status": {
|
||||
"f:conditions": {
|
||||
"k:{\"type\":\"ContainersReady\"}": {
|
||||
".": {},
|
||||
"f:lastProbeTime": {},
|
||||
"f:lastTransitionTime": {},
|
||||
"f:status": {},
|
||||
"f:type": {}
|
||||
},
|
||||
"k:{\"type\":\"Initialized\"}": {
|
||||
".": {},
|
||||
"f:lastProbeTime": {},
|
||||
"f:lastTransitionTime": {},
|
||||
"f:status": {},
|
||||
"f:type": {}
|
||||
},
|
||||
"k:{\"type\":\"Ready\"}": {
|
||||
".": {},
|
||||
"f:lastProbeTime": {},
|
||||
"f:lastTransitionTime": {},
|
||||
"f:status": {},
|
||||
"f:type": {}
|
||||
}
|
||||
},
|
||||
"f:containerStatuses": {},
|
||||
"f:hostIP": {},
|
||||
"f:phase": {},
|
||||
"f:podIP": {},
|
||||
"f:podIPs": {
|
||||
".": {},
|
||||
"k:{\"ip\":\"172.17.0.11\"}": {
|
||||
".": {},
|
||||
"f:ip": {}
|
||||
}
|
||||
},
|
||||
"f:startTime": {}
|
||||
}
|
||||
},
|
||||
"manager": "kubelet",
|
||||
"operation": "Update",
|
||||
"time": "2020-09-25T09:58:55.000Z"
|
||||
}
|
||||
],
|
||||
"name": "dice-roller-6c8646bfd-2m5hv",
|
||||
"namespace": "default",
|
||||
"ownerReferences": [
|
||||
{
|
||||
"apiVersion": "apps/v1",
|
||||
"blockOwnerDeletion": true,
|
||||
"controller": true,
|
||||
"kind": "ReplicaSet",
|
||||
"name": "dice-roller-6c8646bfd",
|
||||
"uid": "5126c354-4310-4e23-a9e4-c9b87cb69792"
|
||||
}
|
||||
],
|
||||
"resourceVersion": "593216",
|
||||
"selfLink": "/api/v1/namespaces/default/pods/dice-roller-6c8646bfd-2m5hv",
|
||||
"uid": "aadb71c0-36fa-43e3-b38a-162f134d4359"
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"image": "nginx:1.14.2",
|
||||
"imagePullPolicy": "IfNotPresent",
|
||||
"name": "nginx",
|
||||
"ports": [
|
||||
{
|
||||
"containerPort": 80,
|
||||
"protocol": "TCP"
|
||||
}
|
||||
],
|
||||
"resources": {},
|
||||
"terminationMessagePath": "/dev/termination-log",
|
||||
"terminationMessagePolicy": "File",
|
||||
"volumeMounts": [
|
||||
{
|
||||
"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
|
||||
"name": "default-token-5gctn",
|
||||
"readOnly": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"dnsPolicy": "ClusterFirst",
|
||||
"enableServiceLinks": true,
|
||||
"nodeName": "minikube",
|
||||
"priority": 0,
|
||||
"restartPolicy": "Always",
|
||||
"schedulerName": "default-scheduler",
|
||||
"securityContext": {},
|
||||
"serviceAccount": "default",
|
||||
"serviceAccountName": "default",
|
||||
"terminationGracePeriodSeconds": 30,
|
||||
"tolerations": [
|
||||
{
|
||||
"effect": "NoExecute",
|
||||
"key": "node.kubernetes.io/not-ready",
|
||||
"operator": "Exists",
|
||||
"tolerationSeconds": 300
|
||||
},
|
||||
{
|
||||
"effect": "NoExecute",
|
||||
"key": "node.kubernetes.io/unreachable",
|
||||
"operator": "Exists",
|
||||
"tolerationSeconds": 300
|
||||
}
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"name": "default-token-5gctn",
|
||||
"secret": {
|
||||
"defaultMode": 420,
|
||||
"secretName": "default-token-5gctn"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"conditions": [
|
||||
{
|
||||
"lastProbeTime": null,
|
||||
"lastTransitionTime": "2020-09-25T09:58:50.000Z",
|
||||
"status": "True",
|
||||
"type": "Initialized"
|
||||
},
|
||||
{
|
||||
"lastProbeTime": null,
|
||||
"lastTransitionTime": "2020-09-25T09:58:55.000Z",
|
||||
"status": "True",
|
||||
"type": "Ready"
|
||||
},
|
||||
{
|
||||
"lastProbeTime": null,
|
||||
"lastTransitionTime": "2020-09-25T09:58:55.000Z",
|
||||
"status": "True",
|
||||
"type": "ContainersReady"
|
||||
},
|
||||
{
|
||||
"lastProbeTime": null,
|
||||
"lastTransitionTime": "2020-09-25T09:58:50.000Z",
|
||||
"status": "True",
|
||||
"type": "PodScheduled"
|
||||
}
|
||||
],
|
||||
"containerStatuses": [
|
||||
{
|
||||
"containerID": "docker://aa4489297c34c48bb33c18474a8d2b33854a82ed42155680b259f635f556ce70",
|
||||
"image": "nginx:1.14.2",
|
||||
"imageID": "docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d",
|
||||
"lastState": {},
|
||||
"name": "nginx",
|
||||
"ready": true,
|
||||
"restartCount": 0,
|
||||
"started": true,
|
||||
"state": {
|
||||
"running": {
|
||||
"startedAt": "2020-09-25T09:58:53.000Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"hostIP": "192.168.64.2",
|
||||
"phase": "Running",
|
||||
"podIP": "172.17.0.11",
|
||||
"podIPs": [
|
||||
{
|
||||
"ip": "172.17.0.11"
|
||||
}
|
||||
],
|
||||
"qosClass": "BestEffort",
|
||||
"startTime": "2020-09-25T09:58:50.000Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"creationTimestamp": "2020-09-25T09:58:50.000Z",
|
||||
"generateName": "dice-roller-6c8646bfd-",
|
||||
"labels": {
|
||||
"app": "dice-roller",
|
||||
"backstage.io/kubernetes-id": "dice-roller",
|
||||
"pod-template-hash": "6c8646bfd"
|
||||
},
|
||||
"managedFields": [
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"fieldsType": "FieldsV1",
|
||||
"fieldsV1": {
|
||||
"f:metadata": {
|
||||
"f:generateName": {},
|
||||
"f:labels": {
|
||||
".": {},
|
||||
"f:app": {},
|
||||
"f:backstage.io/kubernetes-id": {},
|
||||
"f:pod-template-hash": {}
|
||||
},
|
||||
"f:ownerReferences": {
|
||||
".": {},
|
||||
"k:{\"uid\":\"5126c354-4310-4e23-a9e4-c9b87cb69792\"}": {
|
||||
".": {},
|
||||
"f:apiVersion": {},
|
||||
"f:blockOwnerDeletion": {},
|
||||
"f:controller": {},
|
||||
"f:kind": {},
|
||||
"f:name": {},
|
||||
"f:uid": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"f:spec": {
|
||||
"f:containers": {
|
||||
"k:{\"name\":\"nginx\"}": {
|
||||
".": {},
|
||||
"f:image": {},
|
||||
"f:imagePullPolicy": {},
|
||||
"f:name": {},
|
||||
"f:ports": {
|
||||
".": {},
|
||||
"k:{\"containerPort\":80,\"protocol\":\"TCP\"}": {
|
||||
".": {},
|
||||
"f:containerPort": {},
|
||||
"f:protocol": {}
|
||||
}
|
||||
},
|
||||
"f:resources": {},
|
||||
"f:terminationMessagePath": {},
|
||||
"f:terminationMessagePolicy": {}
|
||||
}
|
||||
},
|
||||
"f:dnsPolicy": {},
|
||||
"f:enableServiceLinks": {},
|
||||
"f:restartPolicy": {},
|
||||
"f:schedulerName": {},
|
||||
"f:securityContext": {},
|
||||
"f:terminationGracePeriodSeconds": {}
|
||||
}
|
||||
},
|
||||
"manager": "kube-controller-manager",
|
||||
"operation": "Update",
|
||||
"time": "2020-09-25T09:58:50.000Z"
|
||||
},
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"fieldsType": "FieldsV1",
|
||||
"fieldsV1": {
|
||||
"f:status": {
|
||||
"f:conditions": {
|
||||
"k:{\"type\":\"ContainersReady\"}": {
|
||||
".": {},
|
||||
"f:lastProbeTime": {},
|
||||
"f:lastTransitionTime": {},
|
||||
"f:status": {},
|
||||
"f:type": {}
|
||||
},
|
||||
"k:{\"type\":\"Initialized\"}": {
|
||||
".": {},
|
||||
"f:lastProbeTime": {},
|
||||
"f:lastTransitionTime": {},
|
||||
"f:status": {},
|
||||
"f:type": {}
|
||||
},
|
||||
"k:{\"type\":\"Ready\"}": {
|
||||
".": {},
|
||||
"f:lastProbeTime": {},
|
||||
"f:lastTransitionTime": {},
|
||||
"f:status": {},
|
||||
"f:type": {}
|
||||
}
|
||||
},
|
||||
"f:containerStatuses": {},
|
||||
"f:hostIP": {},
|
||||
"f:phase": {},
|
||||
"f:podIP": {},
|
||||
"f:podIPs": {
|
||||
".": {},
|
||||
"k:{\"ip\":\"172.17.0.7\"}": {
|
||||
".": {},
|
||||
"f:ip": {}
|
||||
}
|
||||
},
|
||||
"f:startTime": {}
|
||||
}
|
||||
},
|
||||
"manager": "kubelet",
|
||||
"operation": "Update",
|
||||
"time": "2020-09-25T09:58:55.000Z"
|
||||
}
|
||||
],
|
||||
"name": "dice-roller-6c8646bfd-4v6s8",
|
||||
"namespace": "default",
|
||||
"ownerReferences": [
|
||||
{
|
||||
"apiVersion": "apps/v1",
|
||||
"blockOwnerDeletion": true,
|
||||
"controller": true,
|
||||
"kind": "ReplicaSet",
|
||||
"name": "dice-roller-6c8646bfd",
|
||||
"uid": "5126c354-4310-4e23-a9e4-c9b87cb69792"
|
||||
}
|
||||
],
|
||||
"resourceVersion": "593221",
|
||||
"selfLink": "/api/v1/namespaces/default/pods/dice-roller-6c8646bfd-4v6s8",
|
||||
"uid": "32e56490-6f20-4757-991f-a0c7fb407358"
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"image": "nginx:1.14.2",
|
||||
"imagePullPolicy": "IfNotPresent",
|
||||
"name": "nginx",
|
||||
"ports": [
|
||||
{
|
||||
"containerPort": 80,
|
||||
"protocol": "TCP"
|
||||
}
|
||||
],
|
||||
"resources": {},
|
||||
"terminationMessagePath": "/dev/termination-log",
|
||||
"terminationMessagePolicy": "File",
|
||||
"volumeMounts": [
|
||||
{
|
||||
"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
|
||||
"name": "default-token-5gctn",
|
||||
"readOnly": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"dnsPolicy": "ClusterFirst",
|
||||
"enableServiceLinks": true,
|
||||
"nodeName": "minikube",
|
||||
"priority": 0,
|
||||
"restartPolicy": "Always",
|
||||
"schedulerName": "default-scheduler",
|
||||
"securityContext": {},
|
||||
"serviceAccount": "default",
|
||||
"serviceAccountName": "default",
|
||||
"terminationGracePeriodSeconds": 30,
|
||||
"tolerations": [
|
||||
{
|
||||
"effect": "NoExecute",
|
||||
"key": "node.kubernetes.io/not-ready",
|
||||
"operator": "Exists",
|
||||
"tolerationSeconds": 300
|
||||
},
|
||||
{
|
||||
"effect": "NoExecute",
|
||||
"key": "node.kubernetes.io/unreachable",
|
||||
"operator": "Exists",
|
||||
"tolerationSeconds": 300
|
||||
}
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"name": "default-token-5gctn",
|
||||
"secret": {
|
||||
"defaultMode": 420,
|
||||
"secretName": "default-token-5gctn"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"conditions": [
|
||||
{
|
||||
"lastProbeTime": null,
|
||||
"lastTransitionTime": "2020-09-25T09:58:50.000Z",
|
||||
"status": "True",
|
||||
"type": "Initialized"
|
||||
},
|
||||
{
|
||||
"lastProbeTime": null,
|
||||
"lastTransitionTime": "2020-09-25T09:58:55.000Z",
|
||||
"status": "True",
|
||||
"type": "Ready"
|
||||
},
|
||||
{
|
||||
"lastProbeTime": null,
|
||||
"lastTransitionTime": "2020-09-25T09:58:55.000Z",
|
||||
"status": "True",
|
||||
"type": "ContainersReady"
|
||||
},
|
||||
{
|
||||
"lastProbeTime": null,
|
||||
"lastTransitionTime": "2020-09-25T09:58:50.000Z",
|
||||
"status": "True",
|
||||
"type": "PodScheduled"
|
||||
}
|
||||
],
|
||||
"containerStatuses": [
|
||||
{
|
||||
"containerID": "docker://d91cc76c41249b8d3dfcf538d180b471b2a7966aae0d17a818307c8a9b4af897",
|
||||
"image": "nginx:1.14.2",
|
||||
"imageID": "docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d",
|
||||
"lastState": {},
|
||||
"name": "nginx",
|
||||
"ready": true,
|
||||
"restartCount": 0,
|
||||
"started": true,
|
||||
"state": {
|
||||
"running": {
|
||||
"startedAt": "2020-09-25T09:58:53.000Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"hostIP": "192.168.64.2",
|
||||
"phase": "Running",
|
||||
"podIP": "172.17.0.7",
|
||||
"podIPs": [
|
||||
{
|
||||
"ip": "172.17.0.7"
|
||||
}
|
||||
],
|
||||
"qosClass": "BestEffort",
|
||||
"startTime": "2020-09-25T09:58:50.000Z"
|
||||
}
|
||||
}
|
||||
],
|
||||
"horizontalPodAutoscalers": [],
|
||||
"replicaSets": [
|
||||
{
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"deployment.kubernetes.io/desired-replicas": "10",
|
||||
"deployment.kubernetes.io/max-replicas": "13",
|
||||
"deployment.kubernetes.io/revision": "2"
|
||||
},
|
||||
"creationTimestamp": "2020-09-24T11:39:26.000Z",
|
||||
"generation": 3,
|
||||
"labels": {
|
||||
"app": "dice-roller",
|
||||
"backstage.io/kubernetes-id": "dice-roller",
|
||||
"pod-template-hash": "6c8646bfd"
|
||||
},
|
||||
"managedFields": [],
|
||||
"name": "dice-roller-6c8646bfd",
|
||||
"namespace": "default",
|
||||
"ownerReferences": [
|
||||
{
|
||||
"apiVersion": "apps/v1",
|
||||
"blockOwnerDeletion": true,
|
||||
"controller": true,
|
||||
"kind": "Rollout",
|
||||
"name": "dice-roller",
|
||||
"uid": "8552f08d-32e8-4f95-a43f-8524763eeg60"
|
||||
}
|
||||
],
|
||||
"resourceVersion": "593228",
|
||||
"selfLink": "/apis/apps/v1/namespaces/default/replicasets/dice-roller-6c8646bfd",
|
||||
"uid": "5126c354-4310-4e23-a9e4-c9b87cb69792"
|
||||
},
|
||||
"spec": {
|
||||
"replicas": 10,
|
||||
"selector": {
|
||||
"matchLabels": {
|
||||
"app": "dice-roller",
|
||||
"pod-template-hash": "6c8646bfd"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"creationTimestamp": null,
|
||||
"labels": {
|
||||
"app": "dice-roller",
|
||||
"backstage.io/kubernetes-id": "dice-roller",
|
||||
"pod-template-hash": "6c8646bfd"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"image": "nginx:1.14.2",
|
||||
"imagePullPolicy": "IfNotPresent",
|
||||
"name": "nginx",
|
||||
"ports": [
|
||||
{
|
||||
"containerPort": 80,
|
||||
"protocol": "TCP"
|
||||
}
|
||||
],
|
||||
"resources": {},
|
||||
"terminationMessagePath": "/dev/termination-log",
|
||||
"terminationMessagePolicy": "File"
|
||||
}
|
||||
],
|
||||
"dnsPolicy": "ClusterFirst",
|
||||
"restartPolicy": "Always",
|
||||
"schedulerName": "default-scheduler",
|
||||
"securityContext": {},
|
||||
"terminationGracePeriodSeconds": 30
|
||||
}
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"availableReplicas": 10,
|
||||
"fullyLabeledReplicas": 10,
|
||||
"observedGeneration": 3,
|
||||
"readyReplicas": 10,
|
||||
"replicas": 10
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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 { PauseStep } from '../types';
|
||||
|
||||
export const steps: PauseStep[] = [
|
||||
{
|
||||
pause: {
|
||||
duration: '1h',
|
||||
},
|
||||
},
|
||||
{
|
||||
pause: {},
|
||||
},
|
||||
];
|
||||
|
||||
export default steps;
|
||||
-135
@@ -1,135 +0,0 @@
|
||||
{
|
||||
"apiVersion": "argoproj.io/v1alpha1",
|
||||
"kind": "Rollout",
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"argoproj.io/v1alpha1\",\"kind\":\"Rollout\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\"},\"name\":\"dice-roller\",\"namespace\":\"default\"},\"spec\":{\"minReadySeconds\":30,\"replicas\":4,\"selector\":{\"matchLabels\":{\"app\":\"dice-roller\",\"backstage.io/kubernetes-id\":\"dice-roller\"}},\"strategy\":{\"canary\":{\"maxSurge\":\"25%\",\"maxUnavailable\":0,\"steps\":[{\"setWeight\":10},{\"analysis\":{\"templates\":[{\"templateName\":\"always-pass\"}]}},{\"pause\":{\"duration\":\"1m\"}},{\"setWeight\":20},{\"pause\":{\"duration\":\"1m\"}},{\"analysis\":{\"templates\":[{\"templateName\":\"always-pass\"}]}}]}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"dice-roller\",\"backstage.io/kubernetes-id\":\"dice-roller\",\"start\":\"1234\"}},\"spec\":{\"containers\":[{\"image\":\"nginx:1.15.4\",\"name\":\"nginx\",\"ports\":[{\"containerPort\":80}]}]}}}}\n",
|
||||
"rollout.argoproj.io/revision": "4"
|
||||
},
|
||||
"creationTimestamp": "2021-03-08T10:38:23Z",
|
||||
"generation": 12,
|
||||
"labels": {
|
||||
"backstage.io/kubernetes-id": "dice-roller"
|
||||
},
|
||||
"name": "dice-roller",
|
||||
"namespace": "default",
|
||||
"resourceVersion": "3336911046",
|
||||
"selfLink": "/apis/argoproj.io/v1alpha1/namespaces/default/rollouts/dice-roller",
|
||||
"uid": "8552f08d-32e8-4f95-a43f-8524763eeg60"
|
||||
},
|
||||
"spec": {
|
||||
"minReadySeconds": 30,
|
||||
"replicas": 4,
|
||||
"selector": {
|
||||
"matchLabels": {
|
||||
"app": "dice-roller",
|
||||
"backstage.io/kubernetes-id": "dice-roller"
|
||||
}
|
||||
},
|
||||
"strategy": {
|
||||
"canary": {
|
||||
"maxSurge": "25%",
|
||||
"maxUnavailable": 0,
|
||||
"steps": [
|
||||
{
|
||||
"setWeight": 10
|
||||
},
|
||||
{
|
||||
"analysis": {
|
||||
"templates": [
|
||||
{
|
||||
"templateName": "always-pass"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"pause": {
|
||||
"duration": "1h"
|
||||
}
|
||||
},
|
||||
{
|
||||
"setWeight": 20
|
||||
},
|
||||
{
|
||||
"pause": {
|
||||
"duration": "1m"
|
||||
}
|
||||
},
|
||||
{
|
||||
"analysis": {
|
||||
"templates": [
|
||||
{
|
||||
"templateName": "always-pass"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"creationTimestamp": null,
|
||||
"labels": {
|
||||
"app": "dice-roller",
|
||||
"backstage.io/kubernetes-id": "dice-roller",
|
||||
"start": "1236"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"image": "nginx:1.15.4",
|
||||
"name": "nginx",
|
||||
"ports": [
|
||||
{
|
||||
"containerPort": 80
|
||||
}
|
||||
],
|
||||
"resources": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"HPAReplicas": 4,
|
||||
"availableReplicas": 4,
|
||||
"blueGreen": {},
|
||||
"canary": {},
|
||||
"pauseConditions": [
|
||||
{
|
||||
"reason": "CanaryPauseStep",
|
||||
"startTime": "SET DYNAMICALLY IN TEST"
|
||||
}
|
||||
],
|
||||
"conditions": [
|
||||
{
|
||||
"lastTransitionTime": "2021-03-08T10:39:31Z",
|
||||
"lastUpdateTime": "2021-03-08T10:39:31Z",
|
||||
"message": "Rollout has minimum availability",
|
||||
"reason": "AvailableReason",
|
||||
"status": "True",
|
||||
"type": "Available"
|
||||
},
|
||||
{
|
||||
"lastTransitionTime": "2021-03-09T16:11:14Z",
|
||||
"lastUpdateTime": "2021-03-09T16:13:09Z",
|
||||
"message": "ReplicaSet \"dice-roller-546c476497\" has successfully progressed.",
|
||||
"reason": "NewReplicaSetAvailable",
|
||||
"status": "True",
|
||||
"type": "Progressing"
|
||||
}
|
||||
],
|
||||
"currentPodHash": "546c476497",
|
||||
"currentStepHash": "5b48bb87dc",
|
||||
"currentStepIndex": 2,
|
||||
"observedGeneration": "12",
|
||||
"readyReplicas": 4,
|
||||
"replicas": 4,
|
||||
"selector": "app=dice-roller,backstage.io/kubernetes-id=dice-roller",
|
||||
"stableRS": "546c476497",
|
||||
"updatedReplicas": 4
|
||||
}
|
||||
}
|
||||
-129
@@ -1,129 +0,0 @@
|
||||
{
|
||||
"apiVersion": "argoproj.io/v1alpha1",
|
||||
"kind": "Rollout",
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"argoproj.io/v1alpha1\",\"kind\":\"Rollout\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\"},\"name\":\"dice-roller\",\"namespace\":\"default\"},\"spec\":{\"minReadySeconds\":30,\"replicas\":4,\"selector\":{\"matchLabels\":{\"app\":\"dice-roller\",\"backstage.io/kubernetes-id\":\"dice-roller\"}},\"strategy\":{\"canary\":{\"maxSurge\":\"25%\",\"maxUnavailable\":0,\"steps\":[{\"setWeight\":10},{\"analysis\":{\"templates\":[{\"templateName\":\"always-pass\"}]}},{\"pause\":{\"duration\":\"1m\"}},{\"setWeight\":20},{\"pause\":{\"duration\":\"1m\"}},{\"analysis\":{\"templates\":[{\"templateName\":\"always-pass\"}]}}]}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"dice-roller\",\"backstage.io/kubernetes-id\":\"dice-roller\",\"start\":\"1234\"}},\"spec\":{\"containers\":[{\"image\":\"nginx:1.15.4\",\"name\":\"nginx\",\"ports\":[{\"containerPort\":80}]}]}}}}\n",
|
||||
"rollout.argoproj.io/revision": "4"
|
||||
},
|
||||
"creationTimestamp": "2021-03-08T10:38:23Z",
|
||||
"generation": 12,
|
||||
"labels": {
|
||||
"backstage.io/kubernetes-id": "dice-roller"
|
||||
},
|
||||
"name": "dice-roller",
|
||||
"namespace": "default",
|
||||
"resourceVersion": "3336911046",
|
||||
"selfLink": "/apis/argoproj.io/v1alpha1/namespaces/default/rollouts/dice-roller",
|
||||
"uid": "8552f08d-32e8-4f95-a43f-8524763eeg60"
|
||||
},
|
||||
"spec": {
|
||||
"minReadySeconds": 30,
|
||||
"replicas": 4,
|
||||
"selector": {
|
||||
"matchLabels": {
|
||||
"app": "dice-roller",
|
||||
"backstage.io/kubernetes-id": "dice-roller"
|
||||
}
|
||||
},
|
||||
"strategy": {
|
||||
"canary": {
|
||||
"maxSurge": "25%",
|
||||
"maxUnavailable": 0,
|
||||
"steps": [
|
||||
{
|
||||
"setWeight": 10
|
||||
},
|
||||
{
|
||||
"analysis": {
|
||||
"templates": [
|
||||
{
|
||||
"templateName": "always-pass"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"pause": {
|
||||
"duration": "1h"
|
||||
}
|
||||
},
|
||||
{
|
||||
"setWeight": 20
|
||||
},
|
||||
{
|
||||
"pause": {
|
||||
"duration": "1m"
|
||||
}
|
||||
},
|
||||
{
|
||||
"analysis": {
|
||||
"templates": [
|
||||
{
|
||||
"templateName": "always-pass"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"creationTimestamp": null,
|
||||
"labels": {
|
||||
"app": "dice-roller",
|
||||
"backstage.io/kubernetes-id": "dice-roller",
|
||||
"start": "1236"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"image": "nginx:1.15.4",
|
||||
"name": "nginx",
|
||||
"ports": [
|
||||
{
|
||||
"containerPort": 80
|
||||
}
|
||||
],
|
||||
"resources": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"HPAReplicas": 4,
|
||||
"availableReplicas": 4,
|
||||
"blueGreen": {},
|
||||
"canary": {},
|
||||
"conditions": [
|
||||
{
|
||||
"lastTransitionTime": "2021-03-08T10:39:31Z",
|
||||
"lastUpdateTime": "2021-03-08T10:39:31Z",
|
||||
"message": "Rollout has minimum availability",
|
||||
"reason": "AvailableReason",
|
||||
"status": "True",
|
||||
"type": "Available"
|
||||
},
|
||||
{
|
||||
"lastTransitionTime": "2021-03-09T16:11:14Z",
|
||||
"lastUpdateTime": "2021-03-09T16:13:09Z",
|
||||
"message": "ReplicaSet \"dice-roller-546c476497\" has successfully progressed.",
|
||||
"reason": "NewReplicaSetAvailable",
|
||||
"status": "True",
|
||||
"type": "Progressing"
|
||||
}
|
||||
],
|
||||
"currentPodHash": "546c476497",
|
||||
"currentStepHash": "5b48bb87dc",
|
||||
"currentStepIndex": 6,
|
||||
"observedGeneration": "12",
|
||||
"readyReplicas": 4,
|
||||
"replicas": 4,
|
||||
"selector": "app=dice-roller,backstage.io/kubernetes-id=dice-roller",
|
||||
"stableRS": "546c476497",
|
||||
"updatedReplicas": 4
|
||||
}
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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 { SetWeightStep } from '../types';
|
||||
|
||||
export const steps: SetWeightStep[] = [
|
||||
{
|
||||
setWeight: 10,
|
||||
},
|
||||
{
|
||||
setWeight: 95,
|
||||
},
|
||||
];
|
||||
|
||||
export default steps;
|
||||
@@ -1,16 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
export * from './Rollout';
|
||||
@@ -1,36 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export interface SetWeightStep {
|
||||
setWeight: number;
|
||||
}
|
||||
|
||||
export interface PauseStep {
|
||||
pause: {
|
||||
duration?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AnalysisStep {
|
||||
analysis: {
|
||||
templates: {
|
||||
templateName: string;
|
||||
clusterScope?: boolean;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
export type ArgoRolloutCanaryStep = SetWeightStep | PauseStep | AnalysisStep;
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
* 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, { useContext } from 'react';
|
||||
import lodash, { Dictionary } from 'lodash';
|
||||
import { RolloutAccordions } from './ArgoRollouts';
|
||||
import { DefaultCustomResourceAccordions } from './DefaultCustomResource';
|
||||
import { GroupedResponsesContext } from '../../hooks';
|
||||
|
||||
interface CustomResourcesProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const kindToResource = (customResources: any[]): Dictionary<any[]> => {
|
||||
return lodash.groupBy(customResources, value => {
|
||||
return value.kind;
|
||||
});
|
||||
};
|
||||
|
||||
export const CustomResources = ({}: CustomResourcesProps) => {
|
||||
const groupedResponses = useContext(GroupedResponsesContext);
|
||||
const kindToResourceMap = kindToResource(groupedResponses.customResources);
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.entries(kindToResourceMap).map(([kind, resources], i) => {
|
||||
switch (kind) {
|
||||
case 'Rollout':
|
||||
return <RolloutAccordions key={i} rollouts={resources} />;
|
||||
default:
|
||||
return (
|
||||
<DefaultCustomResourceAccordions
|
||||
key={i}
|
||||
customResources={resources}
|
||||
customResourceName={kind}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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 { screen } from '@testing-library/react';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { kubernetesProviders } from '../../hooks/test-utils';
|
||||
import * as ar from './__fixtures__/analysis-run.json';
|
||||
import { DefaultCustomResourceAccordions } from './DefaultCustomResource';
|
||||
|
||||
describe('DefaultCustomResource', () => {
|
||||
it('should render DefaultCustomResource Accordion', async () => {
|
||||
const wrapper = kubernetesProviders({}, new Set([]));
|
||||
|
||||
await renderInTestApp(
|
||||
wrapper(
|
||||
<DefaultCustomResourceAccordions
|
||||
customResources={[ar] as any}
|
||||
customResourceName="AnalysisRun"
|
||||
/>,
|
||||
),
|
||||
);
|
||||
expect(screen.getByText('dice-roller-546c476497-4-1')).toBeInTheDocument();
|
||||
expect(screen.getByText('AnalysisRun')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,121 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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 {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Grid,
|
||||
} from '@material-ui/core';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import { DefaultCustomResourceDrawer } from './DefaultCustomResourceDrawer';
|
||||
import { StructuredMetadataTable } from '@backstage/core-components';
|
||||
|
||||
type DefaultCustomResourceAccordionsProps = {
|
||||
customResources: any[];
|
||||
customResourceName: string;
|
||||
defaultExpanded?: boolean;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
type DefaultCustomResourceAccordionProps = {
|
||||
customResource: any;
|
||||
customResourceName: string;
|
||||
defaultExpanded?: boolean;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
type DefaultCustomResourceSummaryProps = {
|
||||
customResource: any;
|
||||
customResourceName: string;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const DefaultCustomResourceSummary = ({
|
||||
customResource,
|
||||
customResourceName,
|
||||
}: DefaultCustomResourceSummaryProps) => {
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
spacing={0}
|
||||
>
|
||||
<Grid xs={12} item>
|
||||
<DefaultCustomResourceDrawer
|
||||
customResource={customResource}
|
||||
customResourceName={customResourceName}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
const DefaultCustomResourceAccordion = ({
|
||||
customResource,
|
||||
customResourceName,
|
||||
defaultExpanded,
|
||||
}: DefaultCustomResourceAccordionProps) => {
|
||||
return (
|
||||
<Accordion
|
||||
defaultExpanded={defaultExpanded}
|
||||
TransitionProps={{ unmountOnExit: true }}
|
||||
variant="outlined"
|
||||
>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<DefaultCustomResourceSummary
|
||||
customResource={customResource}
|
||||
customResourceName={customResourceName}
|
||||
/>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{customResource.hasOwnProperty('status') && (
|
||||
<StructuredMetadataTable metadata={customResource.status} />
|
||||
)}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
export const DefaultCustomResourceAccordions = ({
|
||||
customResources,
|
||||
customResourceName,
|
||||
defaultExpanded = false,
|
||||
}: DefaultCustomResourceAccordionsProps) => {
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
{customResources.map((cr, i) => (
|
||||
<Grid container item key={i} xs>
|
||||
<Grid item xs>
|
||||
<DefaultCustomResourceAccordion
|
||||
defaultExpanded={defaultExpanded}
|
||||
customResource={cr}
|
||||
customResourceName={customResourceName}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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 { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer';
|
||||
import { Typography, Grid } from '@material-ui/core';
|
||||
|
||||
const capitalize = (str: string) =>
|
||||
str.charAt(0).toLocaleUpperCase('en-US') + str.slice(1);
|
||||
|
||||
export const DefaultCustomResourceDrawer = ({
|
||||
customResource,
|
||||
customResourceName,
|
||||
expanded,
|
||||
}: {
|
||||
customResource: any;
|
||||
customResourceName: string;
|
||||
expanded?: boolean;
|
||||
}) => {
|
||||
const capitalizedName = capitalize(customResourceName);
|
||||
|
||||
return (
|
||||
<KubernetesStructuredMetadataTableDrawer
|
||||
object={customResource}
|
||||
expanded={expanded}
|
||||
kind={capitalizedName}
|
||||
renderObject={cr => cr}
|
||||
>
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
spacing={0}
|
||||
>
|
||||
<Grid item>
|
||||
<Typography variant="body1">
|
||||
{customResource.metadata?.name ?? 'unknown object'}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography color="textSecondary" variant="subtitle1">
|
||||
{capitalizedName}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</KubernetesStructuredMetadataTableDrawer>
|
||||
);
|
||||
};
|
||||
@@ -1,129 +0,0 @@
|
||||
{
|
||||
"apiVersion": "argoproj.io/v1alpha1",
|
||||
"kind": "AnalysisRun",
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"rollout.argoproj.io/revision": "4"
|
||||
},
|
||||
"creationTimestamp": "2021-03-09T15:05:49Z",
|
||||
"generation": 11,
|
||||
"labels": {
|
||||
"backstage.io/kubernetes-id": "dice-roller",
|
||||
"rollout-type": "Step",
|
||||
"rollouts-pod-template-hash": "546c476497",
|
||||
"step-index": "1"
|
||||
},
|
||||
"name": "dice-roller-546c476497-4-1",
|
||||
"namespace": "default",
|
||||
"ownerReferences": [
|
||||
{
|
||||
"apiVersion": "argoproj.io/v1alpha1",
|
||||
"blockOwnerDeletion": true,
|
||||
"controller": true,
|
||||
"kind": "Rollout",
|
||||
"name": "dice-roller",
|
||||
"uid": "8552f08d-32e8-4f95-a43f-8524763eef60"
|
||||
}
|
||||
],
|
||||
"resourceVersion": "3342462005",
|
||||
"selfLink": "/apis/argoproj.io/v1alpha1/namespaces/default/analysisruns/dice-roller-546c476497-4-1",
|
||||
"uid": "b4e720ea-0488-42e2-8bd9-924c78843ee2"
|
||||
},
|
||||
"spec": {
|
||||
"metrics": [
|
||||
{
|
||||
"count": 5,
|
||||
"interval": "5s",
|
||||
"name": "always-pass",
|
||||
"provider": {
|
||||
"job": {
|
||||
"metadata": {
|
||||
"creationTimestamp": null
|
||||
},
|
||||
"spec": {
|
||||
"backoffLimit": 1,
|
||||
"template": {
|
||||
"metadata": {
|
||||
"creationTimestamp": null
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"image": "some-image",
|
||||
"name": "always-pass",
|
||||
"resources": {
|
||||
"limits": {
|
||||
"cpu": "800m",
|
||||
"memory": "1G"
|
||||
},
|
||||
"requests": {
|
||||
"cpu": "200m",
|
||||
"memory": "1G"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"restartPolicy": "Never"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"metricResults": [
|
||||
{
|
||||
"count": 5,
|
||||
"measurements": [
|
||||
{
|
||||
"finishedAt": "2021-03-09T15:06:13Z",
|
||||
"metadata": {
|
||||
"job-name": "b4e720ea-0488-42e2-8bd9-924c78843ee2.always-pass.1"
|
||||
},
|
||||
"phase": "Successful",
|
||||
"startedAt": "2021-03-09T15:05:49Z"
|
||||
},
|
||||
{
|
||||
"finishedAt": "2021-03-09T15:06:41Z",
|
||||
"metadata": {
|
||||
"job-name": "b4e720ea-0488-42e2-8bd9-924c78843ee2.always-pass.2"
|
||||
},
|
||||
"phase": "Successful",
|
||||
"startedAt": "2021-03-09T15:06:18Z"
|
||||
},
|
||||
{
|
||||
"finishedAt": "2021-03-09T15:07:08Z",
|
||||
"metadata": {
|
||||
"job-name": "b4e720ea-0488-42e2-8bd9-924c78843ee2.always-pass.3"
|
||||
},
|
||||
"phase": "Successful",
|
||||
"startedAt": "2021-03-09T15:06:46Z"
|
||||
},
|
||||
{
|
||||
"finishedAt": "2021-03-09T15:07:35Z",
|
||||
"metadata": {
|
||||
"job-name": "b4e720ea-0488-42e2-8bd9-924c78843ee2.always-pass.4"
|
||||
},
|
||||
"phase": "Successful",
|
||||
"startedAt": "2021-03-09T15:07:13Z"
|
||||
},
|
||||
{
|
||||
"finishedAt": "2021-03-09T15:08:02Z",
|
||||
"metadata": {
|
||||
"job-name": "b4e720ea-0488-42e2-8bd9-924c78843ee2.always-pass.5"
|
||||
},
|
||||
"phase": "Successful",
|
||||
"startedAt": "2021-03-09T15:07:40Z"
|
||||
}
|
||||
],
|
||||
"name": "always-pass",
|
||||
"phase": "Successful",
|
||||
"successful": 5
|
||||
}
|
||||
],
|
||||
"phase": "Successful",
|
||||
"startedAt": "2021-03-09T15:05:49Z"
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
export { CustomResources } from './CustomResources';
|
||||
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
* 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 * as deployments from '../../__fixtures__/2-deployments.json';
|
||||
import { renderInTestApp, textContentMatcher } from '@backstage/test-utils';
|
||||
import { DeploymentDrawer } from './DeploymentDrawer';
|
||||
|
||||
describe('DeploymentDrawer', () => {
|
||||
it('should render deployment drawer', async () => {
|
||||
const { getByText, getAllByText } = await renderInTestApp(
|
||||
<DeploymentDrawer
|
||||
deployment={(deployments as any).deployments[0]}
|
||||
expanded
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getAllByText('dice-roller')).toHaveLength(2);
|
||||
expect(getAllByText('Deployment')).toHaveLength(2);
|
||||
expect(getByText('YAML')).toBeInTheDocument();
|
||||
expect(getByText('Strategy')).toBeInTheDocument();
|
||||
expect(getByText('Rolling Update:')).toBeInTheDocument();
|
||||
expect(getByText(textContentMatcher('Max Surge: 25%'))).toBeInTheDocument();
|
||||
expect(
|
||||
getByText(textContentMatcher('Max Unavailable: 25%')),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
getByText(textContentMatcher('Type: RollingUpdate')),
|
||||
).toBeInTheDocument();
|
||||
expect(getByText('Min Ready Seconds')).toBeInTheDocument();
|
||||
expect(getByText('???')).toBeInTheDocument();
|
||||
expect(getByText('Progress Deadline Seconds')).toBeInTheDocument();
|
||||
expect(getByText('600')).toBeInTheDocument();
|
||||
expect(getByText('Progressing')).toBeInTheDocument();
|
||||
expect(getByText('Available')).toBeInTheDocument();
|
||||
expect(getByText('namespace: default')).toBeInTheDocument();
|
||||
expect(getAllByText('True')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should render deployment drawer without namespace', async () => {
|
||||
const deployment = (deployments as any).deployments[0];
|
||||
const { queryByText } = await renderInTestApp(
|
||||
<DeploymentDrawer
|
||||
deployment={{
|
||||
...deployment,
|
||||
metadata: { ...deployment.metadata, namespace: undefined },
|
||||
}}
|
||||
expanded
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(queryByText('namespace: default')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,78 +0,0 @@
|
||||
/*
|
||||
* 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 { V1Deployment } from '@kubernetes/client-node';
|
||||
import { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer';
|
||||
import { renderCondition } from '../../utils/pod';
|
||||
import { Typography, Grid, Chip } from '@material-ui/core';
|
||||
|
||||
export const DeploymentDrawer = ({
|
||||
deployment,
|
||||
expanded,
|
||||
}: {
|
||||
deployment: V1Deployment;
|
||||
expanded?: boolean;
|
||||
}) => {
|
||||
const namespace = deployment.metadata?.namespace;
|
||||
return (
|
||||
<KubernetesStructuredMetadataTableDrawer
|
||||
object={deployment}
|
||||
expanded={expanded}
|
||||
kind="Deployment"
|
||||
renderObject={(deploymentObj: V1Deployment) => {
|
||||
const conditions = (deploymentObj.status?.conditions ?? [])
|
||||
.map(renderCondition)
|
||||
.reduce((accum, next) => {
|
||||
accum[next[0]] = next[1];
|
||||
return accum;
|
||||
}, {} as { [key: string]: React.ReactNode });
|
||||
|
||||
return {
|
||||
strategy: deploymentObj.spec?.strategy ?? '???',
|
||||
minReadySeconds: deploymentObj.spec?.minReadySeconds ?? '???',
|
||||
progressDeadlineSeconds:
|
||||
deploymentObj.spec?.progressDeadlineSeconds ?? '???',
|
||||
...conditions,
|
||||
};
|
||||
}}
|
||||
>
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
spacing={0}
|
||||
>
|
||||
<Grid item>
|
||||
<Typography variant="body1">
|
||||
{deployment.metadata?.name ?? 'unknown object'}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography color="textSecondary" variant="subtitle1">
|
||||
Deployment
|
||||
</Typography>
|
||||
</Grid>
|
||||
{namespace && (
|
||||
<Grid item>
|
||||
<Chip size="small" label={`namespace: ${namespace}`} />
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</KubernetesStructuredMetadataTableDrawer>
|
||||
);
|
||||
};
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* 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 { screen } from '@testing-library/react';
|
||||
import { DeploymentsAccordions } from './DeploymentsAccordions';
|
||||
import * as twoDeployFixture from '../../__fixtures__/2-deployments.json';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { kubernetesProviders } from '../../hooks/test-utils';
|
||||
|
||||
describe('DeploymentsAccordions', () => {
|
||||
it('should render 2 deployments', async () => {
|
||||
const wrapper = kubernetesProviders(
|
||||
twoDeployFixture,
|
||||
new Set(['dice-roller-canary-7d64cd756c-vtbdx']),
|
||||
);
|
||||
|
||||
await renderInTestApp(wrapper(<DeploymentsAccordions />));
|
||||
|
||||
expect(screen.getByText('dice-roller')).toBeInTheDocument();
|
||||
expect(screen.getByText('10 pods')).toBeInTheDocument();
|
||||
expect(screen.getByText('No pods with errors')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('min replicas 10 / max replicas 15'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('current CPU usage: 30%')).toBeInTheDocument();
|
||||
expect(screen.getByText('target CPU usage: 50%')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('dice-roller-canary')).toBeInTheDocument();
|
||||
expect(screen.getByText('2 pods')).toBeInTheDocument();
|
||||
expect(screen.getByText('1 pod with errors')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,206 +0,0 @@
|
||||
/*
|
||||
* 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, { useContext } from 'react';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Grid,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import {
|
||||
V1Deployment,
|
||||
V1Pod,
|
||||
V1HorizontalPodAutoscaler,
|
||||
} from '@kubernetes/client-node';
|
||||
import { PodsTable } from '../Pods';
|
||||
import { DeploymentDrawer } from './DeploymentDrawer';
|
||||
import { HorizontalPodAutoscalerDrawer } from '../HorizontalPodAutoscalers';
|
||||
import {
|
||||
getOwnedPodsThroughReplicaSets,
|
||||
getMatchingHpa,
|
||||
} from '../../utils/owner';
|
||||
import {
|
||||
GroupedResponsesContext,
|
||||
PodNamesWithErrorsContext,
|
||||
} from '../../hooks';
|
||||
import { StatusError, StatusOK } from '@backstage/core-components';
|
||||
import { READY_COLUMNS, RESOURCE_COLUMNS } from '../Pods/PodsTable';
|
||||
|
||||
type DeploymentsAccordionsProps = {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
type DeploymentAccordionProps = {
|
||||
deployment: V1Deployment;
|
||||
ownedPods: V1Pod[];
|
||||
matchingHpa?: V1HorizontalPodAutoscaler;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
type DeploymentSummaryProps = {
|
||||
deployment: V1Deployment;
|
||||
numberOfCurrentPods: number;
|
||||
numberOfPodsWithErrors: number;
|
||||
hpa?: V1HorizontalPodAutoscaler;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const DeploymentSummary = ({
|
||||
deployment,
|
||||
numberOfCurrentPods,
|
||||
numberOfPodsWithErrors,
|
||||
hpa,
|
||||
}: DeploymentSummaryProps) => {
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
spacing={0}
|
||||
>
|
||||
<Grid xs={4} item>
|
||||
<DeploymentDrawer deployment={deployment} />
|
||||
</Grid>
|
||||
{hpa && (
|
||||
<Grid item xs={4}>
|
||||
<HorizontalPodAutoscalerDrawer hpa={hpa}>
|
||||
<Grid
|
||||
item
|
||||
container
|
||||
direction="column"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
spacing={0}
|
||||
>
|
||||
<Grid item>
|
||||
<Typography variant="subtitle2">
|
||||
min replicas {hpa.spec?.minReplicas ?? '?'} / max replicas{' '}
|
||||
{hpa.spec?.maxReplicas ?? '?'}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant="subtitle2">
|
||||
current CPU usage:{' '}
|
||||
{hpa.status?.currentCPUUtilizationPercentage ?? '?'}%
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant="subtitle2">
|
||||
target CPU usage:{' '}
|
||||
{hpa.spec?.targetCPUUtilizationPercentage ?? '?'}%
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</HorizontalPodAutoscalerDrawer>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid
|
||||
item
|
||||
container
|
||||
xs={4}
|
||||
direction="column"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-end"
|
||||
spacing={0}
|
||||
>
|
||||
<Grid item>
|
||||
<StatusOK>{numberOfCurrentPods} pods</StatusOK>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
{numberOfPodsWithErrors > 0 ? (
|
||||
<StatusError>
|
||||
{numberOfPodsWithErrors} pod
|
||||
{numberOfPodsWithErrors > 1 ? 's' : ''} with errors
|
||||
</StatusError>
|
||||
) : (
|
||||
<StatusOK>No pods with errors</StatusOK>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
const DeploymentAccordion = ({
|
||||
deployment,
|
||||
ownedPods,
|
||||
matchingHpa,
|
||||
}: DeploymentAccordionProps) => {
|
||||
const podNamesWithErrors = useContext(PodNamesWithErrorsContext);
|
||||
|
||||
const podsWithErrors = ownedPods.filter(p =>
|
||||
podNamesWithErrors.has(p.metadata?.name ?? ''),
|
||||
);
|
||||
|
||||
return (
|
||||
<Accordion TransitionProps={{ unmountOnExit: true }} variant="outlined">
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<DeploymentSummary
|
||||
deployment={deployment}
|
||||
numberOfCurrentPods={ownedPods.length}
|
||||
numberOfPodsWithErrors={podsWithErrors.length}
|
||||
hpa={matchingHpa}
|
||||
/>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<PodsTable
|
||||
pods={ownedPods}
|
||||
extraColumns={[READY_COLUMNS, RESOURCE_COLUMNS]}
|
||||
/>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
export const DeploymentsAccordions = ({}: DeploymentsAccordionsProps) => {
|
||||
const groupedResponses = useContext(GroupedResponsesContext);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
{groupedResponses.deployments.map((deployment, i) => (
|
||||
<Grid container item key={i} xs>
|
||||
<Grid item xs>
|
||||
<DeploymentAccordion
|
||||
matchingHpa={getMatchingHpa(
|
||||
{
|
||||
name: deployment.metadata?.name,
|
||||
namespace: deployment.metadata?.namespace,
|
||||
kind: 'deployment',
|
||||
},
|
||||
groupedResponses.horizontalPodAutoscalers,
|
||||
)}
|
||||
ownedPods={getOwnedPodsThroughReplicaSets(
|
||||
deployment,
|
||||
groupedResponses.replicaSets,
|
||||
groupedResponses.pods,
|
||||
)}
|
||||
deployment={deployment}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
export { DeploymentsAccordions } from './DeploymentsAccordions';
|
||||
@@ -1,113 +0,0 @@
|
||||
/*
|
||||
* 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 { screen } from '@testing-library/react';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { ErrorPanel } from './ErrorPanel';
|
||||
|
||||
describe('ErrorPanel', () => {
|
||||
it('displays path and status code when a cluster has an HTTP error', async () => {
|
||||
await renderInTestApp(
|
||||
<ErrorPanel
|
||||
entityName="THIS_ENTITY"
|
||||
clustersWithErrors={[
|
||||
{
|
||||
cluster: { name: 'THIS_CLUSTER' },
|
||||
resources: [],
|
||||
podMetrics: [],
|
||||
errors: [
|
||||
{
|
||||
errorType: 'SYSTEM_ERROR',
|
||||
statusCode: 500,
|
||||
resourcePath: 'some/resource',
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
// title
|
||||
expect(
|
||||
screen.getByText(
|
||||
'There was a problem retrieving some Kubernetes resources for the entity: THIS_ENTITY. This could mean that the Error Reporting card is not completely accurate.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// message
|
||||
expect(screen.getByText('Errors:')).toBeInTheDocument();
|
||||
expect(screen.getByText('Cluster: THIS_CLUSTER')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
"Error fetching Kubernetes resource: 'some/resource', error: SYSTEM_ERROR, status code: 500",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays message for non-HTTP-status-related fetch errors', async () => {
|
||||
await renderInTestApp(
|
||||
<ErrorPanel
|
||||
entityName="THIS_ENTITY"
|
||||
clustersWithErrors={[
|
||||
{
|
||||
cluster: { name: 'THIS_CLUSTER' },
|
||||
resources: [],
|
||||
podMetrics: [],
|
||||
errors: [
|
||||
{
|
||||
errorType: 'FETCH_ERROR',
|
||||
message: 'description of error',
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
// title
|
||||
expect(
|
||||
screen.getByText(
|
||||
'There was a problem retrieving some Kubernetes resources for the entity: THIS_ENTITY. This could mean that the Error Reporting card is not completely accurate.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// message
|
||||
expect(screen.getByText('Errors:')).toBeInTheDocument();
|
||||
expect(screen.getByText('Cluster: THIS_CLUSTER')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Error communicating with Kubernetes: FETCH_ERROR, message: description of error',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays error message', async () => {
|
||||
await renderInTestApp(
|
||||
<ErrorPanel entityName="THIS_ENTITY" errorMessage="SOME_ERROR_MESSAGE" />,
|
||||
);
|
||||
|
||||
// title
|
||||
expect(
|
||||
screen.getByText(
|
||||
'There was a problem retrieving some Kubernetes resources for the entity: THIS_ENTITY. This could mean that the Error Reporting card is not completely accurate.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// message
|
||||
expect(screen.getByText('Errors: SOME_ERROR_MESSAGE')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
* 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 { Typography } from '@material-ui/core';
|
||||
import { ClusterObjects } from '@backstage/plugin-kubernetes-common';
|
||||
import { WarningPanel } from '@backstage/core-components';
|
||||
|
||||
const clustersWithErrorsToErrorMessage = (
|
||||
clustersWithErrors: ClusterObjects[],
|
||||
): React.ReactNode => {
|
||||
return clustersWithErrors.map((c, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<Typography variant="body2">{`Cluster: ${c.cluster.name}`}</Typography>
|
||||
{c.errors.map((e, j) => {
|
||||
return (
|
||||
<Typography variant="body2" key={j}>
|
||||
{e.errorType === 'FETCH_ERROR'
|
||||
? `Error communicating with Kubernetes: ${e.errorType}, message: ${e.message}`
|
||||
: `Error fetching Kubernetes resource: '${e.resourcePath}', error: ${e.errorType}, status code: ${e.statusCode}`}
|
||||
</Typography>
|
||||
);
|
||||
})}
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
type ErrorPanelProps = {
|
||||
entityName: string;
|
||||
errorMessage?: string;
|
||||
clustersWithErrors?: ClusterObjects[];
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const ErrorPanel = ({
|
||||
entityName,
|
||||
errorMessage,
|
||||
clustersWithErrors,
|
||||
}: ErrorPanelProps) => (
|
||||
<WarningPanel
|
||||
title="There was a problem retrieving Kubernetes objects"
|
||||
message={`There was a problem retrieving some Kubernetes resources for the entity: ${entityName}. This could mean that the Error Reporting card is not completely accurate.`}
|
||||
>
|
||||
{clustersWithErrors && (
|
||||
<div>Errors: {clustersWithErrorsToErrorMessage(clustersWithErrors)}</div>
|
||||
)}
|
||||
{errorMessage && (
|
||||
<Typography variant="body2">Errors: {errorMessage}</Typography>
|
||||
)}
|
||||
</WarningPanel>
|
||||
);
|
||||
@@ -1,16 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
export { ErrorPanel } from './ErrorPanel';
|
||||
@@ -1,90 +0,0 @@
|
||||
/*
|
||||
* 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 * as React from 'react';
|
||||
import { DetectedError, DetectedErrorsByCluster } from '../../error-detection';
|
||||
import { Table, TableColumn } from '@backstage/core-components';
|
||||
|
||||
type ErrorReportingProps = {
|
||||
detectedErrors: DetectedErrorsByCluster;
|
||||
};
|
||||
|
||||
const columns: TableColumn<Row>[] = [
|
||||
{
|
||||
title: 'cluster',
|
||||
width: '10%',
|
||||
render: (row: Row) => row.clusterName,
|
||||
},
|
||||
{
|
||||
title: 'namespace',
|
||||
width: '10%',
|
||||
render: (row: Row) => row.error.sourceRef.namespace,
|
||||
},
|
||||
{
|
||||
title: 'kind',
|
||||
width: '10%',
|
||||
render: (row: Row) => row.error.sourceRef.kind,
|
||||
},
|
||||
{
|
||||
title: 'name',
|
||||
width: '30%',
|
||||
render: (row: Row) => {
|
||||
return <>{row.error.sourceRef.name} </>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'messages',
|
||||
width: '40%',
|
||||
render: (row: Row) => row.error.message,
|
||||
},
|
||||
];
|
||||
|
||||
interface Row {
|
||||
clusterName: string;
|
||||
error: DetectedError;
|
||||
}
|
||||
|
||||
const sortBySeverity = (a: Row, b: Row) => {
|
||||
if (a.error.severity < b.error.severity) {
|
||||
return 1;
|
||||
} else if (b.error.severity < a.error.severity) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const ErrorReporting = ({ detectedErrors }: ErrorReportingProps) => {
|
||||
const errors = Array.from(detectedErrors.entries())
|
||||
.flatMap(([clusterName, resourceErrors]) => {
|
||||
return resourceErrors.map(e => ({
|
||||
clusterName,
|
||||
error: e,
|
||||
}));
|
||||
})
|
||||
.sort(sortBySeverity);
|
||||
|
||||
return (
|
||||
<>
|
||||
{errors.length !== 0 && (
|
||||
<Table
|
||||
title="Error Reporting"
|
||||
data={errors}
|
||||
columns={columns}
|
||||
options={{ paging: true, search: false, emptyRowsWhenPaging: false }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
export { ErrorReporting } from './ErrorReporting';
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
* 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 { screen } from '@testing-library/react';
|
||||
import * as hpas from './__fixtures__/horizontalpodautoscalers.json';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { HorizontalPodAutoscalerDrawer } from './HorizontalPodAutoscalerDrawer';
|
||||
|
||||
describe('HorizontalPodAutoscalersDrawer', () => {
|
||||
it('should render hpa drawer', async () => {
|
||||
await renderInTestApp(
|
||||
<HorizontalPodAutoscalerDrawer hpa={hpas[0] as any} expanded>
|
||||
<h1>CHILD</h1>
|
||||
</HorizontalPodAutoscalerDrawer>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('dice-roller')).toBeInTheDocument();
|
||||
expect(screen.getByText('CHILD')).toBeInTheDocument();
|
||||
expect(screen.getByText('HorizontalPodAutoscaler')).toBeInTheDocument();
|
||||
expect(screen.getByText('YAML')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Target CPU Utilization Percentage'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('50')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Current CPU Utilization Percentage'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('30')).toBeInTheDocument();
|
||||
expect(screen.getByText('Min Replicas')).toBeInTheDocument();
|
||||
expect(screen.getByText('10')).toBeInTheDocument();
|
||||
expect(screen.getByText('Max Replicas')).toBeInTheDocument();
|
||||
expect(screen.getByText('15')).toBeInTheDocument();
|
||||
expect(screen.getByText('Current Replicas')).toBeInTheDocument();
|
||||
expect(screen.getByText('13')).toBeInTheDocument();
|
||||
expect(screen.getByText('Desired Replicas')).toBeInTheDocument();
|
||||
expect(screen.getByText('14')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* 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 { V1HorizontalPodAutoscaler } from '@kubernetes/client-node';
|
||||
import { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer';
|
||||
|
||||
/** @public */
|
||||
export const HorizontalPodAutoscalerDrawer = (props: {
|
||||
hpa: V1HorizontalPodAutoscaler;
|
||||
expanded?: boolean;
|
||||
children?: React.ReactNode;
|
||||
}) => {
|
||||
const { hpa, expanded, children } = props;
|
||||
|
||||
return (
|
||||
<KubernetesStructuredMetadataTableDrawer
|
||||
kind="HorizontalPodAutoscaler"
|
||||
object={hpa}
|
||||
expanded={expanded}
|
||||
renderObject={(hpaObject: V1HorizontalPodAutoscaler) => {
|
||||
return {
|
||||
targetCPUUtilizationPercentage:
|
||||
hpaObject.spec?.targetCPUUtilizationPercentage,
|
||||
currentCPUUtilizationPercentage:
|
||||
hpaObject.status?.currentCPUUtilizationPercentage,
|
||||
minReplicas: hpaObject.spec?.minReplicas,
|
||||
maxReplicas: hpaObject.spec?.maxReplicas,
|
||||
currentReplicas: hpaObject.status?.currentReplicas,
|
||||
desiredReplicas: hpaObject.status?.desiredReplicas,
|
||||
};
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</KubernetesStructuredMetadataTableDrawer>
|
||||
);
|
||||
};
|
||||
-82
@@ -1,82 +0,0 @@
|
||||
[
|
||||
{
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"autoscaling.alpha.kubernetes.io/conditions": "[{\"type\":\"AbleToScale\",\"status\":\"True\",\"lastTransitionTime\":\"2020-09-28T13:28:15Z\",\"reason\":\"SucceededGetScale\",\"message\":\"the HPA controller was able to get the target's current scale\"},{\"type\":\"ScalingActive\",\"status\":\"False\",\"lastTransitionTime\":\"2020-09-28T13:28:15Z\",\"reason\":\"FailedGetResourceMetric\",\"message\":\"the HPA was unable to compute the replica count: unable to get metrics for resource cpu: unable to fetch metrics from resource metrics API: the server could not find the requested resource (get pods.metrics.k8s.io)\"}]",
|
||||
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"autoscaling/v1\",\"kind\":\"HorizontalPodAutoscaler\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\"},\"name\":\"dice-roller\",\"namespace\":\"default\"},\"spec\":{\"maxReplicas\":15,\"minReplicas\":10,\"scaleTargetRef\":{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"name\":\"dice-roller\"},\"targetCPUUtilizationPercentage\":50}}\n"
|
||||
},
|
||||
"creationTimestamp": "2020-09-28T13:28:00.000Z",
|
||||
"labels": {
|
||||
"backstage.io/kubernetes-id": "dice-roller"
|
||||
},
|
||||
"managedFields": [
|
||||
{
|
||||
"apiVersion": "autoscaling/v1",
|
||||
"fieldsType": "FieldsV1",
|
||||
"fieldsV1": {
|
||||
"f:metadata": {
|
||||
"f:annotations": {
|
||||
"f:autoscaling.alpha.kubernetes.io/conditions": {}
|
||||
}
|
||||
},
|
||||
"f:status": {
|
||||
"f:currentReplicas": {}
|
||||
}
|
||||
},
|
||||
"manager": "kube-controller-manager",
|
||||
"operation": "Update",
|
||||
"time": "2020-09-28T13:28:15.000Z"
|
||||
},
|
||||
{
|
||||
"apiVersion": "autoscaling/v1",
|
||||
"fieldsType": "FieldsV1",
|
||||
"fieldsV1": {
|
||||
"f:metadata": {
|
||||
"f:annotations": {
|
||||
".": {},
|
||||
"f:kubectl.kubernetes.io/last-applied-configuration": {}
|
||||
},
|
||||
"f:labels": {
|
||||
".": {},
|
||||
"f:backstage.io/kubernetes-id": {}
|
||||
}
|
||||
},
|
||||
"f:spec": {
|
||||
"f:maxReplicas": {},
|
||||
"f:minReplicas": {},
|
||||
"f:scaleTargetRef": {
|
||||
"f:apiVersion": {},
|
||||
"f:kind": {},
|
||||
"f:name": {}
|
||||
},
|
||||
"f:targetCPUUtilizationPercentage": {}
|
||||
}
|
||||
},
|
||||
"manager": "kubectl",
|
||||
"operation": "Update",
|
||||
"time": "2020-09-28T13:28:21.000Z"
|
||||
}
|
||||
],
|
||||
"name": "dice-roller",
|
||||
"namespace": "default",
|
||||
"resourceVersion": "698957",
|
||||
"selfLink": "/apis/autoscaling/v1/namespaces/default/horizontalpodautoscalers/dice-roller",
|
||||
"uid": "a70c8a90-5605-4d7d-adea-05cfb8d9d446"
|
||||
},
|
||||
"spec": {
|
||||
"maxReplicas": 15,
|
||||
"minReplicas": 10,
|
||||
"scaleTargetRef": {
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"name": "dice-roller"
|
||||
},
|
||||
"targetCPUUtilizationPercentage": 50
|
||||
},
|
||||
"status": {
|
||||
"currentReplicas": 13,
|
||||
"desiredReplicas": 14,
|
||||
"currentCPUUtilizationPercentage": 30
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,17 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { HorizontalPodAutoscalerDrawer } from './HorizontalPodAutoscalerDrawer';
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* 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 { renderInTestApp, textContentMatcher } from '@backstage/test-utils';
|
||||
import { screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { IngressDrawer } from './IngressDrawer';
|
||||
import * as ingresses from './__fixtures__/2-ingresses.json';
|
||||
|
||||
describe('IngressDrawer', () => {
|
||||
it('should render ingress drawer', async () => {
|
||||
await renderInTestApp(
|
||||
<IngressDrawer ingress={(ingresses as any).ingresses[0]} expanded />,
|
||||
);
|
||||
|
||||
expect(screen.getAllByText('awesome-service')).toHaveLength(4);
|
||||
expect(screen.getByText('YAML')).toBeInTheDocument();
|
||||
expect(screen.getByText('Rules')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(textContentMatcher('Host: api.awesome-host.io')),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getAllByText(textContentMatcher('Service Port: 80')),
|
||||
).toHaveLength(2);
|
||||
expect(
|
||||
screen.getAllByText(textContentMatcher('Service Name: awesome-service')),
|
||||
).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* 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 { V1Ingress } from '@kubernetes/client-node';
|
||||
import { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer';
|
||||
import { Typography, Grid } from '@material-ui/core';
|
||||
|
||||
export const IngressDrawer = ({
|
||||
ingress,
|
||||
expanded,
|
||||
}: {
|
||||
ingress: V1Ingress;
|
||||
expanded?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<KubernetesStructuredMetadataTableDrawer
|
||||
object={ingress}
|
||||
expanded={expanded}
|
||||
kind="Ingress"
|
||||
renderObject={(ingressObject: V1Ingress) => {
|
||||
return ingressObject.spec || {};
|
||||
}}
|
||||
>
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
spacing={0}
|
||||
>
|
||||
<Grid item>
|
||||
<Typography variant="body1">
|
||||
{ingress.metadata?.name ?? 'unknown object'}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography color="textSecondary" variant="subtitle1">
|
||||
Ingress
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</KubernetesStructuredMetadataTableDrawer>
|
||||
);
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* 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 { screen } from '@testing-library/react';
|
||||
import * as oneIngressFixture from './__fixtures__/2-ingresses.json';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { IngressesAccordions } from './IngressesAccordions';
|
||||
import { kubernetesProviders } from '../../hooks/test-utils';
|
||||
|
||||
describe('IngressesAccordions', () => {
|
||||
it('should render 1 ingress', async () => {
|
||||
const wrapper = kubernetesProviders(oneIngressFixture, new Set());
|
||||
|
||||
await renderInTestApp(wrapper(<IngressesAccordions />));
|
||||
|
||||
expect(screen.getByText('awesome-service')).toBeInTheDocument();
|
||||
expect(screen.getByText('Ingress')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,97 +0,0 @@
|
||||
/*
|
||||
* 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, { useContext } from 'react';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Grid,
|
||||
} from '@material-ui/core';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import { V1Ingress } from '@kubernetes/client-node';
|
||||
import { IngressDrawer } from './IngressDrawer';
|
||||
import { GroupedResponsesContext } from '../../hooks';
|
||||
import { StructuredMetadataTable } from '@backstage/core-components';
|
||||
|
||||
type IngressesAccordionsProps = {};
|
||||
|
||||
type IngressAccordionProps = {
|
||||
ingress: V1Ingress;
|
||||
};
|
||||
|
||||
type IngressSummaryProps = {
|
||||
ingress: V1Ingress;
|
||||
};
|
||||
|
||||
const IngressSummary = ({ ingress }: IngressSummaryProps) => {
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="center"
|
||||
>
|
||||
<Grid xs={12} item>
|
||||
<IngressDrawer ingress={ingress} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
type IngressCardProps = {
|
||||
ingress: V1Ingress;
|
||||
};
|
||||
|
||||
const IngressCard = ({ ingress }: IngressCardProps) => {
|
||||
return (
|
||||
<StructuredMetadataTable
|
||||
metadata={{
|
||||
...ingress.spec,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const IngressAccordion = ({ ingress }: IngressAccordionProps) => {
|
||||
return (
|
||||
<Accordion TransitionProps={{ unmountOnExit: true }}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<IngressSummary ingress={ingress} />
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<IngressCard ingress={ingress} />
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
export const IngressesAccordions = ({}: IngressesAccordionsProps) => {
|
||||
const groupedResponses = useContext(GroupedResponsesContext);
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
{groupedResponses.ingresses.map((ingress, i) => (
|
||||
<Grid item key={i} xs>
|
||||
<IngressAccordion ingress={ingress} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"ingresses": [
|
||||
{
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"artifact.spinnaker.io/location": "default",
|
||||
"artifact.spinnaker.io/name": "awesome-service",
|
||||
"artifact.spinnaker.io/type": "kubernetes/ingress",
|
||||
"kubernetes.io/ingress.class": "traefik",
|
||||
"kubernetes.io/ingress.global-static-ip-name": "traefik-tcp-lb",
|
||||
"moniker.spinnaker.io/application": "awesome-service",
|
||||
"moniker.spinnaker.io/cluster": "ingress awesome-service"
|
||||
},
|
||||
"creationTimestamp": "2018-11-16T14:00:13.000Z",
|
||||
"generation": 11,
|
||||
"labels": {
|
||||
"app": "awesome-service",
|
||||
"app.kubernetes.io/managed-by": "spinnaker",
|
||||
"app.kubernetes.io/name": "awesome-service"
|
||||
},
|
||||
"name": "awesome-service",
|
||||
"namespace": "default",
|
||||
"resourceVersion": "564824116",
|
||||
"selfLink": "/apis/networking.k8s.io/v1beta1/namespaces/default/ingresses/awesome-service",
|
||||
"uid": "f072e0b4-e9a7-11e8-af65-42010a9c0022"
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"host": "api.awesome-host.io",
|
||||
"http": {
|
||||
"paths": [
|
||||
{
|
||||
"backend": {
|
||||
"serviceName": "awesome-service",
|
||||
"servicePort": 80
|
||||
},
|
||||
"path": "/v1/awesome-service"
|
||||
},
|
||||
{
|
||||
"backend": {
|
||||
"serviceName": "awesome-service",
|
||||
"servicePort": 80
|
||||
},
|
||||
"path": "/v1/awesome-services"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"loadBalancer": {}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
export { IngressesAccordions } from './IngressesAccordions';
|
||||
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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 { screen } from '@testing-library/react';
|
||||
import { JobsAccordions } from './JobsAccordions';
|
||||
import * as oneCronJobsFixture from '../../__fixtures__/1-cronjobs.json';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { kubernetesProviders } from '../../hooks/test-utils';
|
||||
import { V1Job, ObjectSerializer } from '@kubernetes/client-node';
|
||||
|
||||
describe('JobsAccordions', () => {
|
||||
it('should render 2 jobs', async () => {
|
||||
const wrapper = kubernetesProviders(oneCronJobsFixture, new Set<string>());
|
||||
|
||||
const jobs: V1Job[] = oneCronJobsFixture.jobs.map(
|
||||
job => ObjectSerializer.deserialize(job, 'V1Job') as V1Job,
|
||||
);
|
||||
|
||||
await renderInTestApp(wrapper(<JobsAccordions jobs={jobs} />));
|
||||
|
||||
expect(
|
||||
screen.getByText('dice-roller-cronjob-1637028600'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Running')).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByText('dice-roller-cronjob-1637025000'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Succeeded')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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, { useContext } from 'react';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Grid,
|
||||
} from '@material-ui/core';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import { V1Job, V1Pod } from '@kubernetes/client-node';
|
||||
import { PodsTable } from '../Pods';
|
||||
import { JobDrawer } from './JobsDrawer';
|
||||
import { getOwnedResources } from '../../utils/owner';
|
||||
import { GroupedResponsesContext } from '../../hooks';
|
||||
import {
|
||||
StatusError,
|
||||
StatusOK,
|
||||
StatusPending,
|
||||
} from '@backstage/core-components';
|
||||
|
||||
type JobsAccordionsProps = {
|
||||
jobs: V1Job[];
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
type JobAccordionProps = {
|
||||
job: V1Job;
|
||||
ownedPods: V1Pod[];
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
type JobSummaryProps = {
|
||||
job: V1Job;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const JobSummary = ({ job }: JobSummaryProps) => {
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
spacing={0}
|
||||
>
|
||||
<Grid xs={6} item>
|
||||
<JobDrawer job={job} />
|
||||
</Grid>
|
||||
<Grid
|
||||
item
|
||||
container
|
||||
xs={6}
|
||||
direction="column"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-end"
|
||||
spacing={0}
|
||||
>
|
||||
<Grid item>
|
||||
{job.status?.succeeded && <StatusOK>Succeeded</StatusOK>}
|
||||
{job.status?.active && <StatusPending>Running</StatusPending>}
|
||||
{job.status?.failed && <StatusError>Failed</StatusError>}
|
||||
</Grid>
|
||||
<Grid item>Start time: {job.status?.startTime?.toString()}</Grid>
|
||||
{job.status?.completionTime && (
|
||||
<Grid item>
|
||||
Completion time: {job.status.completionTime.toString()}
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
const JobAccordion = ({ job, ownedPods }: JobAccordionProps) => {
|
||||
return (
|
||||
<Accordion TransitionProps={{ unmountOnExit: true }} variant="outlined">
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<JobSummary job={job} />
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<PodsTable pods={ownedPods} />
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
export const JobsAccordions = ({ jobs }: JobsAccordionsProps) => {
|
||||
const groupedResponses = useContext(GroupedResponsesContext);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
{jobs.map((job, i) => (
|
||||
<Grid container item key={i} xs>
|
||||
<Grid item xs>
|
||||
<JobAccordion
|
||||
ownedPods={getOwnedResources(job, groupedResponses.pods)}
|
||||
job={job}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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 * as oneCronJobsFixture from '../../__fixtures__/1-cronjobs.json';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { JobDrawer } from './JobsDrawer';
|
||||
|
||||
describe('JobDrawer', () => {
|
||||
it('should render job drawer', async () => {
|
||||
const { getByText, getAllByText } = await renderInTestApp(
|
||||
<JobDrawer job={(oneCronJobsFixture as any).jobs[0]} expanded />,
|
||||
);
|
||||
|
||||
expect(getAllByText('dice-roller-cronjob-1637025000')).toHaveLength(2);
|
||||
expect(getAllByText('Job')).toHaveLength(2);
|
||||
expect(getByText('YAML')).toBeInTheDocument();
|
||||
expect(getByText('Parallelism')).toBeInTheDocument();
|
||||
expect(getByText('Completions')).toBeInTheDocument();
|
||||
expect(getByText('Backoff Limit')).toBeInTheDocument();
|
||||
expect(getByText('Start Time')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,62 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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 { V1Job } from '@kubernetes/client-node';
|
||||
import { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer';
|
||||
import { Typography, Grid } from '@material-ui/core';
|
||||
|
||||
export const JobDrawer = ({
|
||||
job,
|
||||
expanded,
|
||||
}: {
|
||||
job: V1Job;
|
||||
expanded?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<KubernetesStructuredMetadataTableDrawer
|
||||
object={job}
|
||||
expanded={expanded}
|
||||
kind="Job"
|
||||
renderObject={(jobObj: V1Job) => {
|
||||
return {
|
||||
parallelism: jobObj.spec?.parallelism ?? '???',
|
||||
completions: jobObj.spec?.completions ?? '???',
|
||||
backoffLimit: jobObj.spec?.backoffLimit ?? '???',
|
||||
startTime: jobObj.status?.startTime ?? '???',
|
||||
};
|
||||
}}
|
||||
>
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
spacing={0}
|
||||
>
|
||||
<Grid item>
|
||||
<Typography variant="body1">
|
||||
{job.metadata?.name ?? 'unknown object'}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography color="textSecondary" variant="subtitle1">
|
||||
Job
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</KubernetesStructuredMetadataTableDrawer>
|
||||
);
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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 { JobsAccordions } from './JobsAccordions';
|
||||
@@ -1,183 +0,0 @@
|
||||
/*
|
||||
* 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, { ChangeEvent, useState } from 'react';
|
||||
|
||||
import { IObjectMeta } from '@kubernetes-models/apimachinery/apis/meta/v1/ObjectMeta';
|
||||
import {
|
||||
createStyles,
|
||||
Drawer,
|
||||
makeStyles,
|
||||
Theme,
|
||||
Grid,
|
||||
IconButton,
|
||||
Switch,
|
||||
Typography,
|
||||
Button,
|
||||
withStyles,
|
||||
FormControlLabel,
|
||||
} from '@material-ui/core';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import { ManifestYaml } from './ManifestYaml';
|
||||
|
||||
const useDrawerContentStyles = makeStyles((_theme: Theme) =>
|
||||
createStyles({
|
||||
header: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
content: {
|
||||
height: '80%',
|
||||
},
|
||||
icon: {
|
||||
fontSize: 20,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
interface KubernetesObject {
|
||||
kind: string;
|
||||
metadata?: IObjectMeta;
|
||||
}
|
||||
|
||||
interface KubernetesDrawerContentProps {
|
||||
close: () => void;
|
||||
kubernetesObject: KubernetesObject;
|
||||
header?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const KubernetesDrawerContent = ({
|
||||
children,
|
||||
header,
|
||||
kubernetesObject,
|
||||
close,
|
||||
}: KubernetesDrawerContentProps) => {
|
||||
const classes = useDrawerContentStyles();
|
||||
const [isYaml, setIsYaml] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes.header}>
|
||||
<Grid container justifyContent="flex-start" alignItems="flex-start">
|
||||
<Grid item xs={11}>
|
||||
<Typography variant="h5">
|
||||
{kubernetesObject.metadata?.name}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={1}>
|
||||
<IconButton
|
||||
key="dismiss"
|
||||
title="Close the drawer"
|
||||
onClick={() => close()}
|
||||
color="inherit"
|
||||
>
|
||||
<CloseIcon className={classes.icon} />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
{header}
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={isYaml}
|
||||
onChange={event => {
|
||||
setIsYaml(event.target.checked);
|
||||
}}
|
||||
name="YAML"
|
||||
/>
|
||||
}
|
||||
label="YAML"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
<div className={classes.content}>
|
||||
{isYaml && <ManifestYaml object={kubernetesObject} />}
|
||||
{!isYaml && children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface KubernetesDrawerProps {
|
||||
open?: boolean;
|
||||
kubernetesObject: KubernetesObject;
|
||||
label: React.ReactNode;
|
||||
drawerContentsHeader?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const useDrawerStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
paper: {
|
||||
width: '50%',
|
||||
justifyContent: 'space-between',
|
||||
padding: theme.spacing(2.5),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const DrawerButton = withStyles({
|
||||
root: {
|
||||
padding: '6px 5px',
|
||||
},
|
||||
label: {
|
||||
textTransform: 'none',
|
||||
},
|
||||
})(Button);
|
||||
|
||||
export const KubernetesDrawer = ({
|
||||
open,
|
||||
label,
|
||||
drawerContentsHeader,
|
||||
kubernetesObject,
|
||||
children,
|
||||
}: KubernetesDrawerProps) => {
|
||||
const classes = useDrawerStyles();
|
||||
const [isOpen, setIsOpen] = useState<boolean>(open ?? false);
|
||||
|
||||
const toggleDrawer = (e: ChangeEvent<{}>, newValue: boolean) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DrawerButton onClick={() => setIsOpen(true)}>{label}</DrawerButton>
|
||||
<Drawer
|
||||
classes={{
|
||||
paper: classes.paper,
|
||||
}}
|
||||
anchor="right"
|
||||
open={isOpen}
|
||||
onClose={(e: any) => toggleDrawer(e, false)}
|
||||
onClick={event => event.stopPropagation()}
|
||||
>
|
||||
{isOpen && (
|
||||
<KubernetesDrawerContent
|
||||
header={drawerContentsHeader}
|
||||
kubernetesObject={kubernetesObject}
|
||||
children={children}
|
||||
close={() => setIsOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
-298
@@ -1,298 +0,0 @@
|
||||
/*
|
||||
* 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, { ChangeEvent, useContext, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Typography,
|
||||
makeStyles,
|
||||
IconButton,
|
||||
createStyles,
|
||||
Theme,
|
||||
Drawer,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
Grid,
|
||||
} from '@material-ui/core';
|
||||
import Close from '@material-ui/icons/Close';
|
||||
import OpenInNewIcon from '@material-ui/icons/OpenInNew';
|
||||
import { V1ObjectMeta } from '@kubernetes/client-node';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import {
|
||||
LinkButton as BackstageButton,
|
||||
StructuredMetadataTable,
|
||||
WarningPanel,
|
||||
} from '@backstage/core-components';
|
||||
import { ClusterContext } from '../../hooks';
|
||||
import { formatClusterLink } from '../../utils/clusterLinks';
|
||||
import { ClusterAttributes } from '@backstage/plugin-kubernetes-common';
|
||||
import { FormatClusterLinkOptions } from '../../utils/clusterLinks/formatClusterLink';
|
||||
import { ManifestYaml } from './ManifestYaml';
|
||||
|
||||
const useDrawerStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
paper: {
|
||||
width: '50%',
|
||||
justifyContent: 'space-between',
|
||||
padding: theme.spacing(2.5),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const useDrawerContentStyles = makeStyles((_: Theme) =>
|
||||
createStyles({
|
||||
header: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
errorMessage: {
|
||||
marginTop: '1em',
|
||||
marginBottom: '1em',
|
||||
},
|
||||
options: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
icon: {
|
||||
fontSize: 20,
|
||||
},
|
||||
content: {
|
||||
height: '80%',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const PodDrawerButton = withStyles({
|
||||
root: {
|
||||
padding: '6px 5px',
|
||||
},
|
||||
label: {
|
||||
textTransform: 'none',
|
||||
},
|
||||
})(Button);
|
||||
|
||||
type ErrorPanelProps = {
|
||||
cluster: ClusterAttributes;
|
||||
errorMessage?: string;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const LinkErrorPanel = ({ cluster, errorMessage }: ErrorPanelProps) => (
|
||||
<WarningPanel
|
||||
title="There was a problem formatting the link to the Kubernetes dashboard"
|
||||
message={`Could not format the link to the dashboard of your cluster named '${
|
||||
cluster.name
|
||||
}'. Its dashboardApp property has been set to '${
|
||||
cluster.dashboardApp || 'standard'
|
||||
}.'`}
|
||||
>
|
||||
{errorMessage && (
|
||||
<Typography variant="body2">Errors: {errorMessage}</Typography>
|
||||
)}
|
||||
</WarningPanel>
|
||||
);
|
||||
|
||||
interface KubernetesDrawerable {
|
||||
metadata?: V1ObjectMeta;
|
||||
}
|
||||
|
||||
interface KubernetesStructuredMetadataTableDrawerContentProps<
|
||||
T extends KubernetesDrawerable,
|
||||
> {
|
||||
toggleDrawer: (e: ChangeEvent<{}>, isOpen: boolean) => void;
|
||||
object: T;
|
||||
renderObject: (obj: T) => object;
|
||||
kind: string;
|
||||
}
|
||||
|
||||
function replaceNullsWithUndefined(someObj: any) {
|
||||
const replacer = (_: any, value: any) =>
|
||||
String(value) === 'null' || String(value) === 'undefined'
|
||||
? undefined
|
||||
: value;
|
||||
|
||||
return JSON.parse(JSON.stringify(someObj, replacer));
|
||||
}
|
||||
|
||||
function tryFormatClusterLink(options: FormatClusterLinkOptions) {
|
||||
try {
|
||||
return {
|
||||
clusterLink: formatClusterLink(options),
|
||||
errorMessage: '',
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
clusterLink: '',
|
||||
errorMessage: err.message || err.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const KubernetesStructuredMetadataTableDrawerContent = <
|
||||
T extends KubernetesDrawerable,
|
||||
>({
|
||||
toggleDrawer,
|
||||
object,
|
||||
renderObject,
|
||||
kind,
|
||||
}: KubernetesStructuredMetadataTableDrawerContentProps<T>) => {
|
||||
const [isYaml, setIsYaml] = useState<boolean>(false);
|
||||
|
||||
const classes = useDrawerContentStyles();
|
||||
const cluster = useContext(ClusterContext);
|
||||
const { clusterLink, errorMessage } = tryFormatClusterLink({
|
||||
dashboardUrl: cluster.dashboardUrl,
|
||||
dashboardApp: cluster.dashboardApp,
|
||||
dashboardParameters: cluster.dashboardParameters,
|
||||
object,
|
||||
kind,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes.header}>
|
||||
<Grid container justifyContent="flex-start" alignItems="flex-start">
|
||||
<Grid item xs={11}>
|
||||
<Typography variant="h5">
|
||||
{object.metadata?.name ?? 'unknown name'}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={1}>
|
||||
<IconButton
|
||||
key="dismiss"
|
||||
title="Close the drawer"
|
||||
onClick={e => toggleDrawer(e, false)}
|
||||
color="inherit"
|
||||
>
|
||||
<Close className={classes.icon} />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid item xs={11}>
|
||||
<Typography color="textSecondary" variant="body1">
|
||||
{kind}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={11}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={isYaml}
|
||||
onChange={event => {
|
||||
setIsYaml(event.target.checked);
|
||||
}}
|
||||
name="YAML"
|
||||
/>
|
||||
}
|
||||
label="YAML"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
{errorMessage && (
|
||||
<div className={classes.errorMessage}>
|
||||
<LinkErrorPanel cluster={cluster} errorMessage={errorMessage} />
|
||||
</div>
|
||||
)}
|
||||
<div className={classes.options}>
|
||||
<div>
|
||||
{clusterLink && (
|
||||
<BackstageButton
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
size="small"
|
||||
to={clusterLink}
|
||||
endIcon={<OpenInNewIcon />}
|
||||
>
|
||||
Open Kubernetes Dashboard
|
||||
</BackstageButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.content}>
|
||||
{isYaml && <ManifestYaml object={object} />}
|
||||
{!isYaml && (
|
||||
<StructuredMetadataTable
|
||||
metadata={renderObject(replaceNullsWithUndefined(object))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
interface KubernetesStructuredMetadataTableDrawerProps<
|
||||
T extends KubernetesDrawerable,
|
||||
> {
|
||||
object: T;
|
||||
renderObject: (obj: T) => object;
|
||||
buttonVariant?: 'h5' | 'subtitle2';
|
||||
kind: string;
|
||||
expanded?: boolean;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const KubernetesStructuredMetadataTableDrawer = <
|
||||
T extends KubernetesDrawerable,
|
||||
>({
|
||||
object,
|
||||
renderObject,
|
||||
kind,
|
||||
buttonVariant = 'subtitle2',
|
||||
expanded = false,
|
||||
children,
|
||||
}: KubernetesStructuredMetadataTableDrawerProps<T>) => {
|
||||
const [isOpen, setIsOpen] = useState(expanded);
|
||||
const classes = useDrawerStyles();
|
||||
|
||||
const toggleDrawer = (e: ChangeEvent<{}>, newValue: boolean) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PodDrawerButton
|
||||
onClick={e => toggleDrawer(e, true)}
|
||||
onFocus={event => event.stopPropagation()}
|
||||
>
|
||||
{children === undefined ? (
|
||||
<Typography variant={buttonVariant}>
|
||||
{object.metadata?.name ?? 'unknown object'}
|
||||
</Typography>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</PodDrawerButton>
|
||||
<Drawer
|
||||
classes={{
|
||||
paper: classes.paper,
|
||||
}}
|
||||
anchor="right"
|
||||
open={isOpen}
|
||||
onClose={(e: any) => toggleDrawer(e, false)}
|
||||
onClick={event => event.stopPropagation()}
|
||||
>
|
||||
<KubernetesStructuredMetadataTableDrawerContent
|
||||
kind={kind}
|
||||
toggleDrawer={toggleDrawer}
|
||||
object={object}
|
||||
renderObject={renderObject}
|
||||
/>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
* 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 { CodeSnippet } from '@backstage/core-components';
|
||||
import { FormControlLabel, Switch } from '@material-ui/core';
|
||||
import jsyaml from 'js-yaml';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
export interface ManifestYamlProps {
|
||||
object: object;
|
||||
}
|
||||
|
||||
export const ManifestYaml = ({ object }: ManifestYamlProps) => {
|
||||
// Toggle whether the Kubernetes resource managed fields should be shown in
|
||||
// the YAML display. This toggle is only available when the YAML is being
|
||||
// shown because managed fields are never visible in the structured display.
|
||||
const [managedFields, setManagedFields] = useState<boolean>(false);
|
||||
return (
|
||||
<>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={managedFields}
|
||||
onChange={event => {
|
||||
setManagedFields(event.target.checked);
|
||||
}}
|
||||
name="Managed Fields"
|
||||
/>
|
||||
}
|
||||
label="Managed Fields"
|
||||
/>
|
||||
<CodeSnippet
|
||||
language="yaml"
|
||||
text={jsyaml.dump(object, {
|
||||
// NOTE: this will remove any field called `managedFields`
|
||||
// not just the metadata one
|
||||
// TODO: @mclarke make this only remove the `metadata.managedFields`
|
||||
replacer: (key: string, value: string): any => {
|
||||
if (!managedFields) {
|
||||
return key === 'managedFields' ? undefined : value;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 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 * from './KubernetesStructuredMetadataTableDrawer';
|
||||
export * from './KubernetesDrawer';
|
||||
@@ -1,97 +0,0 @@
|
||||
/*
|
||||
* 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 { DiscoveryApi, discoveryApiRef } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
renderInTestApp,
|
||||
TestApiProvider,
|
||||
textContentMatcher,
|
||||
} from '@backstage/test-utils';
|
||||
import '@testing-library/jest-dom';
|
||||
import { screen } from '@testing-library/react';
|
||||
import WS from 'jest-websocket-mock';
|
||||
import React from 'react';
|
||||
import './matchMedia.mock';
|
||||
import { PodExecTerminal } from './PodExecTerminal';
|
||||
|
||||
global.TextEncoder = require('util').TextEncoder;
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
|
||||
describe('PodExecTerminal', () => {
|
||||
const clusterName = 'cluster1';
|
||||
const containerName = 'container2';
|
||||
const podName = 'pod1';
|
||||
const podNamespace = 'podNamespace';
|
||||
|
||||
const mockDiscoveryApi: Partial<DiscoveryApi> = {
|
||||
getBaseUrl: () => Promise.resolve('http://localhost'),
|
||||
};
|
||||
|
||||
it('Should render an XTerm web terminal', async () => {
|
||||
await renderInTestApp(
|
||||
<TestApiProvider apis={[[discoveryApiRef, mockDiscoveryApi]]}>
|
||||
<PodExecTerminal
|
||||
clusterName={clusterName}
|
||||
containerName={containerName}
|
||||
podName={podName}
|
||||
podNamespace={podNamespace}
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
await expect(
|
||||
screen.findByText(
|
||||
textContentMatcher('Starting terminal, please wait...'),
|
||||
),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should connect to WebSocket server & render response', async () => {
|
||||
const server = new WS(
|
||||
'ws://localhost/proxy/api/v1/namespaces/podNamespace/pods/pod1/exec?container=container2&stdin=true&stdout=true&stderr=true&tty=true&command=%2Fbin%2Fsh',
|
||||
);
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider apis={[[discoveryApiRef, mockDiscoveryApi]]}>
|
||||
<PodExecTerminal
|
||||
clusterName={clusterName}
|
||||
containerName={containerName}
|
||||
podName={podName}
|
||||
podNamespace={podNamespace}
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
// xterm uses a "W" character as a "measure element" -- if it exists, the terminal rendered correctly
|
||||
await expect(
|
||||
screen.findByText(textContentMatcher('W')),
|
||||
).resolves.toBeInTheDocument();
|
||||
|
||||
await server.connected;
|
||||
|
||||
const { buffer } = Uint8Array.from([
|
||||
1,
|
||||
...textEncoder.encode('hello world'),
|
||||
]);
|
||||
|
||||
server.send(buffer);
|
||||
|
||||
await expect(
|
||||
screen.findByText(textContentMatcher('hello world')),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,131 +0,0 @@
|
||||
/*
|
||||
* 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 'xterm/css/xterm.css';
|
||||
|
||||
import { discoveryApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Terminal } from 'xterm';
|
||||
import { FitAddon } from 'xterm-addon-fit';
|
||||
|
||||
import { PodExecTerminalAttachAddon } from './PodExecTerminalAttachAddon';
|
||||
|
||||
/**
|
||||
* Props drilled down to the PodExecTerminal component
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface PodExecTerminalProps {
|
||||
clusterName: string;
|
||||
containerName: string;
|
||||
podName: string;
|
||||
podNamespace: string;
|
||||
}
|
||||
|
||||
const hasSocketProtocol = (url: string | URL) =>
|
||||
/wss?:\/\//.test(url.toString());
|
||||
|
||||
/**
|
||||
* Executes a `/bin/sh` process in the given pod's container and opens a terminal connected to it
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const PodExecTerminal = (props: PodExecTerminalProps) => {
|
||||
const { containerName, podNamespace, podName } = props;
|
||||
|
||||
const [baseUrl, setBaseUrl] = useState(window.location.host);
|
||||
|
||||
const terminalRef = React.useRef(null);
|
||||
const discoveryApi = useApi(discoveryApiRef);
|
||||
const namespace = podNamespace ?? 'default';
|
||||
|
||||
useEffect(() => {
|
||||
discoveryApi
|
||||
.getBaseUrl('kubernetes')
|
||||
.then(url => url ?? window.location.host)
|
||||
.then(url => url.replace(/^http(s?):\/\//, 'ws$1://'))
|
||||
.then(url => setBaseUrl(url));
|
||||
}, [discoveryApi]);
|
||||
|
||||
const urlParams = useMemo(() => {
|
||||
const params = new URLSearchParams({
|
||||
container: containerName,
|
||||
stdin: 'true',
|
||||
stdout: 'true',
|
||||
stderr: 'true',
|
||||
tty: 'true',
|
||||
command: '/bin/sh',
|
||||
});
|
||||
return params;
|
||||
}, [containerName]);
|
||||
|
||||
const socketUrl = useMemo(() => {
|
||||
if (!hasSocketProtocol(baseUrl)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return new URL(
|
||||
`${baseUrl}/proxy/api/v1/namespaces/${namespace}/pods/${podName}/exec?${urlParams}`,
|
||||
);
|
||||
}, [baseUrl, namespace, podName, urlParams]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasSocketProtocol(socketUrl)) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const terminal = new Terminal();
|
||||
const fitAddon = new FitAddon();
|
||||
terminal.loadAddon(fitAddon);
|
||||
|
||||
if (terminalRef.current) {
|
||||
terminal.open(terminalRef.current);
|
||||
fitAddon.fit();
|
||||
}
|
||||
|
||||
terminal.writeln('Starting terminal, please wait...');
|
||||
|
||||
const socket = new WebSocket(socketUrl, ['channel.k8s.io']);
|
||||
|
||||
socket.onopen = () => {
|
||||
terminal.clear();
|
||||
const attachAddon = new PodExecTerminalAttachAddon(socket, {
|
||||
bidirectional: true,
|
||||
});
|
||||
|
||||
terminal.loadAddon(attachAddon);
|
||||
};
|
||||
|
||||
socket.onclose = () => {
|
||||
terminal.writeln('Socket connection closed');
|
||||
};
|
||||
|
||||
return () => {
|
||||
terminal?.clear();
|
||||
socket?.close();
|
||||
};
|
||||
}, [baseUrl, socketUrl]);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="terminal"
|
||||
ref={terminalRef}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* 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 { AttachAddon, IAttachOptions } from 'xterm-addon-attach';
|
||||
|
||||
export class PodExecTerminalAttachAddon extends AttachAddon {
|
||||
#textEncoder = new TextEncoder();
|
||||
|
||||
constructor(socket: WebSocket, options?: IAttachOptions) {
|
||||
super(socket, options);
|
||||
|
||||
const thisAddon = this as any;
|
||||
|
||||
// These methods are private in the original AttachAddon,
|
||||
// thus have to override at runtime like this
|
||||
thisAddon._sendBinary = (data: string) => {
|
||||
if (!thisAddon._checkOpenSocket()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const buffer = Uint8Array.from([0, ...this.#textEncoder.encode(data)]);
|
||||
|
||||
thisAddon._socket.send(buffer);
|
||||
};
|
||||
|
||||
thisAddon._sendData = (data: string) => {
|
||||
if (!thisAddon._checkOpenSocket()) {
|
||||
return;
|
||||
}
|
||||
|
||||
thisAddon._sendBinary(data);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
/*
|
||||
* 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 {
|
||||
Button,
|
||||
createStyles,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
makeStyles,
|
||||
Theme,
|
||||
} from '@material-ui/core';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { useIsPodExecTerminalSupported } from '../../hooks';
|
||||
import { PodExecTerminal, PodExecTerminalProps } from './PodExecTerminal';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
dialogPaper: { minHeight: 'calc(100% - 64px)' },
|
||||
dialogContent: { flexBasis: 0 },
|
||||
closeButton: {
|
||||
position: 'absolute',
|
||||
right: theme.spacing(1),
|
||||
top: theme.spacing(1),
|
||||
color: theme.palette.grey[500],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Opens a terminal connected to the given pod's container in a dialog
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const PodExecTerminalDialog = (props: PodExecTerminalProps) => {
|
||||
const classes = useStyles();
|
||||
const { clusterName, containerName, podName } = props;
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const isPodExecTerminalSupported = useIsPodExecTerminalSupported();
|
||||
|
||||
const openDialog = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isPodExecTerminalSupported.loading &&
|
||||
isPodExecTerminalSupported.value && (
|
||||
<Dialog
|
||||
maxWidth={false}
|
||||
fullWidth
|
||||
open={open}
|
||||
onClose={closeDialog}
|
||||
PaperProps={{ className: classes.dialogPaper }}
|
||||
>
|
||||
<DialogTitle id="dialog-title">
|
||||
{podName} - {containerName} terminal shell on cluster{' '}
|
||||
{clusterName}
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
className={classes.closeButton}
|
||||
onClick={closeDialog}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
<DialogContent className={classes.dialogContent}>
|
||||
<PodExecTerminal {...props} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
aria-label="open terminal"
|
||||
component="label"
|
||||
disabled={
|
||||
isPodExecTerminalSupported.loading ||
|
||||
!isPodExecTerminalSupported.value
|
||||
}
|
||||
onClick={openDialog}
|
||||
startIcon={<OpenInBrowserIcon />}
|
||||
>
|
||||
Terminal
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
/*
|
||||
* 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 * from './PodExecTerminal';
|
||||
export * from './PodExecTerminalDialog';
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation(query => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(), // deprecated
|
||||
removeListener: jest.fn(), // deprecated
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
export {};
|
||||
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
|
||||
import { ErrorList } from './ErrorList';
|
||||
import { Pod } from 'kubernetes-models/v1/Pod';
|
||||
|
||||
describe('ErrorList', () => {
|
||||
it('error highlight should render', () => {
|
||||
const { getByText } = render(
|
||||
<ErrorList
|
||||
podAndErrors={[
|
||||
{
|
||||
clusterName: 'some-cluster',
|
||||
pod: {
|
||||
metadata: {
|
||||
name: 'some-pod',
|
||||
namespace: 'some-namespace',
|
||||
},
|
||||
} as Pod,
|
||||
errors: [
|
||||
{
|
||||
type: 'some-error',
|
||||
severity: 10,
|
||||
message: 'some error message',
|
||||
occurrenceCount: 1,
|
||||
sourceRef: {
|
||||
name: 'some-pod',
|
||||
namespace: 'some-namespace',
|
||||
kind: 'Pod',
|
||||
apiGroup: 'v1',
|
||||
},
|
||||
proposedFix: {
|
||||
type: 'logs',
|
||||
container: 'some-container',
|
||||
errorType: 'some error type',
|
||||
rootCauseExplanation: 'some root cause',
|
||||
actions: ['fix1', 'fix2'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
expect(getByText('some-pod')).toBeInTheDocument();
|
||||
expect(getByText('some error message')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,97 +0,0 @@
|
||||
/*
|
||||
* 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 {
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Divider,
|
||||
createStyles,
|
||||
makeStyles,
|
||||
Theme,
|
||||
Paper,
|
||||
Grid,
|
||||
} from '@material-ui/core';
|
||||
import { PodAndErrors } from '../types';
|
||||
import { FixDialog } from '../FixDialog/FixDialog';
|
||||
|
||||
const useStyles = makeStyles((_theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
overflow: 'auto',
|
||||
},
|
||||
list: {
|
||||
width: '100%',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Props for ErrorList
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface ErrorListProps {
|
||||
podAndErrors: PodAndErrors[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a list of errors found on a Pod
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const ErrorList = ({ podAndErrors }: ErrorListProps) => {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<Paper className={classes.root}>
|
||||
<List className={classes.list}>
|
||||
{podAndErrors
|
||||
.filter(pae => pae.errors.length > 0)
|
||||
.flatMap(onlyPodWithErrors => {
|
||||
return onlyPodWithErrors.errors.map((error, i) => {
|
||||
return (
|
||||
<React.Fragment
|
||||
key={`${
|
||||
onlyPodWithErrors.pod.metadata?.name ?? 'unknown'
|
||||
}-eli-${i}`}
|
||||
>
|
||||
{i > 0 && <Divider key={`error-divider${i}`} />}
|
||||
<ListItem>
|
||||
<Grid container>
|
||||
<Grid item xs={9}>
|
||||
<ListItemText
|
||||
primary={error.message}
|
||||
secondary={onlyPodWithErrors.pod.metadata?.name}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={3}>
|
||||
<FixDialog
|
||||
pod={onlyPodWithErrors.pod}
|
||||
error={error}
|
||||
clusterName={onlyPodWithErrors.clusterName}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ListItem>
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
})}
|
||||
</List>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
/*
|
||||
* 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 * from './ErrorList';
|
||||
@@ -1,96 +0,0 @@
|
||||
/*
|
||||
* 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 { EventsContent } from './Events';
|
||||
import { render } from '@testing-library/react';
|
||||
import { Event } from 'kubernetes-models/v1';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
describe('EventsContent', () => {
|
||||
const oneHourAgo = DateTime.now().minus({ hours: 1 }).toISO();
|
||||
it('should show info events', () => {
|
||||
const { getByText } = render(
|
||||
<EventsContent
|
||||
events={[
|
||||
{
|
||||
type: 'Info',
|
||||
message: 'hello there',
|
||||
reason: 'something happened',
|
||||
count: 52,
|
||||
metadata: {
|
||||
creationTimestamp: oneHourAgo,
|
||||
},
|
||||
} as Event,
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
expect(getByText('First event 1 hour ago (count: 52)')).toBeInTheDocument();
|
||||
expect(getByText('something happened: hello there')).toBeInTheDocument();
|
||||
});
|
||||
it('should show warning events', () => {
|
||||
const { getByText } = render(
|
||||
<EventsContent
|
||||
events={[
|
||||
{
|
||||
type: 'Warning',
|
||||
message: 'uh oh',
|
||||
reason: 'something happened',
|
||||
count: 23,
|
||||
metadata: {
|
||||
creationTimestamp: oneHourAgo,
|
||||
},
|
||||
} as Event,
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
expect(getByText('First event 1 hour ago (count: 23)')).toBeInTheDocument();
|
||||
expect(getByText('something happened: uh oh')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should only show warning events when warningEventsOnly set', () => {
|
||||
const { getByText, queryByText } = render(
|
||||
<EventsContent
|
||||
warningEventsOnly
|
||||
events={
|
||||
[
|
||||
{
|
||||
type: 'Warning',
|
||||
message: 'uh oh',
|
||||
reason: 'something happened',
|
||||
count: 23,
|
||||
metadata: {
|
||||
creationTimestamp: oneHourAgo,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'Info',
|
||||
message: 'hello there',
|
||||
reason: 'something happened',
|
||||
count: 52,
|
||||
metadata: {
|
||||
creationTimestamp: oneHourAgo,
|
||||
},
|
||||
},
|
||||
] as Event[]
|
||||
}
|
||||
/>,
|
||||
);
|
||||
expect(queryByText('First event 1 hour ago (count: 52)')).toBeNull();
|
||||
expect(queryByText('something happened: hello there')).toBeNull();
|
||||
expect(getByText('First event 1 hour ago (count: 23)')).toBeInTheDocument();
|
||||
expect(getByText('something happened: uh oh')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,154 +0,0 @@
|
||||
/*
|
||||
* 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 {
|
||||
Avatar,
|
||||
Container,
|
||||
Grid,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
|
||||
import InfoIcon from '@material-ui/icons/Info';
|
||||
import WarningIcon from '@material-ui/icons/Warning';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { useEvents } from './useEvents';
|
||||
import { Skeleton } from '@material-ui/lab';
|
||||
import { DismissableBanner } from '@backstage/core-components';
|
||||
import { Event } from 'kubernetes-models/v1';
|
||||
|
||||
/**
|
||||
* Props for Events
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface EventsContentProps {
|
||||
warningEventsOnly?: boolean;
|
||||
events: Event[];
|
||||
}
|
||||
|
||||
const getAvatarByType = (type?: string) => {
|
||||
return (
|
||||
<ListItemAvatar>
|
||||
<Avatar>{type === 'Warning' ? <WarningIcon /> : <InfoIcon />}</Avatar>
|
||||
</ListItemAvatar>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows given Kubernetes events
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const EventsContent = ({
|
||||
events,
|
||||
warningEventsOnly,
|
||||
}: EventsContentProps) => {
|
||||
if (events.length === 0) {
|
||||
return <Typography>No events found</Typography>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Grid>
|
||||
<List>
|
||||
{events
|
||||
.filter(event => {
|
||||
if (warningEventsOnly) {
|
||||
return event.type === 'Warning';
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map(event => {
|
||||
const timeAgo = event.metadata.creationTimestamp
|
||||
? DateTime.fromISO(event.metadata.creationTimestamp).toRelative(
|
||||
{
|
||||
locale: 'en',
|
||||
},
|
||||
)
|
||||
: 'unknown';
|
||||
return (
|
||||
<ListItem key={event.metadata.name}>
|
||||
<Tooltip title={`${event.type ?? ''} event`}>
|
||||
{getAvatarByType(event.type)}
|
||||
</Tooltip>
|
||||
<ListItemText
|
||||
primary={`First event ${timeAgo} (count: ${event.count})`}
|
||||
secondary={`${event.reason}: ${event.message}`}
|
||||
/>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for Events
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface EventsProps {
|
||||
involvedObjectName: string;
|
||||
namespace: string;
|
||||
clusterName: string;
|
||||
warningEventsOnly?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves and shows Kubernetes events for the given object
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const Events = ({
|
||||
involvedObjectName,
|
||||
namespace,
|
||||
clusterName,
|
||||
warningEventsOnly,
|
||||
}: EventsProps) => {
|
||||
const { value, error, loading } = useEvents({
|
||||
involvedObjectName,
|
||||
namespace,
|
||||
clusterName,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{error && (
|
||||
<DismissableBanner
|
||||
{...{
|
||||
message: error.message,
|
||||
variant: 'error',
|
||||
fixed: false,
|
||||
}}
|
||||
id="events"
|
||||
/>
|
||||
)}
|
||||
{loading && <Skeleton variant="rect" width="100%" height="100%" />}
|
||||
{!loading && value !== undefined && (
|
||||
<EventsContent warningEventsOnly={warningEventsOnly} events={value} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
/*
|
||||
* 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 * from './Events';
|
||||
export * from './useEvents';
|
||||
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
* 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 { useApi } from '@backstage/core-plugin-api';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { useEvents } from './useEvents';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
jest.mock('@backstage/core-plugin-api');
|
||||
|
||||
jest.mock('@backstage/plugin-kubernetes', () => ({
|
||||
kubernetesProxyApiRef: () => jest.fn(),
|
||||
}));
|
||||
|
||||
const oneHourAgo = DateTime.now().minus({ hours: 1 }).toISO();
|
||||
|
||||
const response = [
|
||||
{
|
||||
type: 'Info',
|
||||
message: 'hello there',
|
||||
reason: 'something happened',
|
||||
count: 52,
|
||||
metadata: {
|
||||
creationTimestamp: oneHourAgo,
|
||||
},
|
||||
},
|
||||
] as any;
|
||||
|
||||
describe('Events', () => {
|
||||
const mockGetEventsByInvolvedObjectName = jest.fn();
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
it('should fetch and show events', async () => {
|
||||
(useApi as any).mockReturnValue({
|
||||
getEventsByInvolvedObjectName:
|
||||
mockGetEventsByInvolvedObjectName.mockResolvedValue(response),
|
||||
});
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useEvents({
|
||||
involvedObjectName: 'some-objecgt',
|
||||
namespace: 'some-namespace',
|
||||
clusterName: 'some-cluster',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.loading).toEqual(true);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current.error).toBeUndefined();
|
||||
expect(result.current.loading).toEqual(false);
|
||||
expect(result.current.value).toStrictEqual(response);
|
||||
});
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
* 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 { useApi } from '@backstage/core-plugin-api';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { kubernetesProxyApiRef } from '../../../api';
|
||||
|
||||
/**
|
||||
* Arguments for useEvents
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface EventsOptions {
|
||||
involvedObjectName: string;
|
||||
namespace: string;
|
||||
clusterName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the events for the given object
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const useEvents = ({
|
||||
involvedObjectName,
|
||||
namespace,
|
||||
clusterName,
|
||||
}: EventsOptions) => {
|
||||
const kubernetesProxyApi = useApi(kubernetesProxyApiRef);
|
||||
return useAsync(async () => {
|
||||
return await kubernetesProxyApi.getEventsByInvolvedObjectName({
|
||||
involvedObjectName,
|
||||
namespace,
|
||||
clusterName,
|
||||
});
|
||||
}, [involvedObjectName, namespace, clusterName]);
|
||||
};
|
||||
@@ -1,159 +0,0 @@
|
||||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
|
||||
import { FixDialog } from './FixDialog';
|
||||
import { Pod } from 'kubernetes-models/v1/Pod';
|
||||
|
||||
jest.mock('../Events', () => ({
|
||||
Events: () => {
|
||||
return <React.Fragment data-testid="events" />;
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../PodLogs', () => ({
|
||||
PodLogs: () => {
|
||||
return <React.Fragment data-testid="logs" />;
|
||||
},
|
||||
}));
|
||||
|
||||
describe('FixDialog', () => {
|
||||
it('docs link should render', () => {
|
||||
const { getByText } = render(
|
||||
<FixDialog
|
||||
open
|
||||
clusterName="some-cluster"
|
||||
pod={
|
||||
{
|
||||
metadata: {
|
||||
name: 'some-pod',
|
||||
namespace: 'some-namespace',
|
||||
},
|
||||
} as Pod
|
||||
}
|
||||
error={{
|
||||
type: 'some error type',
|
||||
severity: 10,
|
||||
message: 'some error message',
|
||||
occurrenceCount: 1,
|
||||
sourceRef: {
|
||||
name: 'some-pod',
|
||||
namespace: 'some-namespace',
|
||||
kind: 'Pod',
|
||||
apiGroup: 'v1',
|
||||
},
|
||||
proposedFix: {
|
||||
type: 'docs',
|
||||
docsLink: 'http://google.com',
|
||||
errorType: 'some error type',
|
||||
rootCauseExplanation: 'some root cause',
|
||||
actions: ['fix1', 'fix2'],
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(getByText('Open docs')).toBeInTheDocument();
|
||||
expect(getByText('some error message')).toBeInTheDocument();
|
||||
expect(getByText('some-pod - some error type')).toBeInTheDocument();
|
||||
expect(getByText('some root cause')).toBeInTheDocument();
|
||||
expect(getByText('fix1')).toBeInTheDocument();
|
||||
expect(getByText('fix2')).toBeInTheDocument();
|
||||
});
|
||||
it('events button should render', () => {
|
||||
const { getByText } = render(
|
||||
<FixDialog
|
||||
open
|
||||
clusterName="some-cluster"
|
||||
pod={
|
||||
{
|
||||
metadata: {
|
||||
name: 'some-pod',
|
||||
namespace: 'some-namespace',
|
||||
},
|
||||
} as Pod
|
||||
}
|
||||
error={{
|
||||
type: 'some error type',
|
||||
severity: 10,
|
||||
message: 'some error message',
|
||||
occurrenceCount: 1,
|
||||
sourceRef: {
|
||||
name: 'some-pod',
|
||||
namespace: 'some-namespace',
|
||||
kind: 'Pod',
|
||||
apiGroup: 'v1',
|
||||
},
|
||||
proposedFix: {
|
||||
type: 'events',
|
||||
podName: 'some-pod',
|
||||
errorType: 'some error type',
|
||||
rootCauseExplanation: 'some root cause',
|
||||
actions: ['fix1', 'fix2'],
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(getByText('Events:')).toBeInTheDocument();
|
||||
expect(getByText('some error message')).toBeInTheDocument();
|
||||
expect(getByText('some-pod - some error type')).toBeInTheDocument();
|
||||
expect(getByText('some root cause')).toBeInTheDocument();
|
||||
expect(getByText('fix1')).toBeInTheDocument();
|
||||
expect(getByText('fix2')).toBeInTheDocument();
|
||||
});
|
||||
it('Logs button should render', () => {
|
||||
const { getByText } = render(
|
||||
<FixDialog
|
||||
open
|
||||
clusterName="some-cluster"
|
||||
pod={
|
||||
{
|
||||
metadata: {
|
||||
name: 'some-pod',
|
||||
namespace: 'some-namespace',
|
||||
},
|
||||
} as Pod
|
||||
}
|
||||
error={{
|
||||
type: 'some error type',
|
||||
severity: 10,
|
||||
message: 'some error message',
|
||||
occurrenceCount: 1,
|
||||
sourceRef: {
|
||||
name: 'some-pod',
|
||||
namespace: 'some-namespace',
|
||||
kind: 'Pod',
|
||||
apiGroup: 'v1',
|
||||
},
|
||||
proposedFix: {
|
||||
type: 'logs',
|
||||
container: 'some-container',
|
||||
errorType: 'some error type',
|
||||
rootCauseExplanation: 'some root cause',
|
||||
actions: ['fix1', 'fix2'],
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(getByText('Crash logs:')).toBeInTheDocument();
|
||||
expect(getByText('some error message')).toBeInTheDocument();
|
||||
expect(getByText('some-pod - some error type')).toBeInTheDocument();
|
||||
expect(getByText('some root cause')).toBeInTheDocument();
|
||||
expect(getByText('fix1')).toBeInTheDocument();
|
||||
expect(getByText('fix2')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,187 +0,0 @@
|
||||
/*
|
||||
* 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, { useState } from 'react';
|
||||
|
||||
import { Button, Grid } from '@material-ui/core';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import { makeStyles, createStyles, Theme } from '@material-ui/core/styles';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import HelpIcon from '@material-ui/icons/Help';
|
||||
import OpenInNewIcon from '@material-ui/icons/OpenInNew';
|
||||
|
||||
import { Pod } from 'kubernetes-models/v1/Pod';
|
||||
import { DetectedError } from '../../../error-detection';
|
||||
import { PodLogs } from '../PodLogs';
|
||||
import { Events } from '../Events';
|
||||
import { LinkButton } from '@backstage/core-components';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
closeButton: {
|
||||
position: 'absolute',
|
||||
right: theme.spacing(1),
|
||||
top: theme.spacing(1),
|
||||
color: theme.palette.grey[500],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Props for FixDialog
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface FixDialogProps {
|
||||
open?: boolean;
|
||||
clusterName: string;
|
||||
pod: Pod;
|
||||
error: DetectedError;
|
||||
}
|
||||
|
||||
/**
|
||||
* A dialog for fixing detected Kubernetes errors
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const FixDialog: React.FC<FixDialogProps> = ({
|
||||
open,
|
||||
pod,
|
||||
error,
|
||||
clusterName,
|
||||
}: FixDialogProps) => {
|
||||
const [isOpen, setOpen] = useState(!!open);
|
||||
const classes = useStyles();
|
||||
|
||||
const openDialog = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const pf = error.proposedFix;
|
||||
|
||||
const dialogContent = () => {
|
||||
return (
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h6">Detected error:</Typography>
|
||||
<Typography>{error.message}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h6">Cause explanation:</Typography>
|
||||
<Typography>
|
||||
{error.proposedFix?.rootCauseExplanation ?? 'unknown'}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h6">Fix:</Typography>
|
||||
<Typography>
|
||||
<ul>
|
||||
{(error.proposedFix?.actions ?? []).map((fix, i) => {
|
||||
return (
|
||||
<li key={`${pod.metadata?.name ?? 'unknown'}-pf-${i}`}>
|
||||
{fix}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
{pf && pf.type === 'logs' && (
|
||||
<>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h6">Crash logs:</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={9}>
|
||||
<PodLogs
|
||||
previous
|
||||
containerScope={{
|
||||
podName: pod.metadata?.name ?? 'unknown',
|
||||
podNamespace: pod.metadata?.namespace ?? 'unknown',
|
||||
clusterName: clusterName,
|
||||
containerName: pf.container,
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
{pf && pf.type === 'events' && (
|
||||
<>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h6">Events:</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={9}>
|
||||
<Events
|
||||
warningEventsOnly
|
||||
involvedObjectName={pod.metadata?.name ?? ''}
|
||||
namespace={pod.metadata?.namespace ?? ''}
|
||||
clusterName={clusterName}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="outlined"
|
||||
aria-label="fix issue"
|
||||
component="label"
|
||||
onClick={openDialog}
|
||||
startIcon={<HelpIcon />}
|
||||
>
|
||||
Help
|
||||
</Button>
|
||||
<Dialog maxWidth="xl" fullWidth open={isOpen} onClose={closeDialog}>
|
||||
<DialogTitle id="dialog-title">
|
||||
{pod.metadata?.name} - {error.type}
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
className={classes.closeButton}
|
||||
onClick={closeDialog}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
<DialogContent>{dialogContent()}</DialogContent>
|
||||
<DialogActions>
|
||||
{pf && pf.type === 'docs' && (
|
||||
<LinkButton
|
||||
to={pf.docsLink}
|
||||
variant="outlined"
|
||||
startIcon={<OpenInNewIcon />}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
Open docs
|
||||
</LinkButton>
|
||||
)}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
/*
|
||||
* 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 * from './FixDialog';
|
||||
@@ -1,135 +0,0 @@
|
||||
/*
|
||||
* 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 { screen } from '@testing-library/react';
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { ContainerCard } from './ContainerCard';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
jest.mock('../../../hooks/useIsPodExecTerminalSupported');
|
||||
|
||||
const now = DateTime.now();
|
||||
const oneHourAgo = now.minus({ hours: 1 }).toISO();
|
||||
const twoHoursAgo = now.minus({ hours: 2 }).toISO();
|
||||
|
||||
describe('ContainerCard', () => {
|
||||
it('show healthy when all checks pass', async () => {
|
||||
await renderInTestApp(
|
||||
<ContainerCard
|
||||
{...({
|
||||
podScope: {
|
||||
name: 'some-name',
|
||||
namespace: 'some-namespace',
|
||||
clusterName: 'some-cluster',
|
||||
},
|
||||
containerSpec: {
|
||||
readinessProbe: {},
|
||||
},
|
||||
containerStatus: {
|
||||
name: 'some-name',
|
||||
image: 'gcr.io/some-proj/some-image',
|
||||
started: true,
|
||||
ready: true,
|
||||
restartCount: 0,
|
||||
state: {
|
||||
running: {
|
||||
startedAt: oneHourAgo,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any)}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText('Started: 1 hour ago')).toBeInTheDocument();
|
||||
expect(screen.getByText('Status: Running')).toBeInTheDocument();
|
||||
expect(screen.getByText('some-name')).toBeInTheDocument();
|
||||
expect(screen.getByText('gcr.io/some-proj/some-image')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('✅')).toHaveLength(5);
|
||||
expect(screen.queryByText('❌')).toBeNull();
|
||||
});
|
||||
|
||||
it('show unhealthy when all checks fail', async () => {
|
||||
await renderInTestApp(
|
||||
<ContainerCard
|
||||
{...({
|
||||
podScope: {
|
||||
podName: 'some-name',
|
||||
podNamespace: 'some-namespace',
|
||||
clusterName: 'some-cluster',
|
||||
},
|
||||
containerSpec: {},
|
||||
containerStatus: {
|
||||
name: 'some-name',
|
||||
image: 'gcr.io/some-proj/some-image',
|
||||
started: false,
|
||||
ready: false,
|
||||
restartCount: 12,
|
||||
state: {
|
||||
waiting: {},
|
||||
},
|
||||
},
|
||||
} as any)}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText('some-name')).toBeInTheDocument();
|
||||
expect(screen.getByText('gcr.io/some-proj/some-image')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('❌')).toHaveLength(5);
|
||||
expect(screen.queryByText('✅')).toBeNull();
|
||||
});
|
||||
|
||||
it('show correct checks for completed container', async () => {
|
||||
await renderInTestApp(
|
||||
<ContainerCard
|
||||
{...({
|
||||
podScope: {
|
||||
podName: 'some-name',
|
||||
podNamespace: 'some-namespace',
|
||||
clusterName: 'some-cluster',
|
||||
},
|
||||
containerSpec: {},
|
||||
containerStatus: {
|
||||
name: 'some-name',
|
||||
image: 'gcr.io/some-proj/some-image',
|
||||
started: false,
|
||||
ready: false,
|
||||
restartCount: 0,
|
||||
state: {
|
||||
terminated: {
|
||||
exitCode: 0,
|
||||
reason: 'Completed',
|
||||
startedAt: twoHoursAgo,
|
||||
finishedAt: oneHourAgo,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any)}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText('some-name')).toBeInTheDocument();
|
||||
expect(screen.getByText('gcr.io/some-proj/some-image')).toBeInTheDocument();
|
||||
expect(screen.getByText('Started: 2 hours ago')).toBeInTheDocument();
|
||||
expect(screen.getByText('Completed: 1 hour ago')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Execution time: 1 hour, 0 minutes, 0 seconds'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Status: Completed')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('✅')).toHaveLength(2);
|
||||
expect(screen.queryByText('❌')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,240 +0,0 @@
|
||||
/*
|
||||
* 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 { StructuredMetadataTable } from '@backstage/core-components';
|
||||
import { ClientContainerStatus } from '@backstage/plugin-kubernetes-common';
|
||||
import {
|
||||
Card,
|
||||
CardActions,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
Grid,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import { IContainer, IContainerStatus } from 'kubernetes-models/v1';
|
||||
import { DateTime } from 'luxon';
|
||||
import React from 'react';
|
||||
|
||||
import { bytesToMiB, formatMillicores } from '../../../utils/resources';
|
||||
import { PodExecTerminalDialog } from '../../PodExecTerminal/PodExecTerminalDialog';
|
||||
import { ResourceUtilization } from '../../ResourceUtilization';
|
||||
import { PodLogsDialog, PodScope } from '../PodLogs';
|
||||
|
||||
const getContainerHealthChecks = (
|
||||
containerSpec: IContainer,
|
||||
containerStatus: IContainerStatus,
|
||||
): { [key: string]: boolean } => {
|
||||
if (containerStatus.state?.terminated?.reason === 'Completed') {
|
||||
return {
|
||||
'not waiting to start': containerStatus.state?.waiting === undefined,
|
||||
'no restarts': containerStatus.restartCount === 0,
|
||||
};
|
||||
}
|
||||
return {
|
||||
'not waiting to start': containerStatus.state?.waiting === undefined,
|
||||
started: !!containerStatus.started,
|
||||
ready: containerStatus.ready,
|
||||
'no restarts': containerStatus.restartCount === 0,
|
||||
'readiness probe set':
|
||||
containerSpec && containerSpec?.readinessProbe !== undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const getCurrentState = (containerStatus: IContainerStatus): string => {
|
||||
return (
|
||||
containerStatus.state?.waiting?.reason ||
|
||||
containerStatus.state?.terminated?.reason ||
|
||||
(containerStatus.state?.running !== undefined ? 'Running' : 'Unknown')
|
||||
);
|
||||
};
|
||||
|
||||
const getStartedAtTime = (
|
||||
containerStatus: IContainerStatus,
|
||||
): string | undefined => {
|
||||
return (
|
||||
containerStatus.state?.running?.startedAt ||
|
||||
containerStatus.state?.terminated?.startedAt
|
||||
);
|
||||
};
|
||||
|
||||
interface ContainerDatetimeProps {
|
||||
prefix: string;
|
||||
dateTime: string;
|
||||
}
|
||||
|
||||
const ContainerDatetime = ({ prefix, dateTime }: ContainerDatetimeProps) => {
|
||||
return (
|
||||
<Typography variant="subtitle2">
|
||||
{prefix}:{' '}
|
||||
{DateTime.fromISO(dateTime).toRelative({
|
||||
locale: 'en',
|
||||
})}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for ContainerCard
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface ContainerCardProps {
|
||||
podScope: PodScope;
|
||||
containerSpec?: IContainer;
|
||||
containerStatus: IContainerStatus;
|
||||
containerMetrics?: ClientContainerStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows details about a container within a pod
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const ContainerCard: React.FC<ContainerCardProps> = ({
|
||||
podScope,
|
||||
containerSpec,
|
||||
containerStatus,
|
||||
containerMetrics,
|
||||
}: ContainerCardProps) => {
|
||||
// This should never be undefined
|
||||
if (containerSpec === undefined) {
|
||||
return <Typography>error reading pod from cluster</Typography>;
|
||||
}
|
||||
const containerStartedTime = getStartedAtTime(containerStatus);
|
||||
const containerFinishedTime = containerStatus.state?.terminated?.finishedAt;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
title={containerStatus.name}
|
||||
subheader={containerStatus.image}
|
||||
/>
|
||||
<CardContent>
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
{containerStartedTime && (
|
||||
<ContainerDatetime
|
||||
prefix="Started"
|
||||
dateTime={containerStartedTime}
|
||||
/>
|
||||
)}
|
||||
{containerFinishedTime && (
|
||||
<ContainerDatetime
|
||||
prefix="Completed"
|
||||
dateTime={containerFinishedTime}
|
||||
/>
|
||||
)}
|
||||
{containerStartedTime && containerFinishedTime && (
|
||||
<Typography variant="subtitle2">
|
||||
Execution time:{' '}
|
||||
{DateTime.fromISO(containerFinishedTime)
|
||||
.diff(DateTime.fromISO(containerStartedTime), [
|
||||
'hours',
|
||||
'minutes',
|
||||
'seconds',
|
||||
])
|
||||
.toHuman()}
|
||||
</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="subtitle2">
|
||||
Status: {getCurrentState(containerStatus)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
{containerStatus.restartCount > 0 && (
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="subtitle2">
|
||||
Restarts: {containerStatus.restartCount}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="subtitle2">Container health</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<StructuredMetadataTable
|
||||
metadata={getContainerHealthChecks(
|
||||
containerSpec,
|
||||
containerStatus,
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
{containerMetrics && (
|
||||
<Grid container item xs={12} spacing={0}>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="subtitle1">
|
||||
Resource utilization
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} style={{ minHeight: '5rem' }}>
|
||||
<ResourceUtilization
|
||||
compressed
|
||||
title="CPU requests"
|
||||
usage={containerMetrics.cpuUsage.currentUsage}
|
||||
total={containerMetrics.cpuUsage.requestTotal}
|
||||
totalFormatted={formatMillicores(
|
||||
containerMetrics.cpuUsage.requestTotal,
|
||||
)}
|
||||
/>
|
||||
<ResourceUtilization
|
||||
compressed
|
||||
title="CPU limits"
|
||||
usage={containerMetrics.cpuUsage.currentUsage}
|
||||
total={containerMetrics.cpuUsage.limitTotal}
|
||||
totalFormatted={formatMillicores(
|
||||
containerMetrics.cpuUsage.limitTotal,
|
||||
)}
|
||||
/>
|
||||
<ResourceUtilization
|
||||
compressed
|
||||
title="Memory requests"
|
||||
usage={containerMetrics.memoryUsage.currentUsage}
|
||||
total={containerMetrics.memoryUsage.requestTotal}
|
||||
totalFormatted={bytesToMiB(
|
||||
containerMetrics.memoryUsage.requestTotal,
|
||||
)}
|
||||
/>
|
||||
<ResourceUtilization
|
||||
compressed
|
||||
title="Memory limits"
|
||||
usage={containerMetrics.memoryUsage.currentUsage}
|
||||
total={containerMetrics.memoryUsage.limitTotal}
|
||||
totalFormatted={bytesToMiB(
|
||||
containerMetrics.memoryUsage.limitTotal,
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<PodLogsDialog
|
||||
containerScope={{
|
||||
containerName: containerStatus.name,
|
||||
...podScope,
|
||||
}}
|
||||
/>
|
||||
<PodExecTerminalDialog
|
||||
clusterName={podScope.clusterName}
|
||||
containerName={containerStatus.name}
|
||||
podName={podScope.podName}
|
||||
podNamespace={podScope.podNamespace}
|
||||
/>
|
||||
</CardActions>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
-186
@@ -1,186 +0,0 @@
|
||||
/*
|
||||
* 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 { screen } from '@testing-library/react';
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { PendingPodContent } from './PendingPodContent';
|
||||
import { IPodCondition } from 'kubernetes-models/v1';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
const podWithConditions = (conditions: IPodCondition[]): any => {
|
||||
return {
|
||||
metadata: {
|
||||
name: 'ok-pod',
|
||||
},
|
||||
spec: {
|
||||
containers: [
|
||||
{
|
||||
name: 'some-container',
|
||||
},
|
||||
],
|
||||
},
|
||||
status: {
|
||||
podIP: '127.0.0.1',
|
||||
conditions: conditions,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe('PendingPodContent', () => {
|
||||
it('show startup conditions - all healthy', async () => {
|
||||
const oneDayAgo = DateTime.now().minus({ days: 1 }).toISO()!;
|
||||
await renderInTestApp(
|
||||
<PendingPodContent
|
||||
{...{
|
||||
pod: podWithConditions([
|
||||
{
|
||||
type: 'Initialized',
|
||||
status: 'True',
|
||||
lastTransitionTime: oneDayAgo,
|
||||
},
|
||||
{
|
||||
type: 'PodScheduled',
|
||||
status: 'True',
|
||||
lastTransitionTime: oneDayAgo,
|
||||
},
|
||||
{
|
||||
type: 'ContainersReady',
|
||||
status: 'True',
|
||||
lastTransitionTime: oneDayAgo,
|
||||
},
|
||||
{
|
||||
type: 'Ready',
|
||||
status: 'True',
|
||||
lastTransitionTime: oneDayAgo,
|
||||
},
|
||||
]),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText('Pod is Pending. Conditions:')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Initialized - (1 day ago)')).toBeInTheDocument();
|
||||
expect(screen.getByText('PodScheduled - (1 day ago)')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('ContainersReady - (1 day ago)'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Ready - (1 day ago)')).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryAllByLabelText('Status ok')).toHaveLength(4);
|
||||
expect(screen.queryByLabelText('Status warning')).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText('Status error')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('show startup conditions - all fail', async () => {
|
||||
const oneHourAgo = DateTime.now().minus({ hours: 1 }).toISO()!;
|
||||
await renderInTestApp(
|
||||
<PendingPodContent
|
||||
{...{
|
||||
pod: podWithConditions([
|
||||
{
|
||||
type: 'Initialized',
|
||||
status: 'False',
|
||||
reason: 'InitializedFailureReason',
|
||||
message: 'reason why Initialized failed',
|
||||
lastTransitionTime: oneHourAgo,
|
||||
},
|
||||
{
|
||||
type: 'PodScheduled',
|
||||
status: 'False',
|
||||
reason: 'PodScheduledFailureReason',
|
||||
message: 'reason why PodScheduled failed',
|
||||
lastTransitionTime: oneHourAgo,
|
||||
},
|
||||
{
|
||||
type: 'ContainersReady',
|
||||
status: 'False',
|
||||
reason: 'ContainersReadyFailureReason',
|
||||
message: 'reason why ContainersReady failed',
|
||||
lastTransitionTime: oneHourAgo,
|
||||
},
|
||||
{
|
||||
type: 'Ready',
|
||||
status: 'False',
|
||||
reason: 'ReadyFailureReason',
|
||||
message: 'reason why Ready failed',
|
||||
lastTransitionTime: oneHourAgo,
|
||||
},
|
||||
]),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText('Pod is Pending. Conditions:')).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Initialized - (InitializedFailureReason 1 hour ago) - reason why Initialized failed',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'PodScheduled - (PodScheduledFailureReason 1 hour ago) - reason why PodScheduled failed',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'ContainersReady - (ContainersReadyFailureReason 1 hour ago) - reason why ContainersReady failed',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Ready - (ReadyFailureReason 1 hour ago) - reason why Ready failed',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByLabelText('Status ok')).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText('Status warning')).not.toBeInTheDocument();
|
||||
expect(screen.queryAllByLabelText('Status error')).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('show startup conditions - show unknown', async () => {
|
||||
const oneHourAgo = DateTime.now().minus({ hours: 1 }).toISO()!;
|
||||
await renderInTestApp(
|
||||
<PendingPodContent
|
||||
{...{
|
||||
pod: podWithConditions([
|
||||
{
|
||||
type: 'Initialized',
|
||||
status: 'Unknown',
|
||||
reason: 'InitializedUnknownReason',
|
||||
message: 'dont know what is happening',
|
||||
lastTransitionTime: oneHourAgo,
|
||||
},
|
||||
]),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText('Pod is Pending. Conditions:')).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Initialized - (1 hour ago) dont know what is happening',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByLabelText('Status ok')).not.toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Status warning')).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText('Status error')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,104 +0,0 @@
|
||||
/*
|
||||
* 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 { Grid, List, ListItem, Typography } from '@material-ui/core';
|
||||
import { IPodCondition, Pod } from 'kubernetes-models/v1';
|
||||
import {
|
||||
StatusError,
|
||||
StatusOK,
|
||||
StatusWarning,
|
||||
} from '@backstage/core-components';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
interface PodConditionProps {
|
||||
condition: IPodCondition;
|
||||
}
|
||||
|
||||
const PodCondition = ({ condition }: PodConditionProps) => {
|
||||
return (
|
||||
<>
|
||||
{condition.status === 'False' && (
|
||||
<StatusError>
|
||||
{condition.type} - ({condition.reason}{' '}
|
||||
{condition.lastTransitionTime &&
|
||||
DateTime.fromISO(condition.lastTransitionTime).toRelative({
|
||||
locale: 'en',
|
||||
})}
|
||||
) - {condition.message}{' '}
|
||||
</StatusError>
|
||||
)}
|
||||
{condition.status === 'True' && (
|
||||
<StatusOK>
|
||||
{condition.type} - (
|
||||
{condition.lastTransitionTime &&
|
||||
DateTime.fromISO(condition.lastTransitionTime).toRelative({
|
||||
locale: 'en',
|
||||
})}
|
||||
)
|
||||
</StatusOK>
|
||||
)}
|
||||
{condition.status === 'Unknown' && (
|
||||
<StatusWarning>
|
||||
{condition.type} - (
|
||||
{condition.lastTransitionTime &&
|
||||
DateTime.fromISO(condition.lastTransitionTime).toRelative({
|
||||
locale: 'en',
|
||||
})}
|
||||
) {condition.message}
|
||||
</StatusWarning>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for PendingPodContent
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface PendingPodContentProps {
|
||||
pod: Pod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows details about pod's conditions as it starts
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const PendingPodContent = ({ pod }: PendingPodContentProps) => {
|
||||
// TODO add PodHasNetwork when it's out of alpha
|
||||
// https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-conditions
|
||||
const startupConditions = [
|
||||
pod.status?.conditions?.find(c => c.type === 'PodScheduled'),
|
||||
pod.status?.conditions?.find(c => c.type === 'Initialized'),
|
||||
pod.status?.conditions?.find(c => c.type === 'ContainersReady'),
|
||||
pod.status?.conditions?.find(c => c.type === 'Ready'),
|
||||
].filter((c): c is IPodCondition => !!c); // filter out undefined
|
||||
return (
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h5">Pod is Pending. Conditions:</Typography>
|
||||
<List>
|
||||
{startupConditions.map(c => (
|
||||
<ListItem key={c.type}>
|
||||
<PodCondition condition={c} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -1,97 +0,0 @@
|
||||
/*
|
||||
* 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 { screen } from '@testing-library/react';
|
||||
import { TestApiProvider, renderInTestApp } from '@backstage/test-utils';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { PodDrawer } from './PodDrawer';
|
||||
import { DiscoveryApi, discoveryApiRef } from '@backstage/core-plugin-api';
|
||||
|
||||
jest.mock('../../../hooks/useIsPodExecTerminalSupported');
|
||||
|
||||
describe('PodDrawer', () => {
|
||||
it('Should show title and container names', async () => {
|
||||
const mockDiscoveryApi: Partial<DiscoveryApi> = {
|
||||
getBaseUrl: () => Promise.resolve('http://localhost'),
|
||||
};
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider apis={[[discoveryApiRef, mockDiscoveryApi]]}>
|
||||
<PodDrawer
|
||||
{...({
|
||||
open: true,
|
||||
podAndErrors: {
|
||||
clusterName: 'some-cluster-1',
|
||||
pod: {
|
||||
metadata: {
|
||||
name: 'some-pod',
|
||||
},
|
||||
spec: {
|
||||
containers: [
|
||||
{
|
||||
name: 'some-container',
|
||||
},
|
||||
],
|
||||
},
|
||||
status: {
|
||||
podIP: '127.0.0.1',
|
||||
containerStatuses: [
|
||||
{
|
||||
name: 'some-container',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
errors: [
|
||||
{
|
||||
type: 'some-error',
|
||||
severity: 10,
|
||||
message: 'some error message',
|
||||
occurrenceCount: 1,
|
||||
sourceRef: {
|
||||
name: 'some-pod',
|
||||
namespace: 'some-namespace',
|
||||
kind: 'Pod',
|
||||
apiGroup: 'v1',
|
||||
},
|
||||
proposedFix: [
|
||||
{
|
||||
type: 'logs',
|
||||
container: 'some-container',
|
||||
errorType: 'some error type',
|
||||
rootCauseExplanation: 'some root cause',
|
||||
actions: ['fix1', 'fix2'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
} as any)}
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getAllByText('some-pod')).toHaveLength(3);
|
||||
expect(screen.getByText('Pod (127.0.0.1)')).toBeInTheDocument();
|
||||
expect(screen.getByText('YAML')).toBeInTheDocument();
|
||||
expect(screen.getByText('Containers')).toBeInTheDocument();
|
||||
expect(screen.getByText('some-container')).toBeInTheDocument();
|
||||
expect(screen.getByText('some error message')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,189 +0,0 @@
|
||||
/*
|
||||
* 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 { ItemCardGrid } from '@backstage/core-components';
|
||||
import {
|
||||
createStyles,
|
||||
makeStyles,
|
||||
Theme,
|
||||
Grid,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
|
||||
import { Pod } from 'kubernetes-models/v1';
|
||||
|
||||
import { ContainerCard } from './ContainerCard';
|
||||
|
||||
import { PodAndErrors } from '../types';
|
||||
import { KubernetesDrawer } from '../../KubernetesDrawer';
|
||||
import { PendingPodContent } from './PendingPodContent';
|
||||
import { ErrorList } from '../ErrorList';
|
||||
import { usePodMetrics } from '../../../hooks/usePodMetrics';
|
||||
import { ResourceUtilization } from '../../ResourceUtilization';
|
||||
import { bytesToMiB, formatMillicores } from '../../../utils/resources';
|
||||
|
||||
const useDrawerContentStyles = makeStyles((_theme: Theme) =>
|
||||
createStyles({
|
||||
header: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
content: {
|
||||
height: '80%',
|
||||
},
|
||||
icon: {
|
||||
fontSize: 20,
|
||||
},
|
||||
podoklist: {
|
||||
width: '100%',
|
||||
maxWidth: 360,
|
||||
maxHeight: 360,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
function getContainerSpecByName(pod: Pod, containerName: string) {
|
||||
return pod.spec?.containers.find(c => c.name === containerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for PodDrawer
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
interface PodDrawerProps {
|
||||
open?: boolean;
|
||||
podAndErrors: PodAndErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* A Drawer for Kubernetes Pods
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const PodDrawer = ({ podAndErrors, open }: PodDrawerProps) => {
|
||||
const classes = useDrawerContentStyles();
|
||||
const podMetrics = usePodMetrics(podAndErrors.clusterName, podAndErrors.pod);
|
||||
|
||||
return (
|
||||
<KubernetesDrawer
|
||||
open={open}
|
||||
drawerContentsHeader={
|
||||
<Typography variant="subtitle1">
|
||||
Pod{' '}
|
||||
{podAndErrors.pod.status?.podIP &&
|
||||
`(${podAndErrors.pod.status?.podIP})`}
|
||||
</Typography>
|
||||
}
|
||||
kubernetesObject={podAndErrors.pod}
|
||||
label={
|
||||
<Typography variant="subtitle1">
|
||||
{podAndErrors.pod.metadata?.name ?? 'unknown'}
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<div className={classes.content}>
|
||||
{podMetrics && (
|
||||
<Grid container item xs={12}>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h5">Resource utilization</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<ResourceUtilization
|
||||
title="CPU requests"
|
||||
usage={podMetrics.cpu.currentUsage}
|
||||
total={podMetrics.cpu.requestTotal}
|
||||
totalFormatted={formatMillicores(podMetrics.cpu.requestTotal)}
|
||||
/>
|
||||
<ResourceUtilization
|
||||
title="CPU limits"
|
||||
usage={podMetrics.cpu.currentUsage}
|
||||
total={podMetrics.cpu.limitTotal}
|
||||
totalFormatted={formatMillicores(podMetrics.cpu.limitTotal)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<ResourceUtilization
|
||||
title="Memory requests"
|
||||
usage={podMetrics.memory.currentUsage}
|
||||
total={podMetrics.memory.requestTotal}
|
||||
totalFormatted={bytesToMiB(podMetrics.memory.requestTotal)}
|
||||
/>
|
||||
<ResourceUtilization
|
||||
title="Memory limits"
|
||||
usage={podMetrics.memory.currentUsage}
|
||||
total={podMetrics.memory.limitTotal}
|
||||
totalFormatted={bytesToMiB(podMetrics.memory.requestTotal)}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
{podAndErrors.pod.status?.phase === 'Pending' && (
|
||||
<PendingPodContent pod={podAndErrors.pod} />
|
||||
)}
|
||||
{podAndErrors.pod.status?.containerStatuses?.length && (
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h5">Containers</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<ItemCardGrid>
|
||||
{podAndErrors.pod.status?.containerStatuses?.map(
|
||||
(containerStatus, i) => {
|
||||
const containerSpec = getContainerSpecByName(
|
||||
podAndErrors.pod,
|
||||
containerStatus.name,
|
||||
);
|
||||
const containerMetrics = (
|
||||
podMetrics?.containers ?? []
|
||||
).find(c => c.container === containerStatus.name);
|
||||
return (
|
||||
<ContainerCard
|
||||
key={`container-card-${podAndErrors.pod.metadata?.name}-${i}`}
|
||||
containerMetrics={containerMetrics}
|
||||
podScope={{
|
||||
podName: podAndErrors.pod.metadata?.name ?? 'unknown',
|
||||
podNamespace:
|
||||
podAndErrors.pod.metadata?.namespace ?? 'unknown',
|
||||
clusterName: podAndErrors.clusterName,
|
||||
}}
|
||||
containerSpec={containerSpec}
|
||||
containerStatus={containerStatus}
|
||||
/>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</ItemCardGrid>
|
||||
</Grid>
|
||||
{podAndErrors.errors.length > 0 && (
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h5">Errors</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
{podAndErrors.errors.length > 0 && (
|
||||
<Grid item xs={12}>
|
||||
<ErrorList podAndErrors={[podAndErrors]} />
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
)}
|
||||
</div>
|
||||
</KubernetesDrawer>
|
||||
);
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
* 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 * from './PodDrawer';
|
||||
export * from './ContainerCard';
|
||||
export * from './PendingPodContent';
|
||||
@@ -1,84 +0,0 @@
|
||||
/*
|
||||
* 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 {
|
||||
DismissableBanner,
|
||||
EmptyState,
|
||||
LogViewer,
|
||||
} from '@backstage/core-components';
|
||||
import { Paper } from '@material-ui/core';
|
||||
import { Skeleton } from '@material-ui/lab';
|
||||
|
||||
import { ContainerScope } from './types';
|
||||
import { usePodLogs } from './usePodLogs';
|
||||
|
||||
/**
|
||||
* Props for PodLogs
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface PodLogsProps {
|
||||
containerScope: ContainerScope;
|
||||
previous?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the logs for the given pod
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const PodLogs: React.FC<PodLogsProps> = ({
|
||||
containerScope,
|
||||
previous,
|
||||
}: PodLogsProps) => {
|
||||
const { value, error, loading } = usePodLogs({
|
||||
containerScope,
|
||||
previous,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{error && (
|
||||
<DismissableBanner
|
||||
{...{
|
||||
message: error.message,
|
||||
variant: 'error',
|
||||
fixed: false,
|
||||
}}
|
||||
id="pod-logs"
|
||||
/>
|
||||
)}
|
||||
<Paper
|
||||
elevation={1}
|
||||
style={{ height: '100%', width: '100%', minHeight: '55rem' }}
|
||||
>
|
||||
{loading && <Skeleton variant="rect" width="100%" height="100%" />}
|
||||
{!loading &&
|
||||
value !== undefined &&
|
||||
(value.text === '' ? (
|
||||
<EmptyState
|
||||
missing="data"
|
||||
title="No logs emitted"
|
||||
description="No logs were emitted by the container"
|
||||
/>
|
||||
) : (
|
||||
<LogViewer text={value.text} />
|
||||
))}
|
||||
</Paper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,100 +0,0 @@
|
||||
/*
|
||||
* 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, { useState } from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
createStyles,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
makeStyles,
|
||||
Theme,
|
||||
} from '@material-ui/core';
|
||||
import SubjectIcon from '@material-ui/icons/Subject';
|
||||
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
|
||||
import { PodLogs } from './PodLogs';
|
||||
import { ContainerScope } from './types';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
closeButton: {
|
||||
position: 'absolute',
|
||||
right: theme.spacing(1),
|
||||
top: theme.spacing(1),
|
||||
color: theme.palette.grey[500],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Props for PodLogsDialog
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface PodLogsDialogProps {
|
||||
containerScope: ContainerScope;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the logs for the given pod in a Dialog
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const PodLogsDialog = ({ containerScope }: PodLogsDialogProps) => {
|
||||
const classes = useStyles();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const openDialog = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Dialog maxWidth="xl" fullWidth open={open} onClose={closeDialog}>
|
||||
<DialogTitle id="dialog-title">
|
||||
{containerScope.podName} - {containerScope.containerName} logs on
|
||||
cluster {containerScope.clusterName}
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
className={classes.closeButton}
|
||||
onClick={closeDialog}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<PodLogs containerScope={containerScope} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Button
|
||||
variant="outlined"
|
||||
aria-label="get logs"
|
||||
component="label"
|
||||
onClick={openDialog}
|
||||
startIcon={<SubjectIcon />}
|
||||
>
|
||||
Logs
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
/*
|
||||
* 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 * from './PodLogs';
|
||||
export * from './PodLogsDialog';
|
||||
export * from './usePodLogs';
|
||||
export * from './types';
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Contains the details needed to make a log request to Kubernetes, except the container name
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface PodScope {
|
||||
podName: string;
|
||||
podNamespace: string;
|
||||
clusterName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains the details needed to make a log request to Kubernetes
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface ContainerScope extends PodScope {
|
||||
containerName: string;
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* 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 { ContainerScope } from './types';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { kubernetesProxyApiRef } from '../../../api';
|
||||
|
||||
/**
|
||||
* Arguments for usePodLogs
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface PodLogsOptions {
|
||||
containerScope: ContainerScope;
|
||||
previous?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the logs for the given pod
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const usePodLogs = ({ containerScope, previous }: PodLogsOptions) => {
|
||||
const kubernetesProxyApi = useApi(kubernetesProxyApiRef);
|
||||
return useAsync(async () => {
|
||||
return await kubernetesProxyApi.getPodLogs({
|
||||
podName: containerScope.podName,
|
||||
namespace: containerScope.podNamespace,
|
||||
containerName: containerScope.containerName,
|
||||
clusterName: containerScope.clusterName,
|
||||
previous,
|
||||
});
|
||||
}, [JSON.stringify(containerScope)]);
|
||||
};
|
||||
@@ -1,181 +0,0 @@
|
||||
/*
|
||||
* 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 { screen } from '@testing-library/react';
|
||||
import * as pod from './__fixtures__/pod.json';
|
||||
import * as crashingPod from './__fixtures__/crashing-pod.json';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { PodsTable, READY_COLUMNS, RESOURCE_COLUMNS } from './PodsTable';
|
||||
import { kubernetesProviders } from '../../hooks/test-utils';
|
||||
import { ClientPodStatus } from '@backstage/plugin-kubernetes-common';
|
||||
|
||||
describe('PodsTable', () => {
|
||||
it('should render pod', async () => {
|
||||
await renderInTestApp(<PodsTable pods={[pod as any]} />);
|
||||
|
||||
// titles
|
||||
expect(screen.getByText('name')).toBeInTheDocument();
|
||||
expect(screen.getByText('phase')).toBeInTheDocument();
|
||||
expect(screen.getByText('status')).toBeInTheDocument();
|
||||
|
||||
// values
|
||||
expect(screen.getByText('dice-roller-6c8646bfd-2m5hv')).toBeInTheDocument();
|
||||
expect(screen.getByText('Running')).toBeInTheDocument();
|
||||
expect(screen.getByText('OK')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render pod with extra columns', async () => {
|
||||
await renderInTestApp(
|
||||
<PodsTable pods={[pod as any]} extraColumns={[READY_COLUMNS]} />,
|
||||
);
|
||||
|
||||
// titles
|
||||
expect(screen.getByText('name')).toBeInTheDocument();
|
||||
expect(screen.getByText('phase')).toBeInTheDocument();
|
||||
expect(screen.getByText('containers ready')).toBeInTheDocument();
|
||||
expect(screen.getByText('total restarts')).toBeInTheDocument();
|
||||
expect(screen.getByText('status')).toBeInTheDocument();
|
||||
|
||||
// values
|
||||
expect(screen.getByText('dice-roller-6c8646bfd-2m5hv')).toBeInTheDocument();
|
||||
expect(screen.getByText('Running')).toBeInTheDocument();
|
||||
expect(screen.getByText('1/1')).toBeInTheDocument();
|
||||
expect(screen.getByText('0')).toBeInTheDocument();
|
||||
expect(screen.getByText('OK')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render pod, with metrics context', async () => {
|
||||
const clusterToClientPodStatus = new Map<string, ClientPodStatus[]>();
|
||||
|
||||
clusterToClientPodStatus.set('some-cluster', [
|
||||
{
|
||||
pod: {
|
||||
metadata: {
|
||||
name: 'dice-roller-6c8646bfd-2m5hv',
|
||||
namespace: 'default',
|
||||
},
|
||||
},
|
||||
memory: {
|
||||
currentUsage: '1069056',
|
||||
requestTotal: '67108864',
|
||||
limitTotal: '134217728',
|
||||
},
|
||||
cpu: {
|
||||
currentUsage: 0.4966115,
|
||||
requestTotal: 0.05,
|
||||
limitTotal: 0.05,
|
||||
},
|
||||
},
|
||||
] as any);
|
||||
|
||||
const wrapper = kubernetesProviders(
|
||||
undefined,
|
||||
undefined,
|
||||
clusterToClientPodStatus,
|
||||
{
|
||||
name: 'some-cluster',
|
||||
},
|
||||
);
|
||||
await renderInTestApp(
|
||||
wrapper(
|
||||
<PodsTable
|
||||
pods={[pod as any]}
|
||||
extraColumns={[READY_COLUMNS, RESOURCE_COLUMNS]}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
// titles
|
||||
expect(screen.getByText('name')).toBeInTheDocument();
|
||||
expect(screen.getByText('phase')).toBeInTheDocument();
|
||||
expect(screen.getByText('containers ready')).toBeInTheDocument();
|
||||
expect(screen.getByText('total restarts')).toBeInTheDocument();
|
||||
expect(screen.getByText('status')).toBeInTheDocument();
|
||||
expect(screen.getByText('CPU usage %')).toBeInTheDocument();
|
||||
expect(screen.getByText('Memory usage %')).toBeInTheDocument();
|
||||
|
||||
// values
|
||||
expect(screen.getByText('dice-roller-6c8646bfd-2m5hv')).toBeInTheDocument();
|
||||
expect(screen.getByText('Running')).toBeInTheDocument();
|
||||
expect(screen.getByText('1/1')).toBeInTheDocument();
|
||||
expect(screen.getByText('0')).toBeInTheDocument();
|
||||
expect(screen.getByText('OK')).toBeInTheDocument();
|
||||
expect(screen.getByText('requests: 99% of 50m')).toBeInTheDocument();
|
||||
expect(screen.getByText('limits: 99% of 50m')).toBeInTheDocument();
|
||||
expect(screen.getByText('requests: 1% of 64MiB')).toBeInTheDocument();
|
||||
expect(screen.getByText('limits: 0% of 128MiB')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render placeholder when empty metrics context', async () => {
|
||||
const podNameToClientPodStatus = new Map<string, ClientPodStatus[]>();
|
||||
|
||||
const wrapper = kubernetesProviders(
|
||||
undefined,
|
||||
undefined,
|
||||
podNameToClientPodStatus,
|
||||
);
|
||||
await renderInTestApp(
|
||||
wrapper(
|
||||
<PodsTable
|
||||
pods={[pod as any]}
|
||||
extraColumns={[READY_COLUMNS, RESOURCE_COLUMNS]}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
// titles
|
||||
expect(screen.getByText('name')).toBeInTheDocument();
|
||||
expect(screen.getByText('phase')).toBeInTheDocument();
|
||||
expect(screen.getByText('containers ready')).toBeInTheDocument();
|
||||
expect(screen.getByText('total restarts')).toBeInTheDocument();
|
||||
expect(screen.getByText('status')).toBeInTheDocument();
|
||||
expect(screen.getByText('CPU usage %')).toBeInTheDocument();
|
||||
expect(screen.getByText('Memory usage %')).toBeInTheDocument();
|
||||
|
||||
// values
|
||||
expect(screen.getByText('dice-roller-6c8646bfd-2m5hv')).toBeInTheDocument();
|
||||
expect(screen.getByText('Running')).toBeInTheDocument();
|
||||
expect(screen.getByText('1/1')).toBeInTheDocument();
|
||||
expect(screen.getByText('0')).toBeInTheDocument();
|
||||
expect(screen.getByText('OK')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('unknown')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should render crashing pod with extra columns', async () => {
|
||||
await renderInTestApp(
|
||||
<PodsTable pods={[crashingPod as any]} extraColumns={[READY_COLUMNS]} />,
|
||||
);
|
||||
|
||||
// titles
|
||||
expect(screen.getByText('name')).toBeInTheDocument();
|
||||
expect(screen.getByText('phase')).toBeInTheDocument();
|
||||
expect(screen.getByText('containers ready')).toBeInTheDocument();
|
||||
expect(screen.getByText('total restarts')).toBeInTheDocument();
|
||||
expect(screen.getByText('status')).toBeInTheDocument();
|
||||
|
||||
// values
|
||||
expect(
|
||||
screen.getByText('dice-roller-canary-7d64cd756c-55rfq'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Running')).toBeInTheDocument();
|
||||
expect(screen.getByText('1/3')).toBeInTheDocument();
|
||||
expect(screen.getByText('76')).toBeInTheDocument();
|
||||
expect(screen.getByText('Container: side-car')).toBeInTheDocument();
|
||||
expect(screen.getByText('Container: other-side-car')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('CrashLoopBackOff')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
-157
@@ -1,157 +0,0 @@
|
||||
/*
|
||||
* 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, { useContext } from 'react';
|
||||
import { PodDrawer } from './PodDrawer';
|
||||
import {
|
||||
containersReady,
|
||||
containerStatuses,
|
||||
podStatusToCpuUtil,
|
||||
podStatusToMemoryUtil,
|
||||
totalRestarts,
|
||||
} from '../../utils/pod';
|
||||
import { Table, TableColumn } from '@backstage/core-components';
|
||||
import { ClusterContext } from '../../hooks/Cluster';
|
||||
import { useMatchingErrors } from '../../hooks/useMatchingErrors';
|
||||
import { Pod } from 'kubernetes-models/v1/Pod';
|
||||
import { V1Pod } from '@kubernetes/client-node';
|
||||
import { usePodMetrics } from '../../hooks/usePodMetrics';
|
||||
import { Typography } from '@material-ui/core';
|
||||
|
||||
export const READY_COLUMNS: PodColumns = 'READY';
|
||||
export const RESOURCE_COLUMNS: PodColumns = 'RESOURCE';
|
||||
export type PodColumns = 'READY' | 'RESOURCE';
|
||||
|
||||
type PodsTablesProps = {
|
||||
pods: Pod | V1Pod[];
|
||||
extraColumns?: PodColumns[];
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const READY: TableColumn<Pod>[] = [
|
||||
{
|
||||
title: 'containers ready',
|
||||
align: 'center',
|
||||
render: containersReady,
|
||||
width: 'auto',
|
||||
},
|
||||
{
|
||||
title: 'total restarts',
|
||||
align: 'center',
|
||||
render: totalRestarts,
|
||||
type: 'numeric',
|
||||
width: 'auto',
|
||||
},
|
||||
];
|
||||
|
||||
const PodDrawerTrigger = ({ pod }: { pod: Pod }) => {
|
||||
const cluster = useContext(ClusterContext);
|
||||
const errors = useMatchingErrors({
|
||||
kind: 'Pod',
|
||||
apiVersion: 'v1',
|
||||
metadata: pod.metadata,
|
||||
});
|
||||
return (
|
||||
<PodDrawer
|
||||
podAndErrors={{
|
||||
pod: pod as any,
|
||||
clusterName: cluster.name,
|
||||
errors: errors,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Cpu = ({ clusterName, pod }: { clusterName: string; pod: Pod }) => {
|
||||
const metrics = usePodMetrics(clusterName, pod);
|
||||
|
||||
if (!metrics) {
|
||||
return <Typography>unknown</Typography>;
|
||||
}
|
||||
|
||||
return <>{podStatusToCpuUtil(metrics)}</>;
|
||||
};
|
||||
|
||||
const Memory = ({ clusterName, pod }: { clusterName: string; pod: Pod }) => {
|
||||
const metrics = usePodMetrics(clusterName, pod);
|
||||
|
||||
if (!metrics) {
|
||||
return <Typography>unknown</Typography>;
|
||||
}
|
||||
|
||||
return <>{podStatusToMemoryUtil(metrics)}</>;
|
||||
};
|
||||
|
||||
export const PodsTable = ({ pods, extraColumns = [] }: PodsTablesProps) => {
|
||||
const cluster = useContext(ClusterContext);
|
||||
const defaultColumns: TableColumn<Pod>[] = [
|
||||
{
|
||||
title: 'name',
|
||||
highlight: true,
|
||||
render: (pod: Pod) => {
|
||||
return <PodDrawerTrigger pod={pod} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'phase',
|
||||
render: (pod: Pod) => pod.status?.phase ?? 'unknown',
|
||||
width: 'auto',
|
||||
},
|
||||
{
|
||||
title: 'status',
|
||||
render: containerStatuses,
|
||||
},
|
||||
];
|
||||
const columns: TableColumn<Pod>[] = [...defaultColumns];
|
||||
|
||||
if (extraColumns.includes(READY_COLUMNS)) {
|
||||
columns.push(...READY);
|
||||
}
|
||||
if (extraColumns.includes(RESOURCE_COLUMNS)) {
|
||||
const resourceColumns: TableColumn<Pod>[] = [
|
||||
{
|
||||
title: 'CPU usage %',
|
||||
render: (pod: Pod) => {
|
||||
return <Cpu clusterName={cluster.name} pod={pod} />;
|
||||
},
|
||||
width: 'auto',
|
||||
},
|
||||
{
|
||||
title: 'Memory usage %',
|
||||
render: (pod: Pod) => {
|
||||
return <Memory clusterName={cluster.name} pod={pod} />;
|
||||
},
|
||||
width: 'auto',
|
||||
},
|
||||
];
|
||||
columns.push(...resourceColumns);
|
||||
}
|
||||
|
||||
const tableStyle = {
|
||||
minWidth: '0',
|
||||
width: '100%',
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={tableStyle}>
|
||||
<Table
|
||||
options={{ paging: true, search: false, emptyRowsWhenPaging: false }}
|
||||
data={pods as Pod[]}
|
||||
columns={columns}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,233 +0,0 @@
|
||||
{
|
||||
"metadata": {
|
||||
"creationTimestamp": "2020-09-25T10:34:01.000Z",
|
||||
"generateName": "dice-roller-canary-7d64cd756c-",
|
||||
"labels": {
|
||||
"app": "dice-roller-canary",
|
||||
"backstage.io/kubernetes-id": "dice-roller",
|
||||
"pod-template-hash": "7d64cd756c"
|
||||
},
|
||||
"name": "dice-roller-canary-7d64cd756c-55rfq",
|
||||
"namespace": "default",
|
||||
"ownerReferences": [
|
||||
{
|
||||
"apiVersion": "apps/v1",
|
||||
"blockOwnerDeletion": true,
|
||||
"controller": true,
|
||||
"kind": "ReplicaSet",
|
||||
"name": "dice-roller-canary-7d64cd756c",
|
||||
"uid": "9208395b-a9a7-4e46-b881-6a189f7fbdb0"
|
||||
}
|
||||
],
|
||||
"resourceVersion": "620452",
|
||||
"selfLink": "/api/v1/namespaces/default/pods/dice-roller-canary-7d64cd756c-55rfq",
|
||||
"uid": "65ad28e3-5d51-4b4b-9bf8-4cb069803034"
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"image": "nginx:1.14.2",
|
||||
"imagePullPolicy": "IfNotPresent",
|
||||
"name": "nginx",
|
||||
"ports": [
|
||||
{
|
||||
"containerPort": 80,
|
||||
"protocol": "TCP"
|
||||
}
|
||||
],
|
||||
"resources": {},
|
||||
"terminationMessagePath": "/dev/termination-log",
|
||||
"terminationMessagePolicy": "File",
|
||||
"volumeMounts": [
|
||||
{
|
||||
"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
|
||||
"name": "default-token-5gctn",
|
||||
"readOnly": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"image": "nginx:1.14.2",
|
||||
"imagePullPolicy": "IfNotPresent",
|
||||
"name": "side-car",
|
||||
"ports": [
|
||||
{
|
||||
"containerPort": 81,
|
||||
"protocol": "TCP"
|
||||
}
|
||||
],
|
||||
"resources": {},
|
||||
"terminationMessagePath": "/dev/termination-log",
|
||||
"terminationMessagePolicy": "File",
|
||||
"volumeMounts": [
|
||||
{
|
||||
"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
|
||||
"name": "default-token-5gctn",
|
||||
"readOnly": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"image": "nginx:1.14.2",
|
||||
"imagePullPolicy": "IfNotPresent",
|
||||
"name": "other-side-car",
|
||||
"ports": [
|
||||
{
|
||||
"containerPort": 82,
|
||||
"protocol": "TCP"
|
||||
}
|
||||
],
|
||||
"resources": {},
|
||||
"terminationMessagePath": "/dev/termination-log",
|
||||
"terminationMessagePolicy": "File",
|
||||
"volumeMounts": [
|
||||
{
|
||||
"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
|
||||
"name": "default-token-5gctn",
|
||||
"readOnly": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"dnsPolicy": "ClusterFirst",
|
||||
"enableServiceLinks": true,
|
||||
"nodeName": "minikube",
|
||||
"priority": 0,
|
||||
"restartPolicy": "Always",
|
||||
"schedulerName": "default-scheduler",
|
||||
"securityContext": {},
|
||||
"serviceAccount": "default",
|
||||
"serviceAccountName": "default",
|
||||
"terminationGracePeriodSeconds": 30,
|
||||
"tolerations": [
|
||||
{
|
||||
"effect": "NoExecute",
|
||||
"key": "node.kubernetes.io/not-ready",
|
||||
"operator": "Exists",
|
||||
"tolerationSeconds": 300
|
||||
},
|
||||
{
|
||||
"effect": "NoExecute",
|
||||
"key": "node.kubernetes.io/unreachable",
|
||||
"operator": "Exists",
|
||||
"tolerationSeconds": 300
|
||||
}
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"name": "default-token-5gctn",
|
||||
"secret": {
|
||||
"defaultMode": 420,
|
||||
"secretName": "default-token-5gctn"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"conditions": [
|
||||
{
|
||||
"lastProbeTime": null,
|
||||
"lastTransitionTime": "2020-09-25T10:34:01.000Z",
|
||||
"status": "True",
|
||||
"type": "Initialized"
|
||||
},
|
||||
{
|
||||
"lastProbeTime": null,
|
||||
"lastTransitionTime": "2020-09-25T14:18:53.000Z",
|
||||
"message": "containers with unready status: [side-car other-side-car]",
|
||||
"reason": "ContainersNotReady",
|
||||
"status": "False",
|
||||
"type": "Ready"
|
||||
},
|
||||
{
|
||||
"lastProbeTime": null,
|
||||
"lastTransitionTime": "2020-09-25T14:18:53.000Z",
|
||||
"message": "containers with unready status: [side-car other-side-car]",
|
||||
"reason": "ContainersNotReady",
|
||||
"status": "False",
|
||||
"type": "ContainersReady"
|
||||
},
|
||||
{
|
||||
"lastProbeTime": null,
|
||||
"lastTransitionTime": "2020-09-25T10:34:01.000Z",
|
||||
"status": "True",
|
||||
"type": "PodScheduled"
|
||||
}
|
||||
],
|
||||
"containerStatuses": [
|
||||
{
|
||||
"containerID": "docker://6ce15178d114a85f3d2e832de45c3355ab5b71ed5f4d4d225ee1c83bf07f69d9",
|
||||
"image": "nginx:1.14.2",
|
||||
"imageID": "docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d",
|
||||
"lastState": {},
|
||||
"name": "nginx",
|
||||
"ready": true,
|
||||
"restartCount": 0,
|
||||
"started": true,
|
||||
"state": {
|
||||
"running": {
|
||||
"startedAt": "2020-09-25T10:34:01.000Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"containerID": "docker://b3ce93d7f90bfe22558c61d2505b8473580574accdebb5fa4e51c0729c3511f4",
|
||||
"image": "nginx:1.14.2",
|
||||
"imageID": "docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d",
|
||||
"lastState": {
|
||||
"terminated": {
|
||||
"containerID": "docker://b3ce93d7f90bfe22558c61d2505b8473580574accdebb5fa4e51c0729c3511f4",
|
||||
"exitCode": 1,
|
||||
"finishedAt": "2020-09-25T14:18:52.000Z",
|
||||
"reason": "Error",
|
||||
"startedAt": "2020-09-25T14:18:50.000Z"
|
||||
}
|
||||
},
|
||||
"name": "other-side-car",
|
||||
"ready": false,
|
||||
"restartCount": 38,
|
||||
"started": false,
|
||||
"state": {
|
||||
"waiting": {
|
||||
"message": "back-off 5m0s restarting failed container=other-side-car pod=dice-roller-canary-7d64cd756c-55rfq_default(65ad28e3-5d51-4b4b-9bf8-4cb069803034)",
|
||||
"reason": "CrashLoopBackOff"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"containerID": "docker://b7f0e65a2b8ab48c5f234616cfe8286aa96b55c3ef09c5cfbc4cdbe67a96f8cb",
|
||||
"image": "nginx:1.14.2",
|
||||
"imageID": "docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d",
|
||||
"lastState": {
|
||||
"terminated": {
|
||||
"containerID": "docker://b7f0e65a2b8ab48c5f234616cfe8286aa96b55c3ef09c5cfbc4cdbe67a96f8cb",
|
||||
"exitCode": 1,
|
||||
"finishedAt": "2020-09-25T14:18:52.000Z",
|
||||
"reason": "Error",
|
||||
"startedAt": "2020-09-25T14:18:50.000Z"
|
||||
}
|
||||
},
|
||||
"name": "side-car",
|
||||
"ready": false,
|
||||
"restartCount": 38,
|
||||
"started": false,
|
||||
"state": {
|
||||
"waiting": {
|
||||
"message": "back-off 5m0s restarting failed container=side-car pod=dice-roller-canary-7d64cd756c-55rfq_default(65ad28e3-5d51-4b4b-9bf8-4cb069803034)",
|
||||
"reason": "CrashLoopBackOff"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"hostIP": "192.168.64.2",
|
||||
"phase": "Running",
|
||||
"podIP": "172.17.0.16",
|
||||
"podIPs": [
|
||||
{
|
||||
"ip": "172.17.0.16"
|
||||
}
|
||||
],
|
||||
"qosClass": "BestEffort",
|
||||
"startTime": "2020-09-25T10:34:01.000Z"
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
{
|
||||
"metadata": {
|
||||
"creationTimestamp": "2020-09-25T09:58:50.000Z",
|
||||
"generateName": "dice-roller-6c8646bfd-",
|
||||
"labels": {
|
||||
"app": "dice-roller",
|
||||
"backstage.io/kubernetes-id": "dice-roller",
|
||||
"pod-template-hash": "6c8646bfd"
|
||||
},
|
||||
"name": "dice-roller-6c8646bfd-2m5hv",
|
||||
"namespace": "default",
|
||||
"ownerReferences": [
|
||||
{
|
||||
"apiVersion": "apps/v1",
|
||||
"blockOwnerDeletion": true,
|
||||
"controller": true,
|
||||
"kind": "ReplicaSet",
|
||||
"name": "dice-roller-6c8646bfd",
|
||||
"uid": "5126c354-4310-4e23-a9e4-c9b87cb69792"
|
||||
}
|
||||
],
|
||||
"resourceVersion": "593216",
|
||||
"selfLink": "/api/v1/namespaces/default/pods/dice-roller-6c8646bfd-2m5hv",
|
||||
"uid": "aadb71c0-36fa-43e3-b38a-162f134d4359"
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"image": "nginx:1.14.2",
|
||||
"imagePullPolicy": "IfNotPresent",
|
||||
"name": "nginx",
|
||||
"ports": [
|
||||
{
|
||||
"containerPort": 80,
|
||||
"protocol": "TCP"
|
||||
}
|
||||
],
|
||||
"resources": {},
|
||||
"terminationMessagePath": "/dev/termination-log",
|
||||
"terminationMessagePolicy": "File",
|
||||
"volumeMounts": [
|
||||
{
|
||||
"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
|
||||
"name": "default-token-5gctn",
|
||||
"readOnly": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"dnsPolicy": "ClusterFirst",
|
||||
"enableServiceLinks": true,
|
||||
"nodeName": "minikube",
|
||||
"priority": 0,
|
||||
"restartPolicy": "Always",
|
||||
"schedulerName": "default-scheduler",
|
||||
"securityContext": {},
|
||||
"serviceAccount": "default",
|
||||
"serviceAccountName": "default",
|
||||
"terminationGracePeriodSeconds": 30,
|
||||
"tolerations": [
|
||||
{
|
||||
"effect": "NoExecute",
|
||||
"key": "node.kubernetes.io/not-ready",
|
||||
"operator": "Exists",
|
||||
"tolerationSeconds": 300
|
||||
},
|
||||
{
|
||||
"effect": "NoExecute",
|
||||
"key": "node.kubernetes.io/unreachable",
|
||||
"operator": "Exists",
|
||||
"tolerationSeconds": 300
|
||||
}
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"name": "default-token-5gctn",
|
||||
"secret": {
|
||||
"defaultMode": 420,
|
||||
"secretName": "default-token-5gctn"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"conditions": [
|
||||
{
|
||||
"lastProbeTime": null,
|
||||
"lastTransitionTime": "2020-09-25T09:58:50.000Z",
|
||||
"status": "True",
|
||||
"type": "Initialized"
|
||||
},
|
||||
{
|
||||
"lastProbeTime": null,
|
||||
"lastTransitionTime": "2020-09-25T09:58:55.000Z",
|
||||
"status": "True",
|
||||
"type": "Ready"
|
||||
},
|
||||
{
|
||||
"lastProbeTime": null,
|
||||
"lastTransitionTime": "2020-09-25T09:58:55.000Z",
|
||||
"status": "True",
|
||||
"type": "ContainersReady"
|
||||
},
|
||||
{
|
||||
"lastProbeTime": null,
|
||||
"lastTransitionTime": "2020-09-25T09:58:50.000Z",
|
||||
"status": "True",
|
||||
"type": "PodScheduled"
|
||||
}
|
||||
],
|
||||
"containerStatuses": [
|
||||
{
|
||||
"containerID": "docker://aa4489297c34c48bb33c18474a8d2b33854a82ed42155680b259f635f556ce70",
|
||||
"image": "nginx:1.14.2",
|
||||
"imageID": "docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d",
|
||||
"lastState": {},
|
||||
"name": "nginx",
|
||||
"ready": true,
|
||||
"restartCount": 0,
|
||||
"started": true,
|
||||
"state": {
|
||||
"running": {
|
||||
"startedAt": "2020-09-25T09:58:53.000Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"hostIP": "192.168.64.2",
|
||||
"phase": "Running",
|
||||
"podIP": "172.17.0.11",
|
||||
"podIPs": [
|
||||
{
|
||||
"ip": "172.17.0.11"
|
||||
}
|
||||
],
|
||||
"qosClass": "BestEffort",
|
||||
"startTime": "2020-09-25T09:58:50.000Z"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user