refactor: Refactors the Kubernetes plugin in line with ADR11 (#19446)

* refactor: k8s plugins

Signed-off-by: Matthew Clarke <mclarke@spotify.com>

* chore: move more files

Signed-off-by: Matthew Clarke <mclarke@spotify.com>

* chore: fix

Signed-off-by: Matthew Clarke <mclarke@spotify.com>

* fix: tsc

Signed-off-by: Matthew Clarke <mclarke@spotify.com>

* chore: changeset

Signed-off-by: Matthew Clarke <mclarke@spotify.com>

* chore: api report

Signed-off-by: Matthew Clarke <mclarke@spotify.com>

* fix: catalog

Signed-off-by: Matthew Clarke <mclarke@spotify.com>

* fix: types

Signed-off-by: Matthew Clarke <mclarke@spotify.com>

* fix: update lock

Signed-off-by: Matthew Clarke <mclarke@spotify.com>

---------

Signed-off-by: Matthew Clarke <mclarke@spotify.com>
This commit is contained in:
Matthew Clarke
2023-09-26 18:40:10 -04:00
committed by GitHub
parent ec13fcc4a7
commit 2d8151061c
215 changed files with 17249 additions and 992 deletions
+119
View File
@@ -5,7 +5,9 @@
```ts
import { BasicPermission } from '@backstage/plugin-permission-common';
import { Entity } from '@backstage/catalog-model';
import { FetchResponse as FetchResponse_2 } from '@backstage/plugin-kubernetes-common';
import type { JsonObject } from '@backstage/types';
import { ObjectsByEntityResponse as ObjectsByEntityResponse_2 } from '@backstage/plugin-kubernetes-common';
import { PodStatus } from '@kubernetes/client-node';
import { V1ConfigMap } from '@kubernetes/client-node';
import { V1CronJob } from '@kubernetes/client-node';
@@ -174,6 +176,67 @@ export interface DeploymentFetchResponse {
type: 'deployments';
}
// @public (undocumented)
export interface DeploymentResources {
// (undocumented)
deployments: V1Deployment[];
// (undocumented)
horizontalPodAutoscalers: V1HorizontalPodAutoscaler[];
// (undocumented)
pods: V1Pod[];
// (undocumented)
replicaSets: V1ReplicaSet[];
}
// @public
export interface DetectedError {
// (undocumented)
message: string;
// (undocumented)
occurrenceCount: number;
// (undocumented)
proposedFix?: ProposedFix;
// (undocumented)
severity: ErrorSeverity;
// (undocumented)
sourceRef: ResourceRef;
// (undocumented)
type: string;
}
// @public
export type DetectedErrorsByCluster = Map<string, DetectedError[]>;
// @public
export const detectErrors: (
objects: ObjectsByEntityResponse_2,
) => DetectedErrorsByCluster;
// @public (undocumented)
export interface DocsSolution extends ProposedFixBase {
// (undocumented)
docsLink: string;
// (undocumented)
type: 'docs';
}
// @public (undocumented)
export interface ErrorMapper<T> {
// (undocumented)
detectErrors: (resource: T) => DetectedError[];
}
// @public
export type ErrorSeverity = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
// @public (undocumented)
export interface EventsSolution extends ProposedFixBase {
// (undocumented)
podName: string;
// (undocumented)
type: 'events';
}
// @public (undocumented)
export type FetchResponse =
| PodFetchResponse
@@ -191,6 +254,29 @@ export type FetchResponse =
| DaemonSetsFetchResponse
| PodStatusFetchResponse;
// @public (undocumented)
export interface GroupedResponses extends DeploymentResources {
// (undocumented)
configMaps: V1ConfigMap[];
// (undocumented)
cronJobs: V1CronJob[];
// (undocumented)
customResources: any[];
// (undocumented)
ingresses: V1Ingress[];
// (undocumented)
jobs: V1Job[];
// (undocumented)
services: V1Service[];
// (undocumented)
statefulsets: V1StatefulSet[];
}
// @public (undocumented)
export const groupResponses: (
fetchResponse: FetchResponse_2[],
) => GroupedResponses;
// @public (undocumented)
export interface HorizontalPodAutoscalersFetchResponse {
// (undocumented)
@@ -251,6 +337,14 @@ export interface LimitRangeFetchResponse {
type: 'limitranges';
}
// @public (undocumented)
export interface LogSolution extends ProposedFixBase {
// (undocumented)
container: string;
// (undocumented)
type: 'logs';
}
// @public (undocumented)
export interface ObjectsByEntityResponse {
// (undocumented)
@@ -273,6 +367,19 @@ export interface PodStatusFetchResponse {
type: 'podstatus';
}
// @public (undocumented)
export type ProposedFix = LogSolution | DocsSolution | EventsSolution;
// @public (undocumented)
export interface ProposedFixBase {
// (undocumented)
actions: string[];
// (undocumented)
errorType: string;
// (undocumented)
rootCauseExplanation: string;
}
// @public (undocumented)
export interface RawFetchError {
// (undocumented)
@@ -289,6 +396,18 @@ export interface ReplicaSetsFetchResponse {
type: 'replicasets';
}
// @public
export interface ResourceRef {
// (undocumented)
apiGroup: string;
// (undocumented)
kind: string;
// (undocumented)
name: string;
// (undocumented)
namespace: string;
}
// @public (undocumented)
export interface ServiceFetchResponse {
// (undocumented)
+11 -2
View File
@@ -40,11 +40,20 @@
},
"dependencies": {
"@backstage/catalog-model": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/plugin-permission-common": "workspace:^",
"@kubernetes/client-node": "0.18.1"
"@backstage/types": "workspace:^",
"@kubernetes/client-node": "0.18.1",
"kubernetes-models": "^4.3.1",
"lodash": "^4.17.21",
"luxon": "^3.0.0"
},
"devDependencies": {
"@backstage/cli": "workspace:^"
"@backstage/cli": "workspace:^",
"@backstage/core-app-api": "workspace:^",
"@backstage/test-utils": "workspace:^",
"msw": "^1.3.1"
},
"jest": {
"roots": [
@@ -0,0 +1,119 @@
{
"metadata": {
"annotations": {
"deployment.kubernetes.io/revision": "3",
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\"},\"name\":\"dice-roller-canary\",\"namespace\":\"default\"},\"spec\":{\"replicas\":2,\"selector\":{\"matchLabels\":{\"app\":\"dice-roller-canary\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"dice-roller-canary\",\"backstage.io/kubernetes-id\":\"dice-roller\"}},\"spec\":{\"containers\":[{\"image\":\"nginx:1.14.2\",\"name\":\"nginx\",\"ports\":[{\"containerPort\":80}]},{\"image\":\"nginx:1.14.2\",\"name\":\"side-car\",\"ports\":[{\"containerPort\":81}]},{\"image\":\"nginx:1.14.2\",\"name\":\"other-side-car\",\"ports\":[{\"containerPort\":82}]}]}}}}\n"
},
"creationTimestamp": "2020-09-25T09:02:53.000Z",
"generation": 3,
"labels": {
"backstage.io/kubernetes-id": "dice-roller"
},
"name": "dice-roller-canary",
"namespace": "default",
"resourceVersion": "620480",
"selfLink": "/apis/apps/v1/namespaces/default/deployments/dice-roller-canary",
"uid": "0b6ae80f-999b-40e9-b116-ea925f0ed07b"
},
"spec": {
"progressDeadlineSeconds": 600,
"replicas": 2,
"revisionHistoryLimit": 10,
"selector": {
"matchLabels": {
"app": "dice-roller-canary"
}
},
"strategy": {
"rollingUpdate": {
"maxSurge": "25%",
"maxUnavailable": "25%"
},
"type": "RollingUpdate"
},
"template": {
"metadata": {
"creationTimestamp": null,
"labels": {
"app": "dice-roller-canary",
"backstage.io/kubernetes-id": "dice-roller"
}
},
"spec": {
"containers": [
{
"image": "nginx:1.14.2",
"imagePullPolicy": "IfNotPresent",
"name": "nginx",
"ports": [
{
"containerPort": 80,
"protocol": "TCP"
}
],
"resources": {},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File"
},
{
"image": "nginx:1.14.2",
"imagePullPolicy": "IfNotPresent",
"name": "side-car",
"ports": [
{
"containerPort": 81,
"protocol": "TCP"
}
],
"resources": {},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File"
},
{
"image": "nginx:1.14.2",
"imagePullPolicy": "IfNotPresent",
"name": "other-side-car",
"ports": [
{
"containerPort": 82,
"protocol": "TCP"
}
],
"resources": {},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File"
}
],
"dnsPolicy": "ClusterFirst",
"restartPolicy": "Always",
"schedulerName": "default-scheduler",
"securityContext": {},
"terminationGracePeriodSeconds": 30
}
}
},
"status": {
"conditions": [
{
"lastTransitionTime": "2020-09-25T09:02:53.000Z",
"lastUpdateTime": "2020-09-25T10:34:04.000Z",
"message": "ReplicaSet \"dice-roller-canary-7d64cd756c\" has successfully progressed.",
"reason": "NewReplicaSetAvailable",
"status": "True",
"type": "Progressing"
},
{
"lastTransitionTime": "2020-09-25T13:48:06.000Z",
"lastUpdateTime": "2020-09-25T13:48:06.000Z",
"message": "Deployment does not have minimum availability.",
"reason": "MinimumReplicasUnavailable",
"status": "False",
"type": "Available"
}
],
"observedGeneration": 3,
"replicas": 2,
"unavailableReplicas": 2,
"updatedReplicas": 2
}
}
@@ -0,0 +1,92 @@
{
"metadata": {
"annotations": {
"deployment.kubernetes.io/revision": "2",
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\"},\"name\":\"dice-roller\",\"namespace\":\"default\"},\"spec\":{\"replicas\":10,\"selector\":{\"matchLabels\":{\"app\":\"dice-roller\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"dice-roller\",\"backstage.io/kubernetes-id\":\"dice-roller\"}},\"spec\":{\"containers\":[{\"image\":\"nginx:1.14.2\",\"name\":\"nginx\",\"ports\":[{\"containerPort\":80}]}]}}}}\n"
},
"creationTimestamp": "2020-09-23T12:00:55.000Z",
"generation": 3,
"labels": {
"backstage.io/kubernetes-id": "dice-roller"
},
"name": "dice-roller",
"namespace": "default",
"resourceVersion": "593230",
"selfLink": "/apis/apps/v1/namespaces/default/deployments/dice-roller",
"uid": "7551e949-42d1-4061-83c5-9da107186e47"
},
"spec": {
"progressDeadlineSeconds": 600,
"replicas": 10,
"revisionHistoryLimit": 10,
"selector": {
"matchLabels": {
"app": "dice-roller"
}
},
"strategy": {
"rollingUpdate": {
"maxSurge": "25%",
"maxUnavailable": "25%"
},
"type": "RollingUpdate"
},
"template": {
"metadata": {
"creationTimestamp": null,
"labels": {
"app": "dice-roller",
"backstage.io/kubernetes-id": "dice-roller"
}
},
"spec": {
"containers": [
{
"image": "nginx:1.14.2",
"imagePullPolicy": "IfNotPresent",
"name": "nginx",
"ports": [
{
"containerPort": 80,
"protocol": "TCP"
}
],
"resources": {},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File"
}
],
"dnsPolicy": "ClusterFirst",
"restartPolicy": "Always",
"schedulerName": "default-scheduler",
"securityContext": {},
"terminationGracePeriodSeconds": 30
}
}
},
"status": {
"availableReplicas": 10,
"conditions": [
{
"lastTransitionTime": "2020-09-23T12:00:55.000Z",
"lastUpdateTime": "2020-09-24T11:39:28.000Z",
"message": "ReplicaSet \"dice-roller-6c8646bfd\" has successfully progressed.",
"reason": "NewReplicaSetAvailable",
"status": "True",
"type": "Progressing"
},
{
"lastTransitionTime": "2020-09-25T09:58:55.000Z",
"lastUpdateTime": "2020-09-25T09:58:55.000Z",
"message": "Deployment has minimum availability.",
"reason": "MinimumReplicasAvailable",
"status": "True",
"type": "Available"
}
],
"observedGeneration": 3,
"readyReplicas": 10,
"replicas": 10,
"updatedReplicas": 10
}
}
@@ -0,0 +1,32 @@
{
"metadata": {
"annotations": {
"autoscaling.alpha.kubernetes.io/conditions": "[{\"type\":\"AbleToScale\",\"status\":\"True\",\"lastTransitionTime\":\"2020-09-28T13:28:15Z\",\"reason\":\"SucceededGetScale\",\"message\":\"the HPA controller was able to get the target's current scale\"},{\"type\":\"ScalingActive\",\"status\":\"False\",\"lastTransitionTime\":\"2020-09-28T13:28:15Z\",\"reason\":\"FailedGetResourceMetric\",\"message\":\"the HPA was unable to compute the replica count: unable to get metrics for resource cpu: unable to fetch metrics from resource metrics API: the server could not find the requested resource (get pods.metrics.k8s.io)\"}]",
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"autoscaling/v1\",\"kind\":\"HorizontalPodAutoscaler\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\"},\"name\":\"dice-roller\",\"namespace\":\"default\"},\"spec\":{\"maxReplicas\":15,\"minReplicas\":10,\"scaleTargetRef\":{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"name\":\"dice-roller\"},\"targetCPUUtilizationPercentage\":50}}\n"
},
"creationTimestamp": "2020-09-28T13:28:00.000Z",
"labels": {
"backstage.io/kubernetes-id": "dice-roller"
},
"name": "dice-roller",
"namespace": "default",
"resourceVersion": "698957",
"selfLink": "/apis/autoscaling/v1/namespaces/default/horizontalpodautoscalers/dice-roller",
"uid": "a70c8a90-5605-4d7d-adea-05cfb8d9d446"
},
"spec": {
"maxReplicas": 15,
"minReplicas": 10,
"scaleTargetRef": {
"apiVersion": "apps/v1",
"kind": "Deployment",
"name": "dice-roller"
},
"targetCPUUtilizationPercentage": 50
},
"status": {
"currentReplicas": 13,
"desiredReplicas": 14,
"currentCPUUtilizationPercentage": 30
}
}
@@ -0,0 +1,32 @@
{
"metadata": {
"annotations": {
"autoscaling.alpha.kubernetes.io/conditions": "[{\"type\":\"AbleToScale\",\"status\":\"True\",\"lastTransitionTime\":\"2020-09-28T13:28:15Z\",\"reason\":\"SucceededGetScale\",\"message\":\"the HPA controller was able to get the target's current scale\"},{\"type\":\"ScalingActive\",\"status\":\"False\",\"lastTransitionTime\":\"2020-09-28T13:28:15Z\",\"reason\":\"FailedGetResourceMetric\",\"message\":\"the HPA was unable to compute the replica count: unable to get metrics for resource cpu: unable to fetch metrics from resource metrics API: the server could not find the requested resource (get pods.metrics.k8s.io)\"}]",
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"autoscaling/v1\",\"kind\":\"HorizontalPodAutoscaler\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\"},\"name\":\"dice-roller\",\"namespace\":\"default\"},\"spec\":{\"maxReplicas\":15,\"minReplicas\":10,\"scaleTargetRef\":{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"name\":\"dice-roller\"},\"targetCPUUtilizationPercentage\":50}}\n"
},
"creationTimestamp": "2020-09-28T13:28:00.000Z",
"labels": {
"backstage.io/kubernetes-id": "dice-roller"
},
"name": "dice-roller",
"namespace": "default",
"resourceVersion": "698957",
"selfLink": "/apis/autoscaling/v1/namespaces/default/horizontalpodautoscalers/dice-roller",
"uid": "a70c8a90-5605-4d7d-adea-05cfb8d9d446"
},
"spec": {
"maxReplicas": 10,
"minReplicas": 5,
"scaleTargetRef": {
"apiVersion": "apps/v1",
"kind": "Deployment",
"name": "dice-roller"
},
"targetCPUUtilizationPercentage": 70
},
"status": {
"currentReplicas": 10,
"desiredReplicas": 10,
"currentCPUUtilizationPercentage": 100
}
}
@@ -0,0 +1,233 @@
{
"metadata": {
"creationTimestamp": "2020-09-25T10:34:01.000Z",
"generateName": "dice-roller-canary-7d64cd756c-",
"labels": {
"app": "dice-roller-canary",
"backstage.io/kubernetes-id": "dice-roller",
"pod-template-hash": "7d64cd756c"
},
"name": "dice-roller-canary-7d64cd756c-55rfq",
"namespace": "default",
"ownerReferences": [
{
"apiVersion": "apps/v1",
"blockOwnerDeletion": true,
"controller": true,
"kind": "ReplicaSet",
"name": "dice-roller-canary-7d64cd756c",
"uid": "9208395b-a9a7-4e46-b881-6a189f7fbdb0"
}
],
"resourceVersion": "620452",
"selfLink": "/api/v1/namespaces/default/pods/dice-roller-canary-7d64cd756c-55rfq",
"uid": "65ad28e3-5d51-4b4b-9bf8-4cb069803034"
},
"spec": {
"containers": [
{
"image": "nginx:1.14.2",
"imagePullPolicy": "IfNotPresent",
"name": "nginx",
"ports": [
{
"containerPort": 80,
"protocol": "TCP"
}
],
"resources": {},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File",
"volumeMounts": [
{
"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
"name": "default-token-5gctn",
"readOnly": true
}
]
},
{
"image": "nginx:1.14.2",
"imagePullPolicy": "IfNotPresent",
"name": "side-car",
"ports": [
{
"containerPort": 81,
"protocol": "TCP"
}
],
"resources": {},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File",
"volumeMounts": [
{
"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
"name": "default-token-5gctn",
"readOnly": true
}
]
},
{
"image": "nginx:1.14.2",
"imagePullPolicy": "IfNotPresent",
"name": "other-side-car",
"ports": [
{
"containerPort": 82,
"protocol": "TCP"
}
],
"resources": {},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File",
"volumeMounts": [
{
"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
"name": "default-token-5gctn",
"readOnly": true
}
]
}
],
"dnsPolicy": "ClusterFirst",
"enableServiceLinks": true,
"nodeName": "minikube",
"priority": 0,
"restartPolicy": "Always",
"schedulerName": "default-scheduler",
"securityContext": {},
"serviceAccount": "default",
"serviceAccountName": "default",
"terminationGracePeriodSeconds": 30,
"tolerations": [
{
"effect": "NoExecute",
"key": "node.kubernetes.io/not-ready",
"operator": "Exists",
"tolerationSeconds": 300
},
{
"effect": "NoExecute",
"key": "node.kubernetes.io/unreachable",
"operator": "Exists",
"tolerationSeconds": 300
}
],
"volumes": [
{
"name": "default-token-5gctn",
"secret": {
"defaultMode": 420,
"secretName": "default-token-5gctn"
}
}
]
},
"status": {
"conditions": [
{
"lastProbeTime": null,
"lastTransitionTime": "2020-09-25T10:34:01.000Z",
"status": "True",
"type": "Initialized"
},
{
"lastProbeTime": null,
"lastTransitionTime": "2020-09-25T14:18:53.000Z",
"message": "containers with unready status: [side-car other-side-car]",
"reason": "ContainersNotReady",
"status": "False",
"type": "Ready"
},
{
"lastProbeTime": null,
"lastTransitionTime": "2020-09-25T14:18:53.000Z",
"message": "containers with unready status: [side-car other-side-car]",
"reason": "ContainersNotReady",
"status": "False",
"type": "ContainersReady"
},
{
"lastProbeTime": null,
"lastTransitionTime": "2020-09-25T10:34:01.000Z",
"status": "True",
"type": "PodScheduled"
}
],
"containerStatuses": [
{
"containerID": "docker://6ce15178d114a85f3d2e832de45c3355ab5b71ed5f4d4d225ee1c83bf07f69d9",
"image": "nginx:1.14.2",
"imageID": "docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d",
"lastState": {},
"name": "nginx",
"ready": true,
"restartCount": 0,
"started": true,
"state": {
"running": {
"startedAt": "2020-09-25T10:34:01.000Z"
}
}
},
{
"containerID": "docker://b3ce93d7f90bfe22558c61d2505b8473580574accdebb5fa4e51c0729c3511f4",
"image": "nginx:1.14.2",
"imageID": "docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d",
"lastState": {
"terminated": {
"containerID": "docker://b3ce93d7f90bfe22558c61d2505b8473580574accdebb5fa4e51c0729c3511f4",
"exitCode": 1,
"finishedAt": "2020-09-25T14:18:52.000Z",
"reason": "Error",
"startedAt": "2020-09-25T14:18:50.000Z"
}
},
"name": "other-side-car",
"ready": false,
"restartCount": 123,
"started": false,
"state": {
"waiting": {
"message": "back-off 5m0s restarting failed container=other-side-car pod=dice-roller-canary-7d64cd756c-55rfq_default(65ad28e3-5d51-4b4b-9bf8-4cb069803034)",
"reason": "CrashLoopBackOff"
}
}
},
{
"containerID": "docker://b7f0e65a2b8ab48c5f234616cfe8286aa96b55c3ef09c5cfbc4cdbe67a96f8cb",
"image": "nginx:1.14.2",
"imageID": "docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d",
"lastState": {
"terminated": {
"containerID": "docker://b7f0e65a2b8ab48c5f234616cfe8286aa96b55c3ef09c5cfbc4cdbe67a96f8cb",
"exitCode": 1,
"finishedAt": "2020-09-25T14:18:52.000Z",
"reason": "Error",
"startedAt": "2020-09-25T14:18:50.000Z"
}
},
"name": "side-car",
"ready": false,
"restartCount": 38,
"started": false,
"state": {
"waiting": {
"message": "back-off 5m0s restarting failed container=side-car pod=dice-roller-canary-7d64cd756c-55rfq_default(65ad28e3-5d51-4b4b-9bf8-4cb069803034)",
"reason": "CrashLoopBackOff"
}
}
}
],
"hostIP": "192.168.64.2",
"phase": "Running",
"podIP": "172.17.0.16",
"podIPs": [
{
"ip": "172.17.0.16"
}
],
"qosClass": "BestEffort",
"startTime": "2020-09-25T10:34:01.000Z"
}
}
@@ -0,0 +1,168 @@
{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"creationTimestamp": "2021-01-06T14:33:54Z",
"generateName": "dice-roller-bad-cm-855bf85464-",
"labels": {
"app": "dice-roller-bad-cm",
"backstage.io/kubernetes-id": "dice-roller",
"pod-template-hash": "855bf85464"
},
"name": "dice-roller-bad-cm-855bf85464-mg6xb",
"namespace": "default",
"ownerReferences": [
{
"apiVersion": "apps/v1",
"blockOwnerDeletion": true,
"controller": true,
"kind": "ReplicaSet",
"name": "dice-roller-bad-cm-855bf85464",
"uid": "9bc2a418-60eb-4dfc-9748-78cf49ea9863"
}
],
"resourceVersion": "2457755284",
"selfLink": "/api/v1/namespaces/default/pods/dice-roller-bad-cm-855bf85464-mg6xb",
"uid": "5f257d3c-a16d-4ef1-9dc9-d11e321a640a"
},
"spec": {
"containers": [
{
"env": [
{
"name": "SOME_ENV_VAR",
"valueFrom": {
"configMapKeyRef": {
"key": "some-key",
"name": "some-cm"
}
}
}
],
"image": "nginx:1.14.2",
"imagePullPolicy": "IfNotPresent",
"name": "nginx",
"ports": [
{
"containerPort": 80,
"protocol": "TCP"
}
],
"resources": {
"limits": {
"cpu": "500m",
"memory": "128Mi"
},
"requests": {
"cpu": "50m",
"memory": "64Mi"
}
},
"securityContext": {
"allowPrivilegeEscalation": false
},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File",
"volumeMounts": [
{
"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
"name": "default-token-dbkkb",
"readOnly": true
}
]
}
],
"dnsPolicy": "ClusterFirst",
"enableServiceLinks": true,
"nodeName": "node1",
"priority": 0,
"restartPolicy": "Always",
"schedulerName": "default-scheduler",
"securityContext": {},
"serviceAccount": "default",
"serviceAccountName": "default",
"terminationGracePeriodSeconds": 30,
"tolerations": [
{
"effect": "NoExecute",
"key": "node.kubernetes.io/not-ready",
"operator": "Exists",
"tolerationSeconds": 300
},
{
"effect": "NoExecute",
"key": "node.kubernetes.io/unreachable",
"operator": "Exists",
"tolerationSeconds": 300
}
],
"volumes": [
{
"name": "default-token-dbkkb",
"secret": {
"defaultMode": 420,
"secretName": "default-token-dbkkb"
}
}
]
},
"status": {
"conditions": [
{
"lastProbeTime": null,
"lastTransitionTime": "2021-01-06T14:33:54Z",
"status": "True",
"type": "Initialized"
},
{
"lastProbeTime": null,
"lastTransitionTime": "2021-01-06T14:33:54Z",
"message": "containers with unready status: [nginx]",
"reason": "ContainersNotReady",
"status": "False",
"type": "Ready"
},
{
"lastProbeTime": null,
"lastTransitionTime": "2021-01-06T14:33:54Z",
"message": "containers with unready status: [nginx]",
"reason": "ContainersNotReady",
"status": "False",
"type": "ContainersReady"
},
{
"lastProbeTime": null,
"lastTransitionTime": "2021-01-06T14:33:54Z",
"status": "True",
"type": "PodScheduled"
}
],
"containerStatuses": [
{
"image": "nginx:1.14.2",
"imageID": "",
"lastState": {},
"name": "nginx",
"ready": false,
"restartCount": 0,
"started": false,
"state": {
"waiting": {
"message": "configmap \"some-cm\" not found",
"reason": "CreateContainerConfigError"
}
}
}
],
"hostIP": "10.1.33.73",
"phase": "Pending",
"podIP": "10.1.115.15",
"podIPs": [
{
"ip": "10.1.115.15"
}
],
"qosClass": "Burstable",
"startTime": "2021-01-06T14:33:54Z"
}
}
@@ -0,0 +1,139 @@
{
"metadata": {
"creationTimestamp": "2020-09-25T09:58:50.000Z",
"generateName": "dice-roller-6c8646bfd-",
"labels": {
"app": "dice-roller",
"backstage.io/kubernetes-id": "dice-roller",
"pod-template-hash": "6c8646bfd"
},
"name": "dice-roller-6c8646bfd-2m5hv",
"namespace": "default",
"ownerReferences": [
{
"apiVersion": "apps/v1",
"blockOwnerDeletion": true,
"controller": true,
"kind": "ReplicaSet",
"name": "dice-roller-6c8646bfd",
"uid": "5126c354-4310-4e23-a9e4-c9b87cb69792"
}
],
"resourceVersion": "593216",
"selfLink": "/api/v1/namespaces/default/pods/dice-roller-6c8646bfd-2m5hv",
"uid": "aadb71c0-36fa-43e3-b38a-162f134d4359"
},
"spec": {
"containers": [
{
"image": "nginx:1.14.2",
"imagePullPolicy": "IfNotPresent",
"name": "nginx",
"ports": [
{
"containerPort": 80,
"protocol": "TCP"
}
],
"resources": {},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File",
"volumeMounts": [
{
"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
"name": "default-token-5gctn",
"readOnly": true
}
]
}
],
"dnsPolicy": "ClusterFirst",
"enableServiceLinks": true,
"nodeName": "minikube",
"priority": 0,
"restartPolicy": "Always",
"schedulerName": "default-scheduler",
"securityContext": {},
"serviceAccount": "default",
"serviceAccountName": "default",
"terminationGracePeriodSeconds": 30,
"tolerations": [
{
"effect": "NoExecute",
"key": "node.kubernetes.io/not-ready",
"operator": "Exists",
"tolerationSeconds": 300
},
{
"effect": "NoExecute",
"key": "node.kubernetes.io/unreachable",
"operator": "Exists",
"tolerationSeconds": 300
}
],
"volumes": [
{
"name": "default-token-5gctn",
"secret": {
"defaultMode": 420,
"secretName": "default-token-5gctn"
}
}
]
},
"status": {
"conditions": [
{
"lastProbeTime": null,
"lastTransitionTime": "2020-09-25T09:58:50.000Z",
"status": "True",
"type": "Initialized"
},
{
"lastProbeTime": null,
"lastTransitionTime": "2020-09-25T09:58:55.000Z",
"status": "True",
"type": "Ready"
},
{
"lastProbeTime": null,
"lastTransitionTime": "2020-09-25T09:58:55.000Z",
"status": "True",
"type": "ContainersReady"
},
{
"lastProbeTime": null,
"lastTransitionTime": "2020-09-25T09:58:50.000Z",
"status": "True",
"type": "PodScheduled"
}
],
"containerStatuses": [
{
"containerID": "docker://aa4489297c34c48bb33c18474a8d2b33854a82ed42155680b259f635f556ce70",
"image": "nginx:1.14.2",
"imageID": "docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d",
"lastState": {},
"name": "nginx",
"ready": true,
"restartCount": 0,
"started": true,
"state": {
"running": {
"startedAt": "2020-09-25T09:58:53.000Z"
}
}
}
],
"hostIP": "192.168.64.2",
"phase": "Running",
"podIP": "172.17.0.11",
"podIPs": [
{
"ip": "172.17.0.11"
}
],
"qosClass": "BestEffort",
"startTime": "2020-09-25T09:58:50.000Z"
}
}
@@ -0,0 +1,28 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DetectedError, ErrorMapper } from './types';
// Run through the each error mapper for each object
// returning a deduplicated (mostly) result
export const detectErrorsInObjects = <T>(
objects: T[],
errorMappers: ErrorMapper<T>[],
): DetectedError[] => {
return objects.flatMap(o => {
return errorMappers.flatMap(em => em.detectErrors(o));
});
};
@@ -0,0 +1,46 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DetectedError, ErrorMapper } from './types';
import { Deployment } from 'kubernetes-models/apps/v1';
import { detectErrorsInObjects } from './common';
const deploymentErrorMappers: ErrorMapper<Deployment>[] = [
{
detectErrors: deployment => {
return (deployment.status?.conditions ?? [])
.filter(c => c.status === 'False')
.filter(c => c.message !== undefined)
.map(c => ({
type: 'condition-message-present',
message: c.message ?? '',
severity: 6,
sourceRef: {
name: deployment.metadata?.name ?? 'unknown hpa',
namespace: deployment.metadata?.namespace ?? 'unknown namespace',
kind: 'Deployment',
apiGroup: 'apps/v1',
},
occurrenceCount: 1,
}));
},
},
];
export const detectErrorsInDeployments = (
deployments: Deployment[],
): DetectedError[] =>
detectErrorsInObjects(deployments, deploymentErrorMappers);
@@ -0,0 +1,494 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
V1Pod,
V1Deployment,
V1HorizontalPodAutoscaler,
} from '@kubernetes/client-node';
import { detectErrors } from './error-detection';
import * as healthyPod from './__fixtures__/pod.json';
import * as podMissingCm from './__fixtures__/pod-missing-cm.json';
import * as crashingPod from './__fixtures__/pod-crashing.json';
import * as healthyDeploy from './__fixtures__/deploy-healthy.json';
import * as failingDeploy from './__fixtures__/deploy-bad.json';
import * as healthyHpa from './__fixtures__/hpa-healthy.json';
import * as maxedOutHpa from './__fixtures__/hpa-maxed-out.json';
import {
FetchResponse,
ObjectsByEntityResponse,
} from '@backstage/plugin-kubernetes-common';
const CLUSTER_NAME = 'cluster-a';
const oneItem = (value: FetchResponse): ObjectsByEntityResponse => {
return {
items: [
{
cluster: { name: CLUSTER_NAME },
errors: [],
podMetrics: [],
resources: [value],
},
],
};
};
const onePod = (pod: V1Pod): ObjectsByEntityResponse => {
return oneItem({
type: 'pods',
resources: [pod],
});
};
const oneDeployment = (deployment: V1Deployment): ObjectsByEntityResponse => {
return oneItem({
type: 'deployments',
resources: [deployment],
});
};
const oneHpa = (hpa: V1HorizontalPodAutoscaler): ObjectsByEntityResponse => {
return oneItem({
type: 'horizontalpodautoscalers',
resources: [hpa],
});
};
describe('detectErrors', () => {
it('should return errors from different clusters', () => {
const result = detectErrors({
items: [
{
cluster: { name: 'cluster-a' },
errors: [],
podMetrics: [],
resources: [
{
type: 'pods',
resources: [crashingPod as any],
},
],
},
{
cluster: { name: 'cluster-b' },
errors: [],
podMetrics: [],
resources: [
{
type: 'horizontalpodautoscalers',
resources: [maxedOutHpa as any],
},
],
},
{
cluster: { name: 'cluster-c' },
errors: [],
podMetrics: [],
resources: [
{
type: 'deployments',
resources: [healthyDeploy as any],
},
],
},
],
});
expect(result.size).toBe(3);
const errorsFromClusterA = result.get('cluster-a');
const errorsFromClusterB = result.get('cluster-b');
const errorsFromClusterC = result.get('cluster-c');
expect(errorsFromClusterA).toBeDefined();
expect(errorsFromClusterA).toHaveLength(4);
expect(errorsFromClusterB).toBeDefined();
expect(errorsFromClusterB).toHaveLength(1);
expect(errorsFromClusterC).toBeDefined();
expect(errorsFromClusterC).toHaveLength(0);
});
it('should detect no errors in healthy pod', () => {
const result = detectErrors(onePod(healthyPod as any));
expect(result.size).toBe(1);
const errors = result.get(CLUSTER_NAME);
expect(errors).toBeDefined();
expect(errors).toHaveLength(0);
});
it('should detect errors in crashing pod', () => {
const result = detectErrors(onePod(crashingPod as any));
expect(result.size).toBe(1);
const errors = result.get(CLUSTER_NAME);
expect(errors).toBeDefined();
expect(errors).toHaveLength(4);
const [err1, err2, err3, err4] = errors ?? [];
expect(err1).toStrictEqual({
sourceRef: {
apiGroup: 'v1',
kind: 'Pod',
name: 'dice-roller-canary-7d64cd756c-55rfq',
namespace: 'default',
},
message:
'back-off 5m0s restarting failed container=other-side-car pod=dice-roller-canary-7d64cd756c-55rfq_default(65ad28e3-5d51-4b4b-9bf8-4cb069803034)',
severity: 4,
proposedFix: {
container: 'other-side-car',
errorType: 'CrashLoopBackOff',
actions: ['Check the crash logs for stacktraces'],
rootCauseExplanation:
'The container other-side-car has crashed many times, it will be exponentially restarted until it stops crashing',
type: 'logs',
},
occurrenceCount: 1,
type: 'container-waiting',
});
expect(err2).toStrictEqual({
sourceRef: {
apiGroup: 'v1',
kind: 'Pod',
name: 'dice-roller-canary-7d64cd756c-55rfq',
namespace: 'default',
},
proposedFix: {
container: 'other-side-car',
errorType: 'CrashLoopBackOff',
actions: ['Check the crash logs for stacktraces'],
rootCauseExplanation:
'The container other-side-car has crashed many times, it will be exponentially restarted until it stops crashing',
type: 'logs',
},
message:
'back-off 5m0s restarting failed container=side-car pod=dice-roller-canary-7d64cd756c-55rfq_default(65ad28e3-5d51-4b4b-9bf8-4cb069803034)',
severity: 4,
occurrenceCount: 1,
type: 'container-waiting',
});
expect(err3).toStrictEqual({
sourceRef: {
apiGroup: 'v1',
kind: 'Pod',
name: 'dice-roller-canary-7d64cd756c-55rfq',
namespace: 'default',
},
message: 'container=other-side-car restarted 123 times',
severity: 4,
occurrenceCount: 123,
proposedFix: {
container: 'other-side-car',
errorType: 'Error',
actions: ['Check the crash logs for stacktraces'],
rootCauseExplanation:
'This container has exited with a non-zero exit code (1)',
type: 'logs',
},
type: 'containers-restarting',
});
expect(err4).toStrictEqual({
sourceRef: {
apiGroup: 'v1',
kind: 'Pod',
name: 'dice-roller-canary-7d64cd756c-55rfq',
namespace: 'default',
},
proposedFix: {
container: 'other-side-car',
errorType: 'Error',
actions: ['Check the crash logs for stacktraces'],
rootCauseExplanation:
'This container has exited with a non-zero exit code (1)',
type: 'logs',
},
message: 'container=side-car restarted 38 times',
severity: 4,
occurrenceCount: 38,
type: 'containers-restarting',
});
});
it('should detect errors in pod with missing Config Map', () => {
const result = detectErrors(onePod(podMissingCm as any));
expect(result.size).toBe(1);
const errors = result.get(CLUSTER_NAME);
expect(errors).toBeDefined();
expect(errors).toHaveLength(1);
const [err1] = errors ?? [];
expect(err1).toStrictEqual({
message: 'configmap "some-cm" not found',
occurrenceCount: 1,
proposedFix: {
docsLink:
'https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/',
errorType: 'CreateContainerConfigError',
actions: [
'Ensure ConfigMaps references in the Deployment manifest are correct and the keys exist',
'Ensure Secrets references in the Deployment manifest are correct and the keys exist',
],
rootCauseExplanation:
'There is missing or mismatching configuration required to start the container',
type: 'docs',
},
severity: 4,
sourceRef: {
apiGroup: 'v1',
kind: 'Pod',
name: 'dice-roller-bad-cm-855bf85464-mg6xb',
namespace: 'default',
},
type: 'container-waiting',
});
});
it('should detect no errors in healthy deployment', () => {
const result = detectErrors(oneDeployment(healthyDeploy as any));
expect(result.size).toBe(1);
const errors = result.get(CLUSTER_NAME);
expect(errors).toBeDefined();
expect(errors).toHaveLength(0);
});
it('should detect in deployment which cannot progress', () => {
const result = detectErrors(oneDeployment(failingDeploy as any));
expect(result.size).toBe(1);
const errors = result.get(CLUSTER_NAME);
expect(errors).toBeDefined();
expect(errors).toHaveLength(1);
const [err1] = errors ?? [];
expect(err1).toStrictEqual({
sourceRef: {
apiGroup: 'apps/v1',
kind: 'Deployment',
name: 'dice-roller-canary',
namespace: 'default',
},
message: 'Deployment does not have minimum availability.',
severity: 6,
occurrenceCount: 1,
type: 'condition-message-present',
});
});
it('should detect no errors in healthy hpa', () => {
const result = detectErrors(oneHpa(healthyHpa as any));
expect(result.size).toBe(1);
const errors = result.get(CLUSTER_NAME);
expect(errors).toBeDefined();
expect(errors).toHaveLength(0);
});
it('should detect in maxed out hpa', () => {
const result = detectErrors(oneHpa(maxedOutHpa as any));
expect(result.size).toBe(1);
const errors = result.get(CLUSTER_NAME);
expect(errors).toBeDefined();
expect(errors).toHaveLength(1);
const [err1] = errors ?? [];
expect(err1).toStrictEqual({
sourceRef: {
apiGroup: 'autoscaling/v1',
kind: 'HorizontalPodAutoscaler',
name: 'dice-roller',
namespace: 'default',
},
message:
'Current number of replicas (10) is equal to the configured max number of replicas (10)',
severity: 8,
occurrenceCount: 1,
type: 'hpa-max-current-replicas',
});
});
it('pending pod is not an error', async () => {
const expiredReadiness = new Date();
expiredReadiness.setFullYear(expiredReadiness.getFullYear() - 1);
const result = await detectErrors(
onePod({
spec: {
containers: [
{
name: 'some-container',
readinessProbe: {
initialDelaySeconds: 20000,
failureThreshold: 5,
periodSeconds: 5,
},
},
],
},
status: {
containerStatuses: [
{
name: 'some-container',
image: 'some-image',
imageID: 'some-image-id',
restartCount: 0,
containerID: 'running-container',
ready: false,
state: {
running: {
startedAt: new Date().toISOString() as any,
},
},
},
],
message: 'Container running',
},
}),
);
const errors = result.get(CLUSTER_NAME);
expect(errors).toBeDefined();
expect(errors).toHaveLength(0);
});
it('no probe pod has no errors', async () => {
const expiredReadiness = new Date();
expiredReadiness.setFullYear(expiredReadiness.getFullYear() - 1);
const result = await detectErrors(
onePod({
spec: {
containers: [
{
name: 'some-container',
},
],
},
status: {
containerStatuses: [
{
name: 'some-container',
image: 'some-image',
imageID: 'some-image-id',
restartCount: 0,
containerID: 'running-container',
ready: false,
state: {
running: {
startedAt: new Date().toISOString() as any,
},
},
},
],
message: 'Container running',
},
}),
);
const errors = result.get(CLUSTER_NAME);
expect(errors).toBeDefined();
expect(errors).toHaveLength(0);
});
it('readiness probe failure results in error', async () => {
const expiredReadiness = new Date();
expiredReadiness.setFullYear(expiredReadiness.getFullYear() - 1);
const result = await detectErrors(
onePod({
spec: {
containers: [
{
name: 'some-container',
readinessProbe: {
initialDelaySeconds: 20,
failureThreshold: 5,
periodSeconds: 5,
},
},
],
},
status: {
containerStatuses: [
{
name: 'some-container',
image: 'some-image',
imageID: 'some-image-id',
restartCount: 0,
containerID: 'running-container',
ready: false,
state: {
running: {
startedAt: expiredReadiness.toISOString() as any,
},
},
},
],
message: 'Container running',
},
}),
);
const errors = result.get(CLUSTER_NAME);
expect(errors).toBeDefined();
expect(errors).toHaveLength(1);
const [err1] = errors ?? [];
expect(err1).toStrictEqual({
message:
'The container some-container failed to start properly, but is not crashing',
occurrenceCount: 1,
proposedFix: {
errorType: 'ReadinessProbeFailed',
podName: '',
actions: [
'Ensure that the container starts correctly locally',
"Check the container's logs looking for error during startup",
],
rootCauseExplanation:
'The container some-container failed to start properly, but is not crashing',
type: 'events',
},
severity: 4,
sourceRef: {
apiGroup: 'v1',
kind: 'Pod',
name: 'unknown pod',
namespace: 'unknown namespace',
},
type: 'readiness-probe-taking-too-long',
});
});
});
@@ -0,0 +1,61 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DetectedError, DetectedErrorsByCluster } from './types';
import { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-common';
import { groupResponses } from '../util';
import { detectErrorsInPods } from './pods';
import { detectErrorsInDeployments } from './deployments';
import { detectErrorsInHpa } from './hpas';
import { Deployment } from 'kubernetes-models/apps/v1';
import { HorizontalPodAutoscaler } from 'kubernetes-models/autoscaling/v1';
import { Pod } from 'kubernetes-models/v1';
/**
* For each cluster try to find errors in each of the object types provided
* returning a map of cluster names to errors in that cluster
*
* @public
*/
export const detectErrors = (
objects: ObjectsByEntityResponse,
): DetectedErrorsByCluster => {
const errors: DetectedErrorsByCluster = new Map<string, DetectedError[]>();
for (const clusterResponse of objects.items) {
let clusterErrors: DetectedError[] = [];
const groupedResponses = groupResponses(clusterResponse.resources);
clusterErrors = clusterErrors.concat(
detectErrorsInPods(groupedResponses.pods as Pod[]),
);
clusterErrors = clusterErrors.concat(
detectErrorsInDeployments(groupedResponses.deployments as Deployment[]),
);
clusterErrors = clusterErrors.concat(
detectErrorsInHpa(
groupedResponses.horizontalPodAutoscalers as HorizontalPodAutoscaler[],
),
);
errors.set(clusterResponse.cluster.name, clusterErrors);
}
return errors;
};
@@ -0,0 +1,51 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { HorizontalPodAutoscaler } from 'kubernetes-models/autoscaling/v1';
import { DetectedError, ErrorMapper } from './types';
import { detectErrorsInObjects } from './common';
const hpaErrorMappers: ErrorMapper<HorizontalPodAutoscaler>[] = [
{
detectErrors: hpa => {
if ((hpa.spec?.maxReplicas ?? -1) === hpa.status?.currentReplicas) {
return [
{
type: 'hpa-max-current-replicas',
message: `Current number of replicas (${
hpa.status?.currentReplicas
}) is equal to the configured max number of replicas (${
hpa.spec?.maxReplicas ?? -1
})`,
severity: 8,
sourceRef: {
name: hpa.metadata?.name ?? 'unknown hpa',
namespace: hpa.metadata?.namespace ?? 'unknown namespace',
kind: 'HorizontalPodAutoscaler',
apiGroup: 'autoscaling/v1',
},
occurrenceCount: 1,
},
];
}
return [];
},
},
];
export const detectErrorsInHpa = (
hpas: HorizontalPodAutoscaler[],
): DetectedError[] => detectErrorsInObjects(hpas, hpaErrorMappers);
@@ -0,0 +1,18 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './types';
export { detectErrors } from './error-detection';
@@ -0,0 +1,257 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Pod, IContainerStatus, IContainer } from 'kubernetes-models/v1';
import { DetectedError, ErrorMapper, ProposedFix } from './types';
import { detectErrorsInObjects } from './common';
import lodash from 'lodash';
import { DateTime } from 'luxon';
function isPodReadinessProbeUnready({
container,
containerStatus,
}: ContainerSpecAndStatus): boolean {
if (
containerStatus.ready ||
containerStatus.state?.running?.startedAt === undefined ||
!container.readinessProbe
) {
return false;
}
const startDateTime = DateTime.fromISO(
containerStatus.state?.running?.startedAt,
)
// Add initial delay
.plus({
seconds: container.readinessProbe?.initialDelaySeconds ?? 0,
})
// Add failure threshold
.plus({
seconds:
(container.readinessProbe?.periodSeconds ?? 0) *
(container.readinessProbe?.failureThreshold ?? 0),
});
return startDateTime < DateTime.now();
}
interface ContainerSpecAndStatus {
container: IContainer;
containerStatus: IContainerStatus;
}
const podToContainerSpecsAndStatuses = (pod: Pod): ContainerSpecAndStatus[] => {
const specs = lodash.groupBy(pod.spec?.containers ?? [], value => value.name);
const result: ContainerSpecAndStatus[] = [];
for (const cs of pod.status?.containerStatuses ?? []) {
const spec = specs[cs.name];
if (spec.length > 0) {
result.push({
container: spec[0],
containerStatus: cs,
});
}
}
return result;
};
const readinessProbeProposedFixes = (pod: Pod): ProposedFix | undefined => {
const firstUnreadyContainerStatus = pod.status?.containerStatuses?.find(
cs => {
return cs.ready === false;
},
);
return {
errorType: 'ReadinessProbeFailed',
rootCauseExplanation: `The container ${firstUnreadyContainerStatus?.name} failed to start properly, but is not crashing`,
actions: [
'Ensure that the container starts correctly locally',
"Check the container's logs looking for error during startup",
],
type: 'events',
podName: pod.metadata?.name ?? '',
};
};
const restartingPodProposedFixes = (pod: Pod): ProposedFix | undefined => {
const lastTerminatedCs = (pod.status?.containerStatuses ?? []).find(
cs => cs.lastState?.terminated !== undefined,
);
const lastTerminated = lastTerminatedCs?.lastState?.terminated;
if (!lastTerminated) {
return undefined;
}
switch (lastTerminated?.reason) {
case 'Unknown':
return {
// TODO check this one, it's more likely a cluster issue
errorType: 'Unknown',
rootCauseExplanation: `This container has exited with a non-zero exit code (${lastTerminated.exitCode})`,
actions: ['Check the crash logs for stacktraces'],
container: lastTerminatedCs.name,
type: 'logs',
};
case 'Error':
return {
errorType: 'Error',
rootCauseExplanation: `This container has exited with a non-zero exit code (${lastTerminated.exitCode})`,
actions: ['Check the crash logs for stacktraces'],
container: lastTerminatedCs.name,
type: 'logs',
};
case 'OOMKilled':
return {
errorType: 'OOMKilled',
rootCauseExplanation: `The container "${lastTerminatedCs.name}" has crashed because it has tried to use more memory that it has been allocated`,
actions: [
`Increase the amount of memory assigned to the container`,
'Ensure the application is memory bounded and is not trying to consume too much memory',
],
docsLink:
'https://kubernetes.io/docs/tasks/configure-pod-container/assign-memory-resource/#exceed-a-container-s-memory-limit',
type: 'docs',
};
default:
return undefined;
}
};
const waitingProposedFix = (pod: Pod): ProposedFix | undefined => {
const waitingCs = (pod.status?.containerStatuses ?? []).find(
cs => cs.state?.waiting !== undefined,
);
const waiting = (pod.status?.containerStatuses ?? [])
.map(cs => cs.state?.waiting)
.find(w => w?.reason !== undefined);
switch (waiting?.reason) {
case 'InvalidImageName':
return {
errorType: 'InvalidImageName',
rootCauseExplanation: 'The image in the pod is invalid',
actions: ['Ensure the image name is correct and valid image name'],
type: 'docs',
docsLink:
'https://docs.docker.com/engine/reference/commandline/tag/#extended-description',
};
case 'ImagePullBackOff':
return {
errorType: 'ImagePullBackOff',
rootCauseExplanation:
'The image either could not be found or Kubernetes does not have permission to pull it',
actions: [
'Ensure the image name is correct',
'Ensure Kubernetes has permission to pull this image',
],
type: 'docs',
docsLink:
'https://kubernetes.io/docs/concepts/containers/images/#imagepullbackoff',
};
case 'CrashLoopBackOff':
return {
errorType: 'CrashLoopBackOff',
rootCauseExplanation: `The container ${waitingCs?.name} has crashed many times, it will be exponentially restarted until it stops crashing`,
actions: ['Check the crash logs for stacktraces'],
type: 'logs',
container: waitingCs?.name ?? 'unknown',
};
case 'CreateContainerConfigError':
return {
errorType: 'CreateContainerConfigError',
rootCauseExplanation:
'There is missing or mismatching configuration required to start the container',
actions: [
'Ensure ConfigMaps references in the Deployment manifest are correct and the keys exist',
'Ensure Secrets references in the Deployment manifest are correct and the keys exist',
],
type: 'docs',
docsLink:
'https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/',
};
default:
return undefined;
}
};
const podErrorMappers: ErrorMapper<Pod>[] = [
{
detectErrors: pod => {
return podToContainerSpecsAndStatuses(pod)
.filter(isPodReadinessProbeUnready)
.map(cs => ({
type: 'readiness-probe-taking-too-long',
message: `The container ${cs.container.name} failed to start properly, but is not crashing`,
severity: 4,
proposedFix: readinessProbeProposedFixes(pod),
sourceRef: {
name: pod.metadata?.name ?? 'unknown pod',
namespace: pod.metadata?.namespace ?? 'unknown namespace',
kind: 'Pod',
apiGroup: 'v1',
},
occurrenceCount: 1,
}));
},
},
{
detectErrors: pod => {
return (pod.status?.containerStatuses ?? [])
.filter(cs => cs.state?.waiting?.message !== undefined)
.map(cs => ({
type: 'container-waiting',
message: cs.state?.waiting?.message ?? 'container waiting',
severity: 4,
proposedFix: waitingProposedFix(pod),
sourceRef: {
name: pod.metadata?.name ?? 'unknown pod',
namespace: pod.metadata?.namespace ?? 'unknown namespace',
kind: 'Pod',
apiGroup: 'v1',
},
occurrenceCount: 1,
}));
},
},
{
detectErrors: pod => {
return (pod.status?.containerStatuses ?? [])
.filter(cs => cs.restartCount > 0)
.map(cs => ({
type: 'containers-restarting',
message: `container=${cs.name} restarted ${cs.restartCount} times`,
severity: 4,
proposedFix: restartingPodProposedFixes(pod),
sourceRef: {
name: pod.metadata?.name ?? 'unknown pod',
namespace: pod.metadata?.namespace ?? 'unknown namespace',
kind: 'Pod',
apiGroup: 'v1',
},
occurrenceCount: cs.restartCount,
}));
},
},
];
export const detectErrorsInPods = (pods: Pod[]): DetectedError[] =>
detectErrorsInObjects(pods, podErrorMappers);
@@ -0,0 +1,88 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Severity of the error, where 10 is critical and 0 is very low.
*
* @public
*/
export type ErrorSeverity = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
/**
* A list of errors keyed by Cluster name
*
* @public
*/
export type DetectedErrorsByCluster = Map<string, DetectedError[]>;
/**
* A reference to a Kubernetes object
*
* @public
*/
export interface ResourceRef {
name: string;
namespace: string;
kind: string;
apiGroup: string;
}
/**
* Represents an error found on a Kubernetes object
*
* @public
*/
export interface DetectedError {
type: string;
severity: ErrorSeverity;
message: string;
proposedFix?: ProposedFix;
sourceRef: ResourceRef;
occurrenceCount: number;
}
/** @public */
export type ProposedFix = LogSolution | DocsSolution | EventsSolution;
/** @public */
export interface ProposedFixBase {
errorType: string;
rootCauseExplanation: string;
actions: string[];
}
/** @public */
export interface LogSolution extends ProposedFixBase {
type: 'logs';
container: string;
}
/** @public */
export interface DocsSolution extends ProposedFixBase {
type: 'docs';
docsLink: string;
}
/** @public */
export interface EventsSolution extends ProposedFixBase {
type: 'events';
podName: string;
}
/** @public */
export interface ErrorMapper<T> {
detectErrors: (resource: T) => DetectedError[];
}
+2
View File
@@ -26,3 +26,5 @@ export {
kubernetesProxyPermission,
kubernetesPermissions,
} from './permissions';
export * from './error-detection';
export * from './util';
+19
View File
@@ -262,3 +262,22 @@ export interface ClientPodStatus {
memory: ClientCurrentResourceUsage;
containers: ClientContainerStatus[];
}
/** @public */
export interface DeploymentResources {
pods: V1Pod[];
replicaSets: V1ReplicaSet[];
deployments: V1Deployment[];
horizontalPodAutoscalers: V1HorizontalPodAutoscaler[];
}
/** @public */
export interface GroupedResponses extends DeploymentResources {
services: V1Service[];
configMaps: V1ConfigMap[];
ingresses: V1Ingress[];
jobs: V1Job[];
cronJobs: V1CronJob[];
customResources: any[];
statefulsets: V1StatefulSet[];
}
@@ -0,0 +1,17 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './response';
@@ -0,0 +1,79 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { FetchResponse } from '@backstage/plugin-kubernetes-common';
import { GroupedResponses } from '../types';
/** @public */
export const groupResponses = (
fetchResponse: FetchResponse[],
): GroupedResponses => {
// TODO this could probably be a lodash groupBy
return fetchResponse.reduce(
(prev, next) => {
switch (next.type) {
case 'deployments':
prev.deployments.push(...next.resources);
break;
case 'pods':
prev.pods.push(...next.resources);
break;
case 'replicasets':
prev.replicaSets.push(...next.resources);
break;
case 'services':
prev.services.push(...next.resources);
break;
case 'configmaps':
prev.configMaps.push(...next.resources);
break;
case 'horizontalpodautoscalers':
prev.horizontalPodAutoscalers.push(...next.resources);
break;
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;
case 'statefulsets':
prev.statefulsets.push(...next.resources);
break;
default:
}
return prev;
},
{
pods: [],
replicaSets: [],
deployments: [],
services: [],
configMaps: [],
horizontalPodAutoscalers: [],
ingresses: [],
jobs: [],
cronJobs: [],
customResources: [],
statefulsets: [],
} as GroupedResponses,
);
};