Merge pull request #2329 from zhuwint/devhealthy

implement InterpretHealth for resource interpreter
This commit is contained in:
karmada-bot 2022-08-11 10:51:09 +08:00 committed by GitHub
commit 431e06a684
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 321 additions and 2 deletions

View File

@ -6,7 +6,7 @@ metadata:
webhooks:
- name: workloads.example.com
rules:
- operations: [ "InterpretReplica","ReviseReplica","Retain","AggregateStatus" ]
- operations: [ "InterpretReplica","ReviseReplica","Retain","AggregateStatus", "InterpretHealth" ]
apiGroups: [ "workload.example.io" ]
apiVersions: [ "v1alpha1" ]
kinds: [ "Workload" ]

View File

@ -7,6 +7,7 @@ import (
"net/http"
"k8s.io/klog/v2"
"k8s.io/utils/pointer"
workloadv1alpha1 "github.com/karmada-io/karmada/examples/customresourceinterpreter/apis/workload/v1alpha1"
configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1"
@ -41,6 +42,8 @@ func (e *workloadInterpreter) Handle(ctx context.Context, req interpreter.Reques
return e.responseWithExploreRetaining(workload, req)
case configv1alpha1.InterpreterOperationAggregateStatus:
return e.responseWithExploreAggregateStatus(workload, req)
case configv1alpha1.InterpreterOperationInterpretHealth:
return e.responseWithExploreInterpretHealth(workload)
default:
return interpreter.Errored(http.StatusBadRequest, fmt.Errorf("wrong request operation type: %s", req.Operation))
}
@ -109,3 +112,14 @@ func (e *workloadInterpreter) responseWithExploreAggregateStatus(workload *workl
}
return interpreter.PatchResponseFromRaw(req.Object.Raw, marshaledBytes)
}
func (e *workloadInterpreter) responseWithExploreInterpretHealth(workload *workloadv1alpha1.Workload) interpreter.Response {
healthy := pointer.Bool(false)
if workload.Status.ReadyReplicas == *workload.Spec.Replicas {
healthy = pointer.Bool(true)
}
res := interpreter.Succeeded("")
res.Healthy = healthy
return res
}

View File

@ -313,3 +313,18 @@ func (e *CustomizedInterpreter) ReflectStatus(ctx context.Context, attributes *w
return &response.RawStatus, matched, nil
}
// InterpretHealth returns the health state of the object.
// return matched value to indicate whether there is a matching hook.
func (e *CustomizedInterpreter) InterpretHealth(ctx context.Context, attributes *webhook.RequestAttributes) (healthy bool, matched bool, err error) {
var response *webhook.ResponseAttributes
response, matched, err = e.interpret(ctx, attributes)
if err != nil {
return
}
if !matched {
return
}
return response.Healthy, matched, nil
}

View File

@ -21,6 +21,7 @@ type DefaultInterpreter struct {
aggregateStatusHandlers map[schema.GroupVersionKind]aggregateStatusInterpreter
dependenciesHandlers map[schema.GroupVersionKind]dependenciesInterpreter
reflectStatusHandlers map[schema.GroupVersionKind]reflectStatusInterpreter
healthHandlers map[schema.GroupVersionKind]healthInterpreter
}
// NewDefaultInterpreter return a new DefaultInterpreter.
@ -32,6 +33,7 @@ func NewDefaultInterpreter() *DefaultInterpreter {
aggregateStatusHandlers: getAllDefaultAggregateStatusInterpreter(),
dependenciesHandlers: getAllDefaultDependenciesInterpreter(),
reflectStatusHandlers: getAllDefaultReflectStatusInterpreter(),
healthHandlers: getAllDefaultHealthInterpreter(),
}
}
@ -60,7 +62,8 @@ func (e *DefaultInterpreter) HookEnabled(kind schema.GroupVersionKind, operation
}
case configv1alpha1.InterpreterOperationInterpretStatus:
return true
case configv1alpha1.InterpreterOperationInterpretHealth:
return true
// TODO(RainbowMango): more cases should be added here
}
@ -123,3 +126,13 @@ func (e *DefaultInterpreter) ReflectStatus(object *unstructured.Unstructured) (s
// for resource types that don't have a build-in handler, try to collect the whole status from '.status' filed.
return reflectWholeStatus(object)
}
// InterpretHealth returns the health state of the object.
func (e *DefaultInterpreter) InterpretHealth(object *unstructured.Unstructured) (bool, error) {
handler, exist := e.healthHandlers[object.GroupVersionKind()]
if exist {
return handler(object)
}
return false, fmt.Errorf("default %s interpreter for %q not found", configv1alpha1.InterpreterOperationInterpretHealth, object.GroupVersionKind())
}

View File

@ -0,0 +1,41 @@
package defaultinterpreter
import (
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/karmada-io/karmada/pkg/util"
"github.com/karmada-io/karmada/pkg/util/helper"
)
type healthInterpreter func(object *unstructured.Unstructured) (bool, error)
func getAllDefaultHealthInterpreter() map[schema.GroupVersionKind]healthInterpreter {
s := make(map[schema.GroupVersionKind]healthInterpreter)
s[appsv1.SchemeGroupVersion.WithKind(util.DeploymentKind)] = interpretDeploymentHealth
s[appsv1.SchemeGroupVersion.WithKind(util.StatefulSetKind)] = interpretStatefulSetHealth
return s
}
func interpretDeploymentHealth(object *unstructured.Unstructured) (bool, error) {
deploy := &appsv1.Deployment{}
err := helper.ConvertToTypedObject(object, deploy)
if err != nil {
return false, err
}
healthy := (deploy.Status.UpdatedReplicas == *deploy.Spec.Replicas) && (deploy.Generation == deploy.Status.ObservedGeneration)
return healthy, nil
}
func interpretStatefulSetHealth(object *unstructured.Unstructured) (bool, error) {
statefulSet := &appsv1.StatefulSet{}
err := helper.ConvertToTypedObject(object, &statefulSet)
if err != nil {
return false, err
}
healthy := (statefulSet.Status.UpdatedReplicas == *statefulSet.Spec.Replicas) && (statefulSet.Generation == statefulSet.Status.ObservedGeneration)
return healthy, nil
}

View File

@ -0,0 +1,213 @@
package defaultinterpreter
import (
"testing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func Test_interpretDeploymentHealth(t *testing.T) {
tests := []struct {
name string
object *unstructured.Unstructured
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{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "fake-deployment",
"generation": 1,
},
"spec": map[string]interface{}{
"replicas": 3,
},
"status": map[string]interface{}{
"updatedReplicas": 3,
"observedGeneration": 1,
},
},
},
want: true,
wantErr: false,
},
{
name: "generation not equal to observedGeneration",
object: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "fake-deployment",
"generation": 1,
},
"spec": map[string]interface{}{
"replicas": 3,
},
"status": map[string]interface{}{
"updatedReplicas": 3,
"observedGeneration": 2,
},
},
},
want: false,
wantErr: false,
},
{
name: "replicas not equal to updatedReplicas",
object: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "fake-deployment",
"generation": 1,
},
"spec": map[string]interface{}{
"replicas": 3,
},
"status": map[string]interface{}{
"updatedReplicas": 1,
"observedGeneration": 1,
},
},
},
want: false,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := interpretDeploymentHealth(tt.object)
if (err != nil) != tt.wantErr {
t.Errorf("interpretDeploymentHealth() err = %v, wantErr %v", err, tt.wantErr)
}
if got != tt.want {
t.Errorf("interpretDeploymentHealth() got = %v, want %v", got, tt.want)
}
})
}
}
func Test_interpretStatefulSetHealth(t *testing.T) {
tests := []struct {
name string
object *unstructured.Unstructured
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{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "StatefulSet",
"metadata": map[string]interface{}{
"name": "fake-statefulSet",
"generation": 1,
},
"spec": map[string]interface{}{
"replicas": 3,
},
"status": map[string]interface{}{
"updatedReplicas": 3,
"observedGeneration": 1,
},
},
},
want: true,
wantErr: false,
},
{
name: "generation not equal to observedGeneration",
object: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "StatefulSet",
"metadata": map[string]interface{}{
"name": "fake-statefulSet",
"generation": 1,
},
"spec": map[string]interface{}{
"replicas": 3,
},
"status": map[string]interface{}{
"updatedReplicas": 3,
"observedGeneration": 2,
},
},
},
want: false,
wantErr: false,
},
{
name: "replicas not equal to updatedReplicas",
object: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "StatefulSet",
"metadata": map[string]interface{}{
"name": "fake-statefulSet",
"generation": 1,
},
"spec": map[string]interface{}{
"replicas": 3,
},
"status": map[string]interface{}{
"updatedReplicas": 1,
"observedGeneration": 1,
},
},
},
want: false,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := interpretStatefulSetHealth(tt.object)
if (err != nil) != tt.wantErr {
t.Errorf("interpretStatefulSetHealth() err = %v, wantErr %v", err, tt.wantErr)
}
if got != tt.want {
t.Errorf("interpretStatefulSetHealth() got = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -42,6 +42,9 @@ type ResourceInterpreter interface {
// ReflectStatus returns the status of the object.
ReflectStatus(object *unstructured.Unstructured) (status *runtime.RawExtension, err error)
// InterpretHealth returns the health state of the object.
InterpretHealth(object *unstructured.Unstructured) (healthy bool, err error)
// other common method
}
@ -198,3 +201,22 @@ func (i *customResourceInterpreterImpl) ReflectStatus(object *unstructured.Unstr
status, err = i.defaultInterpreter.ReflectStatus(object)
return
}
// InterpretHealth returns the health state of the object.
func (i *customResourceInterpreterImpl) InterpretHealth(object *unstructured.Unstructured) (healthy bool, err error) {
klog.V(4).Infof("Begin to check health for object: %v %s/%s.", object.GroupVersionKind(), object.GetNamespace(), object.GetName())
healthy, hookEnabled, err := i.customizedInterpreter.InterpretHealth(context.TODO(), &webhook.RequestAttributes{
Operation: configv1alpha1.InterpreterOperationInterpretHealth,
Object: object,
})
if err != nil {
return
}
if hookEnabled {
return
}
healthy, err = i.defaultInterpreter.InterpretHealth(object)
return
}

View File

@ -69,6 +69,7 @@ var supportedInterpreterOperation = sets.NewString(
string(configv1alpha1.InterpreterOperationRetain),
string(configv1alpha1.InterpreterOperationAggregateStatus),
string(configv1alpha1.InterpreterOperationInterpretStatus),
string(configv1alpha1.InterpreterOperationInterpretHealth),
)
var acceptedInterpreterContextVersions = []string{configv1alpha1.GroupVersion.Version}