diff --git a/pkg/health/health.go b/pkg/health/health.go index b93d8c967..d5e2fcd65 100644 --- a/pkg/health/health.go +++ b/pkg/health/health.go @@ -116,6 +116,11 @@ func GetHealthCheckFunc(gvk schema.GroupVersionKind) func(obj *unstructured.Unst case kube.IngressKind: return getIngressHealth } + case "apiextensions.k8s.io": + switch gvk.Kind { + case kube.CustomResourceDefinitionKind: + return getCustomResourceDefinitionHealth + } case "argoproj.io": switch gvk.Kind { case "Workflow": diff --git a/pkg/health/health_customresourcedefinition.go b/pkg/health/health_customresourcedefinition.go new file mode 100644 index 000000000..0e3fa7b73 --- /dev/null +++ b/pkg/health/health_customresourcedefinition.go @@ -0,0 +1,99 @@ +package health + +import ( + "fmt" + + "github.com/argoproj/gitops-engine/pkg/utils/kube" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +func getCustomResourceDefinitionHealth(obj *unstructured.Unstructured) (*HealthStatus, error) { + gvk := obj.GroupVersionKind() + switch gvk { + case apiextensionsv1.SchemeGroupVersion.WithKind(kube.CustomResourceDefinitionKind): + var crd apiextensionsv1.CustomResourceDefinition + err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &crd) + if err != nil { + return nil, fmt.Errorf("failed to convert unstructured CustomResourceDefinition to typed: %v", err) + } + return getApiExtenstionsV1CustomResourceDefinitionHealth(&crd) + default: + return nil, fmt.Errorf("unsupported CustomResourceDefinition GVK: %s", gvk) + } +} + +func getApiExtenstionsV1CustomResourceDefinitionHealth(crd *apiextensionsv1.CustomResourceDefinition) (*HealthStatus, error) { + + if crd.Status.Conditions == nil || crd.Status.Conditions != nil && len(crd.Status.Conditions) == 0 { + return &HealthStatus{ + Status: HealthStatusProgressing, + Message: "Status conditions not found", + }, nil + } + + var ( + isEstablished bool + isTerminating bool + namesNotAccepted bool + hasViolations bool + conditionMsg string + ) + + // Check conditions + for _, condition := range crd.Status.Conditions { + switch condition.Type { + case apiextensionsv1.Terminating: + if condition.Status == apiextensionsv1.ConditionTrue { + isTerminating = true + conditionMsg = condition.Message + } + case apiextensionsv1.NamesAccepted: + if condition.Status == apiextensionsv1.ConditionFalse { + namesNotAccepted = true + conditionMsg = condition.Message + } + case apiextensionsv1.Established: + if condition.Status == apiextensionsv1.ConditionTrue { + isEstablished = true + } else { + conditionMsg = condition.Message + } + case apiextensionsv1.NonStructuralSchema: + if condition.Status == apiextensionsv1.ConditionTrue { + hasViolations = true + conditionMsg = condition.Message + } + } + } + + // Return appropriate health status + switch { + case isTerminating: + return &HealthStatus{ + Status: HealthStatusProgressing, + Message: fmt.Sprintf("CRD is being terminated: %s", conditionMsg), + }, nil + case namesNotAccepted: + return &HealthStatus{ + Status: HealthStatusDegraded, + Message: fmt.Sprintf("CRD names have not been accepted: %s", conditionMsg), + }, nil + case !isEstablished: + return &HealthStatus{ + Status: HealthStatusDegraded, + Message: fmt.Sprintf("CRD is not established: %s", conditionMsg), + }, nil + case hasViolations: + return &HealthStatus{ + Status: HealthStatusDegraded, + Message: fmt.Sprintf("Schema violations found: %s", conditionMsg), + }, nil + default: + return &HealthStatus{ + Status: HealthStatusHealthy, + Message: "CRD is healthy", + }, nil + } +} diff --git a/pkg/health/health_test.go b/pkg/health/health_test.go index 45ff74941..deb55342e 100644 --- a/pkg/health/health_test.go +++ b/pkg/health/health_test.go @@ -118,6 +118,14 @@ func TestAPIService(t *testing.T) { assertAppHealth(t, "./testdata/apiservice-v1beta1-false.yaml", HealthStatusProgressing) } +func TestCustomResourceDefinitionHealth(t *testing.T) { + assertAppHealth(t, "./testdata/crd-v1-healthy.yaml", HealthStatusHealthy) + assertAppHealth(t, "./testdata/crd-v1-non-structual-degraded.yaml", HealthStatusDegraded) + assertAppHealth(t, "./testdata/crd-v1-not-established-degraded.yaml", HealthStatusDegraded) + assertAppHealth(t, "./testdata/crd-v1-terminating-progressing.yaml", HealthStatusProgressing) + assertAppHealth(t, "./testdata/crd-v1-no-conditions-progressing.yaml", HealthStatusProgressing) +} + func TestGetArgoWorkflowHealth(t *testing.T) { sampleWorkflow := unstructured.Unstructured{Object: map[string]interface{}{ "spec": map[string]interface{}{ diff --git a/pkg/health/testdata/crd-v1-healthy.yaml b/pkg/health/testdata/crd-v1-healthy.yaml new file mode 100644 index 000000000..09e00f70b --- /dev/null +++ b/pkg/health/testdata/crd-v1-healthy.yaml @@ -0,0 +1,56 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: examples.example.io +spec: + conversion: + strategy: None + group: example.io + names: + kind: Example + listKind: ExampleList + plural: examples + shortNames: + - ex + singular: example + preserveUnknownFields: true + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: >- + CreationTimestamp is a timestamp representing the server time when + this object was created. It is not guaranteed to be set in + happens-before order across separate operations. Clients may not set + this value. It is represented in RFC3339 form and is in UTC. + + + Populated by the system. Read-only. Null for lists. More info: + https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + served: true + storage: true + subresources: {} +status: + acceptedNames: + kind: Example + listKind: ExampleList + plural: examples + shortNames: + - ex + singular: example + conditions: + - lastTransitionTime: '2024-05-19T23:35:28Z' + message: no conflicts found + reason: NoConflicts + status: 'True' + type: NamesAccepted + - lastTransitionTime: '2024-05-19T23:35:28Z' + message: the initial names have been accepted + reason: InitialNamesAccepted + status: 'True' + type: Established + storedVersions: + - v1alpha1 \ No newline at end of file diff --git a/pkg/health/testdata/crd-v1-no-conditions-progressing.yaml b/pkg/health/testdata/crd-v1-no-conditions-progressing.yaml new file mode 100644 index 000000000..f12ce016b --- /dev/null +++ b/pkg/health/testdata/crd-v1-no-conditions-progressing.yaml @@ -0,0 +1,46 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: examples.example.io +spec: + conversion: + strategy: None + group: example.io + names: + kind: Example + listKind: ExampleList + plural: examples + shortNames: + - ex + singular: example + preserveUnknownFields: true + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: >- + CreationTimestamp is a timestamp representing the server time when + this object was created. It is not guaranteed to be set in + happens-before order across separate operations. Clients may not set + this value. It is represented in RFC3339 form and is in UTC. + + + Populated by the system. Read-only. Null for lists. More info: + https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + served: true + storage: true + subresources: {} +status: + acceptedNames: + kind: Example + listKind: ExampleList + plural: examples + shortNames: + - ex + singular: example + conditions: [] + storedVersions: + - v1alpha1 \ No newline at end of file diff --git a/pkg/health/testdata/crd-v1-non-structual-degraded.yaml b/pkg/health/testdata/crd-v1-non-structual-degraded.yaml new file mode 100644 index 000000000..3c7a07f3e --- /dev/null +++ b/pkg/health/testdata/crd-v1-non-structual-degraded.yaml @@ -0,0 +1,61 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: examples.example.io +spec: + conversion: + strategy: None + group: example.io + names: + kind: Example + listKind: ExampleList + plural: examples + shortNames: + - ex + singular: example + preserveUnknownFields: true + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: >- + CreationTimestamp is a timestamp representing the server time when + this object was created. It is not guaranteed to be set in + happens-before order across separate operations. Clients may not set + this value. It is represented in RFC3339 form and is in UTC. + + + Populated by the system. Read-only. Null for lists. More info: + https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + served: true + storage: true + subresources: {} +status: + acceptedNames: + kind: Example + listKind: ExampleList + plural: examples + shortNames: + - ex + singular: example + conditions: + - lastTransitionTime: '2024-05-19T23:35:28Z' + message: no conflicts found + reason: NoConflicts + status: 'True' + type: NamesAccepted + - lastTransitionTime: '2024-05-19T23:35:28Z' + message: the initial names have been accepted + reason: InitialNamesAccepted + status: 'True' + type: Established + - lastTransitionTime: '2024-10-26T19:44:57Z' + message: 'spec.preserveUnknownFields: Invalid value: true: must be false' + reason: Violations + status: 'True' + type: NonStructuralSchema + storedVersions: + - v1alpha1 \ No newline at end of file diff --git a/pkg/health/testdata/crd-v1-not-established-degraded.yaml b/pkg/health/testdata/crd-v1-not-established-degraded.yaml new file mode 100644 index 000000000..5982bfa53 --- /dev/null +++ b/pkg/health/testdata/crd-v1-not-established-degraded.yaml @@ -0,0 +1,56 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: examples.example.io +spec: + conversion: + strategy: None + group: example.io + names: + kind: Example + listKind: ExampleList + plural: examples + shortNames: + - ex + singular: example + preserveUnknownFields: true + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: >- + CreationTimestamp is a timestamp representing the server time when + this object was created. It is not guaranteed to be set in + happens-before order across separate operations. Clients may not set + this value. It is represented in RFC3339 form and is in UTC. + + + Populated by the system. Read-only. Null for lists. More info: + https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + served: true + storage: true + subresources: {} +status: + acceptedNames: + kind: Example + listKind: ExampleList + plural: examples + shortNames: + - ex + singular: example + conditions: + - lastTransitionTime: '2024-05-19T23:35:28Z' + message: no conflicts found + reason: NoConflicts + status: 'False' + type: NamesAccepted + - lastTransitionTime: '2024-05-19T23:35:28Z' + message: the initial names have been accepted + reason: InitialNamesAccepted + status: 'False' + type: Established + storedVersions: + - v1alpha1 \ No newline at end of file diff --git a/pkg/health/testdata/crd-v1-terminating-progressing.yaml b/pkg/health/testdata/crd-v1-terminating-progressing.yaml new file mode 100644 index 000000000..73d19e483 --- /dev/null +++ b/pkg/health/testdata/crd-v1-terminating-progressing.yaml @@ -0,0 +1,57 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: examples.example.io + deletionTimestamp: '2024-12-28T13:51:19Z' +spec: + conversion: + strategy: None + group: example.io + names: + kind: Example + listKind: ExampleList + plural: examples + shortNames: + - ex + singular: example + preserveUnknownFields: true + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: >- + CreationTimestamp is a timestamp representing the server time when + this object was created. It is not guaranteed to be set in + happens-before order across separate operations. Clients may not set + this value. It is represented in RFC3339 form and is in UTC. + + + Populated by the system. Read-only. Null for lists. More info: + https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + served: true + storage: true + subresources: {} +status: + acceptedNames: + kind: Example + listKind: ExampleList + plural: examples + shortNames: + - ex + singular: example + conditions: + - lastTransitionTime: '2024-05-19T23:35:28Z' + message: no conflicts found + reason: NoConflicts + status: 'True' + type: NamesAccepted + - lastTransitionTime: '2024-05-19T23:35:28Z' + message: the initial names have been accepted + reason: InitialNamesAccepted + status: 'True' + type: Established + storedVersions: + - v1alpha1 \ No newline at end of file