mirror of
https://github.com/open-cluster-management-io/ocm.git
synced 2026-05-19 07:37:40 +00:00
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:
@@ -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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user