diff --git a/pkg/resourceinterpreter/defaultinterpreter/healthy.go b/pkg/resourceinterpreter/defaultinterpreter/healthy.go index 573fd2a01..b8f5c95c6 100644 --- a/pkg/resourceinterpreter/defaultinterpreter/healthy.go +++ b/pkg/resourceinterpreter/defaultinterpreter/healthy.go @@ -2,6 +2,8 @@ package defaultinterpreter import ( appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" @@ -15,6 +17,11 @@ func getAllDefaultHealthInterpreter() map[schema.GroupVersionKind]healthInterpre s := make(map[schema.GroupVersionKind]healthInterpreter) s[appsv1.SchemeGroupVersion.WithKind(util.DeploymentKind)] = interpretDeploymentHealth s[appsv1.SchemeGroupVersion.WithKind(util.StatefulSetKind)] = interpretStatefulSetHealth + s[appsv1.SchemeGroupVersion.WithKind(util.ReplicaSetKind)] = interpretReplicaSetHealth + s[appsv1.SchemeGroupVersion.WithKind(util.DaemonSetKind)] = interpretDaemonSetHealth + s[corev1.SchemeGroupVersion.WithKind(util.ServiceKind)] = interpretServiceHealth + s[networkingv1.SchemeGroupVersion.WithKind(util.IngressKind)] = interpretIngressHealth + s[corev1.SchemeGroupVersion.WithKind(util.PersistentVolumeClaimKind)] = interpretPersistentVolumeClaimHealth return s } @@ -31,7 +38,7 @@ func interpretDeploymentHealth(object *unstructured.Unstructured) (bool, error) func interpretStatefulSetHealth(object *unstructured.Unstructured) (bool, error) { statefulSet := &appsv1.StatefulSet{} - err := helper.ConvertToTypedObject(object, &statefulSet) + err := helper.ConvertToTypedObject(object, statefulSet) if err != nil { return false, err } @@ -39,3 +46,85 @@ func interpretStatefulSetHealth(object *unstructured.Unstructured) (bool, error) healthy := (statefulSet.Status.UpdatedReplicas == *statefulSet.Spec.Replicas) && (statefulSet.Generation == statefulSet.Status.ObservedGeneration) return healthy, nil } + +func interpretReplicaSetHealth(object *unstructured.Unstructured) (bool, error) { + replicaSet := &appsv1.ReplicaSet{} + err := helper.ConvertToTypedObject(object, replicaSet) + if err != nil { + return false, err + } + + if replicaSet.Generation != replicaSet.Status.ObservedGeneration { + return false, nil + } + if replicaSet.Spec.Replicas != nil && replicaSet.Status.AvailableReplicas < *replicaSet.Spec.Replicas { + return false, nil + } + return true, nil +} + +func interpretDaemonSetHealth(object *unstructured.Unstructured) (bool, error) { + daemonSet := &appsv1.DaemonSet{} + err := helper.ConvertToTypedObject(object, daemonSet) + if err != nil { + return false, err + } + + if daemonSet.Generation != daemonSet.Status.ObservedGeneration { + return false, nil + } + if daemonSet.Status.UpdatedNumberScheduled < daemonSet.Status.DesiredNumberScheduled { + return false, nil + } + if daemonSet.Status.NumberAvailable < daemonSet.Status.DesiredNumberScheduled { + return false, nil + } + + return true, nil +} + +func interpretServiceHealth(object *unstructured.Unstructured) (bool, error) { + service := &corev1.Service{} + err := helper.ConvertToTypedObject(object, service) + if err != nil { + return false, err + } + + if service.Spec.Type != corev1.ServiceTypeLoadBalancer { + return true, nil + } + + for _, ingress := range service.Status.LoadBalancer.Ingress { + if ingress.Hostname != "" || ingress.IP != "" { + return true, nil + } + } + + return false, nil +} + +func interpretIngressHealth(object *unstructured.Unstructured) (bool, error) { + ingress := &networkingv1.Ingress{} + err := helper.ConvertToTypedObject(object, ingress) + if err != nil { + return false, err + } + + for _, ing := range ingress.Status.LoadBalancer.Ingress { + if ing.Hostname != "" || ing.IP != "" { + return true, nil + } + } + + return false, nil +} + +func interpretPersistentVolumeClaimHealth(object *unstructured.Unstructured) (bool, error) { + pvc := &corev1.PersistentVolumeClaim{} + err := helper.ConvertToTypedObject(object, pvc) + if err != nil { + return false, err + } + + return pvc.Status.Phase == corev1.ClaimBound, nil +} diff --git a/pkg/resourceinterpreter/defaultinterpreter/healthy_test.go b/pkg/resourceinterpreter/defaultinterpreter/healthy_test.go index 85d0d2399..645bb2041 100644 --- a/pkg/resourceinterpreter/defaultinterpreter/healthy_test.go +++ b/pkg/resourceinterpreter/defaultinterpreter/healthy_test.go @@ -3,6 +3,7 @@ package defaultinterpreter import ( "testing" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) @@ -13,22 +14,6 @@ func Test_interpretDeploymentHealth(t *testing.T) { want bool wantErr bool }{ - { - name: "failed convert to Deployment object", - object: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "name": "fake-deployment", - }, - "spec": "format error", - "status": "format error", - }, - }, - want: false, - wantErr: true, - }, { name: "deployment healthy", object: &unstructured.Unstructured{ @@ -116,22 +101,6 @@ func Test_interpretStatefulSetHealth(t *testing.T) { want bool wantErr bool }{ - { - name: "failed convert to StatefulSet object", - object: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "StatefulSet", - "metadata": map[string]interface{}{ - "name": "fake-statefulSet", - }, - "spec": "format error", - "status": "format error", - }, - }, - want: false, - wantErr: true, - }, { name: "statefulSet healthy", object: &unstructured.Unstructured{ @@ -211,3 +180,302 @@ func Test_interpretStatefulSetHealth(t *testing.T) { }) } } + +func Test_interpretReplicaSetHealth(t *testing.T) { + tests := []struct { + name string + object *unstructured.Unstructured + want bool + wantErr bool + }{ + { + name: "replicaSet healthy", + object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "generation": 1, + }, + "status": map[string]interface{}{ + "observedGeneration": 1, + }, + }, + }, + want: true, + wantErr: false, + }, + { + name: "generation not equal to observedGeneration", + object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "generation": 1, + }, + "status": map[string]interface{}{ + "observedGeneration": 2, + }, + }, + }, + want: false, + wantErr: false, + }, + { + name: "replicas not equal to availableReplicas", + object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "replicas": 3, + }, + "status": map[string]interface{}{ + "availableReplicas": 2, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := interpretReplicaSetHealth(tt.object) + if (err != nil) != tt.wantErr { + t.Errorf("interpretReplicaSetHealth() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("interpretReplicaSetHealth() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_interpretDaemonSetHealth(t *testing.T) { + tests := []struct { + name string + object *unstructured.Unstructured + want bool + wantErr bool + }{ + { + name: "daemonSet healthy", + object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "generation": 1, + }, + "status": map[string]interface{}{ + "observedGeneration": 1, + }, + }, + }, + want: true, + wantErr: false, + }, + { + name: "generation not equal to observedGeneration", + object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "generation": 1, + }, + "status": map[string]interface{}{ + "observedGeneration": 2, + }, + }, + }, + want: false, + wantErr: false, + }, + { + name: "updatedNumberScheduled < desiredNumberSchedulerd", + object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "status": map[string]interface{}{ + "updatedNumberScheduled": 3, + "desiredNumberScheduled": 5, + }, + }, + }, + want: false, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := interpretDaemonSetHealth(tt.object) + if (err != nil) != tt.wantErr { + t.Errorf("interpretDaemonSetHealth() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("interpretDaemonSetHealth() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_interpretServiceHealth(t *testing.T) { + tests := []struct { + name string + object *unstructured.Unstructured + want bool + wantErr bool + }{ + { + name: "service healthy", + object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "type": corev1.ServiceTypeLoadBalancer, + }, + "status": map[string]interface{}{ + "loadBalancer": map[string]interface{}{ + "ingress": []map[string]interface{}{{"ip": "localhost"}}, + }, + }, + }, + }, + want: true, + wantErr: false, + }, + { + name: "not loadBalancer type", + object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "type": corev1.ServiceTypeNodePort, + }, + "status": map[string]interface{}{ + "loadBalancer": map[string]interface{}{ + "ingress": []map[string]interface{}{{"ip": "localhost"}}, + }, + }, + }, + }, + want: true, + wantErr: false, + }, + { + name: "status.loadBalancer.ingress list is empty", + object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "type": corev1.ServiceTypeLoadBalancer, + }, + "status": map[string]interface{}{ + "loadBalancer": map[string]interface{}{ + "ingress": []map[string]interface{}{}, + }, + }, + }, + }, + want: false, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := interpretServiceHealth(tt.object) + if (err != nil) != tt.wantErr { + t.Errorf("interpretServiceHealth() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("interpretServiceHealth() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_interpretIngressHealth(t *testing.T) { + tests := []struct { + name string + object *unstructured.Unstructured + want bool + wantErr bool + }{ + { + name: "ingress healthy", + object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "status": map[string]interface{}{ + "loadBalancer": map[string]interface{}{ + "ingress": []map[string]interface{}{{"ip": "localhost"}}, + }, + }, + }, + }, + want: true, + wantErr: false, + }, + { + name: "status.loadBalancer.ingress list is empty", + object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "status": map[string]interface{}{ + "loadBalancer": map[string]interface{}{ + "ingress": []map[string]interface{}{}, + }, + }, + }, + }, + want: false, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := interpretIngressHealth(tt.object) + if (err != nil) != tt.wantErr { + t.Errorf("interpretIngressHealth() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("interpretIngressHealth() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_interpretPersistentVolumeClaimHealth(t *testing.T) { + tests := []struct { + name string + object *unstructured.Unstructured + want bool + wantErr bool + }{ + { + name: "persistentVolumeClaim healthy", + object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "status": map[string]interface{}{ + "phase": corev1.ClaimBound, + }, + }, + }, + want: true, + wantErr: false, + }, + { + name: "status.phase not equals to Bound", + object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "status": map[string]interface{}{ + "phase": corev1.ClaimPending, + }, + }, + }, + want: false, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := interpretPersistentVolumeClaimHealth(tt.object) + if (err != nil) != tt.wantErr { + t.Errorf("interpretPersistentVolumeClaimHealth() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("interpretPersistentVolumeClaimHealth() = %v, want %v", got, tt.want) + } + }) + } +}