Merge pull request #6018 from ctripcloud/unstructured
move CreateOrUpdateWork() and related functions to controllers/ctrlutl
This commit is contained in:
commit
253dc794b1
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
@ -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"
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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{}
|
||||
|
|
|
@ -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))
|
||||
|
|
Loading…
Reference in New Issue