From 933343df101eb21ad4adc666a8c69304935e29c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E4=BC=9F=E7=A5=A5?= Date: Wed, 12 Jun 2024 11:50:18 +0800 Subject: [PATCH] fix deployment observed generation logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 孙伟祥 --- .../work/v1alpha2/well_known_constants.go | 11 ++++++ pkg/controllers/binding/common.go | 5 +++ .../default/native/aggregatestatus.go | 36 ++++++++++++------- .../default/native/reflectstatus.go | 26 ++++++++++---- .../default/native/status_type.go | 30 ++++++++++++++++ 5 files changed, 90 insertions(+), 18 deletions(-) create mode 100644 pkg/resourceinterpreter/default/native/status_type.go diff --git a/pkg/apis/work/v1alpha2/well_known_constants.go b/pkg/apis/work/v1alpha2/well_known_constants.go index 3aa122b79..746b318a3 100644 --- a/pkg/apis/work/v1alpha2/well_known_constants.go +++ b/pkg/apis/work/v1alpha2/well_known_constants.go @@ -60,6 +60,17 @@ const ( // BindingManagedByLabel is added to ResourceBinding to represent what kind of resource manages this Binding. BindingManagedByLabel = "binding.karmada.io/managed-by" + + // ResourceTemplateGenerationAnnotationKey records the generation of resource template in Karmada APIServer, + // It will be injected into the resource when propagating to member clusters, to denote the specific version of + // the resource template from which the resource is derived. It might be helpful in the following cases: + // 1. Facilitating observation from member clusters to ascertain if the most recent resource template has been + // completely synced. + // 2. The annotation will be synced back to Karmada during the process of syncing resource status, + // by leveraging this annotation, Karmada can infer if the most recent resource template has been completely + // synced on member clusters, then generates accurate observed generation(like Deployment's .status.observedGeneration) + // which might be required by the release system. + ResourceTemplateGenerationAnnotationKey = "resourcetemplate.karmada.io/generation" ) // Define resource conflict resolution diff --git a/pkg/controllers/binding/common.go b/pkg/controllers/binding/common.go index 0290156b7..d9eda632e 100644 --- a/pkg/controllers/binding/common.go +++ b/pkg/controllers/binding/common.go @@ -17,6 +17,8 @@ limitations under the License. package binding import ( + "strconv" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -168,6 +170,9 @@ func mergeLabel(workload *unstructured.Unstructured, binding metav1.Object, scop func mergeAnnotations(workload *unstructured.Unstructured, binding metav1.Object, scope apiextensionsv1.ResourceScope) map[string]string { annotations := make(map[string]string) + if workload.GetGeneration() > 0 { + util.MergeAnnotation(workload, workv1alpha2.ResourceTemplateGenerationAnnotationKey, strconv.FormatInt(workload.GetGeneration(), 10)) + } if scope == apiextensionsv1.NamespaceScoped { util.MergeAnnotation(workload, workv1alpha2.ResourceBindingNamespaceAnnotationKey, binding.GetNamespace()) diff --git a/pkg/resourceinterpreter/default/native/aggregatestatus.go b/pkg/resourceinterpreter/default/native/aggregatestatus.go index 561f75f02..3f7db8b5a 100644 --- a/pkg/resourceinterpreter/default/native/aggregatestatus.go +++ b/pkg/resourceinterpreter/default/native/aggregatestatus.go @@ -65,27 +65,39 @@ func aggregateDeploymentStatus(object *unstructured.Unstructured, aggregatedStat oldStatus := &deploy.Status newStatus := &appsv1.DeploymentStatus{} + observedLatestResourceTemplateGenerationCount := 0 for _, item := range aggregatedStatusItems { if item.Status == nil { continue } - temp := &appsv1.DeploymentStatus{} - if err = json.Unmarshal(item.Status.Raw, temp); err != nil { + member := &WrappedDeploymentStatus{} + if err = json.Unmarshal(item.Status.Raw, member); err != nil { return nil, err } klog.V(3).Infof("Grab deployment(%s/%s) status from cluster(%s), replicas: %d, ready: %d, updated: %d, available: %d, unavailable: %d", - deploy.Namespace, deploy.Name, item.ClusterName, temp.Replicas, temp.ReadyReplicas, temp.UpdatedReplicas, temp.AvailableReplicas, temp.UnavailableReplicas) + deploy.Namespace, deploy.Name, item.ClusterName, member.Replicas, member.ReadyReplicas, member.UpdatedReplicas, member.AvailableReplicas, member.UnavailableReplicas) - // always set 'observedGeneration' with current generation(.metadata.generation) - // which is the generation Karmada 'observed'. - // The 'observedGeneration' is mainly used by GitOps tools(like 'Argo CD') to assess the health status. - // For more details, please refer to https://argo-cd.readthedocs.io/en/stable/operator-manual/health/. + // `memberStatus.ObservedGeneration >= memberStatus.Generation` means the member's status corresponds the latest spec revision of the member deployment. + // `memberStatus.ResourceTemplateGeneration >= deploy.Generation` means the member deployment has been aligned with the latest spec revision of federated deployment. + // If both conditions are met, we consider the member's status corresponds the latest spec revision of federated deployment. + if member.ObservedGeneration >= member.Generation && + member.ResourceTemplateGeneration >= deploy.Generation { + observedLatestResourceTemplateGenerationCount++ + } + + newStatus.Replicas += member.Replicas + newStatus.ReadyReplicas += member.ReadyReplicas + newStatus.UpdatedReplicas += member.UpdatedReplicas + newStatus.AvailableReplicas += member.AvailableReplicas + newStatus.UnavailableReplicas += member.UnavailableReplicas + } + + // The 'observedGeneration' is mainly used by GitOps tools(like 'Argo CD') to assess the health status. + // For more details, please refer to https://argo-cd.readthedocs.io/en/stable/operator-manual/health/. + if observedLatestResourceTemplateGenerationCount == len(aggregatedStatusItems) { newStatus.ObservedGeneration = deploy.Generation - newStatus.Replicas += temp.Replicas - newStatus.ReadyReplicas += temp.ReadyReplicas - newStatus.UpdatedReplicas += temp.UpdatedReplicas - newStatus.AvailableReplicas += temp.AvailableReplicas - newStatus.UnavailableReplicas += temp.UnavailableReplicas + } else { + newStatus.ObservedGeneration = oldStatus.ObservedGeneration } if oldStatus.ObservedGeneration == newStatus.ObservedGeneration && diff --git a/pkg/resourceinterpreter/default/native/reflectstatus.go b/pkg/resourceinterpreter/default/native/reflectstatus.go index d2da22147..408a8040e 100644 --- a/pkg/resourceinterpreter/default/native/reflectstatus.go +++ b/pkg/resourceinterpreter/default/native/reflectstatus.go @@ -31,6 +31,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/klog/v2" + "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2" "github.com/karmada-io/karmada/pkg/util" "github.com/karmada-io/karmada/pkg/util/helper" ) @@ -68,12 +69,25 @@ func reflectDeploymentStatus(object *unstructured.Unstructured) (*runtime.RawExt return nil, fmt.Errorf("failed to convert DeploymentStatus from map[string]interface{}: %v", err) } - grabStatus := appsv1.DeploymentStatus{ - Replicas: deploymentStatus.Replicas, - UpdatedReplicas: deploymentStatus.UpdatedReplicas, - ReadyReplicas: deploymentStatus.ReadyReplicas, - AvailableReplicas: deploymentStatus.AvailableReplicas, - UnavailableReplicas: deploymentStatus.UnavailableReplicas, + resourceTemplateGenerationInt := int64(0) + resourceTemplateGenerationStr := util.GetAnnotationValue(object.GetAnnotations(), v1alpha2.ResourceTemplateGenerationAnnotationKey) + err = runtime.Convert_string_To_int64(&resourceTemplateGenerationStr, &resourceTemplateGenerationInt, nil) + if err != nil { + klog.Errorf("Failed to parse Deployment(%s/%s) generation from annotation(%s:%s): %v", object.GetNamespace(), object.GetName(), v1alpha2.ResourceTemplateGenerationAnnotationKey, resourceTemplateGenerationStr, err) + return nil, err + } + + grabStatus := &WrappedDeploymentStatus{ + Generation: object.GetGeneration(), + ResourceTemplateGeneration: resourceTemplateGenerationInt, + DeploymentStatus: appsv1.DeploymentStatus{ + Replicas: deploymentStatus.Replicas, + UpdatedReplicas: deploymentStatus.UpdatedReplicas, + ReadyReplicas: deploymentStatus.ReadyReplicas, + AvailableReplicas: deploymentStatus.AvailableReplicas, + UnavailableReplicas: deploymentStatus.UnavailableReplicas, + ObservedGeneration: deploymentStatus.ObservedGeneration, + }, } grabStatusRaw, err := helper.BuildStatusRawExtension(grabStatus) diff --git a/pkg/resourceinterpreter/default/native/status_type.go b/pkg/resourceinterpreter/default/native/status_type.go new file mode 100644 index 000000000..9ba1b42a6 --- /dev/null +++ b/pkg/resourceinterpreter/default/native/status_type.go @@ -0,0 +1,30 @@ +/* +Copyright 2024 The Karmada Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package native + +import appsv1 "k8s.io/api/apps/v1" + +// WrappedDeploymentStatus is a wrapper for appsv1.DeploymentStatus. +type WrappedDeploymentStatus struct { + appsv1.DeploymentStatus `json:",inline"` + + // Generation holds the generation(.metadata.generation) of resource running on member cluster. + Generation int64 `json:"generation,omitempty"` + // ResourceTemplateGeneration holds the value of annotation resourcetemplate.karmada.io/generation grabbed + // from resource running on member cluster. + ResourceTemplateGeneration int64 `json:"resourceTemplateGeneration,omitempty"` +}