From 45f9efdce496710edc567ba8f5600fe4fc9c6901 Mon Sep 17 00:00:00 2001 From: Aditya Thebe Date: Thu, 4 Apr 2024 21:54:29 +0545 Subject: [PATCH] feat: add namespace check & tests for certificates. (#14) * feat: certificate health check * improve ingress health check * add certificate test and remove go impl for certificate * feat: add health check for namespace --- pkg/health/health.go | 5 +- pkg/health/health_argo.go | 2 +- pkg/health/health_ingress.go | 10 +- pkg/health/health_namespace.go | 26 ++++ pkg/health/health_test.go | 141 ++++++++++-------- pkg/health/testdata/certificate-healthy.yaml | 39 +++++ .../testdata/namespace-terminating.yaml | 18 +++ pkg/health/testdata/namespace.yaml | 18 +++ 8 files changed, 191 insertions(+), 68 deletions(-) create mode 100644 pkg/health/health_namespace.go create mode 100644 pkg/health/testdata/certificate-healthy.yaml create mode 100644 pkg/health/testdata/namespace-terminating.yaml create mode 100644 pkg/health/testdata/namespace.yaml diff --git a/pkg/health/health.go b/pkg/health/health.go index bae6844..3c39cb6 100644 --- a/pkg/health/health.go +++ b/pkg/health/health.go @@ -136,7 +136,7 @@ func GetHealthCheckFunc(gvk schema.GroupVersionKind) func(obj *unstructured.Unst case "argoproj.io": switch gvk.Kind { case "Workflow": - return getArgoWorkflowHealth + return GetArgoWorkflowHealth case "Application": return getArgoApplicationHealth } @@ -161,6 +161,7 @@ func GetHealthCheckFunc(gvk schema.GroupVersionKind) func(obj *unstructured.Unst // case APIServiceKind: // return getAPIServiceHealth // } + case "networking.k8s.io": switch gvk.Kind { case IngressKind: @@ -174,6 +175,8 @@ func GetHealthCheckFunc(gvk schema.GroupVersionKind) func(obj *unstructured.Unst return getPVCHealth case PodKind: return getPodHealth + case NamespaceKind: + return getNamespaceHealth } case "batch": switch gvk.Kind { diff --git a/pkg/health/health_argo.go b/pkg/health/health_argo.go index e6969bb..97a8b85 100644 --- a/pkg/health/health_argo.go +++ b/pkg/health/health_argo.go @@ -26,7 +26,7 @@ type argoWorkflow struct { } } -func getArgoWorkflowHealth(obj *unstructured.Unstructured) (*HealthStatus, error) { +func GetArgoWorkflowHealth(obj *unstructured.Unstructured) (*HealthStatus, error) { var wf argoWorkflow err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &wf) if err != nil { diff --git a/pkg/health/health_ingress.go b/pkg/health/health_ingress.go index 87de2d3..ee697f9 100644 --- a/pkg/health/health_ingress.go +++ b/pkg/health/health_ingress.go @@ -1,11 +1,19 @@ package health import ( + "fmt" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) func getIngressHealth(obj *unstructured.Unstructured) (*HealthStatus, error) { - ingresses, _, _ := unstructured.NestedSlice(obj.Object, "status", "loadBalancer", "ingress") + ingresses, found, err := unstructured.NestedSlice(obj.Object, "status", "loadBalancer", "ingress") + if err != nil { + return &HealthStatus{Status: HealthStatusError, Message: fmt.Sprintf("failed to ingress status: %v", err)}, nil + } else if !found { + return &HealthStatus{Status: HealthStatusPending, Message: "ingress loadbalancer status not found"}, nil + } + health := HealthStatus{} if len(ingresses) > 0 { health.Status = HealthStatusHealthy diff --git a/pkg/health/health_namespace.go b/pkg/health/health_namespace.go new file mode 100644 index 0000000..e7ab9b4 --- /dev/null +++ b/pkg/health/health_namespace.go @@ -0,0 +1,26 @@ +package health + +import ( + "fmt" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +func getNamespaceHealth(obj *unstructured.Unstructured) (*HealthStatus, error) { + var node v1.Namespace + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &node); err != nil { + return nil, fmt.Errorf("failed to convert unstructured Node to typed: %v", err) + } + + if node.Status.Phase == v1.NamespaceActive { + return &HealthStatus{ + Status: HealthStatusHealthy, + }, nil + } + + return &HealthStatus{ + Status: HealthStatusDeleting, + }, nil +} diff --git a/pkg/health/health_test.go b/pkg/health/health_test.go index 8c88361..cf3a9ba 100644 --- a/pkg/health/health_test.go +++ b/pkg/health/health_test.go @@ -2,69 +2,80 @@ Package provides functionality that allows assessing the health state of a Kubernetes resource. */ -package health +package health_test import ( "os" "testing" + "github.com/flanksource/is-healthy/pkg/health" + "github.com/flanksource/is-healthy/pkg/lua" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/yaml" ) -func assertAppHealth(t *testing.T, yamlPath string, expectedStatus HealthStatusCode) { +func assertAppHealth(t *testing.T, yamlPath string, expectedStatus health.HealthStatusCode) { health := getHealthStatus(yamlPath, t) assert.NotNil(t, health) assert.Equal(t, expectedStatus, health.Status) } -func getHealthStatus(yamlPath string, t *testing.T) *HealthStatus { +func getHealthStatus(yamlPath string, t *testing.T) *health.HealthStatus { yamlBytes, err := os.ReadFile(yamlPath) require.NoError(t, err) var obj unstructured.Unstructured err = yaml.Unmarshal(yamlBytes, &obj) require.NoError(t, err) - health, err := GetResourceHealth(&obj, nil) + health, err := health.GetResourceHealth(&obj, lua.ResourceHealthOverrides{}) require.NoError(t, err) return health } +func TestNamespace(t *testing.T) { + assertAppHealth(t, "./testdata/namespace.yaml", health.HealthStatusHealthy) + assertAppHealth(t, "./testdata/namespace-terminating.yaml", health.HealthStatusDeleting) +} + +func TestCertificate(t *testing.T) { + assertAppHealth(t, "./testdata/certificate-healthy.yaml", health.HealthStatusHealthy) +} + func TestDeploymentHealth(t *testing.T) { - assertAppHealth(t, "./testdata/nginx.yaml", HealthStatusHealthy) - assertAppHealth(t, "./testdata/deployment-progressing.yaml", HealthStatusProgressing) - assertAppHealth(t, "./testdata/deployment-suspended.yaml", HealthStatusSuspended) - assertAppHealth(t, "./testdata/deployment-degraded.yaml", HealthStatusDegraded) + assertAppHealth(t, "./testdata/nginx.yaml", health.HealthStatusHealthy) + assertAppHealth(t, "./testdata/deployment-progressing.yaml", health.HealthStatusProgressing) + assertAppHealth(t, "./testdata/deployment-suspended.yaml", health.HealthStatusSuspended) + assertAppHealth(t, "./testdata/deployment-degraded.yaml", health.HealthStatusDegraded) } func TestStatefulSetHealth(t *testing.T) { - assertAppHealth(t, "./testdata/statefulset.yaml", HealthStatusHealthy) + assertAppHealth(t, "./testdata/statefulset.yaml", health.HealthStatusHealthy) } func TestStatefulSetOnDeleteHealth(t *testing.T) { - assertAppHealth(t, "./testdata/statefulset-ondelete.yaml", HealthStatusHealthy) + assertAppHealth(t, "./testdata/statefulset-ondelete.yaml", health.HealthStatusHealthy) } func TestDaemonSetOnDeleteHealth(t *testing.T) { - assertAppHealth(t, "./testdata/daemonset-ondelete.yaml", HealthStatusHealthy) + assertAppHealth(t, "./testdata/daemonset-ondelete.yaml", health.HealthStatusHealthy) } func TestPVCHealth(t *testing.T) { - assertAppHealth(t, "./testdata/pvc-bound.yaml", HealthStatusHealthy) - assertAppHealth(t, "./testdata/pvc-pending.yaml", HealthStatusProgressing) + assertAppHealth(t, "./testdata/pvc-bound.yaml", health.HealthStatusHealthy) + assertAppHealth(t, "./testdata/pvc-pending.yaml", health.HealthStatusProgressing) } func TestServiceHealth(t *testing.T) { - assertAppHealth(t, "./testdata/svc-clusterip.yaml", HealthStatusHealthy) - assertAppHealth(t, "./testdata/svc-loadbalancer.yaml", HealthStatusHealthy) - assertAppHealth(t, "./testdata/svc-loadbalancer-unassigned.yaml", HealthStatusProgressing) - assertAppHealth(t, "./testdata/svc-loadbalancer-nonemptylist.yaml", HealthStatusHealthy) + assertAppHealth(t, "./testdata/svc-clusterip.yaml", health.HealthStatusHealthy) + assertAppHealth(t, "./testdata/svc-loadbalancer.yaml", health.HealthStatusHealthy) + assertAppHealth(t, "./testdata/svc-loadbalancer-unassigned.yaml", health.HealthStatusProgressing) + assertAppHealth(t, "./testdata/svc-loadbalancer-nonemptylist.yaml", health.HealthStatusHealthy) } func TestIngressHealth(t *testing.T) { - assertAppHealth(t, "./testdata/ingress.yaml", HealthStatusHealthy) - assertAppHealth(t, "./testdata/ingress-unassigned.yaml", HealthStatusProgressing) - assertAppHealth(t, "./testdata/ingress-nonemptylist.yaml", HealthStatusHealthy) + assertAppHealth(t, "./testdata/ingress.yaml", health.HealthStatusHealthy) + assertAppHealth(t, "./testdata/ingress-unassigned.yaml", health.HealthStatusProgressing) + assertAppHealth(t, "./testdata/ingress-nonemptylist.yaml", health.HealthStatusHealthy) } func TestCRD(t *testing.T) { @@ -72,38 +83,38 @@ func TestCRD(t *testing.T) { } func TestJob(t *testing.T) { - assertAppHealth(t, "./testdata/job-running.yaml", HealthStatusProgressing) - assertAppHealth(t, "./testdata/job-failed.yaml", HealthStatusDegraded) - assertAppHealth(t, "./testdata/job-succeeded.yaml", HealthStatusHealthy) - assertAppHealth(t, "./testdata/job-suspended.yaml", HealthStatusSuspended) + assertAppHealth(t, "./testdata/job-running.yaml", health.HealthStatusProgressing) + assertAppHealth(t, "./testdata/job-failed.yaml", health.HealthStatusDegraded) + assertAppHealth(t, "./testdata/job-succeeded.yaml", health.HealthStatusHealthy) + assertAppHealth(t, "./testdata/job-suspended.yaml", health.HealthStatusSuspended) } func TestHPA(t *testing.T) { - assertAppHealth(t, "./testdata/hpa-v2-healthy.yaml", HealthStatusHealthy) - assertAppHealth(t, "./testdata/hpa-v2-degraded.yaml", HealthStatusDegraded) - assertAppHealth(t, "./testdata/hpa-v2-progressing.yaml", HealthStatusProgressing) - assertAppHealth(t, "./testdata/hpa-v2beta2-healthy.yaml", HealthStatusHealthy) - assertAppHealth(t, "./testdata/hpa-v2beta1-healthy-disabled.yaml", HealthStatusHealthy) - assertAppHealth(t, "./testdata/hpa-v2beta1-healthy.yaml", HealthStatusHealthy) - assertAppHealth(t, "./testdata/hpa-v1-degraded.yaml", HealthStatusDegraded) - assertAppHealth(t, "./testdata/hpa-v1-healthy.yaml", HealthStatusHealthy) - assertAppHealth(t, "./testdata/hpa-v1-healthy-toofew.yaml", HealthStatusHealthy) - assertAppHealth(t, "./testdata/hpa-v1-progressing.yaml", HealthStatusProgressing) - assertAppHealth(t, "./testdata/hpa-v1-progressing-with-no-annotations.yaml", HealthStatusProgressing) + assertAppHealth(t, "./testdata/hpa-v2-healthy.yaml", health.HealthStatusHealthy) + assertAppHealth(t, "./testdata/hpa-v2-degraded.yaml", health.HealthStatusDegraded) + assertAppHealth(t, "./testdata/hpa-v2-progressing.yaml", health.HealthStatusProgressing) + assertAppHealth(t, "./testdata/hpa-v2beta2-healthy.yaml", health.HealthStatusHealthy) + assertAppHealth(t, "./testdata/hpa-v2beta1-healthy-disabled.yaml", health.HealthStatusHealthy) + assertAppHealth(t, "./testdata/hpa-v2beta1-healthy.yaml", health.HealthStatusHealthy) + assertAppHealth(t, "./testdata/hpa-v1-degraded.yaml", health.HealthStatusDegraded) + assertAppHealth(t, "./testdata/hpa-v1-healthy.yaml", health.HealthStatusHealthy) + assertAppHealth(t, "./testdata/hpa-v1-healthy-toofew.yaml", health.HealthStatusHealthy) + assertAppHealth(t, "./testdata/hpa-v1-progressing.yaml", health.HealthStatusProgressing) + assertAppHealth(t, "./testdata/hpa-v1-progressing-with-no-annotations.yaml", health.HealthStatusProgressing) } func TestPod(t *testing.T) { - assertAppHealth(t, "./testdata/pod-pending.yaml", HealthStatusProgressing) - assertAppHealth(t, "./testdata/pod-running-not-ready.yaml", HealthStatusProgressing) - assertAppHealth(t, "./testdata/pod-crashloop.yaml", HealthStatusDegraded) - assertAppHealth(t, "./testdata/pod-imagepullbackoff.yaml", HealthStatusDegraded) - assertAppHealth(t, "./testdata/pod-error.yaml", HealthStatusDegraded) - assertAppHealth(t, "./testdata/pod-running-restart-always.yaml", HealthStatusHealthy) - assertAppHealth(t, "./testdata/pod-running-restart-never.yaml", HealthStatusProgressing) - assertAppHealth(t, "./testdata/pod-running-restart-onfailure.yaml", HealthStatusProgressing) - assertAppHealth(t, "./testdata/pod-failed.yaml", HealthStatusDegraded) - assertAppHealth(t, "./testdata/pod-succeeded.yaml", HealthStatusHealthy) - assertAppHealth(t, "./testdata/pod-deletion.yaml", HealthStatusProgressing) + assertAppHealth(t, "./testdata/pod-pending.yaml", health.HealthStatusProgressing) + assertAppHealth(t, "./testdata/pod-running-not-ready.yaml", health.HealthStatusProgressing) + assertAppHealth(t, "./testdata/pod-crashloop.yaml", health.HealthStatusDegraded) + assertAppHealth(t, "./testdata/pod-imagepullbackoff.yaml", health.HealthStatusDegraded) + assertAppHealth(t, "./testdata/pod-error.yaml", health.HealthStatusDegraded) + assertAppHealth(t, "./testdata/pod-running-restart-always.yaml", health.HealthStatusHealthy) + assertAppHealth(t, "./testdata/pod-running-restart-never.yaml", health.HealthStatusProgressing) + assertAppHealth(t, "./testdata/pod-running-restart-onfailure.yaml", health.HealthStatusProgressing) + assertAppHealth(t, "./testdata/pod-failed.yaml", health.HealthStatusDegraded) + assertAppHealth(t, "./testdata/pod-succeeded.yaml", health.HealthStatusHealthy) + assertAppHealth(t, "./testdata/pod-deletion.yaml", health.HealthStatusProgressing) } func TestApplication(t *testing.T) { @@ -131,10 +142,10 @@ func TestGetArgoWorkflowHealth(t *testing.T) { }, } - health, err := getArgoWorkflowHealth(&sampleWorkflow) + argohealth, err := health.GetArgoWorkflowHealth(&sampleWorkflow) require.NoError(t, err) - assert.Equal(t, HealthStatusProgressing, health.Status) - assert.Equal(t, "This node is running", health.Message) + assert.Equal(t, health.HealthStatusProgressing, argohealth.Status) + assert.Equal(t, "This node is running", argohealth.Message) sampleWorkflow = unstructured.Unstructured{Object: map[string]interface{}{ "spec": map[string]interface{}{ @@ -148,10 +159,10 @@ func TestGetArgoWorkflowHealth(t *testing.T) { }, } - health, err = getArgoWorkflowHealth(&sampleWorkflow) + argohealth, err = health.GetArgoWorkflowHealth(&sampleWorkflow) require.NoError(t, err) - assert.Equal(t, HealthStatusHealthy, health.Status) - assert.Equal(t, "This node is has succeeded", health.Message) + assert.Equal(t, health.HealthStatusHealthy, argohealth.Status) + assert.Equal(t, "This node is has succeeded", argohealth.Message) sampleWorkflow = unstructured.Unstructured{Object: map[string]interface{}{ "spec": map[string]interface{}{ @@ -161,28 +172,28 @@ func TestGetArgoWorkflowHealth(t *testing.T) { }, } - health, err = getArgoWorkflowHealth(&sampleWorkflow) + argohealth, err = health.GetArgoWorkflowHealth(&sampleWorkflow) require.NoError(t, err) - assert.Equal(t, HealthStatusProgressing, health.Status) - assert.Equal(t, "", health.Message) + assert.Equal(t, health.HealthStatusProgressing, argohealth.Status) + assert.Equal(t, "", argohealth.Message) } func TestArgoApplication(t *testing.T) { - assertAppHealth(t, "./testdata/argo-application-healthy.yaml", HealthStatusHealthy) - assertAppHealth(t, "./testdata/argo-application-missing.yaml", HealthStatusMissing) + assertAppHealth(t, "./testdata/argo-application-healthy.yaml", health.HealthStatusHealthy) + assertAppHealth(t, "./testdata/argo-application-missing.yaml", health.HealthStatusMissing) } func TestFluxResources(t *testing.T) { - assertAppHealth(t, "./testdata/flux-kustomization-healthy.yaml", HealthStatusHealthy) - assertAppHealth(t, "./testdata/flux-kustomization-unhealthy.yaml", HealthStatusDegraded) + assertAppHealth(t, "./testdata/flux-kustomization-healthy.yaml", health.HealthStatusHealthy) + assertAppHealth(t, "./testdata/flux-kustomization-unhealthy.yaml", health.HealthStatusDegraded) - assertAppHealth(t, "./testdata/flux-helmrelease-healthy.yaml", HealthStatusHealthy) - assertAppHealth(t, "./testdata/flux-helmrelease-unhealthy.yaml", HealthStatusDegraded) + assertAppHealth(t, "./testdata/flux-helmrelease-healthy.yaml", health.HealthStatusHealthy) + assertAppHealth(t, "./testdata/flux-helmrelease-unhealthy.yaml", health.HealthStatusDegraded) - assertAppHealth(t, "./testdata/flux-helmrepository-healthy.yaml", HealthStatusHealthy) - assertAppHealth(t, "./testdata/flux-helmrepository-unhealthy.yaml", HealthStatusDegraded) + assertAppHealth(t, "./testdata/flux-helmrepository-healthy.yaml", health.HealthStatusHealthy) + assertAppHealth(t, "./testdata/flux-helmrepository-unhealthy.yaml", health.HealthStatusDegraded) - assertAppHealth(t, "./testdata/flux-gitrepository-healthy.yaml", HealthStatusHealthy) - assertAppHealth(t, "./testdata/flux-gitrepository-unhealthy.yaml", HealthStatusDegraded) + assertAppHealth(t, "./testdata/flux-gitrepository-healthy.yaml", health.HealthStatusHealthy) + assertAppHealth(t, "./testdata/flux-gitrepository-unhealthy.yaml", health.HealthStatusDegraded) } diff --git a/pkg/health/testdata/certificate-healthy.yaml b/pkg/health/testdata/certificate-healthy.yaml new file mode 100644 index 0000000..c6c2b90 --- /dev/null +++ b/pkg/health/testdata/certificate-healthy.yaml @@ -0,0 +1,39 @@ +apiVersion: cert-manager.io/v1alpha2 +kind: Certificate +metadata: + creationTimestamp: "2019-02-15T18:17:06Z" + generation: 1 + name: test-cert + namespace: argocd + resourceVersion: "68337322" + selfLink: /apis/cert-manager.io/v1alpha2/namespaces/argocd/certificates/test-cert + uid: e6cfba50-314d-11e9-be3f-42010a800011 +spec: + acme: + config: + - domains: + - cd.apps.argoproj.io + http01: + ingress: http01 + commonName: cd.apps.argoproj.io + dnsNames: + - cd.apps.argoproj.io + issuerRef: + kind: Issuer + name: argo-cd-issuer + secretName: test-secret +status: + acme: + order: + url: https://acme-v02.api.letsencrypt.org/acme/order/45250083/316944902 + conditions: + - lastTransitionTime: "2019-02-15T18:21:10Z" + message: Order validated + reason: OrderValidated + status: "False" + type: ValidateFailed + - lastTransitionTime: null + message: Certificate issued successfully + reason: CertIssued + status: "True" + type: Ready diff --git a/pkg/health/testdata/namespace-terminating.yaml b/pkg/health/testdata/namespace-terminating.yaml new file mode 100644 index 0000000..32e507f --- /dev/null +++ b/pkg/health/testdata/namespace-terminating.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + creationTimestamp: '2024-02-10T10:27:15Z' + labels: + kubernetes.io/metadata.name: media + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: flux-system + kustomize.toolkit.fluxcd.io/prune: disabled + name: media + resourceVersion: '4518738' + uid: c1b2dc7f-7070-4ee5-9ae0-e43df5523cd8 +spec: + finalizers: + - kubernetes +status: + phase: Terminating diff --git a/pkg/health/testdata/namespace.yaml b/pkg/health/testdata/namespace.yaml new file mode 100644 index 0000000..f4a46b5 --- /dev/null +++ b/pkg/health/testdata/namespace.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + creationTimestamp: '2024-02-10T10:27:15Z' + labels: + kubernetes.io/metadata.name: media + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: flux-system + kustomize.toolkit.fluxcd.io/prune: disabled + name: media + resourceVersion: '4518738' + uid: c1b2dc7f-7070-4ee5-9ae0-e43df5523cd8 +spec: + finalizers: + - kubernetes +status: + phase: Active