diff --git a/pkg/controller/v1alpha2/core/scopes/healthscope/healthscope.go b/pkg/controller/v1alpha2/core/scopes/healthscope/healthscope.go index b7ecb2fb..8f703990 100644 --- a/pkg/controller/v1alpha2/core/scopes/healthscope/healthscope.go +++ b/pkg/controller/v1alpha2/core/scopes/healthscope/healthscope.go @@ -41,7 +41,7 @@ import ( const ( infoFmtUnknownWorkload = "APIVersion %v Kind %v workload is unknown for HealthScope " infoFmtReady = "Ready: %d/%d " - infoFmtNoChildRes = "cannot get child resource references of ContainerizedWorkload %v" + infoFmtNoChildRes = "cannot get child resource references of workload %v" errHealthCheck = "error occurs in health check " defaultTimeout = 10 * time.Second @@ -97,6 +97,7 @@ func CheckContainerziedWorkloadHealth(ctx context.Context, c client.Client, ref HealthStatus: StatusHealthy, TargetWorkload: ref, } + cwObj := corev1alpha2.ContainerizedWorkload{} cwObj.SetGroupVersionKind(corev1alpha2.SchemeGroupVersion.WithKind(kindContainerizedWorkload)) if err := c.Get(ctx, types.NamespacedName{Namespace: namespace, Name: ref.Name}, &cwObj); err != nil { @@ -104,19 +105,22 @@ func CheckContainerziedWorkloadHealth(ctx context.Context, c client.Client, ref r.Diagnosis = errors.Wrap(err, errHealthCheck).Error() return r } - r.ComponentName = getComponentNameFromLabel(&cwObj) r.TargetWorkload.UID = cwObj.GetUID() - subConditions := []*WorkloadHealthCondition{} childRefs := cwObj.Status.Resources + updateChildResourcesCondition(ctx, c, namespace, r, ref, childRefs) + return r +} + +func updateChildResourcesCondition(ctx context.Context, c client.Client, namespace string, r *WorkloadHealthCondition, ref runtimev1alpha1.TypedReference, childRefs []runtimev1alpha1.TypedReference) { + subConditions := []*WorkloadHealthCondition{} if len(childRefs) != 2 { // one deployment and one svc are required by containerizedworkload r.Diagnosis = fmt.Sprintf(infoFmtNoChildRes, ref.Name) r.HealthStatus = StatusUnhealthy - return r + return } - for _, childRef := range childRefs { switch childRef.Kind { case kindDeployment: @@ -145,7 +149,6 @@ func CheckContainerziedWorkloadHealth(ctx context.Context, c client.Client, ref } r.Diagnosis = fmt.Sprintf("%s%s", r.Diagnosis, sc.Diagnosis) } - return r } // CheckDeploymentHealth checks health condition of Deployment diff --git a/pkg/controller/v1alpha2/core/scopes/healthscope/healthscope_controller.go b/pkg/controller/v1alpha2/core/scopes/healthscope/healthscope_controller.go index fa666937..19bc8ed0 100644 --- a/pkg/controller/v1alpha2/core/scopes/healthscope/healthscope_controller.go +++ b/pkg/controller/v1alpha2/core/scopes/healthscope/healthscope_controller.go @@ -123,6 +123,7 @@ func NewReconciler(m ctrl.Manager, o ...ReconcilerOption) *Reconciler { record: event.NewNopRecorder(), traitChecker: WorkloadHealthCheckFn(CheckByHealthCheckTrait), checkers: []WorloadHealthChecker{ + WorkloadHealthCheckFn(CheckStandardContainerziedHealth), WorkloadHealthCheckFn(CheckContainerziedWorkloadHealth), WorkloadHealthCheckFn(CheckDeploymentHealth), WorkloadHealthCheckFn(CheckStatefulsetHealth), diff --git a/pkg/controller/v1alpha2/core/scopes/healthscope/standard.go b/pkg/controller/v1alpha2/core/scopes/healthscope/standard.go new file mode 100644 index 00000000..861b7112 --- /dev/null +++ b/pkg/controller/v1alpha2/core/scopes/healthscope/standard.go @@ -0,0 +1,55 @@ +package healthscope + +import ( + "context" + + runtimev1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1" + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + kuberuntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var ( + standardContainerziedGVK = schema.GroupVersionKind{ + Group: "standard.oam.dev", + Version: "v1alpha1", + Kind: "Containerized", + } +) + +// CheckStandardContainerziedHealth check health condition of containerizeds.standard.oam.dev +func CheckStandardContainerziedHealth(ctx context.Context, c client.Client, ref runtimev1alpha1.TypedReference, namespace string) *WorkloadHealthCondition { + if ref.GroupVersionKind() != standardContainerziedGVK { + return nil + } + r := &WorkloadHealthCondition{ + HealthStatus: StatusHealthy, + TargetWorkload: ref, + } + containerizedObj := unstructured.Unstructured{} + containerizedObj.SetGroupVersionKind(ref.GroupVersionKind()) + if err := c.Get(ctx, types.NamespacedName{Namespace: namespace, Name: ref.Name}, &containerizedObj); err != nil { + r.HealthStatus = StatusUnhealthy + r.Diagnosis = errors.Wrap(err, errHealthCheck).Error() + return r + } + r.ComponentName = getComponentNameFromLabel(&containerizedObj) + r.TargetWorkload.UID = containerizedObj.GetUID() + + childRefsData, _, _ := unstructured.NestedSlice(containerizedObj.Object, "status", "resources") + childRefs := []runtimev1alpha1.TypedReference{} + for _, v := range childRefsData { + v := v.(map[string]interface{}) + tmpChildRef := &runtimev1alpha1.TypedReference{} + if err := kuberuntime.DefaultUnstructuredConverter.FromUnstructured(v, tmpChildRef); err != nil { + r.HealthStatus = StatusUnhealthy + r.Diagnosis = errors.Wrap(err, errHealthCheck).Error() + } + childRefs = append(childRefs, *tmpChildRef) + } + updateChildResourcesCondition(ctx, c, namespace, r, ref, childRefs) + return r +} diff --git a/pkg/controller/v1alpha2/core/scopes/healthscope/standard_test.go b/pkg/controller/v1alpha2/core/scopes/healthscope/standard_test.go new file mode 100644 index 00000000..ca7f5813 --- /dev/null +++ b/pkg/controller/v1alpha2/core/scopes/healthscope/standard_test.go @@ -0,0 +1,135 @@ +package healthscope + +import ( + "context" + "testing" + + runtimev1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1" + "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/stretchr/testify/assert" + apps "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + + "github.com/crossplane/oam-kubernetes-runtime/pkg/oam/util" +) + +func TestCheckStandardContainerziedHealth(t *testing.T) { + mockClient := test.NewMockClient() + scRef := runtimev1alpha1.TypedReference{} + scRef.SetGroupVersionKind(standardContainerziedGVK) + + deployRef := runtimev1alpha1.TypedReference{} + deployRef.SetGroupVersionKind(apps.SchemeGroupVersion.WithKind(kindDeployment)) + svcRef := runtimev1alpha1.TypedReference{} + svcRef.SetGroupVersionKind(apps.SchemeGroupVersion.WithKind(kindService)) + + deployRefData, _ := util.Object2Map(deployRef) + svcRefData, _ := util.Object2Map(svcRef) + + scUnstructured := unstructured.Unstructured{} + scUnstructured.SetGroupVersionKind(standardContainerziedGVK) + unstructured.SetNestedSlice(scUnstructured.Object, []interface{}{deployRefData, svcRefData}, "status", "resources") + + tests := []struct { + caseName string + mockGetFn test.MockGetFn + wlRef runtimev1alpha1.TypedReference + expect *WorkloadHealthCondition + }{ + { + caseName: "not matched checker", + wlRef: runtimev1alpha1.TypedReference{}, + expect: nil, + }, + { + caseName: "healthy workload", + wlRef: scRef, + mockGetFn: func(ctx context.Context, key types.NamespacedName, obj runtime.Object) error { + if o, ok := obj.(*unstructured.Unstructured); ok { + *o = scUnstructured + return nil + } + if o, ok := obj.(*apps.Deployment); ok { + *o = apps.Deployment{ + Spec: apps.DeploymentSpec{ + Replicas: &varInt1, + }, + Status: apps.DeploymentStatus{ + ReadyReplicas: 1, // healthy + }, + } + } + return nil + }, + expect: &WorkloadHealthCondition{ + HealthStatus: StatusHealthy, + }, + }, + { + caseName: "unhealthy for deployment not ready", + wlRef: scRef, + mockGetFn: func(ctx context.Context, key types.NamespacedName, obj runtime.Object) error { + if o, ok := obj.(*unstructured.Unstructured); ok { + *o = scUnstructured + return nil + } + if o, ok := obj.(*apps.Deployment); ok { + *o = apps.Deployment{ + Spec: apps.DeploymentSpec{ + Replicas: &varInt1, + }, + Status: apps.DeploymentStatus{ + ReadyReplicas: 0, // unhealthy + }, + } + } + return nil + }, + expect: &WorkloadHealthCondition{ + HealthStatus: StatusUnhealthy, + }, + }, + { + caseName: "unhealthy for ContainerizedWorkload not found", + wlRef: scRef, + mockGetFn: func(ctx context.Context, key types.NamespacedName, obj runtime.Object) error { + return errMockErr + }, + expect: &WorkloadHealthCondition{ + HealthStatus: StatusUnhealthy, + }, + }, + { + caseName: "unhealthy for deployment not found", + wlRef: scRef, + mockGetFn: func(ctx context.Context, key types.NamespacedName, obj runtime.Object) error { + if o, ok := obj.(*unstructured.Unstructured); ok { + *o = scUnstructured + return nil + } + if _, ok := obj.(*apps.Deployment); ok { + return errMockErr + } + return nil + }, + expect: &WorkloadHealthCondition{ + HealthStatus: StatusUnhealthy, + }, + }, + } + + for _, tc := range tests { + func(t *testing.T) { + mockClient.MockGet = tc.mockGetFn + result := CheckStandardContainerziedHealth(ctx, mockClient, tc.wlRef, namespace) + if tc.expect == nil { + assert.Nil(t, result, tc.caseName) + } else { + assert.Equal(t, tc.expect.HealthStatus, result.HealthStatus, tc.caseName) + } + + }(t) + } +}