Skip to content

Commit

Permalink
Merge pull request karmada-io#6018 from ctripcloud/unstructured
Browse files Browse the repository at this point in the history
move CreateOrUpdateWork() and related functions to controllers/ctrlutl
  • Loading branch information
karmada-bot authored Jan 10, 2025
2 parents be674c7 + 807153f commit 253dc79
Show file tree
Hide file tree
Showing 14 changed files with 364 additions and 294 deletions.
7 changes: 4 additions & 3 deletions pkg/controllers/binding/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Expand Down
104 changes: 104 additions & 0 deletions pkg/controllers/ctrlutil/work.go
Original file line number Diff line number Diff line change
@@ -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
}
240 changes: 240 additions & 0 deletions pkg/controllers/ctrlutil/work_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading

0 comments on commit 253dc79

Please sign in to comment.