diff --git a/pkg/controllers/binding/common.go b/pkg/controllers/binding/common.go index 2280c840c4dd..343e0bf5aa70 100644 --- a/pkg/controllers/binding/common.go +++ b/pkg/controllers/binding/common.go @@ -31,6 +31,7 @@ import ( configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1" policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1" workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2" + "github.com/karmada-io/karmada/pkg/controllers/ctrlutil" "github.com/karmada-io/karmada/pkg/features" "github.com/karmada-io/karmada/pkg/resourceinterpreter" "github.com/karmada-io/karmada/pkg/util" @@ -128,13 +129,13 @@ func ensureWork( Annotations: annotations, } - if err = helper.CreateOrUpdateWork( + if err = ctrlutil.CreateOrUpdateWork( ctx, c, workMeta, clonedWorkload, - helper.WithSuspendDispatching(shouldSuspendDispatching(bindingSpec.Suspension, targetCluster)), - helper.WithPreserveResourcesOnDeletion(ptr.Deref(bindingSpec.PreserveResourcesOnDeletion, false)), + ctrlutil.WithSuspendDispatching(shouldSuspendDispatching(bindingSpec.Suspension, targetCluster)), + ctrlutil.WithPreserveResourcesOnDeletion(ptr.Deref(bindingSpec.PreserveResourcesOnDeletion, false)), ); err != nil { return err } diff --git a/pkg/controllers/ctrlutil/work.go b/pkg/controllers/ctrlutil/work.go new file mode 100644 index 000000000000..06207b051325 --- /dev/null +++ b/pkg/controllers/ctrlutil/work.go @@ -0,0 +1,104 @@ +/* +Copyright 2021 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 ctrlutil + +import ( + "context" + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/util/retry" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha1" + workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2" + "github.com/karmada-io/karmada/pkg/util" +) + +// CreateOrUpdateWork creates a Work object if not exist, or updates if it already exists. +func CreateOrUpdateWork(ctx context.Context, client client.Client, workMeta metav1.ObjectMeta, resource *unstructured.Unstructured, options ...WorkOption) error { + if workMeta.Labels[util.PropagationInstruction] != util.PropagationInstructionSuppressed { + resource = resource.DeepCopy() + // set labels + util.MergeLabel(resource, util.ManagedByKarmadaLabel, util.ManagedByKarmadaLabelValue) + // set annotations + util.MergeAnnotation(resource, workv1alpha2.ResourceTemplateUIDAnnotation, string(resource.GetUID())) + util.MergeAnnotation(resource, workv1alpha2.WorkNameAnnotation, workMeta.Name) + util.MergeAnnotation(resource, workv1alpha2.WorkNamespaceAnnotation, workMeta.Namespace) + if conflictResolution, ok := workMeta.GetAnnotations()[workv1alpha2.ResourceConflictResolutionAnnotation]; ok { + util.MergeAnnotation(resource, workv1alpha2.ResourceConflictResolutionAnnotation, conflictResolution) + } + } + + workloadJSON, err := resource.MarshalJSON() + if err != nil { + klog.Errorf("Failed to marshal workload(%s/%s), error: %v", resource.GetNamespace(), resource.GetName(), err) + return err + } + + work := &workv1alpha1.Work{ + ObjectMeta: workMeta, + Spec: workv1alpha1.WorkSpec{ + Workload: workv1alpha1.WorkloadTemplate{ + Manifests: []workv1alpha1.Manifest{ + { + RawExtension: runtime.RawExtension{ + Raw: workloadJSON, + }, + }, + }, + }, + }, + } + + applyWorkOptions(work, options) + + runtimeObject := work.DeepCopy() + var operationResult controllerutil.OperationResult + err = retry.RetryOnConflict(retry.DefaultRetry, func() (err error) { + operationResult, err = controllerutil.CreateOrUpdate(ctx, client, runtimeObject, func() error { + if !runtimeObject.DeletionTimestamp.IsZero() { + return fmt.Errorf("work %s/%s is being deleted", runtimeObject.GetNamespace(), runtimeObject.GetName()) + } + + runtimeObject.Spec = work.Spec + runtimeObject.Labels = util.DedupeAndMergeLabels(runtimeObject.Labels, work.Labels) + runtimeObject.Annotations = util.DedupeAndMergeAnnotations(runtimeObject.Annotations, work.Annotations) + runtimeObject.Finalizers = work.Finalizers + return nil + }) + return err + }) + if err != nil { + klog.Errorf("Failed to create/update work %s/%s. Error: %v", work.GetNamespace(), work.GetName(), err) + return err + } + + if operationResult == controllerutil.OperationResultCreated { + klog.V(2).Infof("Create work %s/%s successfully.", work.GetNamespace(), work.GetName()) + } else if operationResult == controllerutil.OperationResultUpdated { + klog.V(2).Infof("Update work %s/%s successfully.", work.GetNamespace(), work.GetName()) + } else { + klog.V(2).Infof("Work %s/%s is up to date.", work.GetNamespace(), work.GetName()) + } + + return nil +} diff --git a/pkg/controllers/ctrlutil/work_test.go b/pkg/controllers/ctrlutil/work_test.go new file mode 100644 index 000000000000..251e04c81b86 --- /dev/null +++ b/pkg/controllers/ctrlutil/work_test.go @@ -0,0 +1,240 @@ +/* +Copyright 2022 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 ctrlutil + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha1" + workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2" + "github.com/karmada-io/karmada/pkg/util" +) + +func TestCreateOrUpdateWork(t *testing.T) { + scheme := runtime.NewScheme() + assert.NoError(t, workv1alpha1.Install(scheme)) + assert.NoError(t, workv1alpha2.Install(scheme)) + + tests := []struct { + name string + existingWork *workv1alpha1.Work + workMeta metav1.ObjectMeta + resource *unstructured.Unstructured + wantErr bool + verify func(*testing.T, client.Client) + }{ + { + name: "create new work", + workMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "test-work", + }, + resource: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "test-deployment", + "uid": "test-uid", + }, + }, + }, + verify: func(t *testing.T, c client.Client) { + work := &workv1alpha1.Work{} + err := c.Get(context.TODO(), client.ObjectKey{Namespace: "default", Name: "test-work"}, work) + assert.NoError(t, err) + assert.Equal(t, "test-work", work.Name) + assert.Equal(t, 1, len(work.Spec.Workload.Manifests)) + }, + }, + { + name: "create work with PropagationInstruction", + workMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "test-work", + Labels: map[string]string{ + util.PropagationInstruction: "some-value", + }, + Annotations: map[string]string{ + workv1alpha2.ResourceConflictResolutionAnnotation: "overwrite", + }, + }, + resource: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "test-deployment", + "uid": "test-uid", + }, + }, + }, + verify: func(t *testing.T, c client.Client) { + work := &workv1alpha1.Work{} + err := c.Get(context.TODO(), client.ObjectKey{Namespace: "default", Name: "test-work"}, work) + assert.NoError(t, err) + + // Get the resource from manifests + manifest := &unstructured.Unstructured{} + err = manifest.UnmarshalJSON(work.Spec.Workload.Manifests[0].Raw) + assert.NoError(t, err) + + // Verify labels and annotations were set + labels := manifest.GetLabels() + assert.Equal(t, util.ManagedByKarmadaLabelValue, labels[util.ManagedByKarmadaLabel]) + + annotations := manifest.GetAnnotations() + assert.Equal(t, "test-uid", annotations[workv1alpha2.ResourceTemplateUIDAnnotation]) + assert.Equal(t, "test-work", annotations[workv1alpha2.WorkNameAnnotation]) + assert.Equal(t, "default", annotations[workv1alpha2.WorkNamespaceAnnotation]) + assert.Equal(t, "overwrite", annotations[workv1alpha2.ResourceConflictResolutionAnnotation]) + }, + }, + { + name: "create work with PropagationInstructionSuppressed", + workMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "test-work", + Labels: map[string]string{ + util.PropagationInstruction: util.PropagationInstructionSuppressed, + }, + }, + resource: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "test-deployment", + "uid": "test-uid", + }, + }, + }, + verify: func(t *testing.T, c client.Client) { + work := &workv1alpha1.Work{} + err := c.Get(context.TODO(), client.ObjectKey{Namespace: "default", Name: "test-work"}, work) + assert.NoError(t, err) + + // Get the resource from manifests + manifest := &unstructured.Unstructured{} + err = manifest.UnmarshalJSON(work.Spec.Workload.Manifests[0].Raw) + assert.NoError(t, err) + + // Verify labels and annotations were NOT set + labels := manifest.GetLabels() + assert.Empty(t, labels[util.ManagedByKarmadaLabel]) + + annotations := manifest.GetAnnotations() + assert.Empty(t, annotations[workv1alpha2.ResourceTemplateUIDAnnotation]) + }, + }, + { + name: "update existing work", + existingWork: &workv1alpha1.Work{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "test-work", + }, + }, + workMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "test-work", + }, + resource: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "test-deployment", + "uid": "test-uid", + }, + }, + }, + verify: func(t *testing.T, c client.Client) { + work := &workv1alpha1.Work{} + err := c.Get(context.TODO(), client.ObjectKey{Namespace: "default", Name: "test-work"}, work) + assert.NoError(t, err) + assert.Equal(t, 1, len(work.Spec.Workload.Manifests)) + }, + }, + { + name: "error when work is being deleted", + existingWork: &workv1alpha1.Work{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "test-work", + DeletionTimestamp: &metav1.Time{Time: time.Now()}, + Finalizers: []string{"test.finalizer.io"}, // Finalizer to satisfy fake client requirement + }, + Spec: workv1alpha1.WorkSpec{ + Workload: workv1alpha1.WorkloadTemplate{ + Manifests: []workv1alpha1.Manifest{ + { + RawExtension: runtime.RawExtension{ + Raw: []byte(`{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"name":"test-deployment"}}`), + }, + }, + }, + }, + }, + }, + workMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "test-work", + }, + resource: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "test-deployment", + }, + }, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := fake.NewClientBuilder().WithScheme(scheme) + if tt.existingWork != nil { + c = c.WithObjects(tt.existingWork) + } + client := c.Build() + + err := CreateOrUpdateWork(context.TODO(), client, tt.workMeta, tt.resource) + + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + if tt.verify != nil { + tt.verify(t, client) + } + }) + } +} diff --git a/pkg/util/helper/workoption.go b/pkg/controllers/ctrlutil/workoption.go similarity index 98% rename from pkg/util/helper/workoption.go rename to pkg/controllers/ctrlutil/workoption.go index 817fcc288205..ec322281dced 100644 --- a/pkg/util/helper/workoption.go +++ b/pkg/controllers/ctrlutil/workoption.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package helper +package ctrlutil import workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha1" diff --git a/pkg/util/helper/workoption_test.go b/pkg/controllers/ctrlutil/workoption_test.go similarity index 99% rename from pkg/util/helper/workoption_test.go rename to pkg/controllers/ctrlutil/workoption_test.go index c8e798712e9f..3c361c8daab6 100644 --- a/pkg/util/helper/workoption_test.go +++ b/pkg/controllers/ctrlutil/workoption_test.go @@ -13,7 +13,7 @@ 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 helper +package ctrlutil import ( "testing" diff --git a/pkg/controllers/federatedresourcequota/federated_resource_quota_sync_controller.go b/pkg/controllers/federatedresourcequota/federated_resource_quota_sync_controller.go index 59406bb596ac..3ede82464550 100644 --- a/pkg/controllers/federatedresourcequota/federated_resource_quota_sync_controller.go +++ b/pkg/controllers/federatedresourcequota/federated_resource_quota_sync_controller.go @@ -37,6 +37,7 @@ import ( clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1" policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1" workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha1" + "github.com/karmada-io/karmada/pkg/controllers/ctrlutil" "github.com/karmada-io/karmada/pkg/events" "github.com/karmada-io/karmada/pkg/util" "github.com/karmada-io/karmada/pkg/util/helper" @@ -184,7 +185,7 @@ func (c *SyncController) buildWorks(ctx context.Context, quota *policyv1alpha1.F }, } - err = helper.CreateOrUpdateWork(ctx, c.Client, objectMeta, resourceQuotaObj) + err = ctrlutil.CreateOrUpdateWork(ctx, c.Client, objectMeta, resourceQuotaObj) if err != nil { errs = append(errs, err) } diff --git a/pkg/controllers/mcs/service_export_controller.go b/pkg/controllers/mcs/service_export_controller.go index b1ba44a91458..16a682edb797 100644 --- a/pkg/controllers/mcs/service_export_controller.go +++ b/pkg/controllers/mcs/service_export_controller.go @@ -47,6 +47,7 @@ import ( clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1" workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha1" + "github.com/karmada-io/karmada/pkg/controllers/ctrlutil" "github.com/karmada-io/karmada/pkg/util" "github.com/karmada-io/karmada/pkg/util/fedinformer" "github.com/karmada-io/karmada/pkg/util/fedinformer/genericmanager" @@ -494,7 +495,7 @@ func reportEndpointSlice(ctx context.Context, c client.Client, endpointSlice *un return err } - if err := helper.CreateOrUpdateWork(ctx, c, workMeta, endpointSlice); err != nil { + if err := ctrlutil.CreateOrUpdateWork(ctx, c, workMeta, endpointSlice); err != nil { return err } diff --git a/pkg/controllers/multiclusterservice/endpointslice_collect_controller.go b/pkg/controllers/multiclusterservice/endpointslice_collect_controller.go index 2e056910ac29..8f398166b9df 100644 --- a/pkg/controllers/multiclusterservice/endpointslice_collect_controller.go +++ b/pkg/controllers/multiclusterservice/endpointslice_collect_controller.go @@ -43,6 +43,7 @@ import ( clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1" networkingv1alpha1 "github.com/karmada-io/karmada/pkg/apis/networking/v1alpha1" workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha1" + "github.com/karmada-io/karmada/pkg/controllers/ctrlutil" "github.com/karmada-io/karmada/pkg/util" "github.com/karmada-io/karmada/pkg/util/fedinformer" "github.com/karmada-io/karmada/pkg/util/fedinformer/genericmanager" @@ -385,7 +386,7 @@ func reportEndpointSlice(ctx context.Context, c client.Client, endpointSlice *un return err } - if err := helper.CreateOrUpdateWork(ctx, c, workMeta, endpointSlice); err != nil { + if err := ctrlutil.CreateOrUpdateWork(ctx, c, workMeta, endpointSlice); err != nil { klog.Errorf("Failed to create or update work(%s/%s), Error: %v", workMeta.Namespace, workMeta.Name, err) return err } diff --git a/pkg/controllers/multiclusterservice/endpointslice_dispatch_controller.go b/pkg/controllers/multiclusterservice/endpointslice_dispatch_controller.go index cf1e958185d6..f7b7618ff93b 100644 --- a/pkg/controllers/multiclusterservice/endpointslice_dispatch_controller.go +++ b/pkg/controllers/multiclusterservice/endpointslice_dispatch_controller.go @@ -42,6 +42,7 @@ import ( clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1" networkingv1alpha1 "github.com/karmada-io/karmada/pkg/apis/networking/v1alpha1" workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha1" + "github.com/karmada-io/karmada/pkg/controllers/ctrlutil" "github.com/karmada-io/karmada/pkg/events" "github.com/karmada-io/karmada/pkg/util" "github.com/karmada-io/karmada/pkg/util/fedinformer/genericmanager" @@ -395,7 +396,7 @@ func (c *EndpointsliceDispatchController) ensureEndpointSliceWork(ctx context.Co klog.Errorf("Failed to convert typed object to unstructured object, error is: %v", err) return err } - if err := helper.CreateOrUpdateWork(ctx, c.Client, workMeta, unstructuredEPS); err != nil { + if err := ctrlutil.CreateOrUpdateWork(ctx, c.Client, workMeta, unstructuredEPS); err != nil { klog.Errorf("Failed to dispatch EndpointSlice %s/%s from %s to cluster %s:%v", work.GetNamespace(), work.GetName(), providerCluster, consumerCluster, err) return err diff --git a/pkg/controllers/multiclusterservice/mcs_controller.go b/pkg/controllers/multiclusterservice/mcs_controller.go index d439ef38fb8f..a3558b6e4508 100644 --- a/pkg/controllers/multiclusterservice/mcs_controller.go +++ b/pkg/controllers/multiclusterservice/mcs_controller.go @@ -47,6 +47,7 @@ import ( policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1" workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha1" workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2" + "github.com/karmada-io/karmada/pkg/controllers/ctrlutil" "github.com/karmada-io/karmada/pkg/events" "github.com/karmada-io/karmada/pkg/sharedcli/ratelimiterflag" "github.com/karmada-io/karmada/pkg/util" @@ -309,7 +310,7 @@ func (c *MCSController) propagateMultiClusterService(ctx context.Context, mcs *n klog.Errorf("Failed to convert MultiClusterService(%s/%s) to unstructured object, err is %v", mcs.Namespace, mcs.Name, err) return err } - if err = helper.CreateOrUpdateWork(ctx, c, workMeta, mcsObj); err != nil { + if err = ctrlutil.CreateOrUpdateWork(ctx, c, workMeta, mcsObj); err != nil { klog.Errorf("Failed to create or update MultiClusterService(%s/%s) work in the given member cluster %s, err is %v", mcs.Namespace, mcs.Name, clusterName, err) return err diff --git a/pkg/controllers/namespace/namespace_sync_controller.go b/pkg/controllers/namespace/namespace_sync_controller.go index 465f15c1e30b..93145cd650a2 100644 --- a/pkg/controllers/namespace/namespace_sync_controller.go +++ b/pkg/controllers/namespace/namespace_sync_controller.go @@ -41,6 +41,7 @@ import ( clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1" policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1" "github.com/karmada-io/karmada/pkg/controllers/binding" + "github.com/karmada-io/karmada/pkg/controllers/ctrlutil" "github.com/karmada-io/karmada/pkg/util" "github.com/karmada-io/karmada/pkg/util/helper" "github.com/karmada-io/karmada/pkg/util/names" @@ -157,7 +158,7 @@ func (c *Controller) buildWorks(ctx context.Context, namespace *corev1.Namespace Annotations: annotations, } - if err = helper.CreateOrUpdateWork(ctx, c.Client, objectMeta, clonedNamespaced); err != nil { + if err = ctrlutil.CreateOrUpdateWork(ctx, c.Client, objectMeta, clonedNamespaced); err != nil { ch <- fmt.Errorf("sync namespace(%s) to cluster(%s) failed due to: %v", clonedNamespaced.GetName(), cluster.GetName(), err) return } diff --git a/pkg/controllers/unifiedauth/unified_auth_controller.go b/pkg/controllers/unifiedauth/unified_auth_controller.go index ff0886031611..eb8eee232aca 100644 --- a/pkg/controllers/unifiedauth/unified_auth_controller.go +++ b/pkg/controllers/unifiedauth/unified_auth_controller.go @@ -37,6 +37,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1" + "github.com/karmada-io/karmada/pkg/controllers/ctrlutil" "github.com/karmada-io/karmada/pkg/events" "github.com/karmada-io/karmada/pkg/util" "github.com/karmada-io/karmada/pkg/util/helper" @@ -237,7 +238,7 @@ func (c *Controller) buildWorks(ctx context.Context, cluster *clusterv1alpha1.Cl }, } - if err := helper.CreateOrUpdateWork(ctx, c.Client, objectMeta, obj); err != nil { + if err := ctrlutil.CreateOrUpdateWork(ctx, c.Client, objectMeta, obj); err != nil { return err } diff --git a/pkg/util/helper/work.go b/pkg/util/helper/work.go index 987a3dc8e709..2d67630bda50 100644 --- a/pkg/util/helper/work.go +++ b/pkg/util/helper/work.go @@ -21,94 +21,20 @@ import ( "fmt" corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/util/retry" "k8s.io/klog/v2" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha1" workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2" "github.com/karmada-io/karmada/pkg/util" ) -// CreateOrUpdateWork creates a Work object if not exist, or updates if it already exists. -func CreateOrUpdateWork(ctx context.Context, client client.Client, workMeta metav1.ObjectMeta, resource *unstructured.Unstructured, options ...WorkOption) error { - if workMeta.Labels[util.PropagationInstruction] != util.PropagationInstructionSuppressed { - resource = resource.DeepCopy() - // set labels - util.MergeLabel(resource, util.ManagedByKarmadaLabel, util.ManagedByKarmadaLabelValue) - // set annotations - util.MergeAnnotation(resource, workv1alpha2.ResourceTemplateUIDAnnotation, string(resource.GetUID())) - util.MergeAnnotation(resource, workv1alpha2.WorkNameAnnotation, workMeta.Name) - util.MergeAnnotation(resource, workv1alpha2.WorkNamespaceAnnotation, workMeta.Namespace) - if conflictResolution, ok := workMeta.GetAnnotations()[workv1alpha2.ResourceConflictResolutionAnnotation]; ok { - util.MergeAnnotation(resource, workv1alpha2.ResourceConflictResolutionAnnotation, conflictResolution) - } - } - - workloadJSON, err := resource.MarshalJSON() - if err != nil { - klog.Errorf("Failed to marshal workload(%s/%s), error: %v", resource.GetNamespace(), resource.GetName(), err) - return err - } - - work := &workv1alpha1.Work{ - ObjectMeta: workMeta, - Spec: workv1alpha1.WorkSpec{ - Workload: workv1alpha1.WorkloadTemplate{ - Manifests: []workv1alpha1.Manifest{ - { - RawExtension: runtime.RawExtension{ - Raw: workloadJSON, - }, - }, - }, - }, - }, - } - - applyWorkOptions(work, options) - - runtimeObject := work.DeepCopy() - var operationResult controllerutil.OperationResult - err = retry.RetryOnConflict(retry.DefaultRetry, func() (err error) { - operationResult, err = controllerutil.CreateOrUpdate(ctx, client, runtimeObject, func() error { - if !runtimeObject.DeletionTimestamp.IsZero() { - return fmt.Errorf("work %s/%s is being deleted", runtimeObject.GetNamespace(), runtimeObject.GetName()) - } - - runtimeObject.Spec = work.Spec - runtimeObject.Labels = util.DedupeAndMergeLabels(runtimeObject.Labels, work.Labels) - runtimeObject.Annotations = util.DedupeAndMergeAnnotations(runtimeObject.Annotations, work.Annotations) - runtimeObject.Finalizers = work.Finalizers - return nil - }) - return err - }) - if err != nil { - klog.Errorf("Failed to create/update work %s/%s. Error: %v", work.GetNamespace(), work.GetName(), err) - return err - } - - if operationResult == controllerutil.OperationResultCreated { - klog.V(2).Infof("Create work %s/%s successfully.", work.GetNamespace(), work.GetName()) - } else if operationResult == controllerutil.OperationResultUpdated { - klog.V(2).Infof("Update work %s/%s successfully.", work.GetNamespace(), work.GetName()) - } else { - klog.V(2).Infof("Work %s/%s is up to date.", work.GetNamespace(), work.GetName()) - } - - return nil -} - // GetWorksByLabelsSet gets WorkList by matching labels.Set. func GetWorksByLabelsSet(ctx context.Context, c client.Client, ls labels.Set) (*workv1alpha1.WorkList, error) { workList := &workv1alpha1.WorkList{} diff --git a/pkg/util/helper/work_test.go b/pkg/util/helper/work_test.go index 614ee2fe9cb7..541b4d92cbf1 100644 --- a/pkg/util/helper/work_test.go +++ b/pkg/util/helper/work_test.go @@ -19,7 +19,6 @@ package helper import ( "context" "testing" - "time" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" @@ -34,215 +33,8 @@ import ( workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha1" workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2" - "github.com/karmada-io/karmada/pkg/util" ) -func TestCreateOrUpdateWork(t *testing.T) { - scheme := runtime.NewScheme() - assert.NoError(t, workv1alpha1.Install(scheme)) - assert.NoError(t, workv1alpha2.Install(scheme)) - - tests := []struct { - name string - existingWork *workv1alpha1.Work - workMeta metav1.ObjectMeta - resource *unstructured.Unstructured - wantErr bool - verify func(*testing.T, client.Client) - }{ - { - name: "create new work", - workMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "test-work", - }, - resource: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "name": "test-deployment", - "uid": "test-uid", - }, - }, - }, - verify: func(t *testing.T, c client.Client) { - work := &workv1alpha1.Work{} - err := c.Get(context.TODO(), client.ObjectKey{Namespace: "default", Name: "test-work"}, work) - assert.NoError(t, err) - assert.Equal(t, "test-work", work.Name) - assert.Equal(t, 1, len(work.Spec.Workload.Manifests)) - }, - }, - { - name: "create work with PropagationInstruction", - workMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "test-work", - Labels: map[string]string{ - util.PropagationInstruction: "some-value", - }, - Annotations: map[string]string{ - workv1alpha2.ResourceConflictResolutionAnnotation: "overwrite", - }, - }, - resource: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "name": "test-deployment", - "uid": "test-uid", - }, - }, - }, - verify: func(t *testing.T, c client.Client) { - work := &workv1alpha1.Work{} - err := c.Get(context.TODO(), client.ObjectKey{Namespace: "default", Name: "test-work"}, work) - assert.NoError(t, err) - - // Get the resource from manifests - manifest := &unstructured.Unstructured{} - err = manifest.UnmarshalJSON(work.Spec.Workload.Manifests[0].Raw) - assert.NoError(t, err) - - // Verify labels and annotations were set - labels := manifest.GetLabels() - assert.Equal(t, util.ManagedByKarmadaLabelValue, labels[util.ManagedByKarmadaLabel]) - - annotations := manifest.GetAnnotations() - assert.Equal(t, "test-uid", annotations[workv1alpha2.ResourceTemplateUIDAnnotation]) - assert.Equal(t, "test-work", annotations[workv1alpha2.WorkNameAnnotation]) - assert.Equal(t, "default", annotations[workv1alpha2.WorkNamespaceAnnotation]) - assert.Equal(t, "overwrite", annotations[workv1alpha2.ResourceConflictResolutionAnnotation]) - }, - }, - { - name: "create work with PropagationInstructionSuppressed", - workMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "test-work", - Labels: map[string]string{ - util.PropagationInstruction: util.PropagationInstructionSuppressed, - }, - }, - resource: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "name": "test-deployment", - "uid": "test-uid", - }, - }, - }, - verify: func(t *testing.T, c client.Client) { - work := &workv1alpha1.Work{} - err := c.Get(context.TODO(), client.ObjectKey{Namespace: "default", Name: "test-work"}, work) - assert.NoError(t, err) - - // Get the resource from manifests - manifest := &unstructured.Unstructured{} - err = manifest.UnmarshalJSON(work.Spec.Workload.Manifests[0].Raw) - assert.NoError(t, err) - - // Verify labels and annotations were NOT set - labels := manifest.GetLabels() - assert.Empty(t, labels[util.ManagedByKarmadaLabel]) - - annotations := manifest.GetAnnotations() - assert.Empty(t, annotations[workv1alpha2.ResourceTemplateUIDAnnotation]) - }, - }, - { - name: "update existing work", - existingWork: &workv1alpha1.Work{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "test-work", - }, - }, - workMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "test-work", - }, - resource: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "name": "test-deployment", - "uid": "test-uid", - }, - }, - }, - verify: func(t *testing.T, c client.Client) { - work := &workv1alpha1.Work{} - err := c.Get(context.TODO(), client.ObjectKey{Namespace: "default", Name: "test-work"}, work) - assert.NoError(t, err) - assert.Equal(t, 1, len(work.Spec.Workload.Manifests)) - }, - }, - { - name: "error when work is being deleted", - existingWork: &workv1alpha1.Work{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "test-work", - DeletionTimestamp: &metav1.Time{Time: time.Now()}, - Finalizers: []string{"test.finalizer.io"}, // Finalizer to satisfy fake client requirement - }, - Spec: workv1alpha1.WorkSpec{ - Workload: workv1alpha1.WorkloadTemplate{ - Manifests: []workv1alpha1.Manifest{ - { - RawExtension: runtime.RawExtension{ - Raw: []byte(`{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"name":"test-deployment"}}`), - }, - }, - }, - }, - }, - }, - workMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "test-work", - }, - resource: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "name": "test-deployment", - }, - }, - }, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := fake.NewClientBuilder().WithScheme(scheme) - if tt.existingWork != nil { - c = c.WithObjects(tt.existingWork) - } - client := c.Build() - - err := CreateOrUpdateWork(context.TODO(), client, tt.workMeta, tt.resource) - - if tt.wantErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - if tt.verify != nil { - tt.verify(t, client) - } - }) - } -} - func TestGetWorksByLabelsSet(t *testing.T) { scheme := runtime.NewScheme() assert.NoError(t, workv1alpha1.Install(scheme))