support update strategy in manifestwork (#139)

* Add ssa strategy

Signed-off-by: Jian Qiu <jqiu@redhat.com>

* Add test cases

Signed-off-by: Jian Qiu <jqiu@redhat.com>

* Resolve comments

Signed-off-by: Jian Qiu <jqiu@redhat.com>

* Use default manager in api repo

Signed-off-by: Jian Qiu <jqiu@redhat.com>

* Refatcor deleteOption

patch ownerref separately for deleteOption

Signed-off-by: Jian Qiu <jqiu@redhat.com>

* Do not reconcile when ssa conflict

Signed-off-by: Jian Qiu <jqiu@redhat.com>
This commit is contained in:
Jian Qiu
2022-07-07 10:09:42 +08:00
committed by GitHub
parent 718605172f
commit 1a7686ee47
14 changed files with 1363 additions and 187 deletions

View File

@@ -2,6 +2,7 @@ package helper
import (
"context"
"encoding/json"
"testing"
"time"
@@ -11,9 +12,11 @@ import (
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/diff"
fakedynamic "k8s.io/client-go/dynamic/fake"
clienttesting "k8s.io/client-go/testing"
fakeworkclient "open-cluster-management.io/api/client/work/clientset/versioned/fake"
workapiv1 "open-cluster-management.io/api/work/v1"
)
@@ -504,3 +507,132 @@ func TestHubHash(t *testing.T) {
})
}
}
func TestFindManifestConiguration(t *testing.T) {
cases := []struct {
name string
options []workapiv1.ManifestConfigOption
resourceMeta workapiv1.ManifestResourceMeta
expectedOption *workapiv1.ManifestConfigOption
}{
{
name: "nil options",
options: nil,
resourceMeta: workapiv1.ManifestResourceMeta{Group: "", Resource: "configmaps", Name: "test", Namespace: "testns"},
expectedOption: nil,
},
{
name: "options not found",
options: []workapiv1.ManifestConfigOption{
{ResourceIdentifier: workapiv1.ResourceIdentifier{Group: "", Resource: "nodes", Name: "node1"}},
{ResourceIdentifier: workapiv1.ResourceIdentifier{Group: "", Resource: "configmaps", Name: "test1", Namespace: "testns"}},
},
resourceMeta: workapiv1.ManifestResourceMeta{Group: "", Resource: "configmaps", Name: "test", Namespace: "testns"},
expectedOption: nil,
},
{
name: "options found",
options: []workapiv1.ManifestConfigOption{
{ResourceIdentifier: workapiv1.ResourceIdentifier{Group: "", Resource: "nodes", Name: "node1"}},
{ResourceIdentifier: workapiv1.ResourceIdentifier{Group: "", Resource: "configmaps", Name: "test", Namespace: "testns"}},
},
resourceMeta: workapiv1.ManifestResourceMeta{Group: "", Resource: "configmaps", Name: "test", Namespace: "testns"},
expectedOption: &workapiv1.ManifestConfigOption{
ResourceIdentifier: workapiv1.ResourceIdentifier{Group: "", Resource: "configmaps", Name: "test", Namespace: "testns"},
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
option := FindManifestConiguration(c.resourceMeta, c.options)
if !equality.Semantic.DeepEqual(option, c.expectedOption) {
t.Errorf("expect option to be %v, but got %v", c.expectedOption, option)
}
})
}
}
func TestApplyOwnerReferences(t *testing.T) {
testCases := []struct {
name string
existing []metav1.OwnerReference
required metav1.OwnerReference
wantPatch bool
wantOwners []metav1.OwnerReference
}{
{
name: "add a owner",
required: metav1.OwnerReference{Name: "n1", UID: "a"},
wantPatch: true,
wantOwners: []metav1.OwnerReference{{Name: "n1", UID: "a"}},
},
{
name: "append a owner",
existing: []metav1.OwnerReference{{Name: "n2", UID: "b"}},
required: metav1.OwnerReference{Name: "n1", UID: "a"},
wantPatch: true,
wantOwners: []metav1.OwnerReference{{Name: "n2", UID: "b"}, {Name: "n1", UID: "a"}},
},
{
name: "remove a owner",
existing: []metav1.OwnerReference{{Name: "n2", UID: "b"}, {Name: "n1", UID: "a"}},
required: metav1.OwnerReference{Name: "n1", UID: "a-"},
wantPatch: true,
wantOwners: []metav1.OwnerReference{{Name: "n2", UID: "b"}},
},
{
name: "remove a non existing owner",
existing: []metav1.OwnerReference{{Name: "n2", UID: "b"}, {Name: "n1", UID: "a"}},
required: metav1.OwnerReference{Name: "n3", UID: "c-"},
wantPatch: false,
},
{
name: "append an existing owner",
existing: []metav1.OwnerReference{{Name: "n2", UID: "b"}, {Name: "n1", UID: "a"}},
required: metav1.OwnerReference{Name: "n1", UID: "a"},
wantPatch: false,
},
}
scheme := runtime.NewScheme()
if err := corev1.AddToScheme(scheme); err != nil {
t.Fatal(err)
}
for _, c := range testCases {
t.Run(c.name, func(t *testing.T) {
object := newSecret("ns1", "n1", false, "ns1-n1", c.existing...)
fakeClient := fakedynamic.NewSimpleDynamicClient(scheme, object)
gvr := schema.GroupVersionResource{Version: "v1", Resource: "secrets"}
err := ApplyOwnerReferences(context.TODO(), fakeClient, gvr, object, c.required)
if err != nil {
t.Errorf("apply err: %v", err)
}
actions := fakeClient.Actions()
if !c.wantPatch {
if len(actions) > 0 {
t.Fatalf("expect not patch but got %v", actions)
}
return
}
if len(actions) != 1 {
t.Fatalf("expect patch action but got %v", actions)
}
patch := actions[0].(clienttesting.PatchAction).GetPatch()
patchedObject := &metav1.PartialObjectMetadata{}
err = json.Unmarshal(patch, patchedObject)
if err != nil {
t.Fatalf("failed to marshal patch: %v", err)
}
if !equality.Semantic.DeepEqual(c.wantOwners, patchedObject.GetOwnerReferences()) {
t.Errorf("want ownerrefs %v, but got %v", c.wantOwners, patchedObject.GetOwnerReferences())
}
})
}
}

View File

@@ -3,6 +3,7 @@ package helper
import (
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"strings"
"time"
@@ -17,6 +18,7 @@ import (
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
@@ -232,18 +234,9 @@ func DeleteAppliedResources(
continue
}
// Merge with the existing owners to move the owner.
modified := resourcemerge.BoolPtr(false)
resourcemerge.MergeOwnerRefs(modified, &existingOwner, []metav1.OwnerReference{*ownerCopy})
// If there are still any other existing owners (not only ManifestWorks), update ownerrefs only.
if len(existingOwner) > 0 {
if !*modified {
continue
}
u.SetOwnerReferences(existingOwner)
_, err = dynamicClient.Resource(gvr).Namespace(resource.Namespace).Update(ctx, u, metav1.UpdateOptions{})
if len(existingOwner) > 1 {
err := ApplyOwnerReferences(ctx, dynamicClient, gvr, u, *ownerCopy)
if err != nil {
errs = append(errs, fmt.Errorf(
"failed to remove owner from resource %v with key %s/%s: %w",
@@ -361,3 +354,49 @@ func NewAppliedManifestWorkOwner(appliedWork *workapiv1.AppliedManifestWork) *me
UID: appliedWork.UID,
}
}
func FindManifestConiguration(resourceMeta workapiv1.ManifestResourceMeta, manifestOptions []workapiv1.ManifestConfigOption) *workapiv1.ManifestConfigOption {
identifier := workapiv1.ResourceIdentifier{
Group: resourceMeta.Group,
Resource: resourceMeta.Resource,
Namespace: resourceMeta.Namespace,
Name: resourceMeta.Name,
}
for _, config := range manifestOptions {
if config.ResourceIdentifier == identifier {
return &config
}
}
return nil
}
func ApplyOwnerReferences(ctx context.Context, dynamicClient dynamic.Interface, gvr schema.GroupVersionResource, existing runtime.Object, requiredOwner metav1.OwnerReference) error {
accessor, err := meta.Accessor(existing)
if err != nil {
return fmt.Errorf("type %t cannot be accessed: %v", existing, err)
}
patch := &unstructured.Unstructured{}
patch.SetUID(accessor.GetUID())
patch.SetResourceVersion(accessor.GetResourceVersion())
patch.SetOwnerReferences([]metav1.OwnerReference{requiredOwner})
modified := false
patchedOwner := accessor.GetOwnerReferences()
resourcemerge.MergeOwnerRefs(&modified, &patchedOwner, []metav1.OwnerReference{requiredOwner})
patch.SetOwnerReferences(patchedOwner)
if !modified {
return nil
}
patchData, err := json.Marshal(patch)
if err != nil {
return err
}
klog.V(2).Infof("Patching resource %v %s/%s with patch %s", gvr, accessor.GetNamespace(), accessor.GetName(), string(patchData))
_, err = dynamicClient.Resource(gvr).Namespace(accessor.GetNamespace()).Patch(ctx, accessor.GetName(), types.MergePatchType, patchData, metav1.PatchOptions{})
return err
}

View File

@@ -2,6 +2,7 @@ package manifestcontroller
import (
"context"
"encoding/json"
"fmt"
"reflect"
"strings"
@@ -9,7 +10,7 @@ import (
apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -19,13 +20,16 @@ import (
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/retry"
"k8s.io/klog/v2"
"k8s.io/utils/pointer"
"github.com/openshift/library-go/pkg/controller/factory"
"github.com/openshift/library-go/pkg/operator/events"
"github.com/openshift/library-go/pkg/operator/resource/resourceapply"
"github.com/openshift/library-go/pkg/operator/resource/resourcemerge"
"github.com/pkg/errors"
workv1client "open-cluster-management.io/api/client/work/clientset/versioned/typed/work/v1"
workinformer "open-cluster-management.io/api/client/work/informers/externalversions/work/v1"
worklister "open-cluster-management.io/api/client/work/listers/work/v1"
@@ -58,6 +62,14 @@ type applyResult struct {
resourceMeta workapiv1.ManifestResourceMeta
}
type serverSideApplyConflictError struct {
ssaErr error
}
func (e *serverSideApplyConflictError) Error() string {
return e.ssaErr.Error()
}
// NewManifestWorkController returns a ManifestWorkController
func NewManifestWorkController(
ctx context.Context,
@@ -105,7 +117,7 @@ func (m *ManifestWorkController) sync(ctx context.Context, controllerContext fac
klog.V(4).Infof("Reconciling ManifestWork %q", manifestWorkName)
manifestWork, err := m.manifestWorkLister.Get(manifestWorkName)
if errors.IsNotFound(err) {
if apierrors.IsNotFound(err) {
// work not found, could have been deleted, do nothing.
return nil
}
@@ -135,7 +147,7 @@ func (m *ManifestWorkController) sync(ctx context.Context, controllerContext fac
appliedManifestWorkName := fmt.Sprintf("%s-%s", m.hubHash, manifestWork.Name)
appliedManifestWork, err := m.appliedManifestWorkLister.Get(appliedManifestWorkName)
switch {
case errors.IsNotFound(err):
case apierrors.IsNotFound(err):
appliedManifestWork = &workapiv1.AppliedManifestWork{
ObjectMeta: metav1.ObjectMeta{
Name: appliedManifestWorkName,
@@ -162,10 +174,10 @@ func (m *ManifestWorkController) sync(ctx context.Context, controllerContext fac
resourceResults := make([]applyResult, len(manifestWork.Spec.Workload.Manifests))
err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
resourceResults = m.applyManifests(
ctx, manifestWork.Spec.Workload.Manifests, manifestWork.Spec.DeleteOption, controllerContext.Recorder(), *owner, resourceResults)
ctx, manifestWork.Spec.Workload.Manifests, manifestWork.Spec, controllerContext.Recorder(), *owner, resourceResults)
for _, result := range resourceResults {
if errors.IsConflict(result.Error) {
if apierrors.IsConflict(result.Error) {
return result.Error
}
}
@@ -178,7 +190,9 @@ func (m *ManifestWorkController) sync(ctx context.Context, controllerContext fac
newManifestConditions := []workapiv1.ManifestCondition{}
for _, result := range resourceResults {
if result.Error != nil {
// ignore server side apply conflict error since it cannot be resolved by error fallback.
var ssaConflict *serverSideApplyConflictError
if result.Error != nil && !errors.As(result.Error, &ssaConflict) {
errs = append(errs, result.Error)
}
@@ -209,7 +223,7 @@ func (m *ManifestWorkController) sync(ctx context.Context, controllerContext fac
func (m *ManifestWorkController) applyManifests(
ctx context.Context,
manifests []workapiv1.Manifest,
deleteOption *workapiv1.DeleteOption,
workSpec workapiv1.ManifestWorkSpec,
recorder events.Recorder,
owner metav1.OwnerReference,
existingResults []applyResult) []applyResult {
@@ -218,10 +232,10 @@ func (m *ManifestWorkController) applyManifests(
switch {
case existingResults[index].Result == nil:
// Apply if there is not result.
existingResults[index] = m.applyOneManifest(ctx, index, manifest, deleteOption, recorder, owner)
case errors.IsConflict(existingResults[index].Error):
existingResults[index] = m.applyOneManifest(ctx, index, manifest, workSpec, recorder, owner)
case apierrors.IsConflict(existingResults[index].Error):
// Apply if there is a resource confilct error.
existingResults[index] = m.applyOneManifest(ctx, index, manifest, deleteOption, recorder, owner)
existingResults[index] = m.applyOneManifest(ctx, index, manifest, workSpec, recorder, owner)
}
}
@@ -232,7 +246,7 @@ func (m *ManifestWorkController) applyOneManifest(
ctx context.Context,
index int,
manifest workapiv1.Manifest,
deleteOption *workapiv1.DeleteOption,
workSpec workapiv1.ManifestWorkSpec,
recorder events.Recorder,
owner metav1.OwnerReference) applyResult {
@@ -243,59 +257,123 @@ func (m *ManifestWorkController) applyOneManifest(
result := applyResult{}
unstructuredObj := &unstructured.Unstructured{}
if err := unstructuredObj.UnmarshalJSON(manifest.Raw); err != nil {
// parse the required and set resource meta
required := &unstructured.Unstructured{}
if err := required.UnmarshalJSON(manifest.Raw); err != nil {
result.Error = err
return result
}
resMeta, gvr, err := buildResourceMeta(index, unstructuredObj, m.restMapper)
resMeta, gvr, err := buildResourceMeta(index, required, m.restMapper)
result.resourceMeta = resMeta
if err != nil {
result.Error = err
return result
}
owner = manageOwnerRef(gvr, resMeta.Namespace, resMeta.Name, deleteOption, owner)
unstructuredObj.SetOwnerReferences([]metav1.OwnerReference{owner})
// try to get the existing at first.
existing, existingErr := m.spokeDynamicClient.
Resource(gvr).
Namespace(required.GetNamespace()).
Get(ctx, required.GetName(), metav1.GetOptions{})
if existingErr != nil && !apierrors.IsNotFound(existingErr) {
result.Error = existingErr
return result
}
results := resourceapply.ApplyDirectly(ctx, clientHolder, recorder, m.staticResourceCache, func(name string) ([]byte, error) {
return unstructuredObj.MarshalJSON()
}, "manifest")
// compute required ownerrefs based on delete option
requiredOwner := manageOwnerRef(gvr, resMeta.Namespace, resMeta.Name, workSpec.DeleteOption, owner)
result.Result = results[0].Result
result.Changed = results[0].Changed
result.Error = results[0].Error
// find update strategy option.
option := helper.FindManifestConiguration(resMeta, workSpec.ManifestConfigs)
// strategy is update by default
strategy := workapiv1.UpdateStrategy{Type: workapiv1.UpdateStrategyTypeUpdate}
if option != nil && option.UpdateStrategy != nil {
strategy = *option.UpdateStrategy
}
// Try apply with dynamic client if the manifest cannot be decoded by scheme or typed client is not found
// TODO we should check the certain error.
// Use dynamic client when scheme cannot decode manifest or typed client cannot handle the object
if isDecodeError(result.Error) || isUnhandledError(result.Error) || isUnsupportedError(result.Error) {
result.Result, result.Changed, result.Error = m.applyUnstructured(ctx, unstructuredObj, gvr, recorder)
// apply resource based on update strategy
switch strategy.Type {
case workapiv1.UpdateStrategyTypeServerSideApply:
result.Result, result.Changed, result.Error = m.serverSideApply(ctx, gvr, required, option.UpdateStrategy.ServerSideApply, recorder)
case workapiv1.UpdateStrategyTypeCreateOnly, workapiv1.UpdateStrategyTypeUpdate:
if strategy.Type == workapiv1.UpdateStrategyTypeCreateOnly && existingErr == nil {
result.Result = existing
break
}
required.SetOwnerReferences([]metav1.OwnerReference{requiredOwner})
results := resourceapply.ApplyDirectly(ctx, clientHolder, recorder, m.staticResourceCache, func(name string) ([]byte, error) {
return required.MarshalJSON()
}, "manifest")
result.Result = results[0].Result
result.Changed = results[0].Changed
result.Error = results[0].Error
// Try apply with dynamic client if the manifest cannot be decoded by scheme or typed client is not found
// TODO we should check the certain error.
// Use dynamic client when scheme cannot decode manifest or typed client cannot handle the object
if isDecodeError(result.Error) || isUnhandledError(result.Error) || isUnsupportedError(result.Error) {
result.Result, result.Changed, result.Error = m.applyUnstructured(ctx, existing, required, gvr, recorder)
}
}
// patch the ownerref no matter apply fails or not.
if result.Error == nil {
result.Error = helper.ApplyOwnerReferences(ctx, m.spokeDynamicClient, gvr, result.Result, requiredOwner)
}
return result
}
func (m *ManifestWorkController) applyUnstructured(
func (m *ManifestWorkController) serverSideApply(
ctx context.Context,
required *unstructured.Unstructured,
gvr schema.GroupVersionResource,
required *unstructured.Unstructured,
config *workapiv1.ServerSideApplyConfig,
recorder events.Recorder) (*unstructured.Unstructured, bool, error) {
existing, err := m.spokeDynamicClient.
force := false
fieldManager := workapiv1.DefaultFieldManager
if config != nil {
force = config.Force
if len(config.FieldManager) > 0 {
fieldManager = config.FieldManager
}
}
patch, err := json.Marshal(resourcemerge.WithCleanLabelsAndAnnotations(required))
if err != nil {
return nil, false, err
}
// TODO use Apply method instead when upgrading the client-go to 0.25.x
actual, err := m.spokeDynamicClient.
Resource(gvr).
Namespace(required.GetNamespace()).
Get(ctx, required.GetName(), metav1.GetOptions{})
Patch(ctx, required.GetName(), types.ApplyPatchType, patch, metav1.PatchOptions{FieldManager: fieldManager, Force: pointer.Bool(force)})
resourceKey, _ := cache.MetaNamespaceKeyFunc(required)
recorder.Eventf(fmt.Sprintf(
"Server Side Applied %s %s", required.GetKind(), resourceKey), "Patched with field manager %s", fieldManager)
switch {
case errors.IsNotFound(err):
if apierrors.IsConflict(err) {
return actual, true, &serverSideApplyConflictError{ssaErr: err}
}
return actual, true, err
}
func (m *ManifestWorkController) applyUnstructured(
ctx context.Context,
existing, required *unstructured.Unstructured,
gvr schema.GroupVersionResource,
recorder events.Recorder) (*unstructured.Unstructured, bool, error) {
if existing == nil {
actual, err := m.spokeDynamicClient.Resource(gvr).Namespace(required.GetNamespace()).Create(
ctx, resourcemerge.WithCleanLabelsAndAnnotations(required).(*unstructured.Unstructured), metav1.CreateOptions{})
recorder.Eventf(fmt.Sprintf(
"%s Created", required.GetKind()), "Created %s/%s because it was missing", required.GetNamespace(), required.GetName())
return actual, true, err
case err != nil:
return nil, false, err
}
// Merge OwnerRefs, Labels, and Annotations.

View File

@@ -9,6 +9,7 @@ import (
"github.com/openshift/library-go/pkg/operator/resource/resourceapply"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -97,6 +98,7 @@ func assertManifestCondition(
type testCase struct {
name string
workManifest []*unstructured.Unstructured
workManifestConfig []workapiv1.ManifestConfigOption
spokeObject []runtime.Object
spokeDynamicObject []runtime.Object
expectedWorkAction []string
@@ -116,6 +118,7 @@ func newTestCase(name string) *testCase {
return &testCase{
name: name,
workManifest: []*unstructured.Unstructured{},
workManifestConfig: []workapiv1.ManifestConfigOption{},
spokeObject: []runtime.Object{},
spokeDynamicObject: []runtime.Object{},
expectedWorkAction: []string{},
@@ -132,6 +135,11 @@ func (t *testCase) withWorkManifest(objects ...*unstructured.Unstructured) *test
return t
}
func (t *testCase) withManifestConfig(configs ...workapiv1.ManifestConfigOption) *testCase {
t.workManifestConfig = configs
return t
}
func (t *testCase) withSpokeObject(objects ...runtime.Object) *testCase {
t.spokeObject = objects
return t
@@ -270,6 +278,7 @@ func TestSync(t *testing.T) {
withWorkManifest(spoketesting.NewUnstructured("v1", "Secret", "ns1", "test")).
withExpectedWorkAction("update").
withAppliedWorkAction("create").
withExpectedDynamicAction("get").
withExpectedKubeAction("get", "create").
withExpectedManifestCondition(expectedCondition{string(workapiv1.ManifestApplied), metav1.ConditionTrue}).
withExpectedWorkCondition(expectedCondition{string(workapiv1.WorkApplied), metav1.ConditionTrue}),
@@ -283,6 +292,7 @@ func TestSync(t *testing.T) {
newTestCase("update single resource").
withWorkManifest(spoketesting.NewUnstructured("v1", "Secret", "ns1", "test")).
withSpokeObject(spoketesting.NewSecret("test", "ns1", "value2")).
withExpectedDynamicAction("get").
withExpectedWorkAction("update").
withAppliedWorkAction("create").
withExpectedKubeAction("get", "delete", "create").
@@ -308,6 +318,7 @@ func TestSync(t *testing.T) {
withSpokeObject(spoketesting.NewSecret("test", "ns1", "value2")).
withExpectedWorkAction("update").
withAppliedWorkAction("create").
withExpectedDynamicAction("get", "get").
withExpectedKubeAction("get", "delete", "create", "get", "create").
withExpectedManifestCondition(expectedCondition{string(workapiv1.ManifestApplied), metav1.ConditionTrue}, expectedCondition{string(workapiv1.ManifestApplied), metav1.ConditionTrue}).
withExpectedWorkCondition(expectedCondition{string(workapiv1.WorkApplied), metav1.ConditionTrue}),
@@ -338,6 +349,7 @@ func TestFailedToApplyResource(t *testing.T) {
withSpokeObject(spoketesting.NewSecret("test", "ns1", "value2")).
withExpectedWorkAction("update").
withAppliedWorkAction("create").
withExpectedDynamicAction("get", "get").
withExpectedKubeAction("get", "delete", "create", "get", "create").
withExpectedManifestCondition(expectedCondition{string(workapiv1.ManifestApplied), metav1.ConditionTrue}, expectedCondition{string(workapiv1.ManifestApplied), metav1.ConditionFalse}).
withExpectedWorkCondition(expectedCondition{string(workapiv1.WorkApplied), metav1.ConditionFalse})
@@ -369,6 +381,130 @@ func TestFailedToApplyResource(t *testing.T) {
tc.validate(t, controller.dynamicClient, controller.workClient, controller.kubeClient)
}
func TestUpdateStrategy(t *testing.T) {
cases := []*testCase{
newTestCase("update single resource with nil updateStrategy").
withWorkManifest(spoketesting.NewUnstructuredWithContent("v1", "NewObject", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val1"}})).
withSpokeDynamicObject(spoketesting.NewUnstructuredWithContent("v1", "NewObject", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val2"}})).
withManifestConfig(newManifestConfigOption("", "newobjects", "ns1", "n1", nil)).
withExpectedWorkAction("update").
withAppliedWorkAction("create").
withExpectedDynamicAction("get", "update").
withExpectedManifestCondition(expectedCondition{string(workapiv1.ManifestApplied), metav1.ConditionTrue}).
withExpectedWorkCondition(expectedCondition{string(workapiv1.WorkApplied), metav1.ConditionTrue}),
newTestCase("update single resource with update updateStrategy").
withWorkManifest(spoketesting.NewUnstructuredWithContent("v1", "NewObject", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val1"}})).
withSpokeDynamicObject(spoketesting.NewUnstructuredWithContent("v1", "NewObject", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val2"}})).
withManifestConfig(newManifestConfigOption("", "newobjects", "ns1", "n1", &workapiv1.UpdateStrategy{Type: workapiv1.UpdateStrategyTypeUpdate})).
withExpectedWorkAction("update").
withAppliedWorkAction("create").
withExpectedDynamicAction("get", "update").
withExpectedManifestCondition(expectedCondition{string(workapiv1.ManifestApplied), metav1.ConditionTrue}).
withExpectedWorkCondition(expectedCondition{string(workapiv1.WorkApplied), metav1.ConditionTrue}),
newTestCase("create single resource with updateStrategy not found").
withWorkManifest(spoketesting.NewUnstructuredWithContent("v1", "NewObject", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val1"}})).
withSpokeDynamicObject(spoketesting.NewUnstructuredWithContent("v1", "NewObject", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val2"}})).
withManifestConfig(newManifestConfigOption("", "newobjects", "ns1", "n2", &workapiv1.UpdateStrategy{Type: workapiv1.UpdateStrategyTypeServerSideApply})).
withExpectedWorkAction("update").
withAppliedWorkAction("create").
withExpectedDynamicAction("get", "update").
withExpectedManifestCondition(expectedCondition{string(workapiv1.ManifestApplied), metav1.ConditionTrue}).
withExpectedWorkCondition(expectedCondition{string(workapiv1.WorkApplied), metav1.ConditionTrue}),
newTestCase("create single resource with server side apply updateStrategy").
withWorkManifest(spoketesting.NewUnstructuredWithContent("v1", "NewObject", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val1"}})).
withManifestConfig(newManifestConfigOption("", "newobjects", "ns1", "n1", &workapiv1.UpdateStrategy{Type: workapiv1.UpdateStrategyTypeServerSideApply})).
withExpectedWorkAction("update").
withAppliedWorkAction("create").
withExpectedDynamicAction("get", "patch", "patch").
withExpectedManifestCondition(expectedCondition{string(workapiv1.ManifestApplied), metav1.ConditionTrue}).
withExpectedWorkCondition(expectedCondition{string(workapiv1.WorkApplied), metav1.ConditionTrue}),
newTestCase("update single resource with server side apply updateStrategy").
withWorkManifest(spoketesting.NewUnstructuredWithContent("v1", "NewObject", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val1"}})).
withSpokeDynamicObject(spoketesting.NewUnstructuredWithContent("v1", "NewObject", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val2"}})).
withManifestConfig(newManifestConfigOption("", "newobjects", "ns1", "n1", &workapiv1.UpdateStrategy{Type: workapiv1.UpdateStrategyTypeServerSideApply})).
withExpectedWorkAction("update").
withAppliedWorkAction("create").
withExpectedDynamicAction("get", "patch", "patch").
withExpectedManifestCondition(expectedCondition{string(workapiv1.ManifestApplied), metav1.ConditionTrue}).
withExpectedWorkCondition(expectedCondition{string(workapiv1.WorkApplied), metav1.ConditionTrue}),
newTestCase("update single resource with create only updateStrategy").
withWorkManifest(spoketesting.NewUnstructuredWithContent("v1", "NewObject", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val1"}})).
withSpokeDynamicObject(spoketesting.NewUnstructuredWithContent("v1", "NewObject", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val2"}})).
withManifestConfig(newManifestConfigOption("", "newobjects", "ns1", "n1", &workapiv1.UpdateStrategy{Type: workapiv1.UpdateStrategyTypeCreateOnly})).
withExpectedWorkAction("update").
withAppliedWorkAction("create").
withExpectedDynamicAction("get", "patch").
withExpectedManifestCondition(expectedCondition{string(workapiv1.ManifestApplied), metav1.ConditionTrue}).
withExpectedWorkCondition(expectedCondition{string(workapiv1.WorkApplied), metav1.ConditionTrue}),
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
work, workKey := spoketesting.NewManifestWork(0, c.workManifest...)
work.Spec.ManifestConfigs = c.workManifestConfig
work.Finalizers = []string{controllers.ManifestWorkFinalizer}
controller := newController(t, work, nil, spoketesting.NewFakeRestMapper()).
withKubeObject(c.spokeObject...).
withUnstructuredObject(c.spokeDynamicObject...)
// The default reactor doesn't support apply, so we need our own (trivial) reactor
controller.dynamicClient.PrependReactor("patch", "newobjects", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
return true, spoketesting.NewUnstructuredWithContent("v1", "NewObject", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val1"}}), nil // clusterroleaggregator drops returned objects so no point in constructing them
})
syncContext := spoketesting.NewFakeSyncContext(t, workKey)
err := controller.controller.sync(context.TODO(), syncContext)
if err != nil {
t.Errorf("Should be success with no err: %v", err)
}
c.validate(t, controller.dynamicClient, controller.workClient, controller.kubeClient)
})
}
}
func TestServerSideApplyConflict(t *testing.T) {
testCase := newTestCase("update single resource with server side apply updateStrategy").
withWorkManifest(spoketesting.NewUnstructuredWithContent("v1", "NewObject", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val1"}})).
withSpokeDynamicObject(spoketesting.NewUnstructuredWithContent("v1", "NewObject", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val2"}})).
withManifestConfig(newManifestConfigOption("", "newobjects", "ns1", "n1", &workapiv1.UpdateStrategy{Type: workapiv1.UpdateStrategyTypeServerSideApply})).
withExpectedWorkAction("update").
withAppliedWorkAction("create").
withExpectedDynamicAction("get", "patch").
withExpectedManifestCondition(expectedCondition{string(workapiv1.ManifestApplied), metav1.ConditionFalse}).
withExpectedWorkCondition(expectedCondition{string(workapiv1.WorkApplied), metav1.ConditionFalse})
work, workKey := spoketesting.NewManifestWork(0, testCase.workManifest...)
work.Spec.ManifestConfigs = testCase.workManifestConfig
work.Finalizers = []string{controllers.ManifestWorkFinalizer}
controller := newController(t, work, nil, spoketesting.NewFakeRestMapper()).
withKubeObject(testCase.spokeObject...).
withUnstructuredObject(testCase.spokeDynamicObject...)
// The default reactor doesn't support apply, so we need our own (trivial) reactor
controller.dynamicClient.PrependReactor("patch", "newobjects", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
return true, nil, errors.NewConflict(schema.GroupResource{Resource: "newobjects"}, "n1", fmt.Errorf("conflict error"))
})
syncContext := spoketesting.NewFakeSyncContext(t, workKey)
err := controller.controller.sync(context.TODO(), syncContext)
if err != nil {
t.Errorf("Should be success with no err: %v", err)
}
testCase.validate(t, controller.dynamicClient, controller.workClient, controller.kubeClient)
}
func newManifestConfigOption(group, resource, namespace, name string, strategy *workapiv1.UpdateStrategy) workapiv1.ManifestConfigOption {
return workapiv1.ManifestConfigOption{
ResourceIdentifier: workapiv1.ResourceIdentifier{
Resource: resource,
Group: group,
Namespace: namespace,
Name: name,
},
UpdateStrategy: strategy,
}
}
// Test unstructured compare
func TestIsSameUnstructured(t *testing.T) {
cases := []struct {
@@ -742,29 +878,27 @@ func TestApplyUnstructred(t *testing.T) {
cases := []struct {
name string
owner metav1.OwnerReference
existingObject []runtime.Object
existing *unstructured.Unstructured
required *unstructured.Unstructured
gvr schema.GroupVersionResource
validateActions func(t *testing.T, actions []clienttesting.Action)
}{
{
name: "create a new object with owner",
existingObject: []runtime.Object{},
owner: metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: "testowner"},
required: spoketesting.NewUnstructured("v1", "Secret", "ns1", "test"),
gvr: schema.GroupVersionResource{Version: "v1", Resource: "secrets"},
name: "create a new object with owner",
owner: metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: "testowner"},
required: spoketesting.NewUnstructured("v1", "Secret", "ns1", "test"),
gvr: schema.GroupVersionResource{Version: "v1", Resource: "secrets"},
validateActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 2 {
t.Errorf("Expect 2 actions, but have %d", len(actions))
if len(actions) != 1 {
t.Errorf("Expect 1 actions, but have %d", len(actions))
}
spoketesting.AssertAction(t, actions[0], "get")
spoketesting.AssertAction(t, actions[1], "create")
spoketesting.AssertAction(t, actions[0], "create")
obj := actions[1].(clienttesting.CreateActionImpl).Object.(*unstructured.Unstructured)
obj := actions[0].(clienttesting.CreateActionImpl).Object.(*unstructured.Unstructured)
owners := obj.GetOwnerReferences()
if len(owners) != 1 {
t.Errorf("Expect 2 owners, but have %d", len(owners))
t.Errorf("Expect 1 owners, but have %d", len(owners))
}
if owners[0].UID != "testowner" {
@@ -773,20 +907,18 @@ func TestApplyUnstructred(t *testing.T) {
},
},
{
name: "create a new object without owner",
existingObject: []runtime.Object{},
owner: metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: "testowner-"},
required: spoketesting.NewUnstructured("v1", "Secret", "ns1", "test"),
gvr: schema.GroupVersionResource{Version: "v1", Resource: "secrets"},
name: "create a new object without owner",
owner: metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: "testowner-"},
required: spoketesting.NewUnstructured("v1", "Secret", "ns1", "test"),
gvr: schema.GroupVersionResource{Version: "v1", Resource: "secrets"},
validateActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 2 {
t.Errorf("Expect 2 actions, but have %d", len(actions))
if len(actions) != 1 {
t.Errorf("Expect 1 actions, but have %d", len(actions))
}
spoketesting.AssertAction(t, actions[0], "get")
spoketesting.AssertAction(t, actions[1], "create")
spoketesting.AssertAction(t, actions[0], "create")
obj := actions[1].(clienttesting.CreateActionImpl).Object.(*unstructured.Unstructured)
obj := actions[0].(clienttesting.CreateActionImpl).Object.(*unstructured.Unstructured)
owners := obj.GetOwnerReferences()
if len(owners) != 0 {
t.Errorf("Expect 1 owners, but have %d", len(owners))
@@ -795,20 +927,19 @@ func TestApplyUnstructred(t *testing.T) {
},
{
name: "update an object owner",
existingObject: []runtime.Object{spoketesting.NewUnstructured(
"v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"})},
existing: spoketesting.NewUnstructured(
"v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"}),
owner: metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: "testowner"},
required: spoketesting.NewUnstructured("v1", "Secret", "ns1", "test"),
gvr: schema.GroupVersionResource{Version: "v1", Resource: "secrets"},
validateActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 2 {
t.Errorf("Expect 2 actions, but have %d", len(actions))
if len(actions) != 1 {
t.Errorf("Expect 1 actions, but have %d", len(actions))
}
spoketesting.AssertAction(t, actions[0], "get")
spoketesting.AssertAction(t, actions[1], "update")
spoketesting.AssertAction(t, actions[0], "update")
obj := actions[1].(clienttesting.UpdateActionImpl).Object.(*unstructured.Unstructured)
obj := actions[0].(clienttesting.UpdateActionImpl).Object.(*unstructured.Unstructured)
owners := obj.GetOwnerReferences()
if len(owners) != 2 {
t.Errorf("Expect 2 owners, but have %d", len(owners))
@@ -824,35 +955,32 @@ func TestApplyUnstructred(t *testing.T) {
},
{
name: "update an object without owner",
existingObject: []runtime.Object{spoketesting.NewUnstructured(
"v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"})},
existing: spoketesting.NewUnstructured(
"v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"}),
owner: metav1.OwnerReference{Name: "test", UID: "testowner-"},
required: spoketesting.NewUnstructured("v1", "Secret", "ns1", "test"),
gvr: schema.GroupVersionResource{Version: "v1", Resource: "secrets"},
validateActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 0 {
t.Errorf("Expect 0 actions, but have %d", len(actions))
}
},
},
{
name: "remove an object owner",
existing: spoketesting.NewUnstructured(
"v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: "testowner"}),
owner: metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: "testowner-"},
required: spoketesting.NewUnstructured("v1", "Secret", "ns1", "test"),
gvr: schema.GroupVersionResource{Version: "v1", Resource: "secrets"},
validateActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 1 {
t.Errorf("Expect 1 actions, but have %d", len(actions))
}
spoketesting.AssertAction(t, actions[0], "get")
},
},
{
name: "remove an object owner",
existingObject: []runtime.Object{spoketesting.NewUnstructured(
"v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: "testowner"})},
owner: metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: "testowner-"},
required: spoketesting.NewUnstructured("v1", "Secret", "ns1", "test"),
gvr: schema.GroupVersionResource{Version: "v1", Resource: "secrets"},
validateActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 2 {
t.Errorf("Expect 2 actions, but have %d", len(actions))
}
spoketesting.AssertAction(t, actions[0], "update")
spoketesting.AssertAction(t, actions[0], "get")
spoketesting.AssertAction(t, actions[1], "update")
obj := actions[1].(clienttesting.UpdateActionImpl).Object.(*unstructured.Unstructured)
obj := actions[0].(clienttesting.UpdateActionImpl).Object.(*unstructured.Unstructured)
owners := obj.GetOwnerReferences()
if len(owners) != 0 {
t.Errorf("Expect 0 owner, but have %d", len(owners))
@@ -861,14 +989,12 @@ func TestApplyUnstructred(t *testing.T) {
},
{
name: "merge labels",
existingObject: []runtime.Object{
func() runtime.Object {
obj := spoketesting.NewUnstructured(
"v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"})
obj.SetLabels(map[string]string{"foo": "bar"})
return obj
}(),
},
existing: func() *unstructured.Unstructured {
obj := spoketesting.NewUnstructured(
"v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"})
obj.SetLabels(map[string]string{"foo": "bar"})
return obj
}(),
owner: metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"},
required: func() *unstructured.Unstructured {
obj := spoketesting.NewUnstructured(
@@ -878,14 +1004,13 @@ func TestApplyUnstructred(t *testing.T) {
}(),
gvr: schema.GroupVersionResource{Version: "v1", Resource: "secrets"},
validateActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 2 {
t.Errorf("Expect 2 actions, but have %d", len(actions))
if len(actions) != 1 {
t.Errorf("Expect 1 actions, but have %d", len(actions))
}
spoketesting.AssertAction(t, actions[0], "get")
spoketesting.AssertAction(t, actions[1], "update")
spoketesting.AssertAction(t, actions[0], "update")
obj := actions[1].(clienttesting.UpdateActionImpl).Object.(*unstructured.Unstructured)
obj := actions[0].(clienttesting.UpdateActionImpl).Object.(*unstructured.Unstructured)
labels := obj.GetLabels()
if len(labels) != 2 {
t.Errorf("Expect 2 labels, but have %d", len(labels))
@@ -894,14 +1019,12 @@ func TestApplyUnstructred(t *testing.T) {
},
{
name: "merge annotation",
existingObject: []runtime.Object{
func() runtime.Object {
obj := spoketesting.NewUnstructured(
"v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"})
obj.SetAnnotations(map[string]string{"foo": "bar"})
return obj
}(),
},
existing: func() *unstructured.Unstructured {
obj := spoketesting.NewUnstructured(
"v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"})
obj.SetAnnotations(map[string]string{"foo": "bar"})
return obj
}(),
owner: metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"},
required: func() *unstructured.Unstructured {
obj := spoketesting.NewUnstructured(
@@ -911,14 +1034,13 @@ func TestApplyUnstructred(t *testing.T) {
}(),
gvr: schema.GroupVersionResource{Version: "v1", Resource: "secrets"},
validateActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 2 {
t.Errorf("Expect 2 actions, but have %d", len(actions))
if len(actions) != 1 {
t.Errorf("Expect 1 actions, but have %d", len(actions))
}
spoketesting.AssertAction(t, actions[0], "get")
spoketesting.AssertAction(t, actions[1], "update")
spoketesting.AssertAction(t, actions[0], "update")
obj := actions[1].(clienttesting.UpdateActionImpl).Object.(*unstructured.Unstructured)
obj := actions[0].(clienttesting.UpdateActionImpl).Object.(*unstructured.Unstructured)
annotations := obj.GetAnnotations()
if len(annotations) != 2 {
t.Errorf("Expect 2 annotations, but have %d", len(annotations))
@@ -927,14 +1049,12 @@ func TestApplyUnstructred(t *testing.T) {
},
{
name: "set existing finalizer",
existingObject: []runtime.Object{
func() runtime.Object {
obj := spoketesting.NewUnstructured(
"v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"})
obj.SetFinalizers([]string{"foo"})
return obj
}(),
},
existing: func() *unstructured.Unstructured {
obj := spoketesting.NewUnstructured(
"v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"})
obj.SetFinalizers([]string{"foo"})
return obj
}(),
owner: metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"},
required: func() *unstructured.Unstructured {
obj := spoketesting.NewUnstructured(
@@ -944,24 +1064,20 @@ func TestApplyUnstructred(t *testing.T) {
}(),
gvr: schema.GroupVersionResource{Version: "v1", Resource: "secrets"},
validateActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 1 {
t.Errorf("Expect 1 actions, but have %d", len(actions))
if len(actions) != 0 {
t.Errorf("Expect 0 actions, but have %d", len(actions))
}
spoketesting.AssertAction(t, actions[0], "get")
},
},
{
name: "nothing to update",
existingObject: []runtime.Object{
func() runtime.Object {
obj := spoketesting.NewUnstructured(
"v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"})
obj.SetLabels(map[string]string{"foo": "bar"})
obj.SetAnnotations(map[string]string{"foo": "bar"})
return obj
}(),
},
existing: func() *unstructured.Unstructured {
obj := spoketesting.NewUnstructured(
"v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"})
obj.SetLabels(map[string]string{"foo": "bar"})
obj.SetAnnotations(map[string]string{"foo": "bar"})
return obj
}(),
owner: metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"},
required: func() *unstructured.Unstructured {
obj := spoketesting.NewUnstructured(
@@ -972,11 +1088,9 @@ func TestApplyUnstructred(t *testing.T) {
}(),
gvr: schema.GroupVersionResource{Version: "v1", Resource: "secrets"},
validateActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 1 {
t.Errorf("Expect 1 actions, but have %v", actions)
if len(actions) != 0 {
t.Errorf("Expect 0 actions, but have %v", actions)
}
spoketesting.AssertAction(t, actions[0], "get")
},
},
}
@@ -985,13 +1099,16 @@ func TestApplyUnstructred(t *testing.T) {
t.Run(c.name, func(t *testing.T) {
work, workKey := spoketesting.NewManifestWork(0)
work.Finalizers = []string{controllers.ManifestWorkFinalizer}
controller := newController(t, work, nil, spoketesting.NewFakeRestMapper()).
withUnstructuredObject(c.existingObject...)
objects := []runtime.Object{}
if c.existing != nil {
objects = append(objects, c.existing)
}
controller := newController(t, work, nil, spoketesting.NewFakeRestMapper()).withUnstructuredObject(objects...)
syncContext := spoketesting.NewFakeSyncContext(t, workKey)
c.required.SetOwnerReferences([]metav1.OwnerReference{c.owner})
_, _, err := controller.controller.applyUnstructured(
context.TODO(), c.required, c.gvr, syncContext.Recorder())
context.TODO(), c.existing, c.required, c.gvr, syncContext.Recorder())
if err != nil {
t.Errorf("expect no error, but got %v", err)

View File

@@ -22,6 +22,7 @@ import (
workinformer "open-cluster-management.io/api/client/work/informers/externalversions/work/v1"
worklister "open-cluster-management.io/api/client/work/listers/work/v1"
workapiv1 "open-cluster-management.io/api/work/v1"
"open-cluster-management.io/work/pkg/helper"
"open-cluster-management.io/work/pkg/spoke/statusfeedback"
)
@@ -199,26 +200,23 @@ func (c *AvailableStatusController) getFeedbackValues(
errs := []error{}
values := []workapiv1.FeedbackValue{}
identifier := workapiv1.ResourceIdentifier{
Group: resourceMeta.Group,
Resource: resourceMeta.Resource,
Namespace: resourceMeta.Namespace,
Name: resourceMeta.Name,
option := helper.FindManifestConiguration(resourceMeta, manifestOptions)
if option == nil || len(option.FeedbackRules) == 0 {
return values, metav1.Condition{
Type: statusFeedbackConditionType,
Reason: "NoStatusFeedbackSynced",
Status: metav1.ConditionTrue,
}
}
for _, field := range manifestOptions {
if field.ResourceIdentifier != identifier {
continue
for _, rule := range option.FeedbackRules {
valuesByRule, err := c.statusReader.GetValuesByRule(obj, rule)
if err != nil {
errs = append(errs, err)
}
for _, rule := range field.FeedbackRules {
valuesByRule, err := c.statusReader.GetValuesByRule(obj, rule)
if err != nil {
errs = append(errs, err)
}
if len(valuesByRule) > 0 {
values = append(values, valuesByRule...)
}
if len(valuesByRule) > 0 {
values = append(values, valuesByRule...)
}
}
@@ -233,14 +231,6 @@ func (c *AvailableStatusController) getFeedbackValues(
}
}
if len(values) == 0 {
return values, metav1.Condition{
Type: statusFeedbackConditionType,
Reason: "NoStatusFeedbackSynced",
Status: metav1.ConditionTrue,
}
}
return values, metav1.Condition{
Type: statusFeedbackConditionType,
Reason: "StatusFeedbackSynced",