Merge pull request #6018 from ctripcloud/unstructured

move CreateOrUpdateWork() and related functions to controllers/ctrlutl
This commit is contained in:
karmada-bot 2025-01-10 14:20:40 +08:00 committed by GitHub
commit 253dc794b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 364 additions and 294 deletions

View File

@ -31,6 +31,7 @@ import (
configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1" configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1"
policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1" policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1"
workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2" 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/features"
"github.com/karmada-io/karmada/pkg/resourceinterpreter" "github.com/karmada-io/karmada/pkg/resourceinterpreter"
"github.com/karmada-io/karmada/pkg/util" "github.com/karmada-io/karmada/pkg/util"
@ -128,13 +129,13 @@ func ensureWork(
Annotations: annotations, Annotations: annotations,
} }
if err = helper.CreateOrUpdateWork( if err = ctrlutil.CreateOrUpdateWork(
ctx, ctx,
c, c,
workMeta, workMeta,
clonedWorkload, clonedWorkload,
helper.WithSuspendDispatching(shouldSuspendDispatching(bindingSpec.Suspension, targetCluster)), ctrlutil.WithSuspendDispatching(shouldSuspendDispatching(bindingSpec.Suspension, targetCluster)),
helper.WithPreserveResourcesOnDeletion(ptr.Deref(bindingSpec.PreserveResourcesOnDeletion, false)), ctrlutil.WithPreserveResourcesOnDeletion(ptr.Deref(bindingSpec.PreserveResourcesOnDeletion, false)),
); err != nil { ); err != nil {
return err return err
} }

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package helper package ctrlutil
import workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha1" import workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha1"

View File

@ -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 See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package helper package ctrlutil
import ( import (
"testing" "testing"

View File

@ -37,6 +37,7 @@ import (
clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1" clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1"
policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1" policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1"
workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/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/events"
"github.com/karmada-io/karmada/pkg/util" "github.com/karmada-io/karmada/pkg/util"
"github.com/karmada-io/karmada/pkg/util/helper" "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 { if err != nil {
errs = append(errs, err) errs = append(errs, err)
} }

View File

@ -47,6 +47,7 @@ import (
clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1" clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1"
workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/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"
"github.com/karmada-io/karmada/pkg/util/fedinformer" "github.com/karmada-io/karmada/pkg/util/fedinformer"
"github.com/karmada-io/karmada/pkg/util/fedinformer/genericmanager" "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 return err
} }
if err := helper.CreateOrUpdateWork(ctx, c, workMeta, endpointSlice); err != nil { if err := ctrlutil.CreateOrUpdateWork(ctx, c, workMeta, endpointSlice); err != nil {
return err return err
} }

View File

@ -43,6 +43,7 @@ import (
clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1" clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1"
networkingv1alpha1 "github.com/karmada-io/karmada/pkg/apis/networking/v1alpha1" networkingv1alpha1 "github.com/karmada-io/karmada/pkg/apis/networking/v1alpha1"
workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/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"
"github.com/karmada-io/karmada/pkg/util/fedinformer" "github.com/karmada-io/karmada/pkg/util/fedinformer"
"github.com/karmada-io/karmada/pkg/util/fedinformer/genericmanager" "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 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) klog.Errorf("Failed to create or update work(%s/%s), Error: %v", workMeta.Namespace, workMeta.Name, err)
return err return err
} }

View File

@ -42,6 +42,7 @@ import (
clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1" clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1"
networkingv1alpha1 "github.com/karmada-io/karmada/pkg/apis/networking/v1alpha1" networkingv1alpha1 "github.com/karmada-io/karmada/pkg/apis/networking/v1alpha1"
workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/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/events"
"github.com/karmada-io/karmada/pkg/util" "github.com/karmada-io/karmada/pkg/util"
"github.com/karmada-io/karmada/pkg/util/fedinformer/genericmanager" "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) klog.Errorf("Failed to convert typed object to unstructured object, error is: %v", err)
return 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", klog.Errorf("Failed to dispatch EndpointSlice %s/%s from %s to cluster %s:%v",
work.GetNamespace(), work.GetName(), providerCluster, consumerCluster, err) work.GetNamespace(), work.GetName(), providerCluster, consumerCluster, err)
return err return err

View File

@ -47,6 +47,7 @@ import (
policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1" policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1"
workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha1" workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha1"
workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2" 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/events"
"github.com/karmada-io/karmada/pkg/sharedcli/ratelimiterflag" "github.com/karmada-io/karmada/pkg/sharedcli/ratelimiterflag"
"github.com/karmada-io/karmada/pkg/util" "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) klog.Errorf("Failed to convert MultiClusterService(%s/%s) to unstructured object, err is %v", mcs.Namespace, mcs.Name, err)
return 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", 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) mcs.Namespace, mcs.Name, clusterName, err)
return err return err

View File

@ -41,6 +41,7 @@ import (
clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1" clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1"
policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/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/binding"
"github.com/karmada-io/karmada/pkg/controllers/ctrlutil"
"github.com/karmada-io/karmada/pkg/util" "github.com/karmada-io/karmada/pkg/util"
"github.com/karmada-io/karmada/pkg/util/helper" "github.com/karmada-io/karmada/pkg/util/helper"
"github.com/karmada-io/karmada/pkg/util/names" "github.com/karmada-io/karmada/pkg/util/names"
@ -157,7 +158,7 @@ func (c *Controller) buildWorks(ctx context.Context, namespace *corev1.Namespace
Annotations: annotations, 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) ch <- fmt.Errorf("sync namespace(%s) to cluster(%s) failed due to: %v", clonedNamespaced.GetName(), cluster.GetName(), err)
return return
} }

View File

@ -37,6 +37,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/reconcile"
clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1" 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/events"
"github.com/karmada-io/karmada/pkg/util" "github.com/karmada-io/karmada/pkg/util"
"github.com/karmada-io/karmada/pkg/util/helper" "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 return err
} }

View File

@ -21,94 +21,20 @@ import (
"fmt" "fmt"
corev1 "k8s.io/api/core/v1" 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/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"
"k8s.io/klog/v2" "k8s.io/klog/v2"
"k8s.io/utils/ptr" "k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client" "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" workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha1"
workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2" workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2"
"github.com/karmada-io/karmada/pkg/util" "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. // GetWorksByLabelsSet gets WorkList by matching labels.Set.
func GetWorksByLabelsSet(ctx context.Context, c client.Client, ls labels.Set) (*workv1alpha1.WorkList, error) { func GetWorksByLabelsSet(ctx context.Context, c client.Client, ls labels.Set) (*workv1alpha1.WorkList, error) {
workList := &workv1alpha1.WorkList{} workList := &workv1alpha1.WorkList{}

View File

@ -19,7 +19,6 @@ package helper
import ( import (
"context" "context"
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
@ -34,215 +33,8 @@ import (
workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha1" workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha1"
workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2" 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) { func TestGetWorksByLabelsSet(t *testing.T) {
scheme := runtime.NewScheme() scheme := runtime.NewScheme()
assert.NoError(t, workv1alpha1.Install(scheme)) assert.NoError(t, workv1alpha1.Install(scheme))