diff --git a/.changeset/witty-feet-flow.md b/.changeset/witty-feet-flow.md new file mode 100644 index 0000000000..5a6a00cecd --- /dev/null +++ b/.changeset/witty-feet-flow.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes': minor +'@backstage/plugin-kubernetes-backend': minor +--- + +Add initial CRD support framework diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index ab48456a3b..2e9d12bfb9 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -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/ diff --git a/plugins/kubernetes-backend/schema.d.ts b/plugins/kubernetes-backend/schema.d.ts index 4c71e6a839..fa5bc52abf 100644 --- a/plugins/kubernetes-backend/schema.d.ts +++ b/plugins/kubernetes-backend/schema.d.ts @@ -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[]; }; } diff --git a/plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts index 05b1479c85..cd1c6afedf 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts @@ -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); + } } diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts index c0443a28d0..a45b1252fe 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts @@ -87,6 +87,7 @@ describe('handleGetKubernetesObjectsForService', () => { { getClustersByServiceId, }, + [], ); const result = await sut.getKubernetesObjectsByEntity({ @@ -178,6 +179,7 @@ describe('handleGetKubernetesObjectsForService', () => { { getClustersByServiceId, }, + [], ); const result = await sut.getKubernetesObjectsByEntity({ diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts index 0a2fc1488a..489b0631f6 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts @@ -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 { diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts index 35fdfb25f9..683f17e3b9 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts @@ -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({ + 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({ + 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({ + sut.fetchObjectsForService({ serviceId: 'some-service', clusterDetails: { name: 'cluster1', @@ -182,6 +184,7 @@ describe('KubernetesClientProvider', () => { }, objectTypesToFetch: new Set(['foo']), labelSelector: '', + customResources: [], }), ).toThrow('unrecognised type=foo'); @@ -304,7 +307,7 @@ describe('KubernetesClientProvider', () => { }, }); - await sut.fetchObjectsForService({ + 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]; diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts index 4df25bdcb3..10e5640bab 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts @@ -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 { + 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( clusterDetails: ClusterDetails, fn: ( diff --git a/plugins/kubernetes-backend/src/service/router.ts b/plugins/kubernetes-backend/src/service/router.ts index 8d4b14eebf..189f2eb9c5 100644 --- a/plugins/kubernetes-backend/src/service/router.ts +++ b/plugins/kubernetes-backend/src/service/router.ts @@ -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); diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index 4309bc9075..84ca08585b 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -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; } +export interface CustomResourceFetchResponse { + type: 'customresources'; + resources: Array; +} + export interface ObjectFetchParams { serviceId: string; clusterDetails: ClusterDetails; objectTypesToFetch: Set; 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; +} diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index fc1aa59d5f..59033e431a 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -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", diff --git a/plugins/kubernetes/schema.d.ts b/plugins/kubernetes/schema.d.ts index ce8b94e917..196f5ba76d 100644 --- a/plugins/kubernetes/schema.d.ts +++ b/plugins/kubernetes/schema.d.ts @@ -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[]; }; } diff --git a/plugins/kubernetes/src/__fixtures__/1-deployments.json b/plugins/kubernetes/src/__fixtures__/1-deployments.json new file mode 100644 index 0000000000..5ad847dc49 --- /dev/null +++ b/plugins/kubernetes/src/__fixtures__/1-deployments.json @@ -0,0 +1,2911 @@ +{ + "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" + } + }, + { + "metadata": { + "creationTimestamp": "2020-09-24T11:39:26.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-24T11:39:26.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.6\"}": { + ".": {}, + "f:ip": {} + } + }, + "f:startTime": {} + } + }, + "manager": "kubelet", + "operation": "Update", + "time": "2020-09-24T11:39:27.000Z" + } + ], + "name": "dice-roller-6c8646bfd-b9zt5", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "dice-roller-6c8646bfd", + "uid": "5126c354-4310-4e23-a9e4-c9b87cb69792" + } + ], + "resourceVersion": "503886", + "selfLink": "/api/v1/namespaces/default/pods/dice-roller-6c8646bfd-b9zt5", + "uid": "8b6601b6-469e-4e89-8fd0-abb2f1a567e4" + }, + "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-24T11:39:26.000Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-24T11:39:27.000Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-24T11:39:27.000Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-24T11:39:26.000Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "docker://c50e0d0fa96f09a2ed5df7dd5a7f7de88e6b7c7addb8f002f299d6afc52d82ce", + "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-24T11:39:27.000Z" + } + } + } + ], + "hostIP": "192.168.64.2", + "phase": "Running", + "podIP": "172.17.0.6", + "podIPs": [ + { + "ip": "172.17.0.6" + } + ], + "qosClass": "BestEffort", + "startTime": "2020-09-24T11:39:26.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.13\"}": { + ".": {}, + "f:ip": {} + } + }, + "f:startTime": {} + } + }, + "manager": "kubelet", + "operation": "Update", + "time": "2020-09-25T09:58:55.000Z" + } + ], + "name": "dice-roller-6c8646bfd-cfxqc", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "dice-roller-6c8646bfd", + "uid": "5126c354-4310-4e23-a9e4-c9b87cb69792" + } + ], + "resourceVersion": "593211", + "selfLink": "/api/v1/namespaces/default/pods/dice-roller-6c8646bfd-cfxqc", + "uid": "e87e1776-0ca7-41fb-aeea-e1f648387545" + }, + "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:51.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://c1641d986aae424429b7c2c1117baeb17d3794f73206bd57292cdf8264f929b2", + "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.13", + "podIPs": [ + { + "ip": "172.17.0.13" + } + ], + "qosClass": "BestEffort", + "startTime": "2020-09-25T09:58:51.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.15\"}": { + ".": {}, + "f:ip": {} + } + }, + "f:startTime": {} + } + }, + "manager": "kubelet", + "operation": "Update", + "time": "2020-09-25T09:58:54.000Z" + } + ], + "name": "dice-roller-6c8646bfd-dtv5z", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "dice-roller-6c8646bfd", + "uid": "5126c354-4310-4e23-a9e4-c9b87cb69792" + } + ], + "resourceVersion": "593190", + "selfLink": "/api/v1/namespaces/default/pods/dice-roller-6c8646bfd-dtv5z", + "uid": "1638a41b-e47e-4c17-a1f7-7b447df248b6" + }, + "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:51.000Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:53.000Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:53.000Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:50.000Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "docker://e62c32db58dbfe0a74b2d2cba0e0a97b7f222693c68e930037ac8738c4758ca3", + "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.15", + "podIPs": [ + { + "ip": "172.17.0.15" + } + ], + "qosClass": "BestEffort", + "startTime": "2020-09-25T09:58:51.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.12\"}": { + ".": {}, + "f:ip": {} + } + }, + "f:startTime": {} + } + }, + "manager": "kubelet", + "operation": "Update", + "time": "2020-09-25T09:58:53.000Z" + } + ], + "name": "dice-roller-6c8646bfd-hhts2", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "dice-roller-6c8646bfd", + "uid": "5126c354-4310-4e23-a9e4-c9b87cb69792" + } + ], + "resourceVersion": "593186", + "selfLink": "/api/v1/namespaces/default/pods/dice-roller-6c8646bfd-hhts2", + "uid": "ea0b147a-c56a-46e6-9445-7a0b1b0ed3c0" + }, + "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:53.000Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:53.000Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:50.000Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "docker://bcdbc153630f90556d57ff0d62f04d847ca63a5f0a7e5d9230683623d33d4b8d", + "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.12", + "podIPs": [ + { + "ip": "172.17.0.12" + } + ], + "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.14\"}": { + ".": {}, + "f:ip": {} + } + }, + "f:startTime": {} + } + }, + "manager": "kubelet", + "operation": "Update", + "time": "2020-09-25T09:58:55.000Z" + } + ], + "name": "dice-roller-6c8646bfd-j68lm", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "dice-roller-6c8646bfd", + "uid": "5126c354-4310-4e23-a9e4-c9b87cb69792" + } + ], + "resourceVersion": "593226", + "selfLink": "/api/v1/namespaces/default/pods/dice-roller-6c8646bfd-j68lm", + "uid": "b230ff1d-7a13-479b-9c7b-77c26bd79f62" + }, + "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://30fa8a3429f86ab8e9a4cec472b79d74266609082703f48950f4aae021e9d6a2", + "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.14", + "podIPs": [ + { + "ip": "172.17.0.14" + } + ], + "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.9\"}": { + ".": {}, + "f:ip": {} + } + }, + "f:startTime": {} + } + }, + "manager": "kubelet", + "operation": "Update", + "time": "2020-09-25T09:58:53.000Z" + } + ], + "name": "dice-roller-6c8646bfd-m6f9w", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "dice-roller-6c8646bfd", + "uid": "5126c354-4310-4e23-a9e4-c9b87cb69792" + } + ], + "resourceVersion": "593181", + "selfLink": "/api/v1/namespaces/default/pods/dice-roller-6c8646bfd-m6f9w", + "uid": "9b0079f6-ff29-4619-bf6e-71cc10199ac5" + }, + "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:53.000Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:53.000Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:50.000Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "docker://f78767ee90eedffe46ff64bfe76d7f69426800dd89f3df012daf0baf3a9c5bdd", + "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.9", + "podIPs": [ + { + "ip": "172.17.0.9" + } + ], + "qosClass": "BestEffort", + "startTime": "2020-09-25T09:58:50.000Z" + } + }, + { + "metadata": { + "creationTimestamp": "2020-09-24T11:39:27.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-24T11:39:27.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.4\"}": { + ".": {}, + "f:ip": {} + } + }, + "f:startTime": {} + } + }, + "manager": "kubelet", + "operation": "Update", + "time": "2020-09-24T11:39:28.000Z" + } + ], + "name": "dice-roller-6c8646bfd-nv9pk", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "dice-roller-6c8646bfd", + "uid": "5126c354-4310-4e23-a9e4-c9b87cb69792" + } + ], + "resourceVersion": "503918", + "selfLink": "/api/v1/namespaces/default/pods/dice-roller-6c8646bfd-nv9pk", + "uid": "acf7f77f-78c6-4d4b-bc73-637211770ffc" + }, + "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-24T11:39:27.000Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-24T11:39:28.000Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-24T11:39:28.000Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-24T11:39:27.000Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "docker://63dec9b32942c4b09ae43d09d6978261f674a3ee623823b8f1d20da8044c12ac", + "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-24T11:39:28.000Z" + } + } + } + ], + "hostIP": "192.168.64.2", + "phase": "Running", + "podIP": "172.17.0.4", + "podIPs": [ + { + "ip": "172.17.0.4" + } + ], + "qosClass": "BestEffort", + "startTime": "2020-09-24T11:39:27.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.10\"}": { + ".": {}, + "f:ip": {} + } + }, + "f:startTime": {} + } + }, + "manager": "kubelet", + "operation": "Update", + "time": "2020-09-25T09:58:55.000Z" + } + ], + "name": "dice-roller-6c8646bfd-pjhfj", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "dice-roller-6c8646bfd", + "uid": "5126c354-4310-4e23-a9e4-c9b87cb69792" + } + ], + "resourceVersion": "593205", + "selfLink": "/api/v1/namespaces/default/pods/dice-roller-6c8646bfd-pjhfj", + "uid": "78775c3a-7af2-4ebe-a676-bc2e5dfa2bcc" + }, + "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://cbb83dda250db74285dcbe0b81bd0896acf402bac9710af090311f7360f824aa", + "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.10", + "podIPs": [ + { + "ip": "172.17.0.10" + } + ], + "qosClass": "BestEffort", + "startTime": "2020-09-25T09:58:50.000Z" + } + } + ], + "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": [ + { + "apiVersion": "apps/v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:deployment.kubernetes.io/desired-replicas": {}, + "f:deployment.kubernetes.io/max-replicas": {}, + "f:deployment.kubernetes.io/revision": {} + }, + "f:labels": { + ".": {}, + "f:app": {}, + "f:backstage.io/kubernetes-id": {}, + "f:pod-template-hash": {} + }, + "f:ownerReferences": { + ".": {}, + "k:{\"uid\":\"7551e949-42d1-4061-83c5-9da107186e47\"}": { + ".": {}, + "f:apiVersion": {}, + "f:blockOwnerDeletion": {}, + "f:controller": {}, + "f:kind": {}, + "f:name": {}, + "f:uid": {} + } + } + }, + "f:spec": { + "f:replicas": {}, + "f:selector": { + "f:matchLabels": { + ".": {}, + "f:app": {}, + "f:pod-template-hash": {} + } + }, + "f:template": { + "f:metadata": { + "f:labels": { + ".": {}, + "f:app": {}, + "f:backstage.io/kubernetes-id": {}, + "f:pod-template-hash": {} + } + }, + "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:restartPolicy": {}, + "f:schedulerName": {}, + "f:securityContext": {}, + "f:terminationGracePeriodSeconds": {} + } + } + }, + "f:status": { + "f:availableReplicas": {}, + "f:fullyLabeledReplicas": {}, + "f:observedGeneration": {}, + "f:readyReplicas": {}, + "f:replicas": {} + } + }, + "manager": "kube-controller-manager", + "operation": "Update", + "time": "2020-09-25T09:58:55.000Z" + } + ], + "name": "dice-roller-6c8646bfd", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "Deployment", + "name": "dice-roller", + "uid": "7551e949-42d1-4061-83c5-9da107186e47" + } + ], + "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 + } + } + ], + "deployments": [ + { + "metadata": { + "annotations": { + "deployment.kubernetes.io/revision": "2", + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\"},\"name\":\"dice-roller\",\"namespace\":\"default\"},\"spec\":{\"replicas\":10,\"selector\":{\"matchLabels\":{\"app\":\"dice-roller\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"dice-roller\",\"backstage.io/kubernetes-id\":\"dice-roller\"}},\"spec\":{\"containers\":[{\"image\":\"nginx:1.14.2\",\"name\":\"nginx\",\"ports\":[{\"containerPort\":80}]}]}}}}\n" + }, + "creationTimestamp": "2020-09-23T12:00:55.000Z", + "generation": 3, + "labels": { + "backstage.io/kubernetes-id": "dice-roller" + }, + "managedFields": [ + { + "apiVersion": "apps/v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:kubectl.kubernetes.io/last-applied-configuration": {} + }, + "f:labels": { + ".": {}, + "f:backstage.io/kubernetes-id": {} + } + }, + "f:spec": { + "f:progressDeadlineSeconds": {}, + "f:replicas": {}, + "f:revisionHistoryLimit": {}, + "f:selector": { + "f:matchLabels": { + ".": {}, + "f:app": {} + } + }, + "f:strategy": { + "f:rollingUpdate": { + ".": {}, + "f:maxSurge": {}, + "f:maxUnavailable": {} + }, + "f:type": {} + }, + "f:template": { + "f:metadata": { + "f:labels": { + ".": {}, + "f:app": {}, + "f:backstage.io/kubernetes-id": {} + } + }, + "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:restartPolicy": {}, + "f:schedulerName": {}, + "f:securityContext": {}, + "f:terminationGracePeriodSeconds": {} + } + } + } + }, + "manager": "kubectl", + "operation": "Update", + "time": "2020-09-25T09:58:50.000Z" + }, + { + "apiVersion": "apps/v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + "f:deployment.kubernetes.io/revision": {} + } + }, + "f:status": { + "f:availableReplicas": {}, + "f:conditions": { + ".": {}, + "k:{\"type\":\"Available\"}": { + ".": {}, + "f:lastTransitionTime": {}, + "f:lastUpdateTime": {}, + "f:message": {}, + "f:reason": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Progressing\"}": { + ".": {}, + "f:lastTransitionTime": {}, + "f:lastUpdateTime": {}, + "f:message": {}, + "f:reason": {}, + "f:status": {}, + "f:type": {} + } + }, + "f:observedGeneration": {}, + "f:readyReplicas": {}, + "f:replicas": {}, + "f:updatedReplicas": {} + } + }, + "manager": "kube-controller-manager", + "operation": "Update", + "time": "2020-09-25T09:58:55.000Z" + } + ], + "name": "dice-roller", + "namespace": "default", + "resourceVersion": "593230", + "selfLink": "/apis/apps/v1/namespaces/default/deployments/dice-roller", + "uid": "7551e949-42d1-4061-83c5-9da107186e47" + }, + "spec": { + "progressDeadlineSeconds": 600, + "replicas": 10, + "revisionHistoryLimit": 10, + "selector": { + "matchLabels": { + "app": "dice-roller" + } + }, + "strategy": { + "rollingUpdate": { + "maxSurge": "25%", + "maxUnavailable": "25%" + }, + "type": "RollingUpdate" + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "dice-roller", + "backstage.io/kubernetes-id": "dice-roller" + } + }, + "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, + "conditions": [ + { + "lastTransitionTime": "2020-09-23T12:00:55.000Z", + "lastUpdateTime": "2020-09-24T11:39:28.000Z", + "message": "ReplicaSet \"dice-roller-6c8646bfd\" has successfully progressed.", + "reason": "NewReplicaSetAvailable", + "status": "True", + "type": "Progressing" + }, + { + "lastTransitionTime": "2020-09-25T09:58:55.000Z", + "lastUpdateTime": "2020-09-25T09:58:55.000Z", + "message": "Deployment has minimum availability.", + "reason": "MinimumReplicasAvailable", + "status": "True", + "type": "Available" + } + ], + "observedGeneration": 3, + "readyReplicas": 10, + "replicas": 10, + "updatedReplicas": 10 + } + } + ], + "horizontalPodAutoscalers": [ + { + "apiVersion": "autoscaling/v1", + "kind": "HorizontalPodAutoscaler", + "metadata": { + "annotations": { + "autoscaling.alpha.kubernetes.io/conditions": "[{\"type\":\"AbleToScale\",\"status\":\"True\",\"lastTransitionTime\":\"2021-01-05T10:26:04Z\",\"reason\":\"SucceededGetScale\",\"message\":\"the HPA controller was able to get the target's current scale\"},{\"type\":\"ScalingActive\",\"status\":\"False\",\"lastTransitionTime\":\"2021-01-05T10:26:04Z\",\"reason\":\"FailedGetResourceMetric\",\"message\":\"the HPA was unable to compute the replica count: unable to get metrics for resource cpu: no metrics returned from resource metrics API\"}]", + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"autoscaling/v1\",\"kind\":\"HorizontalPodAutoscaler\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\"},\"name\":\"dice-roller\",\"namespace\":\"default\"},\"spec\":{\"maxReplicas\":15,\"minReplicas\":10,\"scaleTargetRef\":{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"name\":\"dice-roller\"},\"targetCPUUtilizationPercentage\":50}}\n" + }, + "creationTimestamp": "2021-01-05T10:25:48Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller" + }, + "managedFields": [ + { + "apiVersion": "autoscaling/v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:kubectl.kubernetes.io/last-applied-configuration": {} + }, + "f:labels": { + ".": {}, + "f:backstage.io/kubernetes-id": {} + } + }, + "f:spec": { + "f:maxReplicas": {}, + "f:minReplicas": {}, + "f:scaleTargetRef": { + "f:apiVersion": {}, + "f:kind": {}, + "f:name": {} + }, + "f:targetCPUUtilizationPercentage": {} + } + }, + "manager": "kubectl-client-side-apply", + "operation": "Update", + "time": "2021-01-05T10:25:48Z" + }, + { + "apiVersion": "autoscaling/v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + "f:autoscaling.alpha.kubernetes.io/conditions": {} + } + }, + "f:status": { + "f:currentReplicas": {} + } + }, + "manager": "kube-controller-manager", + "operation": "Update", + "time": "2021-01-05T10:26:04Z" + } + ], + "name": "dice-roller", + "namespace": "default", + "resourceVersion": "598", + "selfLink": "/apis/autoscaling/v1/namespaces/default/horizontalpodautoscalers/dice-roller", + "uid": "dd7c5329-567c-43c2-b159-756808d90a8e" + }, + "spec": { + "maxReplicas": 15, + "minReplicas": 10, + "scaleTargetRef": { + "apiVersion": "apps/v1", + "kind": "Deployment", + "name": "dice-roller" + }, + "targetCPUUtilizationPercentage": 50 + }, + "status": { + "currentReplicas": 10, + "desiredReplicas": 0, + "currentCPUUtilizationPercentage": 30 + } + } + ] +} diff --git a/plugins/kubernetes/src/components/DeploymentsAccordions/__fixtures__/2-deployments.json b/plugins/kubernetes/src/__fixtures__/2-deployments.json similarity index 100% rename from plugins/kubernetes/src/components/DeploymentsAccordions/__fixtures__/2-deployments.json rename to plugins/kubernetes/src/__fixtures__/2-deployments.json diff --git a/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/Rollout.test.tsx b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/Rollout.test.tsx new file mode 100644 index 0000000000..018ba5f3f1 --- /dev/null +++ b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/Rollout.test.tsx @@ -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()), + ); + + 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()), + ); + + 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(), + ), + ); + + 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( + , + ), + ), + ); + + 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(); + }); +}); diff --git a/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/Rollout.tsx b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/Rollout.tsx new file mode 100644 index 0000000000..32848b4fb5 --- /dev/null +++ b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/Rollout.tsx @@ -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 = ( +
+ + Aborted +
+); + +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 ( + + + + + + + + {hpa && ( + + + + + + min replicas {hpa.spec?.minReplicas ?? '?'} / max replicas{' '} + {hpa.spec?.maxReplicas ?? '?'} + + + + + current CPU usage:{' '} + {hpa.status?.currentCPUUtilizationPercentage ?? '?'}% + + + + + target CPU usage:{' '} + {hpa.spec?.targetCPUUtilizationPercentage ?? '?'}% + + + + + + )} + + + {numberOfCurrentPods} pods + + + {numberOfPodsWithErrors > 0 ? ( + + {numberOfPodsWithErrors} pod + {numberOfPodsWithErrors > 1 ? 's' : ''} with errors + + ) : ( + No pods with errors + )} + + + {pauseTime && ( + +
+ + + Paused ({DateTime.fromISO(pauseTime).toRelative({ locale: 'en' })} + ) + +
+
+ )} + {abortedMessage && ( + + {AbortedTitle} + + )} +
+ ); +}; + +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 ( + + }> + + + +
+
+ Rollout status +
+
+ {abortedMessage && ( + <> + {AbortedTitle} + {abortedMessage} + + )} + +
+
+ +
+
+
+
+ ); +}; + +export const RolloutAccordions = ({ + rollouts, + defaultExpanded = false, +}: RolloutAccordionsProps) => { + const groupedResponses = useContext(GroupedResponsesContext); + + return ( + + {rollouts.map((rollout, i) => ( + + + + + + ))} + + ); +}; diff --git a/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/RolloutDrawer.tsx b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/RolloutDrawer.tsx new file mode 100644 index 0000000000..82d1225773 --- /dev/null +++ b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/RolloutDrawer.tsx @@ -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 ( + ({})} + > + + + + {rollout.metadata?.name ?? 'unknown object'} + + + + + Rollout + + + + + ); +}; diff --git a/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/StepsProgress.test.tsx b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/StepsProgress.test.tsx new file mode 100644 index 0000000000..3744c94928 --- /dev/null +++ b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/StepsProgress.test.tsx @@ -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( + , + ), + ); + + expect(getByText('pause for 1h')).toBeInTheDocument(); + expect(getByText('infinite pause')).toBeInTheDocument(); + }); + it('should render SetWeight step text', async () => { + const { getByText } = render( + wrapInTestApp( + , + ), + ); + + expect(getByText('setWeight 10%')).toBeInTheDocument(); + expect(getByText('setWeight 95%')).toBeInTheDocument(); + }); + it('should render Analysis step text', async () => { + const { getAllByText, getByText } = render( + wrapInTestApp( + , + ), + ); + + 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( + , + ), + ); + + 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( + , + ), + ); + + // 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( + , + ), + ); + + 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( + , + ), + ); + + expect(queryByText('1')).toBeNull(); + expect(queryByText('2')).toBeNull(); + expect(queryByText('3')).toBeNull(); + expect(queryByText('4')).toBeNull(); + }); +}); diff --git a/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/StepsProgress.tsx b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/StepsProgress.tsx new file mode 100644 index 0000000000..5a9f950519 --- /dev/null +++ b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/StepsProgress.tsx @@ -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 ( +
+

analysis templates:

+ {step.analysis.templates.map((t, i) => ( +

{`${t.templateName}${ + t.clusterScope ? ' (cluster scoped)' : '' + }`}

+ ))} +
+ ); + } + 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 ( + + {steps + .map((step, i) => ( + + + {createLabelForStep(step)} + + + )) + .concat( + + Canary promoted + , + )} + + ); +}; diff --git a/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/__fixtures__/aborted-rollout.json b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/__fixtures__/aborted-rollout.json new file mode 100644 index 0000000000..a10f35582e --- /dev/null +++ b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/__fixtures__/aborted-rollout.json @@ -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 + } +} diff --git a/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/__fixtures__/analysis-steps.ts b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/__fixtures__/analysis-steps.ts new file mode 100644 index 0000000000..e1b24030f3 --- /dev/null +++ b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/__fixtures__/analysis-steps.ts @@ -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; diff --git a/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/__fixtures__/grouped-resources.json b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/__fixtures__/grouped-resources.json new file mode 100644 index 0000000000..ea8fb077d2 --- /dev/null +++ b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/__fixtures__/grouped-resources.json @@ -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 + } + } + ] +} diff --git a/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/__fixtures__/pause-steps.ts b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/__fixtures__/pause-steps.ts new file mode 100644 index 0000000000..57d641d888 --- /dev/null +++ b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/__fixtures__/pause-steps.ts @@ -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; diff --git a/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/__fixtures__/paused-rollout.json b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/__fixtures__/paused-rollout.json new file mode 100644 index 0000000000..abb2e0e49a --- /dev/null +++ b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/__fixtures__/paused-rollout.json @@ -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 + } +} diff --git a/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/__fixtures__/rollout.json b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/__fixtures__/rollout.json new file mode 100644 index 0000000000..02805743b7 --- /dev/null +++ b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/__fixtures__/rollout.json @@ -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 + } +} diff --git a/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/__fixtures__/setweight-steps.ts b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/__fixtures__/setweight-steps.ts new file mode 100644 index 0000000000..0c8dfef237 --- /dev/null +++ b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/__fixtures__/setweight-steps.ts @@ -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; diff --git a/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/index.ts b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/index.ts new file mode 100644 index 0000000000..a0298e268d --- /dev/null +++ b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/index.ts @@ -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'; diff --git a/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/types.ts b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/types.ts new file mode 100644 index 0000000000..db41fee709 --- /dev/null +++ b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/types.ts @@ -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; diff --git a/plugins/kubernetes/src/components/CustomResources/CustomResources.tsx b/plugins/kubernetes/src/components/CustomResources/CustomResources.tsx new file mode 100644 index 0000000000..525657f443 --- /dev/null +++ b/plugins/kubernetes/src/components/CustomResources/CustomResources.tsx @@ -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 => { + 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 ; + default: + return ( + + ); + } + })} + + ); +}; diff --git a/plugins/kubernetes/src/components/CustomResources/DefaultCustomResource.test.tsx b/plugins/kubernetes/src/components/CustomResources/DefaultCustomResource.test.tsx new file mode 100644 index 0000000000..b3bfb3dc81 --- /dev/null +++ b/plugins/kubernetes/src/components/CustomResources/DefaultCustomResource.test.tsx @@ -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( + , + ), + ), + ); + expect(getByText('dice-roller-546c476497-4-1')).toBeInTheDocument(); + expect(getByText('AnalysisRun')).toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes/src/components/CustomResources/DefaultCustomResource.tsx b/plugins/kubernetes/src/components/CustomResources/DefaultCustomResource.tsx new file mode 100644 index 0000000000..136a7cd2a8 --- /dev/null +++ b/plugins/kubernetes/src/components/CustomResources/DefaultCustomResource.tsx @@ -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 ( + + + + + + + + + ); +}; + +const DefaultCustomResourceAccordion = ({ + customResource, + customResourceName, + defaultExpanded, +}: DefaultCustomResourceAccordionProps) => { + return ( + + }> + + + + {customResource.hasOwnProperty('status') && ( + + )} + + + ); +}; + +export const DefaultCustomResourceAccordions = ({ + customResources, + customResourceName, + defaultExpanded = false, +}: DefaultCustomResourceAccordionsProps) => { + return ( + + {customResources.map((cr, i) => ( + + + + + + ))} + + ); +}; diff --git a/plugins/kubernetes/src/components/CustomResources/DefaultCustomResourceDrawer.tsx b/plugins/kubernetes/src/components/CustomResources/DefaultCustomResourceDrawer.tsx new file mode 100644 index 0000000000..db109ad096 --- /dev/null +++ b/plugins/kubernetes/src/components/CustomResources/DefaultCustomResourceDrawer.tsx @@ -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 ( + cr} + > + + + + {customResource.metadata?.name ?? 'unknown object'} + + + + + {capitalizedName} + + + + + ); +}; diff --git a/plugins/kubernetes/src/components/CustomResources/__fixtures__/analysis-run.json b/plugins/kubernetes/src/components/CustomResources/__fixtures__/analysis-run.json new file mode 100644 index 0000000000..9bbff19e36 --- /dev/null +++ b/plugins/kubernetes/src/components/CustomResources/__fixtures__/analysis-run.json @@ -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" + } +} diff --git a/plugins/kubernetes/src/components/CustomResources/index.ts b/plugins/kubernetes/src/components/CustomResources/index.ts new file mode 100644 index 0000000000..99b038bffc --- /dev/null +++ b/plugins/kubernetes/src/components/CustomResources/index.ts @@ -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'; diff --git a/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.test.tsx b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.test.tsx index 5bf2c16de2..cace1bcac3 100644 --- a/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.test.tsx +++ b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.test.tsx @@ -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'; diff --git a/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.test.tsx b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.test.tsx index a522af6d4f..7c85d785f3 100644 --- a/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.test.tsx +++ b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.test.tsx @@ -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( - , - ), + wrapper(wrapInTestApp()), ); expect(getByText('dice-roller')).toBeInTheDocument(); diff --git a/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.tsx b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.tsx index e03a749185..00d62b71d5 100644 --- a/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.tsx +++ b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.tsx @@ -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; children?: React.ReactNode; }; @@ -47,7 +50,6 @@ type DeploymentAccordionProps = { deployment: V1Deployment; ownedPods: V1Pod[]; matchingHpa?: V1HorizontalPodAutoscaler; - clusterPodNamesWithErrors: Set; 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 ( - {deploymentResources.deployments.map((deployment, i) => ( + {groupedResponses.deployments.map((deployment, i) => ( - {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 ( - - - - ); - })} + + + ))} diff --git a/plugins/kubernetes/src/components/IngressesAccordions/IngressesAccordions.test.tsx b/plugins/kubernetes/src/components/IngressesAccordions/IngressesAccordions.test.tsx index 580561095e..edb2502382 100644 --- a/plugins/kubernetes/src/components/IngressesAccordions/IngressesAccordions.test.tsx +++ b/plugins/kubernetes/src/components/IngressesAccordions/IngressesAccordions.test.tsx @@ -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( - , - ), + wrapper(wrapInTestApp()), ); expect(getByText('awesome-service')).toBeInTheDocument(); diff --git a/plugins/kubernetes/src/components/IngressesAccordions/IngressesAccordions.tsx b/plugins/kubernetes/src/components/IngressesAccordions/IngressesAccordions.tsx index 6ecfa63e23..9291c4533f 100644 --- a/plugins/kubernetes/src/components/IngressesAccordions/IngressesAccordions.tsx +++ b/plugins/kubernetes/src/components/IngressesAccordions/IngressesAccordions.tsx @@ -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) => { ); }; -export const IngressesAccordions = ({ - deploymentResources, -}: IngressesAccordionsProps) => { +export const IngressesAccordions = ({}: IngressesAccordionsProps) => { + const groupedResponses = useContext(GroupedResponsesContext); return ( - {deploymentResources.ingresses.map((ingress, i) => ( + {groupedResponses.ingresses.map((ingress, i) => ( diff --git a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.test.tsx b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.test.tsx new file mode 100644 index 0000000000..2f4c83355c --- /dev/null +++ b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.test.tsx @@ -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( + , + ), + ); + + 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( + , + ), + ); + + 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( + , + ), + ); + + 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(); + }); +}); diff --git a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx index c61676fc06..aa9df3e8fa 100644 --- a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx +++ b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx @@ -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" > ; children?: React.ReactNode; }; -const Cluster = ({ clusterObjects, detectedErrors }: ClusterProps) => { +const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => { const groupedResponses = groupResponses(clusterObjects.resources); - - const podsWithErrors = new Set( - detectedErrors - ?.filter(de => de.kind === 'Pod') - .map(de => de.names) - .flat() ?? [], - ); - return ( - <> - - }> - - - - - - + + + + }> + + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + ); }; 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(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) => { Your Clusters - {kubernetesObjects?.items.map((item, i) => ( - - - - ))} + {kubernetesObjects?.items.map((item, i) => { + const podsWithErrors = new Set( + detectedErrors + .get(item.cluster.name) + ?.filter(de => de.kind === 'Pod') + .map(de => de.names) + .flat() ?? [], + ); + + return ( + + + + ); + })} )} diff --git a/plugins/kubernetes/src/components/KubernetesDrawer/KubernetesDrawer.tsx b/plugins/kubernetes/src/components/KubernetesDrawer/KubernetesDrawer.tsx index ee4de96177..68321cd589 100644 --- a/plugins/kubernetes/src/components/KubernetesDrawer/KubernetesDrawer.tsx +++ b/plugins/kubernetes/src/components/KubernetesDrawer/KubernetesDrawer.tsx @@ -84,6 +84,15 @@ interface KubernetesDrawerContentProps { 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 = ({ toggleDrawer, object, @@ -139,7 +148,11 @@ const KubernetesDrawerContent = ({
{isYaml && } - {!isYaml && } + {!isYaml && ( + + )}
); diff --git a/plugins/kubernetes/src/components/ServicesAccordions/ServicesAccordions.test.tsx b/plugins/kubernetes/src/components/ServicesAccordions/ServicesAccordions.test.tsx index 46172b4150..f503befc58 100644 --- a/plugins/kubernetes/src/components/ServicesAccordions/ServicesAccordions.test.tsx +++ b/plugins/kubernetes/src/components/ServicesAccordions/ServicesAccordions.test.tsx @@ -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( - , - ), + wrapper(wrapInTestApp()), ); expect(getByText('awesome-service-grpc')).toBeInTheDocument(); diff --git a/plugins/kubernetes/src/components/ServicesAccordions/ServicesAccordions.tsx b/plugins/kubernetes/src/components/ServicesAccordions/ServicesAccordions.tsx index c378f02658..6bfec6f44d 100644 --- a/plugins/kubernetes/src/components/ServicesAccordions/ServicesAccordions.tsx +++ b/plugins/kubernetes/src/components/ServicesAccordions/ServicesAccordions.tsx @@ -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 ( - {deploymentResources.services.map((service, i) => ( + {groupedResponses.services.map((service, i) => ( diff --git a/plugins/kubernetes/src/hooks/GroupedResponses.ts b/plugins/kubernetes/src/hooks/GroupedResponses.ts new file mode 100644 index 0000000000..b8ee1c7877 --- /dev/null +++ b/plugins/kubernetes/src/hooks/GroupedResponses.ts @@ -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({ + pods: [], + replicaSets: [], + deployments: [], + services: [], + configMaps: [], + horizontalPodAutoscalers: [], + ingresses: [], + customResources: [], +}); diff --git a/plugins/kubernetes/src/hooks/PodNamesWithErrors.ts b/plugins/kubernetes/src/hooks/PodNamesWithErrors.ts new file mode 100644 index 0000000000..27c54fa345 --- /dev/null +++ b/plugins/kubernetes/src/hooks/PodNamesWithErrors.ts @@ -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>( + new Set(), +); diff --git a/plugins/kubernetes/src/hooks/index.ts b/plugins/kubernetes/src/hooks/index.ts new file mode 100644 index 0000000000..210888da38 --- /dev/null +++ b/plugins/kubernetes/src/hooks/index.ts @@ -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'; diff --git a/plugins/kubernetes/src/hooks/test-utils.tsx b/plugins/kubernetes/src/hooks/test-utils.tsx new file mode 100644 index 0000000000..2317df7e79 --- /dev/null +++ b/plugins/kubernetes/src/hooks/test-utils.tsx @@ -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) => ( + <> + + + {node} + + + + ); +}; diff --git a/plugins/kubernetes/src/hooks/useKubernetesObjects.ts b/plugins/kubernetes/src/hooks/useKubernetesObjects.ts new file mode 100644 index 0000000000..ade7717f9e --- /dev/null +++ b/plugins/kubernetes/src/hooks/useKubernetesObjects.ts @@ -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(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, + }; +}; diff --git a/plugins/kubernetes/src/types/types.ts b/plugins/kubernetes/src/types/types.ts index ab12730f72..27ee395040 100644 --- a/plugins/kubernetes/src/types/types.ts +++ b/plugins/kubernetes/src/types/types.ts @@ -35,4 +35,5 @@ export interface GroupedResponses extends DeploymentResources { services: V1Service[]; configMaps: V1ConfigMap[]; ingresses: ExtensionsV1beta1Ingress[]; + customResources: any[]; } diff --git a/plugins/kubernetes/src/utils/owner.test.ts b/plugins/kubernetes/src/utils/owner.test.ts new file mode 100644 index 0000000000..3e027a3d5b --- /dev/null +++ b/plugins/kubernetes/src/utils/owner.test.ts @@ -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'); + }); + }); +}); diff --git a/plugins/kubernetes/src/utils/owner.ts b/plugins/kubernetes/src/utils/owner.ts new file mode 100644 index 0000000000..88f99bc0a5 --- /dev/null +++ b/plugins/kubernetes/src/utils/owner.ts @@ -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( + 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') + ); + }); +}; diff --git a/plugins/kubernetes/src/utils/response.ts b/plugins/kubernetes/src/utils/response.ts index 54b319a6ef..a9b10e7759 100644 --- a/plugins/kubernetes/src/utils/response.ts +++ b/plugins/kubernetes/src/utils/response.ts @@ -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, ); };