Kubernetes: initial CRD framework (#4892)
* Kubernetes: initial CRD framework Signed-off-by: mclarke <mclarke@spotify.com> * use luxon instead of date-fns Signed-off-by: mclarke <mclarke@spotify.com> * fix test Signed-off-by: mclarke <mclarke@spotify.com> * update ui schema Signed-off-by: mclarke <mclarke@spotify.com> * update app config Signed-off-by: mclarke <mclarke@spotify.com> * rollout tests Signed-off-by: mclarke <mclarke@spotify.com> * add default custom resource component Signed-off-by: mclarke <mclarke@spotify.com> * make custom resources optional Signed-off-by: mclarke <mclarke@spotify.com> * add docs Signed-off-by: mclarke <mclarke@spotify.com> * prettier Signed-off-by: mclarke <mclarke@spotify.com> * replace core-api usage with core Signed-off-by: mclarke <mclarke@spotify.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes': minor
|
||||
'@backstage/plugin-kubernetes-backend': minor
|
||||
---
|
||||
|
||||
Add initial CRD support framework
|
||||
@@ -123,6 +123,34 @@ The Google Cloud project to look for Kubernetes clusters in.
|
||||
The Google Cloud region to look for Kubernetes clusters in. Defaults to all
|
||||
regions.
|
||||
|
||||
### `customResources` (optional)
|
||||
|
||||
Configures which [custom resources][3] to look for when returning an entity's
|
||||
Kubernetes resources.
|
||||
|
||||
Defaults to empty array. router.ts Example:
|
||||
|
||||
```yaml
|
||||
---
|
||||
kubernetes:
|
||||
customResources:
|
||||
- group: 'argoproj.io'
|
||||
apiVersion: 'v1alpha1'
|
||||
plural: 'rollouts'
|
||||
```
|
||||
|
||||
#### `customResources.\*.group`
|
||||
|
||||
The custom resource's group.
|
||||
|
||||
#### `customResources.\*.apiVersion`
|
||||
|
||||
The custom resource's apiVersion.
|
||||
|
||||
#### `customResources.\*.plural`
|
||||
|
||||
The plural representing the custom resource.
|
||||
|
||||
### Role Based Access Control
|
||||
|
||||
The current RBAC permissions required are read-only cluster wide, for the
|
||||
@@ -176,3 +204,5 @@ for more info.
|
||||
|
||||
[1]: https://cloud.google.com/kubernetes-engine
|
||||
[2]: https://cloud.google.com/docs/authentication/production#linux-or-macos
|
||||
[3]:
|
||||
https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/
|
||||
|
||||
+2
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ClusterLocatorMethod } from './src/types';
|
||||
import { ClusterLocatorMethod, CustomResource } from './src/types';
|
||||
|
||||
export interface Config {
|
||||
kubernetes?: {
|
||||
@@ -22,5 +22,6 @@ export interface Config {
|
||||
type: 'multiTenant';
|
||||
};
|
||||
clusterLocatorMethods: ClusterLocatorMethod[];
|
||||
customResources?: CustomResource[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
CoreV1Api,
|
||||
KubeConfig,
|
||||
NetworkingV1beta1Api,
|
||||
CustomObjectsApi,
|
||||
} from '@kubernetes/client-node';
|
||||
import { ClusterDetails } from '../types/types';
|
||||
|
||||
@@ -78,4 +79,10 @@ export class KubernetesClientProvider {
|
||||
|
||||
return kc.makeApiClient(NetworkingV1beta1Api);
|
||||
}
|
||||
|
||||
getCustomObjectsClient(clusterDetails: ClusterDetails) {
|
||||
const kc = this.getKubeConfig(clusterDetails);
|
||||
|
||||
return kc.makeApiClient(CustomObjectsApi);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
{
|
||||
getClustersByServiceId,
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const result = await sut.getKubernetesObjectsByEntity({
|
||||
@@ -178,6 +179,7 @@ describe('handleGetKubernetesObjectsForService', () => {
|
||||
{
|
||||
getClustersByServiceId,
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const result = await sut.getKubernetesObjectsByEntity({
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import { Logger } from 'winston';
|
||||
import {
|
||||
ClusterDetails,
|
||||
CustomResource,
|
||||
KubernetesFetcher,
|
||||
KubernetesObjectTypes,
|
||||
KubernetesRequestBody,
|
||||
@@ -39,15 +40,18 @@ export class KubernetesFanOutHandler {
|
||||
private readonly logger: Logger;
|
||||
private readonly fetcher: KubernetesFetcher;
|
||||
private readonly serviceLocator: KubernetesServiceLocator;
|
||||
private readonly customResources: CustomResource[];
|
||||
|
||||
constructor(
|
||||
logger: Logger,
|
||||
fetcher: KubernetesFetcher,
|
||||
serviceLocator: KubernetesServiceLocator,
|
||||
customResources: CustomResource[],
|
||||
) {
|
||||
this.logger = logger;
|
||||
this.fetcher = fetcher;
|
||||
this.serviceLocator = serviceLocator;
|
||||
this.customResources = customResources;
|
||||
}
|
||||
|
||||
async getKubernetesObjectsByEntity(
|
||||
@@ -93,6 +97,7 @@ export class KubernetesFanOutHandler {
|
||||
clusterDetails: clusterDetailsItem,
|
||||
objectTypesToFetch,
|
||||
labelSelector,
|
||||
customResources: this.customResources,
|
||||
})
|
||||
.then(result => {
|
||||
return {
|
||||
|
||||
@@ -15,10 +15,9 @@
|
||||
*/
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { ObjectFetchParams } from '../types/types';
|
||||
import { KubernetesClientBasedFetcher } from './KubernetesFetcher';
|
||||
|
||||
describe('KubernetesClientProvider', () => {
|
||||
describe('KubernetesFetcher', () => {
|
||||
let clientMock: any;
|
||||
let kubernetesClientProvider: any;
|
||||
let sut: KubernetesClientBasedFetcher;
|
||||
@@ -35,6 +34,7 @@ describe('KubernetesClientProvider', () => {
|
||||
getAppsClientByClusterDetails: jest.fn(() => clientMock),
|
||||
getAutoscalingClientByClusterDetails: jest.fn(() => clientMock),
|
||||
getNetworkingBeta1Client: jest.fn(() => clientMock),
|
||||
getCustomObjectsClient: jest.fn(() => clientMock),
|
||||
};
|
||||
|
||||
sut = new KubernetesClientBasedFetcher({
|
||||
@@ -58,7 +58,7 @@ describe('KubernetesClientProvider', () => {
|
||||
|
||||
clientMock.listServiceForAllNamespaces.mockRejectedValue(errorResponse);
|
||||
|
||||
const result = await sut.fetchObjectsForService(<ObjectFetchParams>{
|
||||
const result = await sut.fetchObjectsForService({
|
||||
serviceId: 'some-service',
|
||||
clusterDetails: {
|
||||
name: 'cluster1',
|
||||
@@ -68,6 +68,7 @@ describe('KubernetesClientProvider', () => {
|
||||
},
|
||||
objectTypesToFetch: new Set(['pods', 'services']),
|
||||
labelSelector: '',
|
||||
customResources: [],
|
||||
});
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
@@ -122,7 +123,7 @@ describe('KubernetesClientProvider', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const result = await sut.fetchObjectsForService(<ObjectFetchParams>{
|
||||
const result = await sut.fetchObjectsForService({
|
||||
serviceId: 'some-service',
|
||||
clusterDetails: {
|
||||
name: 'cluster1',
|
||||
@@ -132,6 +133,7 @@ describe('KubernetesClientProvider', () => {
|
||||
},
|
||||
objectTypesToFetch: new Set(['pods', 'services']),
|
||||
labelSelector: '',
|
||||
customResources: [],
|
||||
});
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
@@ -172,7 +174,7 @@ describe('KubernetesClientProvider', () => {
|
||||
});
|
||||
it('should throw error on unknown type', () => {
|
||||
expect(() =>
|
||||
sut.fetchObjectsForService(<ObjectFetchParams>{
|
||||
sut.fetchObjectsForService({
|
||||
serviceId: 'some-service',
|
||||
clusterDetails: {
|
||||
name: 'cluster1',
|
||||
@@ -182,6 +184,7 @@ describe('KubernetesClientProvider', () => {
|
||||
},
|
||||
objectTypesToFetch: new Set<any>(['foo']),
|
||||
labelSelector: '',
|
||||
customResources: [],
|
||||
}),
|
||||
).toThrow('unrecognised type=foo');
|
||||
|
||||
@@ -304,7 +307,7 @@ describe('KubernetesClientProvider', () => {
|
||||
},
|
||||
});
|
||||
|
||||
await sut.fetchObjectsForService(<ObjectFetchParams>{
|
||||
await sut.fetchObjectsForService({
|
||||
serviceId: 'some-service',
|
||||
clusterDetails: {
|
||||
name: 'cluster1',
|
||||
@@ -314,6 +317,7 @@ describe('KubernetesClientProvider', () => {
|
||||
},
|
||||
objectTypesToFetch: new Set(['pods', 'services']),
|
||||
labelSelector: '',
|
||||
customResources: [],
|
||||
});
|
||||
|
||||
const mockCall = clientMock.listPodForAllNamespaces.mock.calls[0];
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
KubernetesFetchError,
|
||||
KubernetesObjectTypes,
|
||||
ObjectFetchParams,
|
||||
CustomResource,
|
||||
} from '../types/types';
|
||||
import { KubernetesClientProvider } from './KubernetesClientProvider';
|
||||
|
||||
@@ -120,7 +121,18 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
|
||||
).catch(captureKubernetesErrorsRethrowOthers);
|
||||
});
|
||||
|
||||
return Promise.all(fetchResults).then(fetchResultsToResponseWrapper);
|
||||
const customObjectsFetchResults = params.customResources.map(cr => {
|
||||
return this.fetchCustomResource(
|
||||
params.clusterDetails,
|
||||
cr,
|
||||
params.labelSelector ||
|
||||
`backstage.io/kubernetes-id=${params.serviceId}`,
|
||||
).catch(captureKubernetesErrorsRethrowOthers);
|
||||
});
|
||||
|
||||
return Promise.all(fetchResults.concat(customObjectsFetchResults)).then(
|
||||
fetchResultsToResponseWrapper,
|
||||
);
|
||||
}
|
||||
|
||||
// TODO could probably do with a tidy up
|
||||
@@ -173,6 +185,30 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
|
||||
}
|
||||
}
|
||||
|
||||
private fetchCustomResource(
|
||||
clusterDetails: ClusterDetails,
|
||||
customResource: CustomResource,
|
||||
labelSelector: string,
|
||||
): Promise<FetchResponse> {
|
||||
const customObjects = this.kubernetesClientProvider.getCustomObjectsClient(
|
||||
clusterDetails,
|
||||
);
|
||||
|
||||
return customObjects
|
||||
.listClusterCustomObject(
|
||||
customResource.group,
|
||||
customResource.apiVersion,
|
||||
customResource.plural,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
labelSelector,
|
||||
)
|
||||
.then(r => {
|
||||
return { type: 'customresources', resources: (r.body as any).items };
|
||||
});
|
||||
}
|
||||
|
||||
private singleClusterFetch<T>(
|
||||
clusterDetails: ClusterDetails,
|
||||
fn: (
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
KubernetesRequestBody,
|
||||
KubernetesServiceLocator,
|
||||
ServiceLocatorMethod,
|
||||
CustomResource,
|
||||
} from '../types/types';
|
||||
import { KubernetesClientProvider } from './KubernetesClientProvider';
|
||||
import { KubernetesFanOutHandler } from './KubernetesFanOutHandler';
|
||||
@@ -99,6 +100,21 @@ export async function createRouter(
|
||||
|
||||
logger.info('Initializing Kubernetes backend');
|
||||
|
||||
const customResources: CustomResource[] = (
|
||||
options.config.getOptionalConfigArray('kubernetes.customResources') ?? []
|
||||
).map(
|
||||
c =>
|
||||
({
|
||||
group: c.getString('group'),
|
||||
apiVersion: c.getString('apiVersion'),
|
||||
plural: c.getString('plural'),
|
||||
} as CustomResource),
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`action=LoadingCustomResources numOfCustomResources=${customResources.length}`,
|
||||
);
|
||||
|
||||
const fetcher = new KubernetesClientBasedFetcher({
|
||||
kubernetesClientProvider: new KubernetesClientProvider(),
|
||||
logger,
|
||||
@@ -122,6 +138,7 @@ export async function createRouter(
|
||||
logger,
|
||||
fetcher,
|
||||
serviceLocator,
|
||||
customResources,
|
||||
);
|
||||
|
||||
return makeRouter(logger, kubernetesFanOutHandler, clusterDetails);
|
||||
|
||||
@@ -61,7 +61,8 @@ export type FetchResponse =
|
||||
| DeploymentFetchResponse
|
||||
| ReplicaSetsFetchResponse
|
||||
| HorizontalPodAutoscalersFetchResponse
|
||||
| IngressesFetchResponse;
|
||||
| IngressesFetchResponse
|
||||
| CustomResourceFetchResponse;
|
||||
|
||||
// TODO fairly sure there's a easier way to do this
|
||||
|
||||
@@ -72,7 +73,8 @@ export type KubernetesObjectTypes =
|
||||
| 'deployments'
|
||||
| 'replicasets'
|
||||
| 'horizontalpodautoscalers'
|
||||
| 'ingresses';
|
||||
| 'ingresses'
|
||||
| 'customresources';
|
||||
|
||||
export interface PodFetchResponse {
|
||||
type: 'pods';
|
||||
@@ -109,11 +111,17 @@ export interface IngressesFetchResponse {
|
||||
resources: Array<ExtensionsV1beta1Ingress>;
|
||||
}
|
||||
|
||||
export interface CustomResourceFetchResponse {
|
||||
type: 'customresources';
|
||||
resources: Array<any>;
|
||||
}
|
||||
|
||||
export interface ObjectFetchParams {
|
||||
serviceId: string;
|
||||
clusterDetails: ClusterDetails;
|
||||
objectTypesToFetch: Set<KubernetesObjectTypes>;
|
||||
labelSelector: string;
|
||||
customResources: CustomResource[];
|
||||
}
|
||||
|
||||
// Fetches information from a kubernetes cluster using the cluster details object
|
||||
@@ -192,3 +200,9 @@ export type ClusterLocatorMethod =
|
||||
|
||||
export type ServiceLocatorMethod = 'multiTenant' | 'http'; // TODO implement http
|
||||
export type AuthProviderType = 'google' | 'serviceAccount' | 'aws';
|
||||
|
||||
export interface CustomResource {
|
||||
group: string;
|
||||
apiVersion: string;
|
||||
plural: string;
|
||||
}
|
||||
|
||||
@@ -32,9 +32,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.7.3",
|
||||
"@backstage/plugin-catalog-react": "^0.1.1",
|
||||
"@backstage/config": "^0.1.3",
|
||||
"@backstage/core": "^0.7.0",
|
||||
"@backstage/plugin-catalog-react": "^0.1.1",
|
||||
"@backstage/plugin-kubernetes-backend": "^0.2.8",
|
||||
"@backstage/theme": "^0.2.3",
|
||||
"@kubernetes/client-node": "^0.13.2",
|
||||
@@ -42,6 +42,8 @@
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"js-yaml": "^4.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^1.26.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
|
||||
Vendored
+8
-1
@@ -14,7 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ClusterLocatorMethod } from '@backstage/plugin-kubernetes-backend';
|
||||
import {
|
||||
ClusterLocatorMethod,
|
||||
CustomResource,
|
||||
} from '@backstage/plugin-kubernetes-backend';
|
||||
|
||||
export interface Config {
|
||||
kubernetes?: {
|
||||
@@ -31,5 +34,9 @@ export interface Config {
|
||||
* @visibility frontend
|
||||
*/
|
||||
clusterLocatorMethods: ClusterLocatorMethod[];
|
||||
/**
|
||||
* @visibility frontend
|
||||
*/
|
||||
customResources?: CustomResource[];
|
||||
};
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 { wrapInTestApp } 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([]));
|
||||
|
||||
const { getByText, queryByText } = render(
|
||||
wrapper(wrapInTestApp(<RolloutAccordions rollouts={[rollout] as any} />)),
|
||||
);
|
||||
|
||||
expect(getByText('dice-roller')).toBeInTheDocument();
|
||||
expect(getByText('Rollout')).toBeInTheDocument();
|
||||
expect(getByText('2 pods')).toBeInTheDocument();
|
||||
expect(getByText('No pods with errors')).toBeInTheDocument();
|
||||
expect(queryByText('Paused')).toBeNull();
|
||||
});
|
||||
it('should render RolloutAccordion with error', async () => {
|
||||
const wrapper = kubernetesProviders(
|
||||
groupedResources,
|
||||
new Set(['dice-roller-6c8646bfd-2m5hv']),
|
||||
);
|
||||
|
||||
const { getByText, queryByText } = render(
|
||||
wrapper(wrapInTestApp(<RolloutAccordions rollouts={[rollout] as any} />)),
|
||||
);
|
||||
|
||||
expect(getByText('dice-roller')).toBeInTheDocument();
|
||||
expect(getByText('Rollout')).toBeInTheDocument();
|
||||
expect(getByText('2 pods')).toBeInTheDocument();
|
||||
expect(getByText('1 pod with errors')).toBeInTheDocument();
|
||||
expect(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));
|
||||
|
||||
const { getByText } = render(
|
||||
wrapper(
|
||||
wrapInTestApp(<RolloutAccordions rollouts={[pausedRollout] as any} />),
|
||||
),
|
||||
);
|
||||
|
||||
expect(getByText('dice-roller')).toBeInTheDocument();
|
||||
expect(getByText('Rollout')).toBeInTheDocument();
|
||||
expect(getByText('2 pods')).toBeInTheDocument();
|
||||
expect(getByText('No pods with errors')).toBeInTheDocument();
|
||||
expect(getByText('Paused (45 minutes ago)')).toBeInTheDocument();
|
||||
});
|
||||
it('should render aborted Rollout with aborted text', async () => {
|
||||
const wrapper = kubernetesProviders(groupedResources, new Set([]));
|
||||
|
||||
const { getByText, getAllByText, queryByText } = render(
|
||||
wrapper(
|
||||
wrapInTestApp(
|
||||
<RolloutAccordions
|
||||
defaultExpanded
|
||||
rollouts={[abortedRollout] as any}
|
||||
/>,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(getByText('dice-roller')).toBeInTheDocument();
|
||||
expect(getByText('Rollout')).toBeInTheDocument();
|
||||
expect(getByText('2 pods')).toBeInTheDocument();
|
||||
expect(getByText('No pods with errors')).toBeInTheDocument();
|
||||
expect(queryByText('Paused')).toBeNull();
|
||||
expect(getByText('Rollout status')).toBeInTheDocument();
|
||||
expect(getAllByText('Aborted')).toHaveLength(2);
|
||||
expect(
|
||||
getByText('some metric related failure message'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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,
|
||||
Divider,
|
||||
Grid,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import { V1Pod, V1HorizontalPodAutoscaler } from '@kubernetes/client-node';
|
||||
import { StatusError, StatusOK } from '@backstage/core';
|
||||
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';
|
||||
|
||||
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" justify="flex-start" alignItems="center">
|
||||
<Grid xs={3} item>
|
||||
<RolloutDrawer rollout={rollout} />
|
||||
</Grid>
|
||||
<Grid item xs={1}>
|
||||
<Divider style={{ height: '5em' }} orientation="vertical" />
|
||||
</Grid>
|
||||
{hpa && (
|
||||
<Grid item xs={3}>
|
||||
<HorizontalPodAutoscalerDrawer hpa={hpa}>
|
||||
<Grid
|
||||
item
|
||||
container
|
||||
direction="column"
|
||||
justify="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"
|
||||
justify="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<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 }}
|
||||
>
|
||||
<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} />
|
||||
</div>
|
||||
</div>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
export const RolloutAccordions = ({
|
||||
rollouts,
|
||||
defaultExpanded = false,
|
||||
}: RolloutAccordionsProps) => {
|
||||
const groupedResponses = useContext(GroupedResponsesContext);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
justify="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
{rollouts.map((rollout, i) => (
|
||||
<Grid container item key={i} xs>
|
||||
<Grid item xs>
|
||||
<RolloutAccordion
|
||||
defaultExpanded={defaultExpanded}
|
||||
matchingHpa={getMatchingHpa(
|
||||
rollout.metadata?.name,
|
||||
'rollout',
|
||||
groupedResponses.horizontalPodAutoscalers,
|
||||
)}
|
||||
ownedPods={getOwnedPodsThroughReplicaSets(
|
||||
rollout,
|
||||
groupedResponses.replicaSets,
|
||||
groupedResponses.pods,
|
||||
)}
|
||||
rollout={rollout}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { KubernetesDrawer } from '../../KubernetesDrawer/KubernetesDrawer';
|
||||
import { Typography, Grid } from '@material-ui/core';
|
||||
|
||||
export const RolloutDrawer = ({
|
||||
rollout,
|
||||
expanded,
|
||||
}: {
|
||||
rollout: any;
|
||||
expanded?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<KubernetesDrawer
|
||||
object={rollout}
|
||||
expanded={expanded}
|
||||
kind="Rollout"
|
||||
renderObject={() => ({})}
|
||||
>
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
justify="flex-start"
|
||||
alignItems="flex-start"
|
||||
spacing={0}
|
||||
>
|
||||
<Grid item>
|
||||
<Typography variant="h5">
|
||||
{rollout.metadata?.name ?? 'unknown object'}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography color="textSecondary" variant="body1">
|
||||
Rollout
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</KubernetesDrawer>
|
||||
);
|
||||
};
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 pauseSteps from './__fixtures__/pause-steps';
|
||||
import setWeightSteps from './__fixtures__/setweight-steps';
|
||||
import analysisSteps from './__fixtures__/analysis-steps';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { StepsProgress } from './StepsProgress';
|
||||
|
||||
describe('StepsProgress', () => {
|
||||
it('should render Pause step text', async () => {
|
||||
const { getByText } = render(
|
||||
wrapInTestApp(
|
||||
<StepsProgress
|
||||
currentStepIndex={0}
|
||||
aborted={false}
|
||||
steps={pauseSteps}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(getByText('pause for 1h')).toBeInTheDocument();
|
||||
expect(getByText('infinite pause')).toBeInTheDocument();
|
||||
});
|
||||
it('should render SetWeight step text', async () => {
|
||||
const { getByText } = render(
|
||||
wrapInTestApp(
|
||||
<StepsProgress
|
||||
currentStepIndex={0}
|
||||
aborted={false}
|
||||
steps={setWeightSteps}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(getByText('setWeight 10%')).toBeInTheDocument();
|
||||
expect(getByText('setWeight 95%')).toBeInTheDocument();
|
||||
});
|
||||
it('should render Analysis step text', async () => {
|
||||
const { getAllByText, getByText } = render(
|
||||
wrapInTestApp(
|
||||
<StepsProgress
|
||||
currentStepIndex={0}
|
||||
aborted={false}
|
||||
steps={analysisSteps}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(getAllByText('analysis templates:')).toHaveLength(2);
|
||||
expect(getByText('always-pass')).toBeInTheDocument();
|
||||
expect(getByText('always-fail')).toBeInTheDocument();
|
||||
expect(getByText('req-rate (cluster scoped)')).toBeInTheDocument();
|
||||
});
|
||||
it('should render 3 different steps', async () => {
|
||||
const { getByText } = render(
|
||||
wrapInTestApp(
|
||||
<StepsProgress
|
||||
currentStepIndex={0}
|
||||
aborted={false}
|
||||
steps={[setWeightSteps[0], pauseSteps[0], analysisSteps[0]]}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(getByText('setWeight 10%')).toBeInTheDocument();
|
||||
expect(getByText('pause for 1h')).toBeInTheDocument();
|
||||
expect(getByText('analysis templates:')).toBeInTheDocument();
|
||||
expect(getByText('always-pass')).toBeInTheDocument();
|
||||
expect(getByText('Canary promoted')).toBeInTheDocument();
|
||||
});
|
||||
it('current step is highlighted, previous steps are ticked', async () => {
|
||||
const { getByText, queryByText } = render(
|
||||
wrapInTestApp(
|
||||
<StepsProgress
|
||||
currentStepIndex={1}
|
||||
aborted={false}
|
||||
steps={[setWeightSteps[0], pauseSteps[0], analysisSteps[0]]}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
// It is ticked, so it's not visible
|
||||
expect(queryByText('1')).toBeNull();
|
||||
// The current step
|
||||
expect(getByText('2')).toBeInTheDocument();
|
||||
// The future step
|
||||
expect(getByText('3')).toBeInTheDocument();
|
||||
// The canary promoted step should always be added at the end
|
||||
expect(getByText('4')).toBeInTheDocument();
|
||||
});
|
||||
it('aborted canary has all steps grey', async () => {
|
||||
const { getByText } = render(
|
||||
wrapInTestApp(
|
||||
<StepsProgress
|
||||
currentStepIndex={2}
|
||||
aborted
|
||||
steps={[setWeightSteps[0], pauseSteps[0], analysisSteps[0]]}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(getByText('1')).toBeInTheDocument();
|
||||
expect(getByText('2')).toBeInTheDocument();
|
||||
expect(getByText('3')).toBeInTheDocument();
|
||||
expect(getByText('4')).toBeInTheDocument();
|
||||
});
|
||||
it('promoted canary has all steps ticked', async () => {
|
||||
const { queryByText } = render(
|
||||
wrapInTestApp(
|
||||
<StepsProgress
|
||||
currentStepIndex={3}
|
||||
aborted={false}
|
||||
steps={[setWeightSteps[0], pauseSteps[0], analysisSteps[0]]}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(queryByText('1')).toBeNull();
|
||||
expect(queryByText('2')).toBeNull();
|
||||
expect(queryByText('3')).toBeNull();
|
||||
expect(queryByText('4')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 {
|
||||
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>
|
||||
<p>analysis templates:</p>
|
||||
{step.analysis.templates.map((t, i) => (
|
||||
<p key={i}>{`${t.templateName}${
|
||||
t.clusterScope ? ' (cluster scoped)' : ''
|
||||
}`}</p>
|
||||
))}
|
||||
</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
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"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
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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
@@ -0,0 +1,576 @@
|
||||
{
|
||||
"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
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"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
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"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
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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;
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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';
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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;
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 { wrapInTestApp } 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([]));
|
||||
|
||||
const { getByText } = render(
|
||||
wrapper(
|
||||
wrapInTestApp(
|
||||
<DefaultCustomResourceAccordions
|
||||
customResources={[ar] as any}
|
||||
customResourceName="AnalysisRun"
|
||||
/>,
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(getByText('dice-roller-546c476497-4-1')).toBeInTheDocument();
|
||||
expect(getByText('AnalysisRun')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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,
|
||||
Divider,
|
||||
Grid,
|
||||
} from '@material-ui/core';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import { DefaultCustomResourceDrawer } from './DefaultCustomResourceDrawer';
|
||||
import { StructuredMetadataTable } from '@backstage/core';
|
||||
|
||||
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" justify="flex-start" alignItems="center">
|
||||
<Grid xs={3} item>
|
||||
<DefaultCustomResourceDrawer
|
||||
customResource={customResource}
|
||||
customResourceName={customResourceName}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={1}>
|
||||
<Divider style={{ height: '5em' }} orientation="vertical" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
const DefaultCustomResourceAccordion = ({
|
||||
customResource,
|
||||
customResourceName,
|
||||
defaultExpanded,
|
||||
}: DefaultCustomResourceAccordionProps) => {
|
||||
return (
|
||||
<Accordion
|
||||
defaultExpanded={defaultExpanded}
|
||||
TransitionProps={{ unmountOnExit: true }}
|
||||
>
|
||||
<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"
|
||||
justify="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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer';
|
||||
import { Typography, Grid } from '@material-ui/core';
|
||||
|
||||
const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
|
||||
|
||||
export const DefaultCustomResourceDrawer = ({
|
||||
customResource,
|
||||
customResourceName,
|
||||
expanded,
|
||||
}: {
|
||||
customResource: any;
|
||||
customResourceName: string;
|
||||
expanded?: boolean;
|
||||
}) => {
|
||||
const capitalizedName = capitalize(customResourceName);
|
||||
|
||||
return (
|
||||
<KubernetesDrawer
|
||||
object={customResource}
|
||||
expanded={expanded}
|
||||
kind={capitalizedName}
|
||||
renderObject={cr => cr}
|
||||
>
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
justify="flex-start"
|
||||
alignItems="flex-start"
|
||||
spacing={0}
|
||||
>
|
||||
<Grid item>
|
||||
<Typography variant="h5">
|
||||
{customResource.metadata?.name ?? 'unknown object'}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography color="textSecondary" variant="body1">
|
||||
{capitalizedName}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</KubernetesDrawer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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';
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import * as deployments from './__fixtures__/2-deployments.json';
|
||||
import * as deployments from '../../__fixtures__/2-deployments.json';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { DeploymentDrawer } from './DeploymentDrawer';
|
||||
|
||||
|
||||
+8
-9
@@ -17,20 +17,19 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { DeploymentsAccordions } from './DeploymentsAccordions';
|
||||
import * as twoDeployFixture from './__fixtures__/2-deployments.json';
|
||||
import * as twoDeployFixture from '../../__fixtures__/2-deployments.json';
|
||||
import { wrapInTestApp } 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']),
|
||||
);
|
||||
|
||||
const { getByText } = render(
|
||||
wrapInTestApp(
|
||||
<DeploymentsAccordions
|
||||
deploymentResources={twoDeployFixture as any}
|
||||
clusterPodNamesWithErrors={
|
||||
new Set(['dice-roller-canary-7d64cd756c-vtbdx'])
|
||||
}
|
||||
/>,
|
||||
),
|
||||
wrapper(wrapInTestApp(<DeploymentsAccordions />)),
|
||||
);
|
||||
|
||||
expect(getByText('dice-roller')).toBeInTheDocument();
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DeploymentResources } from '../../types/types';
|
||||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
@@ -25,21 +24,25 @@ import {
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import { V1OwnerReference } from '@kubernetes/client-node/dist/gen/model/v1OwnerReference';
|
||||
import {
|
||||
V1Deployment,
|
||||
V1Pod,
|
||||
V1ReplicaSet,
|
||||
V1HorizontalPodAutoscaler,
|
||||
} from '@kubernetes/client-node';
|
||||
import { StatusError, StatusOK } from '@backstage/core';
|
||||
import { PodsTable } from '../Pods';
|
||||
import { DeploymentDrawer } from './DeploymentDrawer';
|
||||
import { HorizontalPodAutoscalerDrawer } from '../HorizontalPodAutoscalers';
|
||||
import {
|
||||
getOwnedPodsThroughReplicaSets,
|
||||
getMatchingHpa,
|
||||
} from '../../utils/owner';
|
||||
import {
|
||||
GroupedResponsesContext,
|
||||
PodNamesWithErrorsContext,
|
||||
} from '../../hooks';
|
||||
|
||||
type DeploymentsAccordionsProps = {
|
||||
deploymentResources: DeploymentResources;
|
||||
clusterPodNamesWithErrors: Set<string>;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
@@ -47,7 +50,6 @@ type DeploymentAccordionProps = {
|
||||
deployment: V1Deployment;
|
||||
ownedPods: V1Pod[];
|
||||
matchingHpa?: V1HorizontalPodAutoscaler;
|
||||
clusterPodNamesWithErrors: Set<string>;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
@@ -136,10 +138,11 @@ const DeploymentAccordion = ({
|
||||
deployment,
|
||||
ownedPods,
|
||||
matchingHpa,
|
||||
clusterPodNamesWithErrors,
|
||||
}: DeploymentAccordionProps) => {
|
||||
const podNamesWithErrors = useContext(PodNamesWithErrorsContext);
|
||||
|
||||
const podsWithErrors = ownedPods.filter(p =>
|
||||
clusterPodNamesWithErrors.has(p.metadata?.name ?? ''),
|
||||
podNamesWithErrors.has(p.metadata?.name ?? ''),
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -159,16 +162,8 @@ const DeploymentAccordion = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const DeploymentsAccordions = ({
|
||||
deploymentResources,
|
||||
clusterPodNamesWithErrors,
|
||||
}: DeploymentsAccordionsProps) => {
|
||||
const isOwnedBy = (
|
||||
ownerReferences: V1OwnerReference[],
|
||||
obj: V1Pod | V1ReplicaSet | V1Deployment,
|
||||
): boolean => {
|
||||
return ownerReferences?.some(or => or.name === obj.metadata?.name);
|
||||
};
|
||||
export const DeploymentsAccordions = ({}: DeploymentsAccordionsProps) => {
|
||||
const groupedResponses = useContext(GroupedResponsesContext);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
@@ -177,43 +172,23 @@ export const DeploymentsAccordions = ({
|
||||
justify="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
{deploymentResources.deployments.map((deployment, i) => (
|
||||
{groupedResponses.deployments.map((deployment, i) => (
|
||||
<Grid container item key={i} xs>
|
||||
{deploymentResources.replicaSets
|
||||
// Filter out replica sets with no replicas
|
||||
.filter(rs => rs.status && rs.status.replicas > 0)
|
||||
// Find the replica sets this deployment owns
|
||||
.filter(rs =>
|
||||
isOwnedBy(rs.metadata?.ownerReferences ?? [], deployment),
|
||||
)
|
||||
.map((rs, j) => {
|
||||
// Find the pods this replica set owns and render them in the table
|
||||
const ownedPods = deploymentResources.pods.filter(pod =>
|
||||
isOwnedBy(pod.metadata?.ownerReferences ?? [], rs),
|
||||
);
|
||||
|
||||
const matchingHpa = deploymentResources.horizontalPodAutoscalers.find(
|
||||
(hpa: V1HorizontalPodAutoscaler) => {
|
||||
return (
|
||||
(hpa.spec?.scaleTargetRef?.kind ?? '').toLowerCase() ===
|
||||
'deployment' &&
|
||||
(hpa.spec?.scaleTargetRef?.name ?? '') ===
|
||||
(deployment.metadata?.name ?? 'unknown-deployment')
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<Grid item key={j} xs>
|
||||
<DeploymentAccordion
|
||||
deployment={deployment}
|
||||
ownedPods={ownedPods}
|
||||
matchingHpa={matchingHpa}
|
||||
clusterPodNamesWithErrors={clusterPodNamesWithErrors}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
<Grid item xs>
|
||||
<DeploymentAccordion
|
||||
matchingHpa={getMatchingHpa(
|
||||
deployment.metadata?.name,
|
||||
'deployment',
|
||||
groupedResponses.horizontalPodAutoscalers,
|
||||
)}
|
||||
ownedPods={getOwnedPodsThroughReplicaSets(
|
||||
deployment,
|
||||
groupedResponses.replicaSets,
|
||||
groupedResponses.pods,
|
||||
)}
|
||||
deployment={deployment}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
@@ -19,13 +19,14 @@ import { render } from '@testing-library/react';
|
||||
import * as oneIngressFixture from './__fixtures__/2-ingresses.json';
|
||||
import { wrapInTestApp } 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());
|
||||
|
||||
const { getByText } = render(
|
||||
wrapInTestApp(
|
||||
<IngressesAccordions deploymentResources={oneIngressFixture as any} />,
|
||||
),
|
||||
wrapper(wrapInTestApp(<IngressesAccordions />)),
|
||||
);
|
||||
|
||||
expect(getByText('awesome-service')).toBeInTheDocument();
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { GroupedResponses } from '../../types/types';
|
||||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
@@ -27,10 +26,9 @@ import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import { ExtensionsV1beta1Ingress } from '@kubernetes/client-node';
|
||||
import { StructuredMetadataTable } from '@backstage/core';
|
||||
import { IngressDrawer } from './IngressDrawer';
|
||||
import { GroupedResponsesContext } from '../../hooks';
|
||||
|
||||
type IngressesAccordionsProps = {
|
||||
deploymentResources: GroupedResponses;
|
||||
};
|
||||
type IngressesAccordionsProps = {};
|
||||
|
||||
type IngressAccordionProps = {
|
||||
ingress: ExtensionsV1beta1Ingress;
|
||||
@@ -80,9 +78,8 @@ const IngressAccordion = ({ ingress }: IngressAccordionProps) => {
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
export const IngressesAccordions = ({
|
||||
deploymentResources,
|
||||
}: IngressesAccordionsProps) => {
|
||||
export const IngressesAccordions = ({}: IngressesAccordionsProps) => {
|
||||
const groupedResponses = useContext(GroupedResponsesContext);
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
@@ -90,7 +87,7 @@ export const IngressesAccordions = ({
|
||||
justify="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
{deploymentResources.ingresses.map((ingress, i) => (
|
||||
{groupedResponses.ingresses.map((ingress, i) => (
|
||||
<Grid item key={i} xs>
|
||||
<IngressAccordion ingress={ingress} />
|
||||
</Grid>
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { KubernetesContent } from './KubernetesContent';
|
||||
import { useKubernetesObjects } from '../../hooks';
|
||||
|
||||
jest.mock('../../hooks');
|
||||
import * as oneDeployment from '../../__fixtures__/1-deployments.json';
|
||||
import * as twoDeployments from '../../__fixtures__/2-deployments.json';
|
||||
|
||||
describe('KubernetesContent', () => {
|
||||
it('render empty response', async () => {
|
||||
(useKubernetesObjects as any).mockReturnValue({
|
||||
kubernetesObjects: {
|
||||
items: [],
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
const { getByText } = render(
|
||||
wrapInTestApp(
|
||||
<KubernetesContent
|
||||
entity={
|
||||
{
|
||||
metadata: {
|
||||
name: 'some-entity',
|
||||
},
|
||||
} as any
|
||||
}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(getByText('Error Reporting')).toBeInTheDocument();
|
||||
expect(
|
||||
getByText('Nice! There are no errors to report!'),
|
||||
).toBeInTheDocument();
|
||||
expect(getByText('Your Clusters')).toBeInTheDocument();
|
||||
// TODO add a prompt for the user to configure their clusters
|
||||
});
|
||||
it('render 1 cluster happy path', async () => {
|
||||
(useKubernetesObjects as any).mockReturnValue({
|
||||
kubernetesObjects: {
|
||||
items: [
|
||||
{
|
||||
cluster: { name: 'cluster-1' },
|
||||
resources: [
|
||||
{
|
||||
type: 'deployments',
|
||||
resources: oneDeployment.deployments,
|
||||
},
|
||||
{
|
||||
type: 'replicasets',
|
||||
resources: oneDeployment.replicaSets,
|
||||
},
|
||||
{
|
||||
type: 'pods',
|
||||
resources: oneDeployment.pods,
|
||||
},
|
||||
],
|
||||
errors: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
const { getByText } = render(
|
||||
wrapInTestApp(
|
||||
<KubernetesContent
|
||||
entity={
|
||||
{
|
||||
metadata: {
|
||||
name: 'some-entity',
|
||||
},
|
||||
} as any
|
||||
}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
getByText('Nice! There are no errors to report!'),
|
||||
).toBeInTheDocument();
|
||||
expect(getByText('cluster-1')).toBeInTheDocument();
|
||||
expect(getByText('Cluster')).toBeInTheDocument();
|
||||
expect(getByText('10 pods')).toBeInTheDocument();
|
||||
expect(getByText('No pods with errors')).toBeInTheDocument();
|
||||
});
|
||||
it('render 2 clusters happy path, one with errors', async () => {
|
||||
(useKubernetesObjects as any).mockReturnValue({
|
||||
kubernetesObjects: {
|
||||
items: [
|
||||
{
|
||||
cluster: { name: 'cluster-1' },
|
||||
resources: [
|
||||
{
|
||||
type: 'deployments',
|
||||
resources: [twoDeployments.deployments[1]],
|
||||
},
|
||||
{
|
||||
type: 'replicasets',
|
||||
resources: twoDeployments.replicaSets,
|
||||
},
|
||||
{
|
||||
type: 'pods',
|
||||
resources: twoDeployments.pods,
|
||||
},
|
||||
],
|
||||
errors: [],
|
||||
},
|
||||
{
|
||||
cluster: { name: 'cluster-a' },
|
||||
resources: [
|
||||
{
|
||||
type: 'deployments',
|
||||
resources: oneDeployment.deployments,
|
||||
},
|
||||
{
|
||||
type: 'replicasets',
|
||||
resources: oneDeployment.replicaSets,
|
||||
},
|
||||
{
|
||||
type: 'pods',
|
||||
resources: oneDeployment.pods,
|
||||
},
|
||||
],
|
||||
errors: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
const { getByText, getAllByText, queryByText } = render(
|
||||
wrapInTestApp(
|
||||
<KubernetesContent
|
||||
entity={
|
||||
{
|
||||
metadata: {
|
||||
name: 'some-entity',
|
||||
},
|
||||
} as any
|
||||
}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(queryByText('Nice! There are no errors to report!')).toBeNull();
|
||||
expect(getAllByText('Cluster')).toHaveLength(2);
|
||||
expect(getByText('cluster-a')).toBeInTheDocument();
|
||||
expect(getByText('10 pods')).toBeInTheDocument();
|
||||
expect(getByText('No pods with errors')).toBeInTheDocument();
|
||||
|
||||
expect(getAllByText('cluster-1')).toHaveLength(6);
|
||||
expect(getByText('12 pods')).toBeInTheDocument();
|
||||
expect(getByText('2 pods with errors')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React from 'react';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
@@ -29,16 +29,9 @@ import {
|
||||
Progress,
|
||||
StatusError,
|
||||
StatusOK,
|
||||
useApi,
|
||||
} from '@backstage/core';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { kubernetesApiRef } from '../../api/types';
|
||||
import {
|
||||
ClusterObjects,
|
||||
KubernetesRequestBody,
|
||||
ObjectsByEntityResponse,
|
||||
} from '@backstage/plugin-kubernetes-backend';
|
||||
import { kubernetesAuthProvidersApiRef } from '../../kubernetes-auth-provider/types';
|
||||
import { ClusterObjects } from '@backstage/plugin-kubernetes-backend';
|
||||
import { ErrorPanel } from './ErrorPanel';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import { DeploymentsAccordions } from '../DeploymentsAccordions';
|
||||
@@ -47,6 +40,12 @@ import { groupResponses } from '../../utils/response';
|
||||
import { DetectedError, detectErrors } from '../../error-detection';
|
||||
import { IngressesAccordions } from '../IngressesAccordions';
|
||||
import { ServicesAccordions } from '../ServicesAccordions';
|
||||
import { CustomResources } from '../CustomResources';
|
||||
import {
|
||||
GroupedResponsesContext,
|
||||
PodNamesWithErrorsContext,
|
||||
useKubernetesObjects,
|
||||
} from '../../hooks';
|
||||
|
||||
type ClusterSummaryProps = {
|
||||
clusterName: string;
|
||||
@@ -68,8 +67,9 @@ const ClusterSummary = ({
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Grid
|
||||
xs={2}
|
||||
xs={4}
|
||||
item
|
||||
container
|
||||
direction="column"
|
||||
justify="flex-start"
|
||||
alignItems="flex-start"
|
||||
@@ -110,96 +110,49 @@ const ClusterSummary = ({
|
||||
|
||||
type ClusterProps = {
|
||||
clusterObjects: ClusterObjects;
|
||||
detectedErrors?: DetectedError[];
|
||||
podsWithErrors: Set<string>;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const Cluster = ({ clusterObjects, detectedErrors }: ClusterProps) => {
|
||||
const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => {
|
||||
const groupedResponses = groupResponses(clusterObjects.resources);
|
||||
|
||||
const podsWithErrors = new Set<string>(
|
||||
detectedErrors
|
||||
?.filter(de => de.kind === 'Pod')
|
||||
.map(de => de.names)
|
||||
.flat() ?? [],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<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">
|
||||
<Grid item>
|
||||
<DeploymentsAccordions
|
||||
deploymentResources={groupedResponses}
|
||||
clusterPodNamesWithErrors={podsWithErrors}
|
||||
/>
|
||||
<GroupedResponsesContext.Provider value={groupedResponses}>
|
||||
<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">
|
||||
<Grid item>
|
||||
<CustomResources />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<DeploymentsAccordions />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<IngressesAccordions />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<ServicesAccordions />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<IngressesAccordions deploymentResources={groupedResponses} />
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<ServicesAccordions deploymentResources={groupedResponses} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</PodNamesWithErrorsContext.Provider>
|
||||
</GroupedResponsesContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
type KubernetesContentProps = { entity: Entity; children?: React.ReactNode };
|
||||
|
||||
export const KubernetesContent = ({ entity }: KubernetesContentProps) => {
|
||||
const kubernetesApi = useApi(kubernetesApiRef);
|
||||
|
||||
const [kubernetesObjects, setKubernetesObjects] = useState<
|
||||
ObjectsByEntityResponse | undefined
|
||||
>(undefined);
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
|
||||
const kubernetesAuthProvidersApi = useApi(kubernetesAuthProvidersApiRef);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const clusters = await kubernetesApi.getClusters();
|
||||
const authProviders: string[] = [
|
||||
...new Set(clusters.map(c => c.authProvider)),
|
||||
];
|
||||
// For each auth type, invoke decorateRequestBodyForAuth on corresponding KubernetesAuthProvider
|
||||
let requestBody: KubernetesRequestBody = {
|
||||
entity,
|
||||
};
|
||||
for (const authProviderStr of authProviders) {
|
||||
// Multiple asyncs done sequentially instead of all at once to prevent same requestBody from being modified simultaneously
|
||||
requestBody = await kubernetesAuthProvidersApi.decorateRequestBodyForAuth(
|
||||
authProviderStr,
|
||||
requestBody,
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Add validation on contents/format of requestBody
|
||||
kubernetesApi
|
||||
.getObjectsByEntity(requestBody)
|
||||
.then(result => {
|
||||
setKubernetesObjects(result);
|
||||
})
|
||||
.catch(e => {
|
||||
setError(e.message);
|
||||
});
|
||||
})();
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
}, [entity.metadata.name, kubernetesApi, kubernetesAuthProvidersApi]);
|
||||
/* eslint-enable react-hooks/exhaustive-deps */
|
||||
const { kubernetesObjects, error } = useKubernetesObjects(entity);
|
||||
|
||||
const clustersWithErrors =
|
||||
kubernetesObjects?.items.filter(r => r.errors.length > 0) ?? [];
|
||||
@@ -250,14 +203,24 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => {
|
||||
<Typography variant="h3">Your Clusters</Typography>
|
||||
</Grid>
|
||||
<Grid item container>
|
||||
{kubernetesObjects?.items.map((item, i) => (
|
||||
<Grid item key={i} xs={12}>
|
||||
<Cluster
|
||||
clusterObjects={item}
|
||||
detectedErrors={detectedErrors.get(item.cluster.name)}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
{kubernetesObjects?.items.map((item, i) => {
|
||||
const podsWithErrors = new Set<string>(
|
||||
detectedErrors
|
||||
.get(item.cluster.name)
|
||||
?.filter(de => de.kind === 'Pod')
|
||||
.map(de => de.names)
|
||||
.flat() ?? [],
|
||||
);
|
||||
|
||||
return (
|
||||
<Grid item key={i} xs={12}>
|
||||
<Cluster
|
||||
clusterObjects={item}
|
||||
podsWithErrors={podsWithErrors}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
@@ -84,6 +84,15 @@ interface KubernetesDrawerContentProps<T extends KubernetesDrawerable> {
|
||||
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));
|
||||
}
|
||||
|
||||
const KubernetesDrawerContent = <T extends KubernetesDrawerable>({
|
||||
toggleDrawer,
|
||||
object,
|
||||
@@ -139,7 +148,11 @@ const KubernetesDrawerContent = <T extends KubernetesDrawerable>({
|
||||
</div>
|
||||
<div className={classes.content}>
|
||||
{isYaml && <CodeSnippet language="yaml" text={jsYaml.dump(object)} />}
|
||||
{!isYaml && <StructuredMetadataTable metadata={renderObject(object)} />}
|
||||
{!isYaml && (
|
||||
<StructuredMetadataTable
|
||||
metadata={renderObject(replaceNullsWithUndefined(object))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -19,13 +19,14 @@ import { render } from '@testing-library/react';
|
||||
import * as twoDeployFixture from './__fixtures__/2-services.json';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { ServicesAccordions } from './ServicesAccordions';
|
||||
import { kubernetesProviders } from '../../hooks/test-utils';
|
||||
|
||||
describe('ServicesAccordions', () => {
|
||||
it('should render 2 services', async () => {
|
||||
const wrapper = kubernetesProviders(twoDeployFixture, new Set());
|
||||
|
||||
const { getByText } = render(
|
||||
wrapInTestApp(
|
||||
<ServicesAccordions deploymentResources={twoDeployFixture as any} />,
|
||||
),
|
||||
wrapper(wrapInTestApp(<ServicesAccordions />)),
|
||||
);
|
||||
|
||||
expect(getByText('awesome-service-grpc')).toBeInTheDocument();
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { GroupedResponses } from '../../types/types';
|
||||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
@@ -28,6 +27,7 @@ import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import { V1Service } from '@kubernetes/client-node';
|
||||
import { StructuredMetadataTable } from '@backstage/core';
|
||||
import { ServiceDrawer } from './ServiceDrawer';
|
||||
import { GroupedResponsesContext } from '../../hooks';
|
||||
|
||||
type ServiceSummaryProps = {
|
||||
service: V1Service;
|
||||
@@ -82,9 +82,7 @@ const ServiceCard = ({ service }: ServiceCardProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
type ServicesAccordionsProps = {
|
||||
deploymentResources: GroupedResponses;
|
||||
};
|
||||
type ServicesAccordionsProps = {};
|
||||
|
||||
type ServiceAccordionProps = {
|
||||
service: V1Service;
|
||||
@@ -103,9 +101,8 @@ const ServiceAccordion = ({ service }: ServiceAccordionProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const ServicesAccordions = ({
|
||||
deploymentResources,
|
||||
}: ServicesAccordionsProps) => {
|
||||
export const ServicesAccordions = ({}: ServicesAccordionsProps) => {
|
||||
const groupedResponses = useContext(GroupedResponsesContext);
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
@@ -113,7 +110,7 @@ export const ServicesAccordions = ({
|
||||
justify="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
{deploymentResources.services.map((service, i) => (
|
||||
{groupedResponses.services.map((service, i) => (
|
||||
<Grid item key={i} xs>
|
||||
<ServiceAccordion service={service} />
|
||||
</Grid>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 { GroupedResponses } from '../types/types';
|
||||
|
||||
export const GroupedResponsesContext = React.createContext<GroupedResponses>({
|
||||
pods: [],
|
||||
replicaSets: [],
|
||||
deployments: [],
|
||||
services: [],
|
||||
configMaps: [],
|
||||
horizontalPodAutoscalers: [],
|
||||
ingresses: [],
|
||||
customResources: [],
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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';
|
||||
|
||||
export const PodNamesWithErrorsContext = React.createContext<Set<string>>(
|
||||
new Set<string>(),
|
||||
);
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 './useKubernetesObjects';
|
||||
export * from './PodNamesWithErrors';
|
||||
export * from './GroupedResponses';
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 { GroupedResponsesContext } from './GroupedResponses';
|
||||
import { PodNamesWithErrorsContext } from './PodNamesWithErrors';
|
||||
|
||||
export const kubernetesProviders = (
|
||||
groupedResponses: any,
|
||||
podsWithErrors: any,
|
||||
) => {
|
||||
return (node: React.ReactNode) => (
|
||||
<>
|
||||
<GroupedResponsesContext.Provider value={groupedResponses}>
|
||||
<PodNamesWithErrorsContext.Provider value={podsWithErrors}>
|
||||
{node}
|
||||
</PodNamesWithErrorsContext.Provider>
|
||||
</GroupedResponsesContext.Provider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 { Entity } from '@backstage/catalog-model';
|
||||
import { useApi } from '@backstage/core';
|
||||
import { kubernetesApiRef } from '../api/types';
|
||||
import { kubernetesAuthProvidersApiRef } from '../kubernetes-auth-provider/types';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
KubernetesRequestBody,
|
||||
ObjectsByEntityResponse,
|
||||
} from '@backstage/plugin-kubernetes-backend';
|
||||
|
||||
export interface KubernetesObjects {
|
||||
kubernetesObjects: ObjectsByEntityResponse | undefined;
|
||||
error: string | undefined;
|
||||
}
|
||||
|
||||
export const useKubernetesObjects = (entity: Entity): KubernetesObjects => {
|
||||
const kubernetesApi = useApi(kubernetesApiRef);
|
||||
const kubernetesAuthProvidersApi = useApi(kubernetesAuthProvidersApiRef);
|
||||
const [kubernetesObjects, setKubernetesObjects] = useState<
|
||||
ObjectsByEntityResponse | undefined
|
||||
>(undefined);
|
||||
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const clusters = await kubernetesApi.getClusters();
|
||||
const authProviders: string[] = [
|
||||
...new Set(clusters.map(c => c.authProvider)),
|
||||
];
|
||||
// For each auth type, invoke decorateRequestBodyForAuth on corresponding KubernetesAuthProvider
|
||||
let requestBody: KubernetesRequestBody = {
|
||||
entity,
|
||||
};
|
||||
for (const authProviderStr of authProviders) {
|
||||
// Multiple asyncs done sequentially instead of all at once to prevent same requestBody from being modified simultaneously
|
||||
requestBody = await kubernetesAuthProvidersApi.decorateRequestBodyForAuth(
|
||||
authProviderStr,
|
||||
requestBody,
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Add validation on contents/format of requestBody
|
||||
kubernetesApi
|
||||
.getObjectsByEntity(requestBody)
|
||||
.then(result => {
|
||||
setKubernetesObjects(result);
|
||||
})
|
||||
.catch(e => {
|
||||
setError(e.message);
|
||||
});
|
||||
})();
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
}, [entity.metadata.name, kubernetesApi, kubernetesAuthProvidersApi]);
|
||||
/* eslint-enable react-hooks/exhaustive-deps */
|
||||
|
||||
return {
|
||||
kubernetesObjects,
|
||||
error,
|
||||
};
|
||||
};
|
||||
@@ -35,4 +35,5 @@ export interface GroupedResponses extends DeploymentResources {
|
||||
services: V1Service[];
|
||||
configMaps: V1ConfigMap[];
|
||||
ingresses: ExtensionsV1beta1Ingress[];
|
||||
customResources: any[];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 { getOwnedResources } from './owner';
|
||||
import * as fixture from '../__fixtures__/2-deployments.json';
|
||||
|
||||
describe('owner', () => {
|
||||
describe('getOwnedResources', () => {
|
||||
it('should find replicaset ownership from deployment', () => {
|
||||
const result = getOwnedResources(
|
||||
fixture.deployments[0] as any,
|
||||
fixture.replicaSets as any,
|
||||
);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]?.metadata?.name).toBe('dice-roller-6c8646bfd');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 { V1ObjectMeta } from '@kubernetes/client-node/dist/gen/model/v1ObjectMeta';
|
||||
import {
|
||||
V1HorizontalPodAutoscaler,
|
||||
V1Pod,
|
||||
V1ReplicaSet,
|
||||
} from '@kubernetes/client-node';
|
||||
|
||||
interface CanOwn {
|
||||
metadata?: V1ObjectMeta;
|
||||
}
|
||||
|
||||
interface CanBeOwned {
|
||||
metadata?: V1ObjectMeta;
|
||||
}
|
||||
|
||||
export function getOwnedResources<R extends CanBeOwned>(
|
||||
potentialOwner: CanOwn,
|
||||
possiblyOwned: R[],
|
||||
): R[] {
|
||||
return possiblyOwned.filter(
|
||||
p =>
|
||||
p.metadata?.ownerReferences?.some(
|
||||
o => o.uid === potentialOwner.metadata?.uid,
|
||||
) ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
export const getOwnedPodsThroughReplicaSets = (
|
||||
potentialOwner: CanOwn,
|
||||
replicaSets: V1ReplicaSet[],
|
||||
pods: V1Pod[],
|
||||
) => {
|
||||
return getOwnedResources(
|
||||
potentialOwner,
|
||||
replicaSets.filter(rs => rs.status && rs.status.replicas > 0),
|
||||
).reduce((accum, rs) => {
|
||||
return accum.concat(getOwnedResources(rs, pods));
|
||||
}, [] as V1Pod[]);
|
||||
};
|
||||
|
||||
export const getMatchingHpa = (
|
||||
ownerName: string | undefined,
|
||||
ownerKind: string,
|
||||
hpas: V1HorizontalPodAutoscaler[],
|
||||
): V1HorizontalPodAutoscaler | undefined => {
|
||||
return hpas.find(hpa => {
|
||||
return (
|
||||
(hpa.spec?.scaleTargetRef?.kind ?? '').toLowerCase() ===
|
||||
ownerKind.toLowerCase() &&
|
||||
(hpa.spec?.scaleTargetRef?.name ?? '') ===
|
||||
(ownerName ?? 'unknown-deployment')
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -45,6 +45,9 @@ export const groupResponses = (
|
||||
case 'ingresses':
|
||||
prev.ingresses.push(...next.resources);
|
||||
break;
|
||||
case 'customresources':
|
||||
prev.customResources.push(...next.resources);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
return prev;
|
||||
@@ -57,6 +60,7 @@ export const groupResponses = (
|
||||
configMaps: [],
|
||||
horizontalPodAutoscalers: [],
|
||||
ingresses: [],
|
||||
customResources: [],
|
||||
} as GroupedResponses,
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user