Merge branch 'master' of https://github.com/backstage/backstage into marley/7678-pr-dashboard-backend

This commit is contained in:
Marley Powell
2021-12-01 08:01:06 +00:00
35 changed files with 1630 additions and 38 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-kubernetes-backend': patch
---
Added apiVersionOverrides config to allow for specifying api versions to use for kubernetes objects
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
build(dependencies): bump `style-loader` from 1.2.1 to 3.3.1
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Add options to spawn in runCommand helper
+5
View File
@@ -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.
+2
View File
@@ -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)
+20
View File
@@ -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
+1 -1
View File
@@ -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",
+8
View File
@@ -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)
+1 -1
View File
@@ -22,4 +22,4 @@ export {
AzurePullRequestsPage,
} from './plugin';
export * from './components/AzurePullRequestsIcon';
export { AzurePullRequestsIcon } from './components/AzurePullRequestsIcon';
+18
View File
@@ -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 };
};
}
@@ -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;
}
}
+28
View File
@@ -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()
</TestApiProvider>
),
})
.addPage({
path: '/fixture-3',
title: 'Fixture 3',
element: (
<TestApiProvider
apis={[[kubernetesApiRef, new MockKubernetesClient(fixture3)]]}
>
<EntityProvider entity={mockEntity}>
<EntityKubernetesContent />
</EntityProvider>
</TestApiProvider>
),
})
.addPage({
path: '/fixture-4',
title: 'Fixture 4',
element: (
<TestApiProvider
apis={[[kubernetesApiRef, new MockKubernetesClient(fixture4)]]}
>
<EntityProvider entity={mockEntity}>
<EntityKubernetesContent />
</EntityProvider>
</TestApiProvider>
),
})
.registerPlugin(kubernetesPlugin)
.render();
+1
View File
@@ -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",
@@ -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"
}
}
]
}
@@ -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"
}
}
]
}
@@ -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) => {
<Grid item>
<ServicesAccordions />
</Grid>
<Grid item>
<CronJobsAccordions />
</Grid>
</Grid>
</AccordionDetails>
</Accordion>
@@ -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(<CronJobsAccordions />)),
);
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(<CronJobsAccordions />)),
);
expect(getByText('dice-roller-cronjob')).toBeInTheDocument();
expect(getByText('CronJob')).toBeInTheDocument();
expect(getByText('namespace: default')).toBeInTheDocument();
expect(getByText('Suspended')).toBeInTheDocument();
});
});
@@ -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 (
<Grid
container
direction="row"
justifyContent="flex-start"
alignItems="center"
>
<Grid xs={3} item>
<CronJobDrawer cronJob={cronJob} />
</Grid>
<Grid item xs={1}>
<Divider style={{ height: '5em' }} orientation="vertical" />
</Grid>
<Grid
item
container
xs={5}
direction="column"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item>
{cronJob.spec?.suspend ? (
<StatusError>Suspended</StatusError>
) : (
<StatusOK>Active</StatusOK>
)}
</Grid>
<Grid item>
<Typography variant="body1">
Schedule:{' '}
{cronJob.spec?.schedule
? `${cronJob.spec.schedule} (${cronstrue.toString(
cronJob.spec.schedule,
)})`
: 'N/A'}
</Typography>
</Grid>
</Grid>
</Grid>
);
};
const CronJobAccordion = ({ cronJob, ownedJobs }: CronJobAccordionProps) => {
return (
<Accordion TransitionProps={{ unmountOnExit: true }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<CronJobSummary cronJob={cronJob} />
</AccordionSummary>
<AccordionDetails>
<JobsAccordions jobs={ownedJobs.reverse()} />
</AccordionDetails>
</Accordion>
);
};
export const CronJobsAccordions = ({}: CronJobsAccordionsProps) => {
const groupedResponses = useContext(GroupedResponsesContext);
return (
<Grid
container
direction="column"
justifyContent="flex-start"
alignItems="flex-start"
>
{groupedResponses.cronJobs.map((cronJob, i) => (
<Grid container item key={i} xs>
<Grid item xs>
<CronJobAccordion
ownedJobs={getOwnedResources(cronJob, groupedResponses.jobs)}
cronJob={cronJob}
/>
</Grid>
</Grid>
))}
</Grid>
);
};
@@ -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(
<CronJobDrawer
cronJob={(oneCronJobsFixture as any).cronJobs[0]}
expanded
/>,
);
expect(getAllByText('dice-roller-cronjob')).toHaveLength(2);
expect(getAllByText('CronJob')).toHaveLength(2);
expect(getByText('YAML')).toBeInTheDocument();
expect(getByText('Schedule')).toBeInTheDocument();
expect(getByText('30 5 * * *')).toBeInTheDocument();
expect(getByText('Starting Deadline Seconds')).toBeInTheDocument();
expect(getByText('Last Schedule Time')).toBeInTheDocument();
});
});
@@ -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 (
<KubernetesDrawer
object={cronJob}
expanded={expanded}
kind="CronJob"
renderObject={(cronJobObj: V1CronJob) => ({
schedule: cronJobObj.spec?.schedule ?? '???',
startingDeadlineSeconds:
cronJobObj.spec?.startingDeadlineSeconds ?? '???',
concurrencyPolicy: cronJobObj.spec?.concurrencyPolicy ?? '???',
lastScheduleTime: cronJobObj.status?.lastScheduleTime ?? '???',
})}
>
<Grid
container
direction="column"
justifyContent="flex-start"
alignItems="flex-start"
spacing={0}
>
<Grid item>
<Typography variant="h5">
{cronJob.metadata?.name ?? 'unknown object'}
</Typography>
</Grid>
<Grid item>
<Typography color="textSecondary" variant="body1">
CronJob
</Typography>
</Grid>
{namespace && (
<Grid item>
<Chip size="small" label={`namespace: ${namespace}`} />
</Grid>
)}
</Grid>
</KubernetesDrawer>
);
};
@@ -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';
@@ -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<V1Pod>[] = [
{
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 = ({
/>
</AccordionSummary>
<AccordionDetails>
<PodsTable pods={ownedPods} />
<PodsTable pods={ownedPods} extraColumns={deploymentPodColumns} />
</AccordionDetails>
</Accordion>
);
@@ -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(<JobsAccordions jobs={jobs} />)),
);
expect(getByText('dice-roller-cronjob-1637028600')).toBeInTheDocument();
expect(getByText('Running')).toBeInTheDocument();
expect(getByText('dice-roller-cronjob-1637025000')).toBeInTheDocument();
expect(getByText('Succeeded')).toBeInTheDocument();
});
});
@@ -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 (
<Grid
container
direction="row"
justifyContent="flex-start"
alignItems="center"
>
<Grid xs={3} item>
<JobDrawer job={job} />
</Grid>
<Grid item xs={1}>
<Divider style={{ height: '5em' }} orientation="vertical" />
</Grid>
<Grid
item
container
xs={8}
direction="column"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item>
{job.status?.succeeded && <StatusOK>Succeeded</StatusOK>}
{job.status?.active && <StatusPending>Running</StatusPending>}
{job.status?.failed && <StatusError>Failed</StatusError>}
</Grid>
<Grid item>Start time: {job.status?.startTime?.toString()}</Grid>
{job.status?.completionTime && (
<Grid item>
Completion time: {job.status.completionTime.toString()}
</Grid>
)}
</Grid>
</Grid>
);
};
const JobAccordion = ({ job, ownedPods }: JobAccordionProps) => {
return (
<Accordion TransitionProps={{ unmountOnExit: true }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<JobSummary job={job} />
</AccordionSummary>
<AccordionDetails>
<PodsTable pods={ownedPods} />
</AccordionDetails>
</Accordion>
);
};
export const JobsAccordions = ({ jobs }: JobsAccordionsProps) => {
const groupedResponses = useContext(GroupedResponsesContext);
return (
<Grid
container
direction="column"
justifyContent="flex-start"
alignItems="flex-start"
>
{jobs.map((job, i) => (
<Grid container item key={i} xs>
<Grid item xs>
<JobAccordion
ownedPods={getOwnedResources(job, groupedResponses.pods)}
job={job}
/>
</Grid>
</Grid>
))}
</Grid>
);
};
@@ -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(
<JobDrawer job={(oneCronJobsFixture as any).jobs[0]} expanded />,
);
expect(getAllByText('dice-roller-cronjob-1637025000')).toHaveLength(2);
expect(getAllByText('Job')).toHaveLength(2);
expect(getByText('YAML')).toBeInTheDocument();
expect(getByText('Parallelism')).toBeInTheDocument();
expect(getByText('Completions')).toBeInTheDocument();
expect(getByText('Backoff Limit')).toBeInTheDocument();
expect(getByText('Start Time')).toBeInTheDocument();
});
});
@@ -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 (
<KubernetesDrawer
object={job}
expanded={expanded}
kind="Job"
renderObject={(jobObj: V1Job) => {
return {
parallelism: jobObj.spec?.parallelism ?? '???',
completions: jobObj.spec?.completions ?? '???',
backoffLimit: jobObj.spec?.backoffLimit ?? '???',
startTime: jobObj.status?.startTime ?? '???',
};
}}
>
<Grid
container
direction="column"
justifyContent="flex-start"
alignItems="flex-start"
spacing={0}
>
<Grid item>
<Typography variant="h6">
{job.metadata?.name ?? 'unknown object'}
</Typography>
</Grid>
<Grid item>
<Typography color="textSecondary" variant="body1">
Job
</Typography>
</Grid>
</Grid>
</KubernetesDrawer>
);
};
@@ -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';
+40 -2
View File
@@ -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<V1Pod>[] = [
{
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(<PodsTable pods={[pod as any]} />),
);
// 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(
<PodsTable pods={[pod as any]} extraColumns={extraColumns} />,
),
);
// 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(<PodsTable pods={[crashingPod as any]} />),
wrapInTestApp(
<PodsTable pods={[crashingPod as any]} extraColumns={extraColumns} />,
),
);
// titles
+6 -20
View File
@@ -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<V1Pod>[] = [
const DEFAULT_COLUMNS: TableColumn<V1Pod>[] = [
{
title: 'name',
highlight: true,
@@ -34,29 +30,19 @@ const columns: TableColumn<V1Pod>[] = [
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<V1Pod>[];
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) => {
<Table
options={{ paging: true, search: false }}
data={pods}
columns={columns}
columns={DEFAULT_COLUMNS.concat(extraColumns)}
/>
</div>
);
@@ -24,5 +24,7 @@ export const GroupedResponsesContext = React.createContext<GroupedResponses>({
configMaps: [],
horizontalPodAutoscalers: [],
ingresses: [],
jobs: [],
cronJobs: [],
customResources: [],
});
+4
View File
@@ -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[];
}
+8
View File
@@ -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,
);
+3 -1
View File
@@ -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<void>;
// @public (undocumented)
@@ -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<void>((resolve, reject) => {
const process = spawn(command, args);
const process = spawn(command, args, options);
process.stdout.on('data', stream => {
logStream.write(stream);
+11 -9
View File
@@ -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"