diff --git a/pkg/controllers/common/constants.go b/pkg/controllers/common/constants.go index 3833dd5e..8e8ed27d 100644 --- a/pkg/controllers/common/constants.go +++ b/pkg/controllers/common/constants.go @@ -121,6 +121,9 @@ const ( MigrationConfigurationAnnotation = DefaultPrefix + "migration-configuration" // AppliedMigrationConfigurationAnnotation contains the applied custom migration configuration. AppliedMigrationConfigurationAnnotation = DefaultPrefix + "applied-migration-configuration" + // DryRunAnnotation indicates resource is in dry run process. It will prevent new resources from being dispatched by the sync controller. + // It works only when resources have not been propagated to member clusters. + DryRunAnnotation = DefaultPrefix + "dry-run" ) // PropagatedAnnotationKeys and PropagatedLabelKeys are used to store the keys of annotations and labels that are present diff --git a/pkg/controllers/federate/util.go b/pkg/controllers/federate/util.go index 4a281ae5..bff5261f 100644 --- a/pkg/controllers/federate/util.go +++ b/pkg/controllers/federate/util.go @@ -266,12 +266,14 @@ var ( common.FollowersAnnotation, common.DisableFollowingAnnotation, common.MigrationConfigurationAnnotation, + common.DryRunAnnotation, ) // List of annotations that should be ignored on the source object ignoredAnnotationSet = sets.New( common.LatestReplicasetDigestsAnnotation, common.AppliedMigrationConfigurationAnnotation, + scheduler.SchedulingAnnotation, ) federatedLabelSet = sets.New[string]( diff --git a/pkg/controllers/scheduler/scheduler.go b/pkg/controllers/scheduler/scheduler.go index e2a5e4e2..ff6a8d2e 100644 --- a/pkg/controllers/scheduler/scheduler.go +++ b/pkg/controllers/scheduler/scheduler.go @@ -770,6 +770,15 @@ func (s *Scheduler) applySchedulingResult( } } + if objectModified { + value, err := getSchedulingAnnotationValue(fedObject, result) + if err != nil { + return false, err + } + annotations[SchedulingAnnotation] = value + annotationsModified = true + } + if annotationsModified { fedObject.SetAnnotations(annotations) objectModified = true diff --git a/pkg/controllers/scheduler/schedulingannotation.go b/pkg/controllers/scheduler/schedulingannotation.go new file mode 100644 index 00000000..bb36c9ab --- /dev/null +++ b/pkg/controllers/scheduler/schedulingannotation.go @@ -0,0 +1,66 @@ +/* +Copyright 2023 The KubeAdmiral 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 scheduler + +import ( + "encoding/json" + + "k8s.io/klog/v2" + + fedcorev1a1 "github.com/kubewharf/kubeadmiral/pkg/apis/core/v1alpha1" + "github.com/kubewharf/kubeadmiral/pkg/controllers/common" + "github.com/kubewharf/kubeadmiral/pkg/controllers/scheduler/core" +) + +var SchedulingAnnotation = common.DefaultPrefix + "scheduling" + +type Scheduling struct { + // SourceGeneration is the generation of the source object + // observed in the federated object when this placement is sampled. + SourceGeneration int64 `json:"sourceGeneration"` + + // FederatedGeneration is the generation of the federated object + // observed when this placement is sampled. + FederatedGeneration int64 `json:"fedGeneration"` + + // Placement contains a list of FederatedCluster object names and replicas on them. + Placement map[string]*int64 `json:"placement,omitempty"` +} + +func getSchedulingAnnotationValue( + fedObject fedcorev1a1.GenericFederatedObject, + result core.ScheduleResult, +) (value string, err error) { + scheduling := Scheduling{} + + srcMeta, err := fedObject.GetSpec().GetTemplateMetadata() + if err != nil { + return "", err + } + + scheduling.SourceGeneration = srcMeta.GetGeneration() + scheduling.FederatedGeneration = fedObject.GetGeneration() + scheduling.Placement = result.SuggestedClusters + + jsonBuf, err := json.Marshal(scheduling) + if err != nil { + klog.Errorf("Cannot marshal JSON: %v", err) + return "", err + } + + return string(jsonBuf), nil +} diff --git a/pkg/controllers/scheduler/schedulingtriggers.go b/pkg/controllers/scheduler/schedulingtriggers.go index 277f0538..e7166940 100644 --- a/pkg/controllers/scheduler/schedulingtriggers.go +++ b/pkg/controllers/scheduler/schedulingtriggers.go @@ -304,6 +304,7 @@ var knownSchedulingAnnotations = sets.New( MaxClustersAnnotations, FollowsObjectAnnotation, common.AppliedMigrationConfigurationAnnotation, + common.DryRunAnnotation, ) func getSchedulingAnnotationsHash(fedObject fedcorev1a1.GenericFederatedObject) (string, error) { diff --git a/pkg/controllers/statusaggregator/controller.go b/pkg/controllers/statusaggregator/controller.go index 9ac4f27c..3db3b85f 100644 --- a/pkg/controllers/statusaggregator/controller.go +++ b/pkg/controllers/statusaggregator/controller.go @@ -40,6 +40,7 @@ import ( fedcorev1a1informers "github.com/kubewharf/kubeadmiral/pkg/client/informers/externalversions/core/v1alpha1" "github.com/kubewharf/kubeadmiral/pkg/controllers/federate" "github.com/kubewharf/kubeadmiral/pkg/controllers/statusaggregator/plugins" + "github.com/kubewharf/kubeadmiral/pkg/controllers/statusaggregator/plugins/sourcefeedback" "github.com/kubewharf/kubeadmiral/pkg/stats" "github.com/kubewharf/kubeadmiral/pkg/stats/metrics" clusterutil "github.com/kubewharf/kubeadmiral/pkg/util/cluster" @@ -342,16 +343,7 @@ func (a *StatusAggregator) reconcile(ctx context.Context, key reconcileKey) (sta return worker.StatusError } - needUpdate := false - for _, plugin := range plugins.DefaultCommonPlugins { - newObj, newNeedUpdate, err := plugin.AggregateStatuses(ctx, sourceObject, fedObject, clusterObjs, clusterObjsUpToDate) - if err != nil { - return worker.StatusError - } - sourceObject = newObj - needUpdate = needUpdate || newNeedUpdate - } - + needUpdate := sourcefeedback.PopulateAnnotations(sourceObject, fedObject) if needUpdate { logger.V(1).Info("Updating metadata of source object") _, err = a.dynamicClient.Resource(ftc.GetSourceTypeGVR()).Namespace(key.namespace). diff --git a/pkg/controllers/statusaggregator/plugins/common/migrationconfig.go b/pkg/controllers/statusaggregator/plugins/common/migrationconfig.go deleted file mode 100644 index 2b671fee..00000000 --- a/pkg/controllers/statusaggregator/plugins/common/migrationconfig.go +++ /dev/null @@ -1,73 +0,0 @@ -/* -Copyright 2023 The KubeAdmiral 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 common - -import ( - "context" - "reflect" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - - fedcorev1a1 "github.com/kubewharf/kubeadmiral/pkg/apis/core/v1alpha1" - "github.com/kubewharf/kubeadmiral/pkg/controllers/common" -) - -type MigrationConfigPlugin struct{} - -func NewMigrationConfigPlugin() *MigrationConfigPlugin { - return &MigrationConfigPlugin{} -} - -// TODO: Create a new interface that is different from the original `AggregateStatuses` -func (receiver *MigrationConfigPlugin) AggregateStatuses( - ctx context.Context, - sourceObject *unstructured.Unstructured, - fedObject fedcorev1a1.GenericFederatedObject, - clusterObjs map[string]interface{}, - clusterObjsUpToDate bool, -) (*unstructured.Unstructured, bool, error) { - needUpdate := false - - fedObjMigrationConfig, fedAnnotationExists := fedObject.GetAnnotations()[common.AppliedMigrationConfigurationAnnotation] - if sourceObject.GetAnnotations() == nil { - if fedAnnotationExists { - newAnnotation := make(map[string]string, 1) - newAnnotation[common.AppliedMigrationConfigurationAnnotation] = fedObjMigrationConfig - sourceObject.SetAnnotations(newAnnotation) - needUpdate = true - } - return sourceObject, needUpdate, nil - } - sourceObjMigrationConfig, sourceAnnotationExists := sourceObject.GetAnnotations()[common.AppliedMigrationConfigurationAnnotation] - - // update annotations of source object if needed - if !fedAnnotationExists && sourceAnnotationExists { - updatedAnnotation := sourceObject.GetAnnotations() - delete(updatedAnnotation, common.AppliedMigrationConfigurationAnnotation) - sourceObject.SetAnnotations(updatedAnnotation) - needUpdate = true - } - - if fedAnnotationExists && !reflect.DeepEqual(sourceObjMigrationConfig, fedObjMigrationConfig) { - updatedAnnotation := sourceObject.GetAnnotations() - updatedAnnotation[common.AppliedMigrationConfigurationAnnotation] = fedObjMigrationConfig - sourceObject.SetAnnotations(updatedAnnotation) - needUpdate = true - } - - return sourceObject, needUpdate, nil -} diff --git a/pkg/controllers/statusaggregator/plugins/common/migrationconfig_test.go b/pkg/controllers/statusaggregator/plugins/common/migrationconfig_test.go deleted file mode 100644 index e8c8d137..00000000 --- a/pkg/controllers/statusaggregator/plugins/common/migrationconfig_test.go +++ /dev/null @@ -1,132 +0,0 @@ -/* -Copyright 2023 The KubeAdmiral 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 common - -import ( - "context" - "reflect" - "testing" - - batchv1 "k8s.io/api/batch/v1" - 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" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/klog/v2" - - fedcorev1a1 "github.com/kubewharf/kubeadmiral/pkg/apis/core/v1alpha1" - "github.com/kubewharf/kubeadmiral/pkg/controllers/common" -) - -const testValue = "{\"unavailableClusters\":[{\"cluster\":\"kubeadmiral-member-1\",\"validUntil\":\"2023-11-24T07:12:52Z\"}]}" - -func generateFedObj(workload *unstructured.Unstructured) *fedcorev1a1.FederatedObject { - rawTargetTemplate, _ := workload.MarshalJSON() - return &fedcorev1a1.FederatedObject{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{}, - Generation: 1, - }, - Spec: fedcorev1a1.GenericFederatedObjectSpec{ - Template: apiextensionsv1.JSON{Raw: rawTargetTemplate}, - }, - } -} - -func TestMigrationConfigPlugin(t *testing.T) { - jobWithoutAnno := &batchv1.Job{ - ObjectMeta: metav1.ObjectMeta{ - Name: "completed", - Namespace: "default", - Annotations: map[string]string{}, - }, - Status: batchv1.JobStatus{ - Active: 1, - Succeeded: 1, - Failed: 1, - }, - } - jobWithAnno := jobWithoutAnno.DeepCopy() - jobWithAnno.Annotations[common.AppliedMigrationConfigurationAnnotation] = testValue - jobEmptyAnnos := jobWithoutAnno.DeepCopy() - jobEmptyAnnos.Annotations = nil - - u1, err := runtime.DefaultUnstructuredConverter.ToUnstructured(jobWithoutAnno) - if err != nil { - t.Fatalf(err.Error()) - } - unstructuredJobWithoutAnno := &unstructured.Unstructured{Object: u1} - fedObjWithoutAnno := generateFedObj(unstructuredJobWithoutAnno) - - u2, err := runtime.DefaultUnstructuredConverter.ToUnstructured(jobWithAnno) - if err != nil { - t.Fatalf(err.Error()) - } - unstructuredJobWithAnno := &unstructured.Unstructured{Object: u2} - fedObjWithAnno := fedObjWithoutAnno.DeepCopy() - fedObjWithAnno.Annotations[common.AppliedMigrationConfigurationAnnotation] = "" - - u3, err := runtime.DefaultUnstructuredConverter.ToUnstructured(jobEmptyAnnos) - if err != nil { - t.Fatalf(err.Error()) - } - unstructuredJobEmptyAnnos := &unstructured.Unstructured{Object: u3} - - ctx := klog.NewContext(context.Background(), klog.Background()) - receiver := NewMigrationConfigPlugin() - - tests := []struct { - name string - sourceObj *unstructured.Unstructured - fedObj *fedcorev1a1.FederatedObject - wantNeedUpdate bool - }{ - { - name: "sourceObj without anno, fedObj with anno", - sourceObj: unstructuredJobWithoutAnno, - fedObj: fedObjWithAnno, - wantNeedUpdate: true, - }, - { - name: "sourceObj with empty anno, fedObj with anno", - sourceObj: unstructuredJobEmptyAnnos, - fedObj: fedObjWithAnno, - wantNeedUpdate: true, - }, - { - name: "sourceObj with anno, fedObj with anno", - sourceObj: unstructuredJobWithAnno, - fedObj: fedObjWithAnno, - wantNeedUpdate: true, - }, - { - name: "sourceObj with anno, fedObj without anno", - sourceObj: unstructuredJobWithAnno, - fedObj: fedObjWithoutAnno, - wantNeedUpdate: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, needUpdate, _ := receiver.AggregateStatuses(ctx, tt.sourceObj, tt.fedObj, nil, false) - if !reflect.DeepEqual(needUpdate, tt.wantNeedUpdate) { - t.Errorf("AggregateStatuses = %v, want %v", needUpdate, tt.wantNeedUpdate) - } - }) - } -} diff --git a/pkg/controllers/statusaggregator/plugins/plugin.go b/pkg/controllers/statusaggregator/plugins/plugin.go index dca7b66c..587addaa 100644 --- a/pkg/controllers/statusaggregator/plugins/plugin.go +++ b/pkg/controllers/statusaggregator/plugins/plugin.go @@ -27,7 +27,6 @@ import ( fedcorev1a1 "github.com/kubewharf/kubeadmiral/pkg/apis/core/v1alpha1" "github.com/kubewharf/kubeadmiral/pkg/controllers/common" - plugincommon "github.com/kubewharf/kubeadmiral/pkg/controllers/statusaggregator/plugins/common" ) type Plugin interface { @@ -48,10 +47,6 @@ var pluginsMap = map[schema.GroupVersionKind]Plugin{ corev1.SchemeGroupVersion.WithKind(common.PodKind): NewPodPlugin(), } -var DefaultCommonPlugins = []Plugin{ - plugincommon.NewMigrationConfigPlugin(), -} - func GetPlugin(gvk schema.GroupVersionKind) Plugin { return pluginsMap[gvk] } diff --git a/pkg/controllers/statusaggregator/plugins/sourcefeedback/annotations.go b/pkg/controllers/statusaggregator/plugins/sourcefeedback/annotations.go new file mode 100644 index 00000000..86094b3d --- /dev/null +++ b/pkg/controllers/statusaggregator/plugins/sourcefeedback/annotations.go @@ -0,0 +1,77 @@ +/* +Copyright 2023 The KubeAdmiral 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 sourcefeedback + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + fedcorev1a1 "github.com/kubewharf/kubeadmiral/pkg/apis/core/v1alpha1" + "github.com/kubewharf/kubeadmiral/pkg/controllers/common" + "github.com/kubewharf/kubeadmiral/pkg/controllers/scheduler" +) + +var knownAnnotations = []string{ + common.AppliedMigrationConfigurationAnnotation, + scheduler.SchedulingAnnotation, +} + +func PopulateAnnotations( + sourceObject *unstructured.Unstructured, + fedObject fedcorev1a1.GenericFederatedObject, +) bool { + sourceAnnotations := sourceObject.GetAnnotations() + needUpdate := false + for _, annotation := range knownAnnotations { + newSourceAnnotations, newNeedUpdate := populateAnnotation(sourceAnnotations, fedObject.GetAnnotations(), annotation) + sourceAnnotations = newSourceAnnotations + needUpdate = needUpdate || newNeedUpdate + } + sourceObject.SetAnnotations(sourceAnnotations) + return needUpdate +} + +func populateAnnotation( + sourceAnnotations map[string]string, + fedAnnotations map[string]string, + key string, +) (map[string]string, bool) { + needUpdate := false + fedValue, fedAnnotationExists := fedAnnotations[key] + if sourceAnnotations == nil { + if fedAnnotationExists { + newAnnotation := make(map[string]string, 1) + newAnnotation[key] = fedValue + sourceAnnotations = newAnnotation + needUpdate = true + } + return sourceAnnotations, needUpdate + } + sourceValue, sourceAnnotationExists := sourceAnnotations[key] + + // update annotations of source object if needed + if !fedAnnotationExists && sourceAnnotationExists { + delete(sourceAnnotations, key) + needUpdate = true + } + + if fedAnnotationExists && sourceValue != fedValue { + sourceAnnotations[key] = fedValue + needUpdate = true + } + + return sourceAnnotations, needUpdate +} diff --git a/pkg/controllers/statusaggregator/plugins/sourcefeedback/annotations_test.go b/pkg/controllers/statusaggregator/plugins/sourcefeedback/annotations_test.go new file mode 100644 index 00000000..a1f49298 --- /dev/null +++ b/pkg/controllers/statusaggregator/plugins/sourcefeedback/annotations_test.go @@ -0,0 +1,183 @@ +/* +Copyright 2023 The KubeAdmiral 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 sourcefeedback + +import ( + "reflect" + "testing" + + batchv1 "k8s.io/api/batch/v1" + 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" + "k8s.io/apimachinery/pkg/runtime" + + fedcorev1a1 "github.com/kubewharf/kubeadmiral/pkg/apis/core/v1alpha1" + "github.com/kubewharf/kubeadmiral/pkg/controllers/common" + "github.com/kubewharf/kubeadmiral/pkg/controllers/scheduler" +) + +const testValue = "{\"unavailableClusters\":[{\"cluster\":\"kubeadmiral-member-1\",\"validUntil\":\"2023-11-24T07:12:52Z\"}]}" + +func generateFedObj(workload *unstructured.Unstructured) *fedcorev1a1.FederatedObject { + rawTargetTemplate, _ := workload.MarshalJSON() + return &fedcorev1a1.FederatedObject{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Spec: fedcorev1a1.GenericFederatedObjectSpec{ + Template: apiextensionsv1.JSON{Raw: rawTargetTemplate}, + }, + } +} + +func Test_PopulateAnnotations(t *testing.T) { + testJob := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + Annotations: map[string]string{}, + }, + Status: batchv1.JobStatus{ + Active: 1, + Succeeded: 1, + Failed: 1, + }, + } + u1, err := runtime.DefaultUnstructuredConverter.ToUnstructured(testJob) + if err != nil { + t.Fatalf(err.Error()) + } + unstructuredJob := &unstructured.Unstructured{Object: u1} + fedObj := generateFedObj(unstructuredJob) + annotations := map[string]string{ + common.AppliedMigrationConfigurationAnnotation: testValue, + scheduler.SchedulingAnnotation: "{}", + } + fedObj.SetAnnotations(annotations) + + tests := []struct { + name string + sourceObj *unstructured.Unstructured + fedObj fedcorev1a1.GenericFederatedObject + wantNeedUpdate bool + wantAnnotations map[string]string + }{ + { + name: "feedback annotations", + sourceObj: unstructuredJob, + fedObj: fedObj, + wantNeedUpdate: true, + wantAnnotations: annotations, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + needUpdate := PopulateAnnotations(tt.sourceObj, tt.fedObj) + if needUpdate != tt.wantNeedUpdate || + !reflect.DeepEqual(unstructuredJob.GetAnnotations(), tt.wantAnnotations) { + t.Errorf("PopulateAnnotations() want (%v, %v), but got (%v, %v)", + needUpdate, unstructuredJob.GetAnnotations(), tt.wantNeedUpdate, tt.wantAnnotations) + } + }) + } +} + +func Test_populateAnnotation(t *testing.T) { + tests := []struct { + name string + sourceAnnotations map[string]string + fedAnnotations map[string]string + key string + wantNeedUpdate bool + wantAnnotations map[string]string + }{ + { + name: "sourceAnnotations is nil, fedAnnotations with anno", + sourceAnnotations: nil, + fedAnnotations: map[string]string{ + common.AppliedMigrationConfigurationAnnotation: "{}", + }, + key: common.AppliedMigrationConfigurationAnnotation, + wantNeedUpdate: true, + wantAnnotations: map[string]string{ + common.AppliedMigrationConfigurationAnnotation: "{}", + }, + }, + { + name: "sourceAnnotations without anno, fedAnnotations with anno", + sourceAnnotations: map[string]string{}, + fedAnnotations: map[string]string{ + common.AppliedMigrationConfigurationAnnotation: "{}", + }, + key: common.AppliedMigrationConfigurationAnnotation, + wantNeedUpdate: true, + wantAnnotations: map[string]string{ + common.AppliedMigrationConfigurationAnnotation: "{}", + }, + }, + { + name: "sourceAnnotations with anno, fedAnnotations with the same value", + sourceAnnotations: map[string]string{ + common.AppliedMigrationConfigurationAnnotation: testValue, + }, + fedAnnotations: map[string]string{ + common.AppliedMigrationConfigurationAnnotation: testValue, + }, + key: common.AppliedMigrationConfigurationAnnotation, + wantNeedUpdate: false, + wantAnnotations: map[string]string{ + common.AppliedMigrationConfigurationAnnotation: testValue, + }, + }, + { + name: "sourceAnnotations with anno, fedAnnotations with different value", + sourceAnnotations: map[string]string{ + common.AppliedMigrationConfigurationAnnotation: "{}", + }, + fedAnnotations: map[string]string{ + common.AppliedMigrationConfigurationAnnotation: testValue, + }, + key: common.AppliedMigrationConfigurationAnnotation, + wantNeedUpdate: true, + wantAnnotations: map[string]string{ + common.AppliedMigrationConfigurationAnnotation: testValue, + }, + }, + { + name: "sourceAnnotations with anno, fedAnnotations without anno", + sourceAnnotations: map[string]string{ + common.AppliedMigrationConfigurationAnnotation: "{}", + }, + fedAnnotations: map[string]string{}, + key: common.AppliedMigrationConfigurationAnnotation, + wantNeedUpdate: true, + wantAnnotations: map[string]string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotAnnotations, needUpdate := populateAnnotation(tt.sourceAnnotations, tt.fedAnnotations, tt.key) + if needUpdate != tt.wantNeedUpdate || !reflect.DeepEqual(gotAnnotations, tt.wantAnnotations) { + t.Errorf("populateAnnotation() want (%v, %v), but got (%v, %v)", tt.wantNeedUpdate, + needUpdate, tt.wantAnnotations, gotAnnotations) + } + }) + } +} diff --git a/pkg/controllers/sync/controller.go b/pkg/controllers/sync/controller.go index 92c4354d..e63e8ee6 100644 --- a/pkg/controllers/sync/controller.go +++ b/pkg/controllers/sync/controller.go @@ -422,6 +422,12 @@ func (s *SyncController) reconcile(ctx context.Context, federatedName common.Qua fedResource.RecordError("EnsureFinalizerError", errors.Wrap(err, "Failed to ensure finalizer")) return worker.StatusError } + + if skipSync(fedResource.Object()) { + fedResource.RecordEvent("SyncSkipped", "Skip Syncing for %s", fedResource.FederatedName()) + return worker.StatusAllOK + } + clustersToSync, selectedClusters, err := s.prepareToSync(ctx, fedResource) if err != nil { fedResource.RecordError("PrepareToSyncError", errors.Wrap(err, "Failed to prepare to sync")) diff --git a/pkg/controllers/sync/util.go b/pkg/controllers/sync/util.go new file mode 100644 index 00000000..65665140 --- /dev/null +++ b/pkg/controllers/sync/util.go @@ -0,0 +1,31 @@ +/* +Copyright 2023 The KubeAdmiral 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 sync + +import ( + fedcorev1a1 "github.com/kubewharf/kubeadmiral/pkg/apis/core/v1alpha1" + "github.com/kubewharf/kubeadmiral/pkg/controllers/common" +) + +func skipSync(federatedObject fedcorev1a1.GenericFederatedObject) bool { + // if workloads have been propagated to member clusters, + // sync is always not skipped. + if len(federatedObject.GetStatus().Clusters) > 0 { + return false + } + return federatedObject.GetAnnotations()[common.DryRunAnnotation] == common.AnnotationValueTrue +} diff --git a/pkg/controllers/sync/util_test.go b/pkg/controllers/sync/util_test.go new file mode 100644 index 00000000..da143ca6 --- /dev/null +++ b/pkg/controllers/sync/util_test.go @@ -0,0 +1,45 @@ +/* +Copyright 2023 The KubeAdmiral 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 sync + +import ( + "testing" + + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + fedcorev1a1 "github.com/kubewharf/kubeadmiral/pkg/apis/core/v1alpha1" + "github.com/kubewharf/kubeadmiral/pkg/controllers/common" +) + +func Test_skipSync(t *testing.T) { + federatedObject := &fedcorev1a1.FederatedObject{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{common.DryRunAnnotation: common.AnnotationValueTrue}, + }, + } + assert.True(t, skipSync(federatedObject)) + federatedObject.GetAnnotations()[common.DryRunAnnotation] = common.AnnotationValueFalse + assert.False(t, skipSync(federatedObject)) + federatedObject.GetAnnotations()[common.DryRunAnnotation] = common.AnnotationValueTrue + federatedObject.GetStatus().Clusters = []fedcorev1a1.PropagationStatus{ + { + Cluster: "cluster1", + }, + } + assert.False(t, skipSync(federatedObject)) +}