diff --git a/.changeset/early-bees-think.md b/.changeset/early-bees-think.md new file mode 100644 index 0000000000..ea77fa04c7 --- /dev/null +++ b/.changeset/early-bees-think.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +Added apiVersionOverrides config to allow for specifying api versions to use for kubernetes objects diff --git a/.changeset/rich-teachers-hide.md b/.changeset/rich-teachers-hide.md new file mode 100644 index 0000000000..27f437cbed --- /dev/null +++ b/.changeset/rich-teachers-hide.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +build(dependencies): bump `style-loader` from 1.2.1 to 3.3.1 diff --git a/.changeset/twenty-worms-provide.md b/.changeset/twenty-worms-provide.md new file mode 100644 index 0000000000..ae231a1fa0 --- /dev/null +++ b/.changeset/twenty-worms-provide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Add options to spawn in runCommand helper diff --git a/.changeset/weak-rivers-perform.md b/.changeset/weak-rivers-perform.md new file mode 100644 index 0000000000..92f96ab6e0 --- /dev/null +++ b/.changeset/weak-rivers-perform.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes': patch +--- + +Added accordions to display information on Jobs and CronJobs in the kubernetes plugin. Updated the PodsTable with fewer default columns and the ability to pass in additional ones depending on the use case. diff --git a/docs/auth/index.md b/docs/auth/index.md index 0c03e33900..4a8d225048 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -16,8 +16,10 @@ Backstage identity information in your app or plugins. Backstage comes with many common authentication providers in the core library: +- [Atlassian](atlassian/provider.md) - [Auth0](auth0/provider.md) - [Azure](microsoft/provider.md) +- [Bitbucket](bitbucket/provider.md) - [GitHub](github/provider.md) - [GitLab](gitlab/provider.md) - [Google](google/provider.md) diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index 6b3991144b..a7f9b0cd1e 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -219,6 +219,26 @@ The custom resource's apiVersion. The plural representing the custom resource. +### `apiVersionOverrides` (optional) + +Overrides for the API versions used to make requests for the corresponding +objects. If using a legacy Kubernetes version, you may use this config to +override the default API versions to ones that are supported by your cluster. + +Example: + +```yaml +--- +kubernetes: + apiVersionOverrides: + cronjobs: 'v1beta1' +``` + +For more information on which API versions are supported by your cluster, please +view the Kubernetes API docs for your Kubernetes version (e.g. +[API Groups for v1.22](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#-strong-api-groups-strong-) +) + ### Role Based Access Control The current RBAC permissions required are read-only cluster wide, for the diff --git a/packages/cli/package.json b/packages/cli/package.json index dd0a49cb7f..1d40e2788c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -102,7 +102,7 @@ "rollup-pluginutils": "^2.8.2", "run-script-webpack-plugin": "^0.0.11", "semver": "^7.3.2", - "style-loader": "^1.2.1", + "style-loader": "^3.3.1", "sucrase": "^3.20.2", "tar": "^6.1.2", "terser-webpack-plugin": "^5.1.3", diff --git a/plugins/api-docs/README.md b/plugins/api-docs/README.md index d5ed198dc1..4919690568 100644 --- a/plugins/api-docs/README.md +++ b/plugins/api-docs/README.md @@ -160,6 +160,14 @@ by this plugin. Grab a copy of [oauth2-redirect.html](https://github.com/swagger-api/swagger-ui/blob/master/dist/oauth2-redirect.html) and put it in the `app/public/` directory in order to enable Swagger UI to complete this redirection. +This also may require you to adjust `Content Security Policy` header settings of your Backstage application, so that the script in `oauth2-redirect.html` can be executed. Since the script is static we can add the hash of it directly to our CSP policy, which we do by adding the following to the `csp` section of the app configuration: + +```yaml +script-src: + - "'self'" + - "'sha256-GeDavzSZ8O71Jggf/pQkKbt52dfZkrdNMQ3e+Ox+AkI='" # oauth2-redirect.html +``` + #### Configuring your OAuth2 Client You'll need to make sure your OAuth2 client has been registered in your OAuth2 Authentication Server (AS) diff --git a/plugins/azure-devops/src/index.ts b/plugins/azure-devops/src/index.ts index 622f5560b9..c9b804bc8d 100644 --- a/plugins/azure-devops/src/index.ts +++ b/plugins/azure-devops/src/index.ts @@ -22,4 +22,4 @@ export { AzurePullRequestsPage, } from './plugin'; -export * from './components/AzurePullRequestsIcon'; +export { AzurePullRequestsIcon } from './components/AzurePullRequestsIcon'; diff --git a/plugins/kubernetes-backend/schema.d.ts b/plugins/kubernetes-backend/schema.d.ts index e69ac052f1..7f5183e337 100644 --- a/plugins/kubernetes-backend/schema.d.ts +++ b/plugins/kubernetes-backend/schema.d.ts @@ -63,5 +63,23 @@ export interface Config { apiVersion: string; plural: string; }>; + + /** + * (Optional) API Version Overrides + * If set, the specified api version will be used to make requests for the corresponding object. + * If running a legacy Kubernetes version, you may use this to override the default api versions + * that are not supported in your cluster. + */ + apiVersionOverrides?: { + pods?: string; + services?: string; + configmaps?: string; + deployments?: string; + replicasets?: string; + horizontalpodautoscalers?: string; + cronjobs?: string; + jobs?: string; + ingresses?: string; + } & { [pluralKind: string]: string }; }; } diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index 11cca4917f..3333f7a0fc 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -243,6 +243,10 @@ export class KubernetesBuilder { 'kubernetes.objectTypes', ) as KubernetesObjectTypes[]; + const apiVersionOverrides = this.env.config.getOptionalConfig( + 'kubernetes.apiVersionOverrides', + ); + let objectTypesToFetch; if (objectTypesToFetchStrings) { @@ -250,6 +254,17 @@ export class KubernetesBuilder { objectTypesToFetchStrings.includes(obj.objectType), ); } + + if (apiVersionOverrides) { + objectTypesToFetch = objectTypesToFetch ?? DEFAULT_OBJECTS; + + for (const obj of objectTypesToFetch) { + if (apiVersionOverrides.has(obj.objectType)) { + obj.apiVersion = apiVersionOverrides.getString(obj.objectType); + } + } + } + return objectTypesToFetch; } } diff --git a/plugins/kubernetes/dev/index.tsx b/plugins/kubernetes/dev/index.tsx index 12de5f098f..c7a99e7334 100644 --- a/plugins/kubernetes/dev/index.tsx +++ b/plugins/kubernetes/dev/index.tsx @@ -29,6 +29,8 @@ import { } from '@backstage/plugin-kubernetes-common'; import fixture1 from '../src/__fixtures__/1-deployments.json'; import fixture2 from '../src/__fixtures__/2-deployments.json'; +import fixture3 from '../src/__fixtures__/1-cronjobs.json'; +import fixture4 from '../src/__fixtures__/2-cronjobs.json'; import { TestApiProvider } from '@backstage/test-utils'; const mockEntity: Entity = { @@ -102,5 +104,31 @@ createDevApp() ), }) + .addPage({ + path: '/fixture-3', + title: 'Fixture 3', + element: ( + + + + + + ), + }) + .addPage({ + path: '/fixture-4', + title: 'Fixture 4', + element: ( + + + + + + ), + }) .registerPlugin(kubernetesPlugin) .render(); diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 0c78649cf7..a127c9edf6 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -42,6 +42,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", + "cronstrue": "^1.122.0", "js-yaml": "^4.0.0", "lodash": "^4.17.21", "luxon": "^2.0.2", diff --git a/plugins/kubernetes/src/__fixtures__/1-cronjobs.json b/plugins/kubernetes/src/__fixtures__/1-cronjobs.json new file mode 100644 index 0000000000..1edc43710d --- /dev/null +++ b/plugins/kubernetes/src/__fixtures__/1-cronjobs.json @@ -0,0 +1,446 @@ +{ + "cronJobs": [ + { + "metadata": { + "name": "dice-roller-cronjob", + "namespace": "default", + "uid": "9ea073bc-7a4b-4b99-8321-0305bce85568", + "resourceVersion": "1362732552", + "creationTimestamp": "2021-07-16T22:39:58Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller" + }, + "annotations": {} + }, + "spec": { + "schedule": "30 5 * * *", + "startingDeadlineSeconds": 1800, + "concurrencyPolicy": "Forbid", + "suspend": false, + "jobTemplate": { + "metadata": { "creationTimestamp": null }, + "spec": { + "backoffLimit": 2, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "backstage.io/kubernetes-id": "dice-roller" + }, + "annotations": {} + }, + "spec": { + "containers": [ + { + "name": "busybox", + "image": "busybox", + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "imagePullPolicy": "IfNotPresent", + "command": ["/bin/sh", "-c", "date; echo Rolling a die!"] + } + ], + "restartPolicy": "Never", + "terminationGracePeriodSeconds": 30, + "dnsPolicy": "ClusterFirst", + "securityContext": {}, + "schedulerName": "default-scheduler" + } + } + } + }, + "successfulJobsHistoryLimit": 2, + "failedJobsHistoryLimit": 2 + }, + "status": { + "active": [ + { + "kind": "Job", + "namespace": "default", + "name": "dice-roller-cronjob-1637028600", + "uid": "32be1b89-5b53-45b2-aa84-277e75214f61", + "apiVersion": "batch/v1", + "resourceVersion": "1361174163" + } + ], + "lastScheduleTime": "2021-11-16T02:10:00Z" + } + } + ], + "jobs": [ + { + "metadata": { + "name": "dice-roller-cronjob-1637025000", + "namespace": "default", + "uid": "69d5d242-a9a4-47b8-b9c7-c536ae8f151a", + "resourceVersion": "1361029181", + "creationTimestamp": "2021-11-16T01:10:24Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller" + }, + "ownerReferences": [ + { + "apiVersion": "batch/v1", + "kind": "CronJob", + "name": "dice-roller-cronjob", + "uid": "9ea073bc-7a4b-4b99-8321-0305bce85568", + "controller": true, + "blockOwnerDeletion": true + } + ] + }, + "spec": { + "parallelism": 1, + "completions": 1, + "backoffLimit": 2, + "selector": { + "matchLabels": { + "controller-uid": "69d5d242-a9a4-47b8-b9c7-c536ae8f151a" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "controller-uid": "69d5d242-a9a4-47b8-b9c7-c536ae8f151a", + "job-name": "dice-roller-cronjob-1637025000" + }, + "annotations": {} + }, + "spec": { + "containers": [ + { + "command": ["/bin/sh", "-c", "date; echo Rolling a die!"], + "image": "busybox", + "imagePullPolicy": "IfNotPresent", + "name": "busybox", + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "restartPolicy": "Never", + "terminationGracePeriodSeconds": 30, + "dnsPolicy": "ClusterFirst", + "securityContext": {}, + "schedulerName": "default-scheduler" + } + } + }, + "status": { + "conditions": [ + { + "type": "Complete", + "status": "True", + "lastProbeTime": "2021-11-16T01:11:31Z", + "lastTransitionTime": "2021-11-16T01:11:31Z" + } + ], + "startTime": "2021-11-16T01:10:24Z", + "completionTime": "2021-11-16T01:11:31Z", + "succeeded": 1 + } + }, + { + "metadata": { + "name": "dice-roller-cronjob-1637028600", + "namespace": "default", + "uid": "32be1b89-5b53-45b2-aa84-277e75214f61", + "resourceVersion": "1361174166", + "creationTimestamp": "2021-11-16T02:10:22Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller" + }, + "ownerReferences": [ + { + "apiVersion": "batch/v1", + "kind": "CronJob", + "name": "dice-roller-cronjob", + "uid": "9ea073bc-7a4b-4b99-8321-0305bce85568", + "controller": true, + "blockOwnerDeletion": true + } + ] + }, + "spec": { + "parallelism": 1, + "completions": 1, + "backoffLimit": 2, + "selector": { + "matchLabels": { + "controller-uid": "32be1b89-5b53-45b2-aa84-277e75214f61" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "controller-uid": "32be1b89-5b53-45b2-aa84-277e75214f61", + "job-name": "dice-roller-cronjob-1637028600" + }, + "annotations": {} + }, + "spec": { + "containers": [ + { + "command": ["/bin/sh", "-c", "date; echo Rolling a die!"], + "image": "busybox", + "imagePullPolicy": "IfNotPresent", + "name": "busybox", + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "restartPolicy": "Never", + "terminationGracePeriodSeconds": 30, + "dnsPolicy": "ClusterFirst", + "securityContext": {}, + "schedulerName": "default-scheduler" + } + } + }, + "status": { "startTime": "2021-11-16T02:10:22Z", "active": 1 } + } + ], + "pods": [ + { + "metadata": { + "name": "dice-roller-cronjob-1637025000-gstc4", + "generateName": "dice-roller-cronjob-1637025000-", + "namespace": "default", + "uid": "3b0f2b65-5ae2-441a-beda-bdc92bcafaf0", + "resourceVersion": "1361029179", + "creationTimestamp": "2021-11-16T01:10:24Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "controller-uid": "69d5d242-a9a4-47b8-b9c7-c536ae8f151a", + "job-name": "dice-roller-cronjob-1637025000" + }, + "annotations": {}, + "ownerReferences": [ + { + "apiVersion": "batch/v1", + "kind": "Job", + "name": "dice-roller-cronjob-1637025000", + "uid": "69d5d242-a9a4-47b8-b9c7-c536ae8f151a", + "controller": true, + "blockOwnerDeletion": true + } + ] + }, + "spec": { + "volumes": [], + "containers": [ + { + "command": ["/bin/sh", "-c", "date; echo Rolling a die!"], + "image": "busybox", + "imagePullPolicy": "IfNotPresent", + "name": "busybox", + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "restartPolicy": "Never", + "terminationGracePeriodSeconds": 30, + "dnsPolicy": "ClusterFirst", + "serviceAccountName": "default", + "serviceAccount": "default", + "nodeName": "minikube", + "securityContext": {}, + "schedulerName": "default-scheduler", + "tolerations": [ + { + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "effect": "NoExecute", + "tolerationSeconds": 300 + }, + { + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "effect": "NoExecute", + "tolerationSeconds": 300 + } + ], + "priority": 0, + "enableServiceLinks": true + }, + "status": { + "phase": "Succeeded", + "conditions": [ + { + "type": "Initialized", + "status": "True", + "lastProbeTime": null, + "lastTransitionTime": "2021-11-16T01:10:27Z", + "reason": "PodCompleted" + }, + { + "type": "Ready", + "status": "False", + "lastProbeTime": null, + "lastTransitionTime": "2021-11-16T01:11:31Z", + "reason": "PodCompleted" + }, + { + "type": "ContainersReady", + "status": "False", + "lastProbeTime": null, + "lastTransitionTime": "2021-11-16T01:11:31Z", + "reason": "PodCompleted" + }, + { + "type": "PodScheduled", + "status": "True", + "lastProbeTime": null, + "lastTransitionTime": "2021-11-16T01:10:24Z" + } + ], + "hostIP": "192.168.49.2", + "podIP": "172.17.0.25", + "podIPs": [{ "ip": "172.17.0.25" }], + "startTime": "2021-11-16T01:10:24Z", + "containerStatuses": [ + { + "name": "busybox", + "state": { + "terminated": { + "exitCode": 0, + "reason": "Completed", + "startedAt": "2021-11-16T01:10:31Z", + "finishedAt": "2021-11-16T01:11:30Z", + "containerID": "docker://1062343e1bb3625e36717bc15617c1bbca976366c3c9dd87035c71f33d015615" + } + }, + "lastState": {}, + "ready": false, + "restartCount": 0, + "image": "busybox:latest/node", + "imageID": "docker-pullable://busybox@sha256:e7157b6d7ebbe2cce5eaa8cfe8aa4fa82d173999b9f90a9ec42e57323546c353", + "containerID": "docker://1062343e1bb3625e36717bc15617c1bbca976366c3c9dd87035c71f33d015615", + "started": false + } + ], + "qosClass": "Burstable" + } + }, + { + "metadata": { + "name": "dice-roller-cronjob-1637028600-p4mlc", + "generateName": "dice-roller-cronjob-1637028600-", + "namespace": "default", + "uid": "acddd5d2-ac7f-473b-a9d8-17a89f99ac39", + "resourceVersion": "1361174579", + "creationTimestamp": "2021-11-16T02:10:22Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "controller-uid": "32be1b89-5b53-45b2-aa84-277e75214f61", + "job-name": "dice-roller-cronjob-1637028600" + }, + "annotations": {}, + "ownerReferences": [ + { + "apiVersion": "batch/v1", + "kind": "Job", + "name": "dice-roller-cronjob-1637028600", + "uid": "32be1b89-5b53-45b2-aa84-277e75214f61", + "controller": true, + "blockOwnerDeletion": true + } + ] + }, + "spec": { + "volumes": [], + "containers": [ + { + "command": ["/bin/sh", "-c", "date; echo Rolling a die!"], + "image": "busybox", + "imagePullPolicy": "IfNotPresent", + "name": "busybox", + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "restartPolicy": "Never", + "terminationGracePeriodSeconds": 30, + "dnsPolicy": "ClusterFirst", + "serviceAccountName": "default", + "serviceAccount": "default", + "nodeName": "minikube", + "securityContext": {}, + "schedulerName": "default-scheduler", + "tolerations": [ + { + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "effect": "NoExecute", + "tolerationSeconds": 300 + }, + { + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "effect": "NoExecute", + "tolerationSeconds": 300 + } + ], + "priority": 0, + "enableServiceLinks": true + }, + "status": { + "phase": "Running", + "conditions": [ + { + "type": "Initialized", + "status": "True", + "lastProbeTime": null, + "lastTransitionTime": "2021-11-16T02:10:25Z" + }, + { + "type": "Ready", + "status": "True", + "lastProbeTime": null, + "lastTransitionTime": "2021-11-16T02:10:35Z" + }, + { + "type": "ContainersReady", + "status": "True", + "lastProbeTime": null, + "lastTransitionTime": "2021-11-16T02:10:35Z" + }, + { + "type": "PodScheduled", + "status": "True", + "lastProbeTime": null, + "lastTransitionTime": "2021-11-16T02:10:22Z" + } + ], + "hostIP": "192.168.49.2", + "podIP": "172.17.0.25", + "podIPs": [{ "ip": "172.17.0.25" }], + "startTime": "2021-11-16T02:10:22Z", + "containerStatuses": [ + { + "name": "busybox", + "state": { + "running": { "startedAt": "2021-11-16T02:10:31Z" } + }, + "lastState": {}, + "ready": true, + "restartCount": 0, + "image": "busybox:latest", + "imageID": "docker-pullable://busybox@sha256:e7157b6d7ebbe2cce5eaa8cfe8aa4fa82d173999b9f90a9ec42e57323546c353", + "containerID": "docker://1062343e1bb3625e36717bc15617c1bbca976366c3c9dd87035c71f33d015615", + "started": true + } + ], + "qosClass": "Burstable" + } + } + ] +} diff --git a/plugins/kubernetes/src/__fixtures__/2-cronjobs.json b/plugins/kubernetes/src/__fixtures__/2-cronjobs.json new file mode 100644 index 0000000000..6c087023b7 --- /dev/null +++ b/plugins/kubernetes/src/__fixtures__/2-cronjobs.json @@ -0,0 +1,385 @@ +{ + "cronJobs": [ + { + "metadata": { + "name": "dice-roller-cronjob", + "namespace": "default", + "uid": "9ea073bc-7a4b-4b99-8321-0305bce85568", + "resourceVersion": "1362732552", + "creationTimestamp": "2021-07-16T22:39:58Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller" + }, + "annotations": {} + }, + "spec": { + "schedule": "* */2 * * *", + "startingDeadlineSeconds": 1800, + "concurrencyPolicy": "Forbid", + "suspend": true, + "jobTemplate": { + "metadata": { "creationTimestamp": null }, + "spec": { + "backoffLimit": 2, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "backstage.io/kubernetes-id": "dice-roller" + }, + "annotations": {} + }, + "spec": { + "containers": [ + { + "name": "busybox", + "image": "busybox", + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "imagePullPolicy": "IfNotPresent", + "command": ["/bin/sh", "-c", "date; echo Rolling a die!"] + } + ], + "restartPolicy": "Never", + "terminationGracePeriodSeconds": 30, + "dnsPolicy": "ClusterFirst", + "securityContext": {}, + "schedulerName": "default-scheduler" + } + } + } + }, + "successfulJobsHistoryLimit": 2, + "failedJobsHistoryLimit": 2 + }, + "status": { + "lastScheduleTime": "2021-11-16T02:10:00Z" + } + } + ], + "jobs": [ + { + "metadata": { + "name": "dice-roller-cronjob-1637025000", + "namespace": "default", + "uid": "69d5d242-a9a4-47b8-b9c7-c536ae8f151a", + "resourceVersion": "1361029181", + "creationTimestamp": "2021-11-16T01:10:24Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller" + }, + "ownerReferences": [ + { + "apiVersion": "batch/v1", + "kind": "CronJob", + "name": "dice-roller-cronjob", + "uid": "9ea073bc-7a4b-4b99-8321-0305bce85568", + "controller": true, + "blockOwnerDeletion": true + } + ] + }, + "spec": { + "parallelism": 1, + "completions": 1, + "backoffLimit": 2, + "selector": { + "matchLabels": { + "controller-uid": "69d5d242-a9a4-47b8-b9c7-c536ae8f151a" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "controller-uid": "69d5d242-a9a4-47b8-b9c7-c536ae8f151a", + "job-name": "dice-roller-cronjob-1637025000" + }, + "annotations": {} + }, + "spec": { + "containers": [ + { + "command": ["/bin/sh", "-c", "date; echo Rolling a die!"], + "image": "busybox", + "imagePullPolicy": "IfNotPresent", + "name": "busybox", + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "restartPolicy": "Never", + "terminationGracePeriodSeconds": 30, + "dnsPolicy": "ClusterFirst", + "securityContext": {}, + "schedulerName": "default-scheduler" + } + } + }, + "status": { + "conditions": [ + { + "type": "Failed", + "status": "True", + "reason": "BackoffLimitExceeded", + "lastProbeTime": "2021-11-16T01:11:31Z", + "lastTransitionTime": "2021-11-16T01:11:31Z" + } + ], + "startTime": "2021-11-16T01:10:24Z", + "failed": 2 + } + } + ], + "pods": [ + { + "metadata": { + "name": "dice-roller-cronjob-1637025000-gstc4", + "generateName": "dice-roller-cronjob-1637025000-", + "namespace": "default", + "uid": "3b0f2b65-5ae2-441a-beda-bdc92bcafaf0", + "resourceVersion": "1361029179", + "creationTimestamp": "2021-11-16T01:10:24Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "controller-uid": "69d5d242-a9a4-47b8-b9c7-c536ae8f151a", + "job-name": "dice-roller-cronjob-1637025000" + }, + "annotations": {}, + "ownerReferences": [ + { + "apiVersion": "batch/v1", + "kind": "Job", + "name": "dice-roller-cronjob-1637025000", + "uid": "69d5d242-a9a4-47b8-b9c7-c536ae8f151a", + "controller": true, + "blockOwnerDeletion": true + } + ] + }, + "spec": { + "volumes": [], + "containers": [ + { + "command": ["/bin/sh", "-c", "date; echo Rolling a die!"], + "image": "busybox", + "imagePullPolicy": "IfNotPresent", + "name": "busybox", + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "restartPolicy": "Never", + "terminationGracePeriodSeconds": 30, + "dnsPolicy": "ClusterFirst", + "serviceAccountName": "default", + "serviceAccount": "default", + "nodeName": "minikube", + "securityContext": {}, + "schedulerName": "default-scheduler", + "tolerations": [ + { + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "effect": "NoExecute", + "tolerationSeconds": 300 + }, + { + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "effect": "NoExecute", + "tolerationSeconds": 300 + } + ], + "priority": 0, + "enableServiceLinks": true + }, + "status": { + "phase": "Failed", + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2021-11-18T19:10:13Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2021-11-18T19:11:02Z", + "message": "containers with unready status: [busybox]", + "reason": "ContainersNotReady", + "status": "False", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2021-11-18T19:11:02Z", + "message": "containers with unready status: [busybox]", + "reason": "ContainersNotReady", + "status": "False", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2021-11-18T19:10:08Z", + "status": "True", + "type": "PodScheduled" + } + ], + "hostIP": "192.168.49.2", + "podIP": "172.17.0.25", + "podIPs": [{ "ip": "172.17.0.25" }], + "startTime": "2021-11-18T19:10:08Z", + "containerStatuses": [ + { + "name": "busybox", + "state": { + "terminated": { + "exitCode": 1, + "finishedAt": "2021-11-18T19:11:01Z", + "reason": "Error", + "startedAt": "2021-11-18T19:10:17Z", + "containerID": "docker://1062343e1bb3625e36717bc15617c1bbca976366c3c9dd87035c71f33d015615" + } + }, + "lastState": {}, + "ready": false, + "restartCount": 0, + "image": "busybox:latest", + "imageID": "docker-pullable://busybox@sha256:e7157b6d7ebbe2cce5eaa8cfe8aa4fa82d173999b9f90a9ec42e57323546c353", + "containerID": "docker://1062343e1bb3625e36717bc15617c1bbca976366c3c9dd87035c71f33d015615", + "started": false + } + ], + "qosClass": "Burstable" + } + }, + { + "metadata": { + "name": "dice-roller-cronjob-1637025000-p4mlc", + "generateName": "dice-roller-cronjob-1637025000-", + "namespace": "default", + "uid": "acddd5d2-ac7f-473b-a9d8-17a89f99ac39", + "resourceVersion": "1361174579", + "creationTimestamp": "2021-11-16T02:10:22Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "controller-uid": "69d5d242-a9a4-47b8-b9c7-c536ae8f151a", + "job-name": "dice-roller-cronjob-1637025000" + }, + "annotations": {}, + "ownerReferences": [ + { + "apiVersion": "batch/v1", + "kind": "Job", + "name": "dice-roller-cronjob-1637025000", + "uid": "69d5d242-a9a4-47b8-b9c7-c536ae8f151a", + "controller": true, + "blockOwnerDeletion": true + } + ] + }, + "spec": { + "volumes": [], + "containers": [ + { + "command": ["/bin/sh", "-c", "date; echo Rolling a die!"], + "image": "busybox", + "imagePullPolicy": "IfNotPresent", + "name": "busybox", + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "restartPolicy": "Never", + "terminationGracePeriodSeconds": 30, + "dnsPolicy": "ClusterFirst", + "serviceAccountName": "default", + "serviceAccount": "default", + "nodeName": "minikube", + "securityContext": {}, + "schedulerName": "default-scheduler", + "tolerations": [ + { + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "effect": "NoExecute", + "tolerationSeconds": 300 + }, + { + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "effect": "NoExecute", + "tolerationSeconds": 300 + } + ], + "priority": 0, + "enableServiceLinks": true + }, + "status": { + "phase": "Failed", + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2021-11-18T19:10:13Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2021-11-18T19:11:02Z", + "message": "containers with unready status: [busybox]", + "reason": "ContainersNotReady", + "status": "False", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2021-11-18T19:11:02Z", + "message": "containers with unready status: [busybox]", + "reason": "ContainersNotReady", + "status": "False", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2021-11-18T19:10:08Z", + "status": "True", + "type": "PodScheduled" + } + ], + "hostIP": "192.168.49.2", + "podIP": "172.17.0.25", + "podIPs": [{ "ip": "172.17.0.25" }], + "startTime": "2021-11-16T02:10:22Z", + "containerStatuses": [ + { + "name": "busybox", + "state": { + "terminated": { + "exitCode": 1, + "finishedAt": "2021-11-18T19:11:01Z", + "reason": "Error", + "startedAt": "2021-11-18T19:10:17Z", + "containerID": "docker://2659c4d0f8a68f2b49863c18738322f1686d5b87275428e5e641fd9fd9e06739" + } + }, + "lastState": {}, + "ready": true, + "restartCount": 0, + "image": "busybox:latest", + "imageID": "docker-pullable://busybox@sha256:e7157b6d7ebbe2cce5eaa8cfe8aa4fa82d173999b9f90a9ec42e57323546c353", + "containerID": "docker://2659c4d0f8a68f2b49863c18738322f1686d5b87275428e5e641fd9fd9e06739", + "started": true + } + ], + "qosClass": "Burstable" + } + } + ] +} diff --git a/plugins/kubernetes/src/components/Cluster/Cluster.tsx b/plugins/kubernetes/src/components/Cluster/Cluster.tsx index d238396f53..416d6e23e3 100644 --- a/plugins/kubernetes/src/components/Cluster/Cluster.tsx +++ b/plugins/kubernetes/src/components/Cluster/Cluster.tsx @@ -29,6 +29,7 @@ import { DeploymentsAccordions } from '../DeploymentsAccordions'; import { groupResponses } from '../../utils/response'; import { IngressesAccordions } from '../IngressesAccordions'; import { ServicesAccordions } from '../ServicesAccordions'; +import { CronJobsAccordions } from '../CronJobsAccordions'; import { CustomResources } from '../CustomResources'; import { ClusterContext, @@ -133,6 +134,9 @@ export const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => { + + + diff --git a/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsAccordions.test.tsx b/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsAccordions.test.tsx new file mode 100644 index 0000000000..2434631fd0 --- /dev/null +++ b/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsAccordions.test.tsx @@ -0,0 +1,50 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { render } from '@testing-library/react'; +import { CronJobsAccordions } from './CronJobsAccordions'; +import * as oneCronJobsFixture from '../../__fixtures__/1-cronjobs.json'; +import * as twoCronJobsFixture from '../../__fixtures__/2-cronjobs.json'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { kubernetesProviders } from '../../hooks/test-utils'; + +describe('CronJobsAccordions', () => { + it('should render 1 active cronjobs', async () => { + const wrapper = kubernetesProviders(oneCronJobsFixture, []); + + const { getByText } = render( + wrapper(wrapInTestApp()), + ); + + expect(getByText('dice-roller-cronjob')).toBeInTheDocument(); + expect(getByText('CronJob')).toBeInTheDocument(); + expect(getByText('namespace: default')).toBeInTheDocument(); + expect(getByText('Active')).toBeInTheDocument(); + }); + + it('should render 1 suspended cronjobs', async () => { + const wrapper = kubernetesProviders(twoCronJobsFixture, []); + + const { getByText } = render( + wrapper(wrapInTestApp()), + ); + + expect(getByText('dice-roller-cronjob')).toBeInTheDocument(); + expect(getByText('CronJob')).toBeInTheDocument(); + expect(getByText('namespace: default')).toBeInTheDocument(); + expect(getByText('Suspended')).toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsAccordions.tsx b/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsAccordions.tsx new file mode 100644 index 0000000000..2581678928 --- /dev/null +++ b/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsAccordions.tsx @@ -0,0 +1,128 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { useContext } from 'react'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + Divider, + Grid, + Typography, +} from '@material-ui/core'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { V1CronJob, V1Job } from '@kubernetes/client-node'; +import { JobsAccordions } from '../JobsAccordions'; +import { CronJobDrawer } from './CronJobsDrawer'; +import { getOwnedResources } from '../../utils/owner'; +import { GroupedResponsesContext } from '../../hooks'; +import { StatusError, StatusOK } from '@backstage/core-components'; +import cronstrue from 'cronstrue'; + +type CronJobsAccordionsProps = { + children?: React.ReactNode; +}; + +type CronJobAccordionProps = { + cronJob: V1CronJob; + ownedJobs: V1Job[]; + children?: React.ReactNode; +}; + +type CronJobSummaryProps = { + cronJob: V1CronJob; + children?: React.ReactNode; +}; + +const CronJobSummary = ({ cronJob }: CronJobSummaryProps) => { + return ( + + + + + + + + + + {cronJob.spec?.suspend ? ( + Suspended + ) : ( + Active + )} + + + + Schedule:{' '} + {cronJob.spec?.schedule + ? `${cronJob.spec.schedule} (${cronstrue.toString( + cronJob.spec.schedule, + )})` + : 'N/A'} + + + + + ); +}; + +const CronJobAccordion = ({ cronJob, ownedJobs }: CronJobAccordionProps) => { + return ( + + }> + + + + + + + ); +}; + +export const CronJobsAccordions = ({}: CronJobsAccordionsProps) => { + const groupedResponses = useContext(GroupedResponsesContext); + + return ( + + {groupedResponses.cronJobs.map((cronJob, i) => ( + + + + + + ))} + + ); +}; diff --git a/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsDrawer.test.tsx b/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsDrawer.test.tsx new file mode 100644 index 0000000000..b16e83ee50 --- /dev/null +++ b/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsDrawer.test.tsx @@ -0,0 +1,38 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import * as oneCronJobsFixture from '../../__fixtures__/1-cronjobs.json'; +import { renderInTestApp } from '@backstage/test-utils'; +import { CronJobDrawer } from './CronJobsDrawer'; + +describe('CronJobDrawer', () => { + it('should render cronJob drawer', async () => { + const { getByText, getAllByText } = await renderInTestApp( + , + ); + + expect(getAllByText('dice-roller-cronjob')).toHaveLength(2); + expect(getAllByText('CronJob')).toHaveLength(2); + expect(getByText('YAML')).toBeInTheDocument(); + expect(getByText('Schedule')).toBeInTheDocument(); + expect(getByText('30 5 * * *')).toBeInTheDocument(); + expect(getByText('Starting Deadline Seconds')).toBeInTheDocument(); + expect(getByText('Last Schedule Time')).toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsDrawer.tsx b/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsDrawer.tsx new file mode 100644 index 0000000000..758f17ade0 --- /dev/null +++ b/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsDrawer.tsx @@ -0,0 +1,67 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { V1CronJob } from '@kubernetes/client-node'; +import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer'; +import { Typography, Grid, Chip } from '@material-ui/core'; + +export const CronJobDrawer = ({ + cronJob, + expanded, +}: { + cronJob: V1CronJob; + expanded?: boolean; +}) => { + const namespace = cronJob.metadata?.namespace; + return ( + ({ + schedule: cronJobObj.spec?.schedule ?? '???', + startingDeadlineSeconds: + cronJobObj.spec?.startingDeadlineSeconds ?? '???', + concurrencyPolicy: cronJobObj.spec?.concurrencyPolicy ?? '???', + lastScheduleTime: cronJobObj.status?.lastScheduleTime ?? '???', + })} + > + + + + {cronJob.metadata?.name ?? 'unknown object'} + + + + + CronJob + + + {namespace && ( + + + + )} + + + ); +}; diff --git a/plugins/kubernetes/src/components/CronJobsAccordions/index.ts b/plugins/kubernetes/src/components/CronJobsAccordions/index.ts new file mode 100644 index 0000000000..e72e3f7e6d --- /dev/null +++ b/plugins/kubernetes/src/components/CronJobsAccordions/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { CronJobsAccordions } from './CronJobsAccordions'; diff --git a/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.tsx b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.tsx index e27c2bfbe1..2fa140b5f7 100644 --- a/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.tsx +++ b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.tsx @@ -36,11 +36,12 @@ import { getOwnedPodsThroughReplicaSets, getMatchingHpa, } from '../../utils/owner'; +import { containersReady, totalRestarts } from '../../utils/pod'; import { GroupedResponsesContext, PodNamesWithErrorsContext, } from '../../hooks'; -import { StatusError, StatusOK } from '@backstage/core-components'; +import { StatusError, StatusOK, TableColumn } from '@backstage/core-components'; type DeploymentsAccordionsProps = { children?: React.ReactNode; @@ -61,6 +62,20 @@ type DeploymentSummaryProps = { children?: React.ReactNode; }; +const deploymentPodColumns: TableColumn[] = [ + { + title: 'containers ready', + align: 'center', + render: containersReady, + }, + { + title: 'total restarts', + align: 'center', + render: totalRestarts, + type: 'numeric', + }, +]; + const DeploymentSummary = ({ deployment, numberOfCurrentPods, @@ -161,7 +176,7 @@ const DeploymentAccordion = ({ /> - + ); diff --git a/plugins/kubernetes/src/components/JobsAccordions/JobsAccordions.test.tsx b/plugins/kubernetes/src/components/JobsAccordions/JobsAccordions.test.tsx new file mode 100644 index 0000000000..093d616319 --- /dev/null +++ b/plugins/kubernetes/src/components/JobsAccordions/JobsAccordions.test.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { render } from '@testing-library/react'; +import { JobsAccordions } from './JobsAccordions'; +import * as oneCronJobsFixture from '../../__fixtures__/1-cronjobs.json'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { kubernetesProviders } from '../../hooks/test-utils'; +import { V1Job, ObjectSerializer } from '@kubernetes/client-node'; + +describe('JobsAccordions', () => { + it('should render 2 jobs', async () => { + const wrapper = kubernetesProviders(oneCronJobsFixture, []); + + const jobs: V1Job[] = oneCronJobsFixture.jobs.map( + job => ObjectSerializer.deserialize(job, 'V1Job') as V1Job, + ); + + const { getByText } = render( + wrapper(wrapInTestApp()), + ); + + expect(getByText('dice-roller-cronjob-1637028600')).toBeInTheDocument(); + expect(getByText('Running')).toBeInTheDocument(); + + expect(getByText('dice-roller-cronjob-1637025000')).toBeInTheDocument(); + expect(getByText('Succeeded')).toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes/src/components/JobsAccordions/JobsAccordions.tsx b/plugins/kubernetes/src/components/JobsAccordions/JobsAccordions.tsx new file mode 100644 index 0000000000..715be4c88a --- /dev/null +++ b/plugins/kubernetes/src/components/JobsAccordions/JobsAccordions.tsx @@ -0,0 +1,125 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { useContext } from 'react'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + Divider, + Grid, +} from '@material-ui/core'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { V1Job, V1Pod } from '@kubernetes/client-node'; +import { PodsTable } from '../Pods'; +import { JobDrawer } from './JobsDrawer'; +import { getOwnedResources } from '../../utils/owner'; +import { GroupedResponsesContext } from '../../hooks'; +import { + StatusError, + StatusOK, + StatusPending, +} from '@backstage/core-components'; + +type JobsAccordionsProps = { + jobs: V1Job[]; + children?: React.ReactNode; +}; + +type JobAccordionProps = { + job: V1Job; + ownedPods: V1Pod[]; + children?: React.ReactNode; +}; + +type JobSummaryProps = { + job: V1Job; + children?: React.ReactNode; +}; + +const JobSummary = ({ job }: JobSummaryProps) => { + return ( + + + + + + + + + + {job.status?.succeeded && Succeeded} + {job.status?.active && Running} + {job.status?.failed && Failed} + + Start time: {job.status?.startTime?.toString()} + {job.status?.completionTime && ( + + Completion time: {job.status.completionTime.toString()} + + )} + + + ); +}; + +const JobAccordion = ({ job, ownedPods }: JobAccordionProps) => { + return ( + + }> + + + + + + + ); +}; + +export const JobsAccordions = ({ jobs }: JobsAccordionsProps) => { + const groupedResponses = useContext(GroupedResponsesContext); + + return ( + + {jobs.map((job, i) => ( + + + + + + ))} + + ); +}; diff --git a/plugins/kubernetes/src/components/JobsAccordions/JobsDrawer.test.tsx b/plugins/kubernetes/src/components/JobsAccordions/JobsDrawer.test.tsx new file mode 100644 index 0000000000..48a4e6fe7e --- /dev/null +++ b/plugins/kubernetes/src/components/JobsAccordions/JobsDrawer.test.tsx @@ -0,0 +1,35 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import * as oneCronJobsFixture from '../../__fixtures__/1-cronjobs.json'; +import { renderInTestApp } from '@backstage/test-utils'; +import { JobDrawer } from './JobsDrawer'; + +describe('JobDrawer', () => { + it('should render job drawer', async () => { + const { getByText, getAllByText } = await renderInTestApp( + , + ); + + expect(getAllByText('dice-roller-cronjob-1637025000')).toHaveLength(2); + expect(getAllByText('Job')).toHaveLength(2); + expect(getByText('YAML')).toBeInTheDocument(); + expect(getByText('Parallelism')).toBeInTheDocument(); + expect(getByText('Completions')).toBeInTheDocument(); + expect(getByText('Backoff Limit')).toBeInTheDocument(); + expect(getByText('Start Time')).toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes/src/components/JobsAccordions/JobsDrawer.tsx b/plugins/kubernetes/src/components/JobsAccordions/JobsDrawer.tsx new file mode 100644 index 0000000000..a7217725b2 --- /dev/null +++ b/plugins/kubernetes/src/components/JobsAccordions/JobsDrawer.tsx @@ -0,0 +1,62 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { V1Job } from '@kubernetes/client-node'; +import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer'; +import { Typography, Grid } from '@material-ui/core'; + +export const JobDrawer = ({ + job, + expanded, +}: { + job: V1Job; + expanded?: boolean; +}) => { + return ( + { + return { + parallelism: jobObj.spec?.parallelism ?? '???', + completions: jobObj.spec?.completions ?? '???', + backoffLimit: jobObj.spec?.backoffLimit ?? '???', + startTime: jobObj.status?.startTime ?? '???', + }; + }} + > + + + + {job.metadata?.name ?? 'unknown object'} + + + + + Job + + + + + ); +}; diff --git a/plugins/kubernetes/src/components/JobsAccordions/index.ts b/plugins/kubernetes/src/components/JobsAccordions/index.ts new file mode 100644 index 0000000000..392309de79 --- /dev/null +++ b/plugins/kubernetes/src/components/JobsAccordions/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { JobsAccordions } from './JobsAccordions'; diff --git a/plugins/kubernetes/src/components/Pods/PodsTable.test.tsx b/plugins/kubernetes/src/components/Pods/PodsTable.test.tsx index 7c6c7e97f7..868b74bbc9 100644 --- a/plugins/kubernetes/src/components/Pods/PodsTable.test.tsx +++ b/plugins/kubernetes/src/components/Pods/PodsTable.test.tsx @@ -18,8 +18,25 @@ import React from 'react'; import { render } from '@testing-library/react'; import * as pod from './__fixtures__/pod.json'; import * as crashingPod from './__fixtures__/crashing-pod.json'; +import { TableColumn } from '@backstage/core-components'; import { wrapInTestApp } from '@backstage/test-utils'; +import { V1Pod } from '@kubernetes/client-node'; import { PodsTable } from './PodsTable'; +import { containersReady, totalRestarts } from '../../utils/pod'; + +const extraColumns: TableColumn[] = [ + { + title: 'containers ready', + align: 'center', + render: containersReady, + }, + { + title: 'total restarts', + align: 'center', + render: totalRestarts, + type: 'numeric', + }, +]; describe('PodsTable', () => { it('should render pod', async () => { @@ -27,6 +44,24 @@ describe('PodsTable', () => { wrapInTestApp(), ); + // titles + expect(getByText('name')).toBeInTheDocument(); + expect(getByText('phase')).toBeInTheDocument(); + expect(getByText('status')).toBeInTheDocument(); + + // values + expect(getByText('dice-roller-6c8646bfd-2m5hv')).toBeInTheDocument(); + expect(getByText('Running')).toBeInTheDocument(); + expect(getByText('OK')).toBeInTheDocument(); + }); + + it('should render pod with extra columns', async () => { + const { getByText } = render( + wrapInTestApp( + , + ), + ); + // titles expect(getByText('name')).toBeInTheDocument(); expect(getByText('phase')).toBeInTheDocument(); @@ -41,9 +76,12 @@ describe('PodsTable', () => { expect(getByText('0')).toBeInTheDocument(); expect(getByText('OK')).toBeInTheDocument(); }); - it('should render crashing pod', async () => { + + it('should render crashing pod with extra columns', async () => { const { getByText, getAllByText } = render( - wrapInTestApp(), + wrapInTestApp( + , + ), ); // titles diff --git a/plugins/kubernetes/src/components/Pods/PodsTable.tsx b/plugins/kubernetes/src/components/Pods/PodsTable.tsx index 7b5497ff37..8ce5f813b3 100644 --- a/plugins/kubernetes/src/components/Pods/PodsTable.tsx +++ b/plugins/kubernetes/src/components/Pods/PodsTable.tsx @@ -17,14 +17,10 @@ import React from 'react'; import { V1Pod } from '@kubernetes/client-node'; import { PodDrawer } from './PodDrawer'; -import { - containersReady, - containerStatuses, - totalRestarts, -} from '../../utils/pod'; +import { containerStatuses } from '../../utils/pod'; import { Table, TableColumn } from '@backstage/core-components'; -const columns: TableColumn[] = [ +const DEFAULT_COLUMNS: TableColumn[] = [ { title: 'name', highlight: true, @@ -34,29 +30,19 @@ const columns: TableColumn[] = [ title: 'phase', render: (pod: V1Pod) => pod.status?.phase ?? 'unknown', }, - { - title: 'containers ready', - align: 'center', - render: containersReady, - }, - { - title: 'total restarts', - align: 'center', - render: totalRestarts, - type: 'numeric', - }, { title: 'status', render: containerStatuses, }, ]; -type DeploymentTablesProps = { +type PodsTablesProps = { pods: V1Pod[]; + extraColumns?: TableColumn[]; children?: React.ReactNode; }; -export const PodsTable = ({ pods }: DeploymentTablesProps) => { +export const PodsTable = ({ pods, extraColumns = [] }: PodsTablesProps) => { const tableStyle = { minWidth: '0', width: '100%', @@ -67,7 +53,7 @@ export const PodsTable = ({ pods }: DeploymentTablesProps) => { ); diff --git a/plugins/kubernetes/src/hooks/GroupedResponses.ts b/plugins/kubernetes/src/hooks/GroupedResponses.ts index d000086b7c..35f7e10b5c 100644 --- a/plugins/kubernetes/src/hooks/GroupedResponses.ts +++ b/plugins/kubernetes/src/hooks/GroupedResponses.ts @@ -24,5 +24,7 @@ export const GroupedResponsesContext = React.createContext({ configMaps: [], horizontalPodAutoscalers: [], ingresses: [], + jobs: [], + cronJobs: [], customResources: [], }); diff --git a/plugins/kubernetes/src/types/types.ts b/plugins/kubernetes/src/types/types.ts index 05337086be..d6df99cb95 100644 --- a/plugins/kubernetes/src/types/types.ts +++ b/plugins/kubernetes/src/types/types.ts @@ -22,6 +22,8 @@ import { V1Service, V1ConfigMap, ExtensionsV1beta1Ingress, + V1Job, + V1CronJob, } from '@kubernetes/client-node'; export interface DeploymentResources { @@ -35,6 +37,8 @@ export interface GroupedResponses extends DeploymentResources { services: V1Service[]; configMaps: V1ConfigMap[]; ingresses: ExtensionsV1beta1Ingress[]; + jobs: V1Job[]; + cronJobs: V1CronJob[]; customResources: any[]; } diff --git a/plugins/kubernetes/src/utils/response.ts b/plugins/kubernetes/src/utils/response.ts index 97501bb102..cfe3d20580 100644 --- a/plugins/kubernetes/src/utils/response.ts +++ b/plugins/kubernetes/src/utils/response.ts @@ -45,6 +45,12 @@ export const groupResponses = ( case 'ingresses': prev.ingresses.push(...next.resources); break; + case 'jobs': + prev.jobs.push(...next.resources); + break; + case 'cronjobs': + prev.cronJobs.push(...next.resources); + break; case 'customresources': prev.customResources.push(...next.resources); break; @@ -60,6 +66,8 @@ export const groupResponses = ( configMaps: [], horizontalPodAutoscalers: [], ingresses: [], + jobs: [], + cronJobs: [], customResources: [], } as GroupedResponses, ); diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 4b27b938c9..6ed4625f66 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -24,6 +24,7 @@ import { PluginDatabaseManager } from '@backstage/backend-common'; import { Schema } from 'jsonschema'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { ScmIntegrations } from '@backstage/integration'; +import { SpawnOptionsWithoutStdio } from 'child_process'; import { TemplateEntityV1beta2 } from '@backstage/catalog-model'; import { UrlReader } from '@backstage/backend-common'; import { Writable } from 'stream'; @@ -304,11 +305,12 @@ export interface RouterOptions { // Warning: (ae-forgotten-export) The symbol "RunCommandOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "runCommand" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public export const runCommand: ({ command, args, logStream, + options, }: RunCommandOptions) => Promise; // @public (undocumented) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts index eff65e329f..90f060fd88 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { spawn } from 'child_process'; +import { SpawnOptionsWithoutStdio, spawn } from 'child_process'; import { PassThrough, Writable } from 'stream'; import { Logger } from 'winston'; import { Git } from '@backstage/backend-common'; @@ -22,18 +22,27 @@ import { Octokit } from '@octokit/rest'; import { assertError } from '@backstage/errors'; export type RunCommandOptions = { + /** command to run */ command: string; + /** arguments to pass the command */ args: string[]; + /** options to pass to spawn */ + options?: SpawnOptionsWithoutStdio; + /** stream to capture stdout and stderr output */ logStream?: Writable; }; +/** + * Run a command in a sub-process, normally a shell command. + */ export const runCommand = async ({ command, args, logStream = new PassThrough(), + options, }: RunCommandOptions) => { await new Promise((resolve, reject) => { - const process = spawn(command, args); + const process = spawn(command, args, options); process.stdout.on('data', stream => { logStream.write(stream); diff --git a/yarn.lock b/yarn.lock index 1628c2eea1..1e5d199349 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11978,6 +11978,11 @@ create-require@^1.1.0: resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cronstrue@^1.122.0: + version "1.122.0" + resolved "https://registry.npmjs.org/cronstrue/-/cronstrue-1.122.0.tgz#bd6838077b476d28f61d381398b47b8c3912a126" + integrity sha512-PFuhZd+iPQQ0AWTXIEYX+t3nFGzBrWxmTWUKJOrsGRewaBSLKZ4I1f8s2kryU75nNxgyugZgiGh2OJsCTA/XlA== + cross-env@^7.0.0: version "7.0.3" resolved "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" @@ -25541,7 +25546,7 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" -schema-utils@^2.6.5, schema-utils@^2.6.6, schema-utils@^2.7.0: +schema-utils@^2.6.5, schema-utils@^2.7.0: version "2.7.1" resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== @@ -26804,14 +26809,6 @@ style-inject@^0.3.0: resolved "https://registry.npmjs.org/style-inject/-/style-inject-0.3.0.tgz#d21c477affec91811cc82355832a700d22bf8dd3" integrity sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw== -style-loader@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/style-loader/-/style-loader-1.2.1.tgz#c5cbbfbf1170d076cfdd86e0109c5bba114baa1a" - integrity sha512-ByHSTQvHLkWE9Ir5+lGbVOXhxX10fbprhLvdg96wedFZb4NDekDPxVKv5Fwmio+QcMlkkNfuK+5W1peQ5CUhZg== - dependencies: - loader-utils "^2.0.0" - schema-utils "^2.6.6" - style-loader@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/style-loader/-/style-loader-1.3.0.tgz#828b4a3b3b7e7aa5847ce7bae9e874512114249e" @@ -26820,6 +26817,11 @@ style-loader@^1.3.0: loader-utils "^2.0.0" schema-utils "^2.7.0" +style-loader@^3.3.1: + version "3.3.1" + resolved "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575" + integrity sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ== + style-to-object@0.3.0, style-to-object@^0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz#b1b790d205991cc783801967214979ee19a76e46"