diff --git a/examples/preflight/node-resources.yaml b/examples/preflight/node-resources.yaml new file mode 100644 index 00000000..50d7e2cd --- /dev/null +++ b/examples/preflight/node-resources.yaml @@ -0,0 +1,38 @@ +apiVersion: troubleshoot.replicated.com/v1beta1 +kind: Preflight +metadata: + name: sample +spec: + analyzers: + - nodeResources: + checkName: Must have at least 3 nodes in the cluster + outcomes: + - fail: + when: "< 3" + message: This application requires at least 3 nodes + - warn: + when: "< 5" + message: This application recommends at last 5 nodes. + - pass: + message: This cluster has enough nodes. + - nodeResources: + checkName: Must have 3 nodes with at least 6 cores + filters: + cpuCapacity: "6" + outcomes: + - fail: + when: "< 3" + message: This application requires at least 3 nodes with 6 cores each + - pass: + message: This cluster has enough nodes with enough codes + - nodeResources: + checkName: Must have 1 node with 16 GB (available) memory and 5 cores (on a single node) + filters: + allocatableMemory: 16Gi + cpuCapacity: "5" + outcomes: + - fail: + when: "< 1" + message: This application requires at least 1 node with 16GB available memory + - pass: + message: This cluster has a node with enough memory. diff --git a/pkg/analyze/data_test.go b/pkg/analyze/data_test.go index c278647c..b847c3c6 100644 --- a/pkg/analyze/data_test.go +++ b/pkg/analyze/data_test.go @@ -1,618 +1,5 @@ package analyzer -var collectedNodes = ` -[ - { - "metadata": { - "name": "ip-192-168-28-59.us-east-2.compute.internal", - "selfLink": "/api/v1/nodes/ip-192-168-28-59.us-east-2.compute.internal", - "uid": "6d679c35-1dfe-11ea-89c7-0ab299bbd38c", - "resourceVersion": "7417849", - "creationTimestamp": "2019-12-13T23:15:18Z", - "labels": { - "alpha.eksctl.io/cluster-name": "schemahero-demo", - "alpha.eksctl.io/instance-id": "i-026467ed98dc19788", - "alpha.eksctl.io/nodegroup-name": "ng-f2f5f9a5", - "beta.kubernetes.io/arch": "amd64", - "beta.kubernetes.io/instance-type": "m5.large", - "beta.kubernetes.io/os": "linux", - "failure-domain.beta.kubernetes.io/region": "us-east-2", - "failure-domain.beta.kubernetes.io/zone": "us-east-2a", - "kubernetes.io/arch": "amd64", - "kubernetes.io/hostname": "ip-192-168-28-59.us-east-2.compute.internal", - "kubernetes.io/os": "linux" - }, - "annotations": { - "node.alpha.kubernetes.io/ttl": "0", - "volumes.kubernetes.io/controller-managed-attach-detach": "true" - } - }, - "spec": { - "providerID": "aws:///us-east-2a/i-026467ed98dc19788" - }, - "status": { - "capacity": { - "attachable-volumes-aws-ebs": "25", - "cpu": "2", - "ephemeral-storage": "20959212Ki", - "hugepages-1Gi": "0", - "hugepages-2Mi": "0", - "memory": "7951376Ki", - "pods": "29" - }, - "allocatable": { - "attachable-volumes-aws-ebs": "25", - "cpu": "2", - "ephemeral-storage": "19316009748", - "hugepages-1Gi": "0", - "hugepages-2Mi": "0", - "memory": "7848976Ki", - "pods": "29" - }, - "conditions": [ - { - "type": "MemoryPressure", - "status": "False", - "lastHeartbeatTime": "2020-01-29T18:50:15Z", - "lastTransitionTime": "2020-01-22T02:01:14Z", - "reason": "KubeletHasSufficientMemory", - "message": "kubelet has sufficient memory available" - }, - { - "type": "DiskPressure", - "status": "False", - "lastHeartbeatTime": "2020-01-29T18:50:15Z", - "lastTransitionTime": "2020-01-24T01:31:53Z", - "reason": "KubeletHasNoDiskPressure", - "message": "kubelet has no disk pressure" - }, - { - "type": "PIDPressure", - "status": "False", - "lastHeartbeatTime": "2020-01-29T18:50:15Z", - "lastTransitionTime": "2020-01-22T02:01:14Z", - "reason": "KubeletHasSufficientPID", - "message": "kubelet has sufficient PID available" - }, - { - "type": "Ready", - "status": "True", - "lastHeartbeatTime": "2020-01-29T18:50:15Z", - "lastTransitionTime": "2020-01-22T02:01:14Z", - "reason": "KubeletReady", - "message": "kubelet is posting ready status" - } - ], - "addresses": [ - { - "type": "InternalIP", - "address": "***HIDDEN***" - }, - { - "type": "ExternalIP", - "address": "***HIDDEN***" - }, - { - "type": "Hostname", - "address": "ip-192-168-28-59.us-east-2.compute.internal" - }, - { - "type": "InternalDNS", - "address": "ip-192-168-28-59.us-east-2.compute.internal" - }, - { - "type": "ExternalDNS", - "address": "ec2-3-133-126-65.us-east-2.compute.amazonaws.com" - } - ], - "daemonEndpoints": { - "kubeletEndpoint": { - "Port": 10250 - } - }, - "nodeInfo": { - "machineID": "ec2d1877f782e9caa8d0f7cb5c6154b8", - "systemUUID": "EC2D1877-F782-E9CA-A8D0-F7CB5C6154B8", - "bootID": "8e91eddd-e115-4efe-a4e1-a32affdbab61", - "kernelVersion": "4.14.146-119.123.amzn2.x86_64", - "osImage": "Amazon Linux 2", - "containerRuntimeVersion": "docker://18.6.1", - "kubeletVersion": "v1.14.7-eks-1861c5", - "kubeProxyVersion": "v1.14.7-eks-1861c5", - "operatingSystem": "linux", - "architecture": "amd64" - }, - "images": [ - { - "names": [ - "kotsadm/kotsadm-api@sha256:257efb64c42c4e83f51618bc94b2898687292b7a1763c8c1165a0b8fb52b2c47", - "kotsadm/kotsadm-api:v1.11.4" - ], - "sizeBytes": 1025604685 - }, - { - "names": [ - "kotsadm/kotsadm-api@sha256:bbdaf7b3abf9864953e3a25fb5d58746ee8b3056d4dbf1c9c477c4c08d7b3e6f", - "kotsadm/kotsadm-api:v1.11.1" - ], - "sizeBytes": 1025603901 - }, - { - "names": [ - "kotsadm/kotsadm-api@sha256:0781a0d3ab73147db616a5359e7385dad5c1f942eb4dbf0032d965fc56342600" - ], - "sizeBytes": 1025603901 - }, - { - "names": [ - "kotsadm/kotsadm-api@sha256:29084f5f9896baaf947caf96b56e05af1d28a662dd437f222063df7d835f90e4" - ], - "sizeBytes": 1025603901 - }, - { - "names": [ - "kotsadm/kotsadm-api@sha256:000572c198f73af001713b10bd4869710c99313dde81d5589445a069271c0338" - ], - "sizeBytes": 1025603901 - }, - { - "names": [ - "sentry@sha256:5a9fb82278c8ee4deb0fc9cb98dfcb6e1e0e184f7267a6a2c9074e0c687a0cd2", - "sentry:9.1.2" - ], - "sizeBytes": 868746022 - }, - { - "names": [ - "602401143452.dkr.ecr.us-east-2.amazonaws.com/amazon-k8s-cni@sha256:c071dfc45cd957fc6ab2db769ae6374b1f59a08db90b0ff0b9166b8531497a35", - "602401143452.dkr.ecr.us-east-2.amazonaws.com/amazon-k8s-cni:v1.5.3" - ], - "sizeBytes": 290731139 - }, - { - "names": [ - "bitnami/postgresql@sha256:7b8f251a3ffdc3a5392b6b7bd1ac863d34f7cb1e9cc0ec3b2f92a45f9570eae5", - "bitnami/postgresql:11.5.0-debian-9-r60" - ], - "sizeBytes": 165095931 - }, - { - "names": [ - "kotsadm/kotsadm-migrations@sha256:1c19f3d507876e62889c0f592b20e15324effc579f2cd0591039fa0cdbac633d", - "kotsadm/kotsadm-migrations:v1.11.1" - ], - "sizeBytes": 156079510 - }, - { - "names": [ - "kotsadm/kotsadm-migrations@sha256:5ae3fd834b72a37d92c801cc5b281b2339c17865be0c5298e4fdff62a9c4dde4" - ], - "sizeBytes": 156079510 - }, - { - "names": [ - "kotsadm/kotsadm-migrations@sha256:c1a7dce8fc27c2fedbf567370d24e0a3759c1840fa16a46a8569d6c4a3e09152", - "kotsadm/kotsadm-migrations:v1.11.4" - ], - "sizeBytes": 156079510 - }, - { - "names": [ - "bitnami/redis@sha256:505188ab03eae7d63902fed9e2ab1bcfc2bf98a0244ba69f488cc6018eb6f330", - "bitnami/redis:5.0.5-debian-9-r141" - ], - "sizeBytes": 96707700 - }, - { - "names": [ - "602401143452.dkr.ecr.us-east-2.amazonaws.com/eks/kube-proxy@sha256:d3a6122f63202665aa50f3c08644ef504dbe56c76a1e0ab05f8e296328f3a6b4", - "602401143452.dkr.ecr.us-east-2.amazonaws.com/eks/kube-proxy:v1.14.6" - ], - "sizeBytes": 82044796 - }, - { - "names": [ - "bitnami/minideb@sha256:7f79535202f3610cf637b4ce9d92d7e28600ce9d7e05284f7c861c6ef35dcd1f", - "bitnami/minideb:stretch" - ], - "sizeBytes": 53743451 - }, - { - "names": [ - "ttl.sh/sdfsdfsdf/minideb@sha256:b02b0c29f37f90a013e0a7a38f47667a219a5785b55aba6af0bbb54c5ad691b8", - "ttl.sh/sdfsdfsdf/minideb:stretch" - ], - "sizeBytes": 53743418 - }, - { - "names": [ - "kotsadm/minio@sha256:a68fb7b34d58c8167d11a93ebe887ab44ccb9447593e9ce7c36ac940c78221d4", - "kotsadm/minio:v1.11.1" - ], - "sizeBytes": 51885319 - }, - { - "names": [ - "602401143452.dkr.ecr.us-east-2.amazonaws.com/eks/coredns@sha256:c85954b828a5627b9f3c4540893ab9d8a4be5f8da7513882ad122e08f5c2e60a", - "602401143452.dkr.ecr.us-east-2.amazonaws.com/eks/coredns:v1.3.1" - ], - "sizeBytes": 35174083 - }, - { - "names": [ - "602401143452.dkr.ecr.us-east-2.amazonaws.com/eks/pause-amd64@sha256:bea77c323c47f7b573355516acf927691182d1333333d1f41b7544012fab7adf", - "602401143452.dkr.ecr.us-east-2.amazonaws.com/eks/pause-amd64:3.1" - ], - "sizeBytes": 742472 - } - ] - } - }, - { - "metadata": { - "name": "ip-192-168-71-129.us-east-2.compute.internal", - "selfLink": "/api/v1/nodes/ip-192-168-71-129.us-east-2.compute.internal", - "uid": "6c8f3260-1dfe-11ea-89c7-0ab299bbd38c", - "resourceVersion": "7417782", - "creationTimestamp": "2019-12-13T23:15:16Z", - "labels": { - "alpha.eksctl.io/cluster-name": "schemahero-demo", - "alpha.eksctl.io/instance-id": "i-0b7ad3f63b3a123b8", - "alpha.eksctl.io/nodegroup-name": "ng-f2f5f9a5", - "beta.kubernetes.io/arch": "amd64", - "beta.kubernetes.io/instance-type": "m5.large", - "beta.kubernetes.io/os": "linux", - "failure-domain.beta.kubernetes.io/region": "us-east-2", - "failure-domain.beta.kubernetes.io/zone": "us-east-2c", - "kubernetes.io/arch": "amd64", - "kubernetes.io/hostname": "ip-192-168-71-129.us-east-2.compute.internal", - "kubernetes.io/os": "linux" - }, - "annotations": { - "node.alpha.kubernetes.io/ttl": "0", - "volumes.kubernetes.io/controller-managed-attach-detach": "true" - } - }, - "spec": { - "providerID": "aws:///us-east-2c/i-0b7ad3f63b3a123b8" - }, - "status": { - "capacity": { - "attachable-volumes-aws-ebs": "25", - "cpu": "2", - "ephemeral-storage": "20959212Ki", - "hugepages-1Gi": "0", - "hugepages-2Mi": "0", - "memory": "7865360Ki", - "pods": "29" - }, - "allocatable": { - "attachable-volumes-aws-ebs": "25", - "cpu": "2", - "ephemeral-storage": "19316009748", - "hugepages-1Gi": "0", - "hugepages-2Mi": "0", - "memory": "7762960Ki", - "pods": "29" - }, - "conditions": [ - { - "type": "MemoryPressure", - "status": "False", - "lastHeartbeatTime": "2020-01-29T18:49:36Z", - "lastTransitionTime": "2019-12-13T23:15:16Z", - "reason": "KubeletHasSufficientMemory", - "message": "kubelet has sufficient memory available" - }, - { - "type": "DiskPressure", - "status": "False", - "lastHeartbeatTime": "2020-01-29T18:49:36Z", - "lastTransitionTime": "2020-01-22T14:30:11Z", - "reason": "KubeletHasNoDiskPressure", - "message": "kubelet has no disk pressure" - }, - { - "type": "PIDPressure", - "status": "False", - "lastHeartbeatTime": "2020-01-29T18:49:36Z", - "lastTransitionTime": "2019-12-13T23:15:16Z", - "reason": "KubeletHasSufficientPID", - "message": "kubelet has sufficient PID available" - }, - { - "type": "Ready", - "status": "True", - "lastHeartbeatTime": "2020-01-29T18:49:36Z", - "lastTransitionTime": "2019-12-13T23:16:06Z", - "reason": "KubeletReady", - "message": "kubelet is posting ready status" - } - ], - "addresses": [ - { - "type": "InternalIP", - "address": "***HIDDEN***" - }, - { - "type": "ExternalIP", - "address": "***HIDDEN***" - }, - { - "type": "Hostname", - "address": "ip-192-168-71-129.us-east-2.compute.internal" - }, - { - "type": "InternalDNS", - "address": "ip-192-168-71-129.us-east-2.compute.internal" - }, - { - "type": "ExternalDNS", - "address": "ec2-3-18-214-18.us-east-2.compute.amazonaws.com" - } - ], - "daemonEndpoints": { - "kubeletEndpoint": { - "Port": 10250 - } - }, - "nodeInfo": { - "machineID": "ec2502eb42ac572c0fc598fd2854029d", - "systemUUID": "EC2502EB-42AC-572C-0FC5-98FD2854029D", - "bootID": "d6ce6c46-98af-44c0-8f0a-7c6f0affba35", - "kernelVersion": "4.14.146-119.123.amzn2.x86_64", - "osImage": "Amazon Linux 2", - "containerRuntimeVersion": "docker://18.6.1", - "kubeletVersion": "v1.14.7-eks-1861c5", - "kubeProxyVersion": "v1.14.7-eks-1861c5", - "operatingSystem": "linux", - "architecture": "amd64" - }, - "images": [ - { - "names": [ - "kotsadm/kotsadm-api@sha256:9bc79559156f04a1e086b865db962c7e3ca32575f654c0f09d5fdc4acf118d8a", - "kotsadm/kotsadm-api:alpha" - ], - "sizeBytes": 1025599949 - }, - { - "names": [ - "codescope/mjml-tcpserver@sha256:5a3f0c82a483f10255a06be5c74a34686f844b37b818a8b07c137f9c1bb1e8d7", - "codescope/mjml-tcpserver:0.8.0" - ], - "sizeBytes": 920297781 - }, - { - "names": [ - "sentry@sha256:5a9fb82278c8ee4deb0fc9cb98dfcb6e1e0e184f7267a6a2c9074e0c687a0cd2", - "sentry:9.1.2" - ], - "sizeBytes": 868746022 - }, - { - "names": [ - "kotsadm/kotsadm-operator@sha256:849ba88648a4d85e8eff5c845477af593b139d0a695a1ad5c44ba0a9eec80b54" - ], - "sizeBytes": 478334600 - }, - { - "names": [ - "kotsadm/kotsadm-operator@sha256:e8ebbbad7cdc44f9ddf8237ec38e75a9211d83e2ebec7dffd9c8c5f40f888cd3", - "kotsadm/kotsadm-operator:v1.11.1" - ], - "sizeBytes": 478334600 - }, - { - "names": [ - "kotsadm/kotsadm-operator@sha256:207b23336e6ac227c5bba39990107e42eaffe9f237e94e49abf9520e70826aa8", - "kotsadm/kotsadm-operator:v1.11.0" - ], - "sizeBytes": 478334600 - }, - { - "names": [ - "kotsadm/kotsadm-operator@sha256:9790dd5bc5450520db67b272b4bd38da595ee4d99af17307ae01ecf05b2844db" - ], - "sizeBytes": 478334600 - }, - { - "names": [ - "kotsadm/kotsadm-operator@sha256:61fdc4c1b80106717ea364de6a1dff31f0634215d5408b4bca86e1cfa84f37eb" - ], - "sizeBytes": 478334600 - }, - { - "names": [ - "kotsadm/kotsadm-operator@sha256:66cefcfd42ebab1ac441a9bf0ff755584d82631da853e40f84e5059ec928a985", - "kotsadm/kotsadm-operator:v1.11.4" - ], - "sizeBytes": 478334600 - }, - { - "names": [ - "kotsadm/kotsadm-operator@sha256:a19bf2afcbc318c169db4dbd6c6f8cdca02e6b0ee9922555441025f67c9e21f4", - "kotsadm/kotsadm-operator:v1.10.3" - ], - "sizeBytes": 478317692 - }, - { - "names": [ - "codescope/core@sha256:c8914b21b47d8394969e71054f1b964466fc1fe69cd70a778c142b947d8d08bf", - "codescope/core:1.5.0" - ], - "sizeBytes": 405019853 - }, - { - "names": [ - "kotsadm/kotsadm@sha256:6363777cbc9e57939ee33032dcfdd4619cee1b73428d031c8966948ec8172499", - "kotsadm/kotsadm:v1.11.4" - ], - "sizeBytes": 300000312 - }, - { - "names": [ - "kotsadm/kotsadm@sha256:dcff0ff224cb18e19026928a5d7a27ccfa9950032900b0c2a5fa5de8d6456ef2", - "kotsadm/kotsadm:v1.11.1" - ], - "sizeBytes": 299996710 - }, - { - "names": [ - "kotsadm/kotsadm@sha256:3fdeedc495df96c5831a3a198190c1b5b2708f5a438fea940f4798085e0a70c1" - ], - "sizeBytes": 299996710 - }, - { - "names": [ - "kotsadm/kotsadm@sha256:271ba33be8a1d0d51fe387e7df8709809fcaa00a5501e7f107253afb5628999a" - ], - "sizeBytes": 299996560 - }, - { - "names": [ - "602401143452.dkr.ecr.us-east-2.amazonaws.com/amazon-k8s-cni@sha256:c071dfc45cd957fc6ab2db769ae6374b1f59a08db90b0ff0b9166b8531497a35", - "602401143452.dkr.ecr.us-east-2.amazonaws.com/amazon-k8s-cni:v1.5.3" - ], - "sizeBytes": 290731139 - }, - { - "names": [ - "kotsadm/kotsadm@sha256:08de237443b718d8b0ee260701dddc4b8c9b67fee5b5929b051a20312bf9aa39" - ], - "sizeBytes": 255742861 - }, - { - "names": [ - "postgres@sha256:cc8fb6b149b387fed332b5bebd144f810df544e2df514383f82f6e61698b2aea", - "postgres:10.7" - ], - "sizeBytes": 229651900 - }, - { - "names": [ - "bitnami/postgresql@sha256:7b8f251a3ffdc3a5392b6b7bd1ac863d34f7cb1e9cc0ec3b2f92a45f9570eae5", - "bitnami/postgresql:11.5.0-debian-9-r60" - ], - "sizeBytes": 165095931 - }, - { - "names": [ - "kotsadm/kotsadm-migrations@sha256:9ebee83999219df4226d7f85b1da71420c3ebd3011cb79012a15c0fb805b9b3e" - ], - "sizeBytes": 156079510 - }, - { - "names": [ - "kotsadm/kotsadm-migrations@sha256:1c19f3d507876e62889c0f592b20e15324effc579f2cd0591039fa0cdbac633d" - ], - "sizeBytes": 156079510 - }, - { - "names": [ - "kotsadm/kotsadm-migrations@sha256:5ae3fd834b72a37d92c801cc5b281b2339c17865be0c5298e4fdff62a9c4dde4", - "kotsadm/kotsadm-migrations:v1.11.1" - ], - "sizeBytes": 156079510 - }, - { - "names": [ - "kotsadm/kotsadm-migrations@sha256:1f467665d4e6714b19d8b82a9c859e958537c91452588c67a09db6f66b751af3", - "kotsadm/kotsadm-migrations:alpha" - ], - "sizeBytes": 156079510 - }, - { - "names": [ - "bitnami/redis@sha256:505188ab03eae7d63902fed9e2ab1bcfc2bf98a0244ba69f488cc6018eb6f330", - "bitnami/redis:5.0.5-debian-9-r141" - ], - "sizeBytes": 96707700 - }, - { - "names": [ - "602401143452.dkr.ecr.us-east-2.amazonaws.com/eks/kube-proxy@sha256:d3a6122f63202665aa50f3c08644ef504dbe56c76a1e0ab05f8e296328f3a6b4", - "602401143452.dkr.ecr.us-east-2.amazonaws.com/eks/kube-proxy:v1.14.6" - ], - "sizeBytes": 82044796 - }, - { - "names": [ - "bitnami/minideb@sha256:7f79535202f3610cf637b4ce9d92d7e28600ce9d7e05284f7c861c6ef35dcd1f", - "bitnami/minideb:stretch" - ], - "sizeBytes": 53743451 - }, - { - "names": [ - "kotsadm/minio@sha256:3b1aadcd350f2c5b003b1e736bc89b23a636c2cf4eb3bbc7e459452a504e18ef", - "kotsadm/minio:alpha" - ], - "sizeBytes": 51885319 - }, - { - "names": [ - "kotsadm/minio@sha256:a68fb7b34d58c8167d11a93ebe887ab44ccb9447593e9ce7c36ac940c78221d4", - "kotsadm/minio:v1.11.1" - ], - "sizeBytes": 51885319 - }, - { - "names": [ - "kotsadm/minio@sha256:38c18cc2d92573cfce813931aaf04183b8c23b87a5ba0d672a8cfc1ca4f1acc6", - "kotsadm/minio:v1.10.3" - ], - "sizeBytes": 51885319 - }, - { - "names": [ - "kotsadm/minio@sha256:1c7c8a0e953fccbe44f44134a29431fd86a5cfc7845b84adb12a808b503cf847", - "kotsadm/minio:v1.11.0" - ], - "sizeBytes": 51885319 - }, - { - "names": [ - "kotsadm/minio@sha256:ffc3a26ce3fca3a6f5802444ceb6fee7a98a136c8e60b7f0020c6ce036ec628c", - "kotsadm/minio:v1.11.4" - ], - "sizeBytes": 51885319 - }, - { - "names": [ - "flungo/netutils@sha256:cf2a22cf9edee0640bae64fc33b8916fef524cc7f454e0279d91509cc1aecd60", - "flungo/netutils:latest" - ], - "sizeBytes": 42668818 - }, - { - "names": [ - "codescope/ui@sha256:147be2359690e9b4237462010111986234ec253cf19f2f7fed8e9a5c1ea59938", - "codescope/ui:1.6.3" - ], - "sizeBytes": 25522501 - }, - { - "names": [ - "codescope/router@sha256:fea5f5bf3b2fe8c872c769c91762108e7d6ff791e793c92d068035462d149de7", - "codescope/router:0.4.2" - ], - "sizeBytes": 23233618 - }, - { - "names": [ - "602401143452.dkr.ecr.us-east-2.amazonaws.com/eks/pause-amd64@sha256:bea77c323c47f7b573355516acf927691182d1333333d1f41b7544012fab7adf", - "602401143452.dkr.ecr.us-east-2.amazonaws.com/eks/pause-amd64:3.1" - ], - "sizeBytes": 742472 - } - ] - } - } -] - -` - var collectedDeployments = `[ { "metadata": { diff --git a/pkg/analyze/node_resources.go b/pkg/analyze/node_resources.go index 03ec26ab..f23afa3f 100644 --- a/pkg/analyze/node_resources.go +++ b/pkg/analyze/node_resources.go @@ -2,16 +2,17 @@ package analyzer import ( "encoding/json" + "strconv" "strings" - "github.com/blang/semver" "github.com/pkg/errors" troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1" - "github.com/replicatedhq/troubleshoot/pkg/collect" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" ) -func analyzeNoderesources(analyzer *troubleshootv1beta1.NodeResources, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) { - collected, err := getCollectedFileContents("cluster-info/nods.json") +func analyzeNodeResources(analyzer *troubleshootv1beta1.NodeResources, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) { + collected, err := getCollectedFileContents("cluster-resources/nodes.json") if err != nil { return nil, errors.Wrap(err, "failed to get contents of nodes.json") } @@ -24,18 +25,194 @@ func analyzeNoderesources(analyzer *troubleshootv1beta1.NodeResources, getCollec matchingNodeCount := 0 for _, node := range nodes { - matches, err := nodeMatchesFilters(node, analyzer.Filters) + isMatch, err := nodeMatchesFilters(node, analyzer.Filters) if err != nil { return nil, errors.Wrap(err, "failed to check if node matches filter") } + + if isMatch { + matchingNodeCount++ + } } - return analyzeClusterVersionResult(k8sVersion, analyzer.Outcomes, analyzer.CheckName) + + title := analyzer.CheckName + if title == "" { + title = "Node Resources" + } + + result := &AnalyzeResult{ + Title: title, + } + + for _, outcome := range analyzer.Outcomes { + if outcome.Fail != nil { + isWhenMatch, err := compareNodeResourceConditionalToActual(outcome.Fail.When, matchingNodeCount) + if err != nil { + return nil, errors.Wrap(err, "failed to parse when") + } + + if isWhenMatch { + result.IsFail = true + result.Message = outcome.Fail.Message + result.URI = outcome.Fail.URI + + return result, nil + } + } else if outcome.Warn != nil { + isWhenMatch, err := compareNodeResourceConditionalToActual(outcome.Warn.When, matchingNodeCount) + if err != nil { + return nil, errors.Wrap(err, "failed to parse when") + } + + if isWhenMatch { + result.IsWarn = true + result.Message = outcome.Warn.Message + result.URI = outcome.Warn.URI + + return result, nil + } + } else if outcome.Pass != nil { + isWhenMatch, err := compareNodeResourceConditionalToActual(outcome.Pass.When, matchingNodeCount) + if err != nil { + return nil, errors.Wrap(err, "failed to parse when") + } + + if isWhenMatch { + result.IsPass = true + result.Message = outcome.Pass.Message + result.URI = outcome.Pass.URI + + return result, nil + } + } + } + + return result, nil } -func nodeMatchesFilters(node *corev1.Node, filters *troubleshootv1beta1.NodeResourceFilters) (bool, error) { +func compareNodeResourceConditionalToActual(conditional string, actual int) (bool, error) { + if conditional == "" { + return true, nil + } + + parts := strings.Split(strings.TrimSpace(conditional), " ") + + if len(parts) != 2 { + return false, errors.New("unable to parse nodeResources conditional") + } + + operator := parts[0] + desiredValue, err := strconv.Atoi(parts[1]) + if err != nil { + return false, errors.Wrap(err, "failed to parse nodeResource value") + } + + switch operator { + case "=", "==", "===": + return desiredValue == actual, nil + case "<": + return actual < desiredValue, nil + case "<=": + return actual <= desiredValue, nil + case ">": + return actual > desiredValue, nil + case ">=": + return actual >= desiredValue, nil + } + + return false, errors.New("unexpected conditional in nodeResources") +} + +func nodeMatchesFilters(node corev1.Node, filters *troubleshootv1beta1.NodeResourceFilters) (bool, error) { if filters == nil { return true, nil } - return false, nil + // all filters must pass for this to pass + + if filters.CPUCapacity != "" { + parsed, err := resource.ParseQuantity(filters.CPUCapacity) + if err != nil { + return false, errors.Wrap(err, "failed to parse cpu capacity") + } + + if node.Status.Capacity.Cpu().Cmp(parsed) == -1 { + return false, nil + } + } + if filters.CPUAllocatable != "" { + parsed, err := resource.ParseQuantity(filters.CPUAllocatable) + if err != nil { + return false, errors.Wrap(err, "failed to parse cpu allocatable") + } + + if node.Status.Allocatable.Cpu().Cmp(parsed) == -1 { + return false, nil + } + } + + if filters.MemoryCapacity != "" { + parsed, err := resource.ParseQuantity(filters.MemoryCapacity) + if err != nil { + return false, errors.Wrap(err, "failed to parse memory capacity") + } + + if node.Status.Capacity.Memory().Cmp(parsed) == -1 { + return false, nil + } + } + if filters.MemoryAllocatable != "" { + parsed, err := resource.ParseQuantity(filters.MemoryAllocatable) + if err != nil { + return false, errors.Wrap(err, "failed to parse memory allocatable") + } + + if node.Status.Allocatable.Memory().Cmp(parsed) == -1 { + return false, nil + } + } + + if filters.PodCapacity != "" { + parsed, err := resource.ParseQuantity(filters.PodCapacity) + if err != nil { + return false, errors.Wrap(err, "failed to parse pod capacity") + } + + if node.Status.Capacity.Pods().Cmp(parsed) == -1 { + return false, nil + } + } + if filters.PodAllocatable != "" { + parsed, err := resource.ParseQuantity(filters.PodAllocatable) + if err != nil { + return false, errors.Wrap(err, "failed to parse pod allocatable") + } + + if node.Status.Allocatable.Pods().Cmp(parsed) == -1 { + return false, nil + } + } + + if filters.EphemeralStorageCapacity != "" { + parsed, err := resource.ParseQuantity(filters.EphemeralStorageCapacity) + if err != nil { + return false, errors.Wrap(err, "failed to parse ephemeralstorage capacity") + } + + if node.Status.Capacity.StorageEphemeral().Cmp(parsed) == -1 { + return false, nil + } + } + if filters.EphemeralStorageAllocatable != "" { + parsed, err := resource.ParseQuantity(filters.EphemeralStorageAllocatable) + if err != nil { + return false, errors.Wrap(err, "failed to parse ephemeralstorage allocatable") + } + + if node.Status.Allocatable.StorageEphemeral().Cmp(parsed) == -1 { + return false, nil + } + } + + return true, nil } diff --git a/pkg/analyze/node_resources_test.go b/pkg/analyze/node_resources_test.go index 9ab0b555..9353c271 100644 --- a/pkg/analyze/node_resources_test.go +++ b/pkg/analyze/node_resources_test.go @@ -6,21 +6,91 @@ import ( troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" ) -func Test_nodeMatchesRfilters(t *testing.T) { +func Test_compareNodeResourceConditionalToActual(t *testing.T) { + tests := []struct { + name string + conditional string + actual int + expected bool + }{ + { + name: "=", + conditional: "= 5", + actual: 5, + expected: true, + }, + { + name: "<= (pass)", + conditional: "<= 5", + actual: 4, + expected: true, + }, + { + name: "<= (fail)", + conditional: "<= 5", + actual: 6, + expected: false, + }, + { + name: "> (pass)", + conditional: "> 5", + actual: 6, + expected: true, + }, + { + name: ">=(fail)", + conditional: ">= 5", + actual: 4, + expected: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req := require.New(t) + + actual, err := compareNodeResourceConditionalToActual(test.conditional, test.actual) + req.NoError(err) + + assert.Equal(t, test.expected, actual) + + }) + } +} + +func Test_nodeMatchesFilters(t *testing.T) { tests := []struct { name string - node *corev1.Node + node corev1.Node filters *troubleshootv1beta1.NodeResourceFilters expectResult bool }{ { name: "true when empty filters", - node: &corev1.Node{ + node: corev1.Node{ Status: corev1.NodeStatus{ - Capacity: corev1.Sometrhing{}, - Allocatable: corev1.Something{}, + Capacity: corev1.ResourceList{ + "attachable-volumes-aws-ebs": resource.MustParse("25"), + "cpu": resource.MustParse("2"), + "ephemeral-storage": resource.MustParse("20959212Ki"), + "hugepages-1Gi": resource.MustParse("0"), + "hugepages-2Mi": resource.MustParse("0"), + "memory": resource.MustParse("7951376Ki"), + "pods": resource.MustParse("29"), + }, + Allocatable: corev1.ResourceList{ + "attachable-volumes-aws-ebs": resource.MustParse("25"), + "cpu": resource.MustParse("2"), + "ephemeral-storage": resource.MustParse("19316009748"), + "hugepages-1Gi": resource.MustParse("0"), + "hugepages-2Mi": resource.MustParse("0"), + "memory": resource.MustParse("7848976Ki"), + "pods": resource.MustParse("29"), + }, }, }, filters: &troubleshootv1beta1.NodeResourceFilters{}, @@ -28,33 +98,81 @@ func Test_nodeMatchesRfilters(t *testing.T) { }, { name: "true while nil/missing filters", - node: &corev1.Node{ + node: corev1.Node{ Status: corev1.NodeStatus{ - Capacity: corev1.Sometrhing{}, - Allocatable: corev1.Something{}, + Capacity: corev1.ResourceList{ + "attachable-volumes-aws-ebs": resource.MustParse("25"), + "cpu": resource.MustParse("2"), + "ephemeral-storage": resource.MustParse("20959212Ki"), + "hugepages-1Gi": resource.MustParse("0"), + "hugepages-2Mi": resource.MustParse("0"), + "memory": resource.MustParse("7951376Ki"), + "pods": resource.MustParse("29"), + }, + Allocatable: corev1.ResourceList{ + "attachable-volumes-aws-ebs": resource.MustParse("25"), + "cpu": resource.MustParse("2"), + "ephemeral-storage": resource.MustParse("19316009748"), + "hugepages-1Gi": resource.MustParse("0"), + "hugepages-2Mi": resource.MustParse("0"), + "memory": resource.MustParse("7848976Ki"), + "pods": resource.MustParse("29"), + }, }, }, expectResult: true, }, { name: "false when allocatable memory is too high", - node: &corev1.Node{ + node: corev1.Node{ Status: corev1.NodeStatus{ - Capacity: corev1.Sometrhing{}, - Allocatable: corev1.Something{}, + Capacity: corev1.ResourceList{ + "attachable-volumes-aws-ebs": resource.MustParse("25"), + "cpu": resource.MustParse("2"), + "ephemeral-storage": resource.MustParse("20959212Ki"), + "hugepages-1Gi": resource.MustParse("0"), + "hugepages-2Mi": resource.MustParse("0"), + "memory": resource.MustParse("7951376Ki"), + "pods": resource.MustParse("29"), + }, + Allocatable: corev1.ResourceList{ + "attachable-volumes-aws-ebs": resource.MustParse("25"), + "cpu": resource.MustParse("2"), + "ephemeral-storage": resource.MustParse("19316009748"), + "hugepages-1Gi": resource.MustParse("0"), + "hugepages-2Mi": resource.MustParse("0"), + "memory": resource.MustParse("7848976Ki"), + "pods": resource.MustParse("29"), + }, }, }, filters: &troubleshootv1beta1.NodeResourceFilters{ - MemoryAllocatable: "32Gi", + MemoryAllocatable: "16Gi", }, expectResult: false, }, { name: "true when allocatable memory is available", - node: &corev1.Node{ + node: corev1.Node{ Status: corev1.NodeStatus{ - Capacity: corev1.Sometrhing{}, - Allocatable: corev1.Something{}, + Capacity: corev1.ResourceList{ + "attachable-volumes-aws-ebs": resource.MustParse("25"), + "cpu": resource.MustParse("2"), + "ephemeral-storage": resource.MustParse("20959212Ki"), + "hugepages-1Gi": resource.MustParse("0"), + "hugepages-2Mi": resource.MustParse("0"), + "memory": resource.MustParse("7951376Ki"), + "pods": resource.MustParse("29"), + }, + Allocatable: corev1.ResourceList{ + "attachable-volumes-aws-ebs": resource.MustParse("25"), + "cpu": resource.MustParse("2"), + "ephemeral-storage": resource.MustParse("19316009748"), + "hugepages-1Gi": resource.MustParse("0"), + "hugepages-2Mi": resource.MustParse("0"), + "memory": resource.MustParse("7848976Ki"), + "pods": resource.MustParse("29"), + }, }, }, filters: &troubleshootv1beta1.NodeResourceFilters{ @@ -71,7 +189,7 @@ func Test_nodeMatchesRfilters(t *testing.T) { actual, err := nodeMatchesFilters(test.node, test.filters) req.NoError(err) - assert.Equal(t, &test.expectResult, actual) + assert.Equal(t, test.expectResult, actual) }) } diff --git a/pkg/apis/troubleshoot/v1beta1/analyzer_shared.go b/pkg/apis/troubleshoot/v1beta1/analyzer_shared.go index f48e185d..ab437495 100644 --- a/pkg/apis/troubleshoot/v1beta1/analyzer_shared.go +++ b/pkg/apis/troubleshoot/v1beta1/analyzer_shared.go @@ -1,9 +1,5 @@ package v1beta1 -import ( - "k8s.io/apimachinery/pkg/util/intstr" -) - type SingleOutcome struct { When string `json:"when,omitempty" yaml:"when,omitempty"` Message string `json:"message,omitempty" yaml:"message,omitempty"` @@ -79,20 +75,20 @@ type Distribution struct { } type NodeResources struct { - // AnalyzeMeta `json:",inline" yaml:",inline"` - // Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"` - // Filters *NodeResourceFilters `json:"filters,omitempty" yaml:"filters,omitempty"` + AnalyzeMeta `json:",inline" yaml:",inline"` + Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"` + Filters *NodeResourceFilters `json:"filters,omitempty" yaml:"filters,omitempty"` } type NodeResourceFilters struct { - // CPUCapacity *intstr.Intstr `json:"cpuCapacity,omitempty" yaml:"cpuCapacity,omitempty"` - // CPUAllocatable *intstr.Intstr `json:"cpuAllocatable,omitempty" yaml:"cpuAllocatable,omitempty"` - // MemoryCapacity *intstr.Intstr `json:"memoryCapacity,omitempty" yaml:"memoryCapacity,omitempty"` - // MemoryAllocatable *intstr.Intstr `json:"memoryAllocatable,omitempty" yaml:"memoryAllocatable,omitempty"` - // PodCapacity *intstr.Intstr `json:"podCapacity,omitempty" yaml:"podCapacity,omitempty"` - // PodAllocatable *intstr.Intstr `json:"podAllocatable,omitempty" yaml:"podAllocatable,omitempty"` - // EphemeralStorageCapacity *intstr.Intstr `json:"ephemeralStorageCapacity,omitempty" yaml:"ephemeralStorageCapacity,omitempty"` - // EphemeralStorageAllocatable*intstr.Intstr `json:"ephemeralStorageAllocatable,omitempty" yaml:"ephemeralStorageAllocatable,omitempty"` + CPUCapacity string `json:"cpuCapacity,omitempty" yaml:"cpuCapacity,omitempty"` + CPUAllocatable string `json:"cpuAllocatable,omitempty" yaml:"cpuAllocatable,omitempty"` + MemoryCapacity string `json:"memoryCapacity,omitempty" yaml:"memoryCapacity,omitempty"` + MemoryAllocatable string `json:"memoryAllocatable,omitempty" yaml:"memoryAllocatable,omitempty"` + PodCapacity string `json:"podCapacity,omitempty" yaml:"podCapacity,omitempty"` + PodAllocatable string `json:"podAllocatable,omitempty" yaml:"podAllocatable,omitempty"` + EphemeralStorageCapacity string `json:"ephemeralStorageCapacity,omitempty" yaml:"ephemeralStorageCapacity,omitempty"` + EphemeralStorageAllocatable string `json:"ephemeralStorageAllocatable,omitempty" yaml:"ephemeralStorageAllocatable,omitempty"` } type TextAnalyze struct { diff --git a/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go b/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go index ce74b652..b7af7d4d 100644 --- a/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go @@ -105,7 +105,7 @@ func (in *Analyze) DeepCopyInto(out *Analyze) { if in.NodeResources != nil { in, out := &in.NodeResources, &out.NodeResources *out = new(NodeResources) - **out = **in + (*in).DeepCopyInto(*out) } if in.TextAnalyze != nil { in, out := &in.TextAnalyze, &out.TextAnalyze @@ -1067,6 +1067,23 @@ func (in *NodeResourceFilters) DeepCopy() *NodeResourceFilters { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NodeResources) DeepCopyInto(out *NodeResources) { *out = *in + out.AnalyzeMeta = in.AnalyzeMeta + if in.Outcomes != nil { + in, out := &in.Outcomes, &out.Outcomes + *out = make([]*Outcome, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Outcome) + (*in).DeepCopyInto(*out) + } + } + } + if in.Filters != nil { + in, out := &in.Filters, &out.Filters + *out = new(NodeResourceFilters) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeResources.