move apply method to seperate package (#149)

* move apply method to seperate package

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

* make sure the update apply func returns existing object

Signed-off-by: zhujian <jiazhu@redhat.com>

Signed-off-by: Jian Qiu <jqiu@redhat.com>
Signed-off-by: zhujian <jiazhu@redhat.com>
Co-authored-by: Jian Qiu <jqiu@redhat.com>
This commit is contained in:
Jian Zhu
2022-08-29 16:10:16 +08:00
committed by GitHub
parent 8f206974dd
commit 787d7c5d45
20 changed files with 1738 additions and 504 deletions

42
pkg/spoke/apply/apply.go Normal file
View File

@@ -0,0 +1,42 @@
package apply
import (
"context"
"github.com/openshift/library-go/pkg/operator/events"
apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
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/client-go/dynamic"
"k8s.io/client-go/kubernetes"
workapiv1 "open-cluster-management.io/api/work/v1"
)
type Applier interface {
Apply(ctx context.Context,
gvr schema.GroupVersionResource,
required *unstructured.Unstructured,
owner metav1.OwnerReference,
applyOption *workapiv1.ManifestConfigOption,
recorder events.Recorder) (runtime.Object, error)
}
type Appliers struct {
appliers map[workapiv1.UpdateStrategyType]Applier
}
func NewAppliers(dynamicClient dynamic.Interface, kubeclient kubernetes.Interface, apiExtensionClient apiextensionsclient.Interface) *Appliers {
return &Appliers{
appliers: map[workapiv1.UpdateStrategyType]Applier{
workapiv1.UpdateStrategyTypeCreateOnly: NewCreateOnlyApply(dynamicClient),
workapiv1.UpdateStrategyTypeServerSideApply: NewServerSideApply(dynamicClient),
workapiv1.UpdateStrategyTypeUpdate: NewUpdateApply(dynamicClient, kubeclient, apiExtensionClient),
},
}
}
func (a *Appliers) GetApplier(strategy workapiv1.UpdateStrategyType) Applier {
return a.appliers[strategy]
}

View File

@@ -0,0 +1,48 @@
package apply
import (
"context"
"fmt"
"github.com/openshift/library-go/pkg/operator/events"
"github.com/openshift/library-go/pkg/operator/resource/resourcemerge"
apierrors "k8s.io/apimachinery/pkg/api/errors"
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/client-go/dynamic"
workapiv1 "open-cluster-management.io/api/work/v1"
)
type CreateOnlyApply struct {
client dynamic.Interface
}
func NewCreateOnlyApply(client dynamic.Interface) *CreateOnlyApply {
return &CreateOnlyApply{client: client}
}
func (c *CreateOnlyApply) Apply(ctx context.Context,
gvr schema.GroupVersionResource,
required *unstructured.Unstructured,
owner metav1.OwnerReference,
_ *workapiv1.ManifestConfigOption,
recorder events.Recorder) (runtime.Object, error) {
obj, err := c.client.
Resource(gvr).
Namespace(required.GetNamespace()).
Get(ctx, required.GetName(), metav1.GetOptions{})
if apierrors.IsNotFound(err) {
required.SetOwnerReferences([]metav1.OwnerReference{owner})
obj, err = c.client.Resource(gvr).Namespace(required.GetNamespace()).Create(
ctx, resourcemerge.WithCleanLabelsAndAnnotations(required).(*unstructured.Unstructured), metav1.CreateOptions{})
if err != nil {
recorder.Eventf(fmt.Sprintf(
"%s Created", required.GetKind()), "Created %s/%s because it was missing", required.GetNamespace(), required.GetName())
}
}
return obj, err
}

View File

@@ -0,0 +1,101 @@
package apply
import (
"context"
"testing"
"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"
fakedynamic "k8s.io/client-go/dynamic/fake"
clienttesting "k8s.io/client-go/testing"
"open-cluster-management.io/work/pkg/spoke/spoketesting"
)
func TestCreateOnlyApply(t *testing.T) {
cases := []struct {
name string
owner metav1.OwnerReference
existing *unstructured.Unstructured
required *unstructured.Unstructured
gvr schema.GroupVersionResource
validateActions func(t *testing.T, actions []clienttesting.Action)
}{
{
name: "create a non exist object",
owner: metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: "testowner"},
existing: nil,
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], "get")
spoketesting.AssertAction(t, actions[1], "create")
obj := actions[1].(clienttesting.CreateActionImpl).Object.(*unstructured.Unstructured)
owners := obj.GetOwnerReferences()
if len(owners) != 1 {
t.Errorf("Expect 1 owners, but have %d", len(owners))
}
if owners[0].UID != "testowner" {
t.Errorf("Owner UId is not correct, got %s", owners[0].UID)
}
},
},
{
name: "create an already existing object",
owner: metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: "testowner"},
existing: spoketesting.NewUnstructured("v1", "Secret", "ns1", "test"),
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")
action := actions[0].(clienttesting.GetActionImpl)
if action.Namespace != "ns1" || action.Name != "test" {
t.Errorf("Expect get secret ns1/test, but %s/%s", action.Namespace, action.Name)
}
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
objects := []runtime.Object{}
if c.existing != nil {
objects = append(objects, c.existing)
}
scheme := runtime.NewScheme()
dynamicClient := fakedynamic.NewSimpleDynamicClient(scheme, objects...)
applier := NewCreateOnlyApply(dynamicClient)
syncContext := spoketesting.NewFakeSyncContext(t, "test")
obj, err := applier.Apply(
context.TODO(), c.gvr, c.required, c.owner, nil, syncContext.Recorder())
if err != nil {
t.Errorf("expect no error, but got %v", obj)
}
accessor, err := meta.Accessor(obj)
if err != nil {
t.Errorf("type %t cannot be accessed: %v", obj, err)
}
if accessor.GetNamespace() != c.required.GetNamespace() || accessor.GetName() != c.required.GetName() {
t.Errorf("Expect resource %s/%s, but %s/%s",
c.required.GetNamespace(), c.required.GetName(), accessor.GetNamespace(), accessor.GetName())
}
c.validateActions(t, dynamicClient.Actions())
})
}
}

View File

@@ -0,0 +1,77 @@
package apply
import (
"context"
"encoding/json"
"fmt"
"github.com/openshift/library-go/pkg/operator/events"
"k8s.io/apimachinery/pkg/api/errors"
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"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/tools/cache"
"k8s.io/utils/pointer"
workapiv1 "open-cluster-management.io/api/work/v1"
)
type ServerSideApply struct {
client dynamic.Interface
}
type ServerSideApplyConflictError struct {
ssaErr error
}
func (e *ServerSideApplyConflictError) Error() string {
return e.ssaErr.Error()
}
func NewServerSideApply(client dynamic.Interface) *ServerSideApply {
return &ServerSideApply{client: client}
}
func (c *ServerSideApply) Apply(
ctx context.Context,
gvr schema.GroupVersionResource,
required *unstructured.Unstructured,
owner metav1.OwnerReference,
applyOption *workapiv1.ManifestConfigOption,
recorder events.Recorder) (runtime.Object, error) {
force := false
fieldManager := workapiv1.DefaultFieldManager
if applyOption.UpdateStrategy.ServerSideApply != nil {
force = applyOption.UpdateStrategy.ServerSideApply.Force
if len(applyOption.UpdateStrategy.ServerSideApply.FieldManager) > 0 {
fieldManager = applyOption.UpdateStrategy.ServerSideApply.FieldManager
}
}
patch, err := json.Marshal(required)
if err != nil {
return nil, err
}
// TODO use Apply method instead when upgrading the client-go to 0.25.x
obj, err := c.client.
Resource(gvr).
Namespace(required.GetNamespace()).
Patch(ctx, required.GetName(), types.ApplyPatchType, patch, metav1.PatchOptions{FieldManager: fieldManager, Force: pointer.Bool(force)})
resourceKey, _ := cache.MetaNamespaceKeyFunc(required)
if err != nil {
recorder.Eventf(fmt.Sprintf(
"Server Side Applied %s %s", required.GetKind(), resourceKey), "Patched with field manager %s", fieldManager)
}
if errors.IsConflict(err) {
return obj, &ServerSideApplyConflictError{ssaErr: err}
}
return obj, err
}

View File

@@ -0,0 +1,141 @@
package apply
import (
"context"
"fmt"
"testing"
"github.com/pkg/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"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
fakedynamic "k8s.io/client-go/dynamic/fake"
clienttesting "k8s.io/client-go/testing"
workapiv1 "open-cluster-management.io/api/work/v1"
"open-cluster-management.io/work/pkg/spoke/spoketesting"
)
func TestServerSideApply(t *testing.T) {
cases := []struct {
name string
owner metav1.OwnerReference
existing *unstructured.Unstructured
required *unstructured.Unstructured
gvr schema.GroupVersionResource
conflict bool
validateActions func(t *testing.T, actions []clienttesting.Action)
}{
{
name: "server side apply successfully",
owner: metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: "testowner"},
existing: nil,
required: spoketesting.NewUnstructured("v1", "Namespace", "", "test"),
gvr: schema.GroupVersionResource{Version: "v1", Resource: "namespaces"},
validateActions: func(t *testing.T, actions []clienttesting.Action) {},
},
{
name: "server side apply successfully conflict",
owner: metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: "testowner"},
existing: spoketesting.NewUnstructured("v1", "Secret", "ns1", "test"),
required: spoketesting.NewUnstructured("v1", "Secret", "ns1", "test"),
gvr: schema.GroupVersionResource{Version: "v1", Resource: "secrets"},
conflict: true,
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], "patch")
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
objects := []runtime.Object{}
if c.existing != nil {
objects = append(objects, c.existing)
}
scheme := runtime.NewScheme()
dynamicClient := fakedynamic.NewSimpleDynamicClient(scheme, objects...)
// The fake client does not support PatchType ApplyPatchType, add an reactor to mock apply patch
// see issue: https://github.com/kubernetes/kubernetes/issues/103816
reactor := &reactor{}
reactors := []clienttesting.Reactor{reactor}
dynamicClient.Fake.ReactionChain = append(reactors, dynamicClient.Fake.ReactionChain...)
applier := NewServerSideApply(dynamicClient)
syncContext := spoketesting.NewFakeSyncContext(t, "test")
option := &workapiv1.ManifestConfigOption{
UpdateStrategy: &workapiv1.UpdateStrategy{
Type: workapiv1.UpdateStrategyTypeServerSideApply,
},
}
obj, err := applier.Apply(
context.TODO(), c.gvr, c.required, c.owner, option, syncContext.Recorder())
c.validateActions(t, dynamicClient.Actions())
if !c.conflict {
if err != nil {
t.Errorf("expect no error, but got %v", err)
}
accessor, err := meta.Accessor(obj)
if err != nil {
t.Errorf("type %t cannot be accessed: %v", obj, err)
}
if accessor.GetNamespace() != c.required.GetNamespace() || accessor.GetName() != c.required.GetName() {
t.Errorf("Expect resource %s/%s, but %s/%s",
c.required.GetNamespace(), c.required.GetName(), accessor.GetNamespace(), accessor.GetName())
}
return
}
var ssaConflict *ServerSideApplyConflictError
if !errors.As(err, &ssaConflict) {
t.Errorf("expect serverside apply conflict error, but got %v", err)
}
})
}
}
type reactor struct {
}
func (r *reactor) Handles(action clienttesting.Action) bool {
switch action := action.(type) {
case clienttesting.PatchActionImpl:
if action.GetPatchType() == types.ApplyPatchType {
return true
}
default:
return false
}
return true
}
// React handles the action and returns results. It may choose to
// delegate by indicated handled=false.
func (r *reactor) React(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
switch action.GetResource().Resource {
case "namespaces":
return true, spoketesting.NewUnstructured("v1", "Namespace", "", "test"), nil
case "secrets":
return true, nil, apierrors.NewApplyConflict([]metav1.StatusCause{
{
Type: metav1.CauseTypeFieldManagerConflict,
Message: "field managed configl",
Field: "metadata.annotations",
},
}, "server side apply secret failed")
}
return true, nil, fmt.Errorf("PatchType is not supported")
}

View File

@@ -0,0 +1,172 @@
package apply
import (
"context"
"fmt"
"reflect"
"strings"
"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"
apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apimachinery/pkg/api/equality"
apierrors "k8s.io/apimachinery/pkg/api/errors"
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/client-go/dynamic"
"k8s.io/client-go/kubernetes"
workapiv1 "open-cluster-management.io/api/work/v1"
)
type UpdateApply struct {
dynamicClient dynamic.Interface
kubeclient kubernetes.Interface
apiExtensionClient apiextensionsclient.Interface
staticResourceCache resourceapply.ResourceCache
}
func NewUpdateApply(dynamicClient dynamic.Interface, kubeclient kubernetes.Interface, apiExtensionClient apiextensionsclient.Interface) *UpdateApply {
return &UpdateApply{
dynamicClient: dynamicClient,
kubeclient: kubeclient,
apiExtensionClient: apiExtensionClient,
// TODO we did not gc resources in cache, which may cause more memory usage. It
// should be refactored using own cache implementation in the future.
staticResourceCache: resourceapply.NewResourceCache(),
}
}
func (c *UpdateApply) Apply(
ctx context.Context,
gvr schema.GroupVersionResource,
required *unstructured.Unstructured,
owner metav1.OwnerReference,
_ *workapiv1.ManifestConfigOption,
recorder events.Recorder) (runtime.Object, error) {
clientHolder := resourceapply.NewClientHolder().
WithAPIExtensionsClient(c.apiExtensionClient).
WithKubernetes(c.kubeclient).
WithDynamicClient(c.dynamicClient)
required.SetOwnerReferences([]metav1.OwnerReference{owner})
results := resourceapply.ApplyDirectly(ctx, clientHolder, recorder, c.staticResourceCache, func(name string) ([]byte, error) {
return required.MarshalJSON()
}, "manifest")
obj, err := results[0].Result, 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(err) || isUnhandledError(err) || isUnsupportedError(err) {
obj, _, err = c.applyUnstructured(ctx, required, gvr, recorder)
}
if err == nil && (!reflect.ValueOf(obj).IsValid() || reflect.ValueOf(obj).IsNil()) {
// ApplyDirectly may return a nil Result when there is no error, we get the latest object for the Result
return c.dynamicClient.
Resource(gvr).
Namespace(required.GetNamespace()).
Get(ctx, required.GetName(), metav1.GetOptions{})
}
return obj, err
}
func (c *UpdateApply) applyUnstructured(
ctx context.Context,
required *unstructured.Unstructured,
gvr schema.GroupVersionResource,
recorder events.Recorder) (*unstructured.Unstructured, bool, error) {
existing, err := c.dynamicClient.
Resource(gvr).
Namespace(required.GetNamespace()).
Get(ctx, required.GetName(), metav1.GetOptions{})
if apierrors.IsNotFound(err) {
actual, err := c.dynamicClient.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
}
if err != nil {
return nil, false, err
}
// Merge OwnerRefs, Labels, and Annotations.
existingOwners := existing.GetOwnerReferences()
existingLabels := existing.GetLabels()
existingAnnotations := existing.GetAnnotations()
modified := resourcemerge.BoolPtr(false)
resourcemerge.MergeMap(modified, &existingLabels, required.GetLabels())
resourcemerge.MergeMap(modified, &existingAnnotations, required.GetAnnotations())
resourcemerge.MergeOwnerRefs(modified, &existingOwners, required.GetOwnerReferences())
// Always overwrite required from existing, since required has been merged to existing
required.SetOwnerReferences(existingOwners)
required.SetLabels(existingLabels)
required.SetAnnotations(existingAnnotations)
// Keep the finalizers unchanged
required.SetFinalizers(existing.GetFinalizers())
// Compare and update the unstrcuctured.
if !*modified && isSameUnstructured(required, existing) {
return existing, false, nil
}
required.SetResourceVersion(existing.GetResourceVersion())
actual, err := c.dynamicClient.Resource(gvr).Namespace(required.GetNamespace()).Update(
ctx, required, metav1.UpdateOptions{})
recorder.Eventf(fmt.Sprintf(
"%s Updated", required.GetKind()), "Updated %s/%s", required.GetNamespace(), required.GetName())
return actual, true, err
}
// isDecodeError is to check if the error returned from resourceapply is due to that the object cannot
// be decoded or no typed client can handle the object.
func isDecodeError(err error) bool {
return err != nil && strings.HasPrefix(err.Error(), "cannot decode")
}
// isUnhandledError is to check if the error returned from resourceapply is due to that no typed
// client can handle the object
func isUnhandledError(err error) bool {
return err != nil && strings.HasPrefix(err.Error(), "unhandled type")
}
// isUnsupportedError is to check if the error returned from resourceapply is due to
// the PR https://github.com/openshift/library-go/pull/1042
func isUnsupportedError(err error) bool {
return err != nil && strings.HasPrefix(err.Error(), "unsupported object type")
}
// isSameUnstructured compares the two unstructured object.
// The comparison ignores the metadata and status field, and check if the two objects are semantically equal.
func isSameUnstructured(obj1, obj2 *unstructured.Unstructured) bool {
obj1Copy := obj1.DeepCopy()
obj2Copy := obj2.DeepCopy()
// Compare gvk, name, namespace at first
if obj1Copy.GroupVersionKind() != obj2Copy.GroupVersionKind() {
return false
}
if obj1Copy.GetName() != obj2Copy.GetName() {
return false
}
if obj1Copy.GetNamespace() != obj2Copy.GetNamespace() {
return false
}
// Compare semantically after removing metadata and status field
delete(obj1Copy.Object, "metadata")
delete(obj2Copy.Object, "metadata")
delete(obj1Copy.Object, "status")
delete(obj2Copy.Object, "status")
return equality.Semantic.DeepEqual(obj1Copy.Object, obj2Copy.Object)
}

View File

@@ -0,0 +1,564 @@
package apply
import (
"context"
"testing"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
fakeapiextensions "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake"
"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"
fakedynamic "k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/kubernetes/fake"
clienttesting "k8s.io/client-go/testing"
"open-cluster-management.io/work/pkg/spoke/spoketesting"
)
// Test unstructured compare
func TestIsSameUnstructured(t *testing.T) {
cases := []struct {
name string
obj1 *unstructured.Unstructured
obj2 *unstructured.Unstructured
expected bool
}{
{
name: "different kind",
obj1: spoketesting.NewUnstructured("v1", "Kind1", "ns1", "n1"),
obj2: spoketesting.NewUnstructured("v1", "Kind2", "ns1", "n1"),
expected: false,
},
{
name: "different namespace",
obj1: spoketesting.NewUnstructured("v1", "Kind1", "ns1", "n1"),
obj2: spoketesting.NewUnstructured("v1", "Kind1", "ns2", "n1"),
expected: false,
},
{
name: "different name",
obj1: spoketesting.NewUnstructured("v1", "Kind1", "ns1", "n1"),
obj2: spoketesting.NewUnstructured("v1", "Kind1", "ns1", "n2"),
expected: false,
},
{
name: "different spec",
obj1: spoketesting.NewUnstructuredWithContent("v1", "Kind1", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val1"}}),
obj2: spoketesting.NewUnstructuredWithContent("v1", "Kind1", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val2"}}),
expected: false,
},
{
name: "same spec, different status",
obj1: spoketesting.NewUnstructuredWithContent("v1", "Kind1", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val1"}, "status": "status1"}),
obj2: spoketesting.NewUnstructuredWithContent("v1", "Kind1", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val1"}, "status": "status2"}),
expected: true,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
actual := isSameUnstructured(c.obj1, c.obj2)
if c.expected != actual {
t.Errorf("expected %t, but %t", c.expected, actual)
}
})
}
}
func TestApplyUnstructred(t *testing.T) {
cases := []struct {
name string
owner metav1.OwnerReference
existing *unstructured.Unstructured
required *unstructured.Unstructured
gvr schema.GroupVersionResource
validateActions func(t *testing.T, actions []clienttesting.Action)
}{
{
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))
}
spoketesting.AssertAction(t, actions[0], "get")
spoketesting.AssertAction(t, actions[1], "create")
obj := actions[1].(clienttesting.CreateActionImpl).Object.(*unstructured.Unstructured)
owners := obj.GetOwnerReferences()
if len(owners) != 1 {
t.Errorf("Expect 1 owners, but have %d", len(owners))
}
if owners[0].UID != "testowner" {
t.Errorf("Owner UId is not correct, got %s", owners[0].UID)
}
},
},
{
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))
}
spoketesting.AssertAction(t, actions[0], "get")
spoketesting.AssertAction(t, actions[1], "create")
obj := actions[1].(clienttesting.CreateActionImpl).Object.(*unstructured.Unstructured)
owners := obj.GetOwnerReferences()
if len(owners) != 0 {
t.Errorf("Expect 1 owners, but have %d", len(owners))
}
},
},
{
name: "update an object owner",
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))
}
spoketesting.AssertAction(t, actions[0], "get")
spoketesting.AssertAction(t, actions[1], "update")
obj := actions[1].(clienttesting.UpdateActionImpl).Object.(*unstructured.Unstructured)
owners := obj.GetOwnerReferences()
if len(owners) != 2 {
t.Errorf("Expect 2 owners, but have %d", len(owners))
}
if owners[0].UID != "testowner1" {
t.Errorf("Owner UId is not correct, got %s", owners[0].UID)
}
if owners[1].UID != "testowner" {
t.Errorf("Owner UId is not correct, got %s", owners[1].UID)
}
},
},
{
name: "update an object without owner",
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) != 1 {
t.Errorf("Expect 1 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) != 2 {
t.Errorf("Expect 2 actions, but have %d", len(actions))
}
spoketesting.AssertAction(t, actions[0], "get")
spoketesting.AssertAction(t, actions[1], "update")
obj := actions[1].(clienttesting.UpdateActionImpl).Object.(*unstructured.Unstructured)
owners := obj.GetOwnerReferences()
if len(owners) != 0 {
t.Errorf("Expect 0 owner, but have %d", len(owners))
}
},
},
{
name: "merge labels",
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(
"v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"})
obj.SetLabels(map[string]string{"foo1": "bar1"})
return obj
}(),
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], "get")
spoketesting.AssertAction(t, actions[1], "update")
obj := actions[1].(clienttesting.UpdateActionImpl).Object.(*unstructured.Unstructured)
labels := obj.GetLabels()
if len(labels) != 2 {
t.Errorf("Expect 2 labels, but have %d", len(labels))
}
},
},
{
name: "merge annotation",
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(
"v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"})
obj.SetAnnotations(map[string]string{"foo1": "bar1"})
return obj
}(),
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], "get")
spoketesting.AssertAction(t, actions[1], "update")
obj := actions[1].(clienttesting.UpdateActionImpl).Object.(*unstructured.Unstructured)
annotations := obj.GetAnnotations()
if len(annotations) != 2 {
t.Errorf("Expect 2 annotations, but have %d", len(annotations))
}
},
},
{
name: "set existing finalizer",
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(
"v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"})
obj.SetFinalizers([]string{"foo1"})
return obj
}(),
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))
}
},
},
{
name: "nothing to update",
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(
"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
}(),
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)
}
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
objects := []runtime.Object{}
if c.existing != nil {
objects = append(objects, c.existing)
}
scheme := runtime.NewScheme()
dynamicClient := fakedynamic.NewSimpleDynamicClient(scheme, objects...)
applier := NewUpdateApply(dynamicClient, nil, nil)
c.required.SetOwnerReferences([]metav1.OwnerReference{c.owner})
syncContext := spoketesting.NewFakeSyncContext(t, "test")
_, _, err := applier.applyUnstructured(
context.TODO(), c.required, c.gvr, syncContext.Recorder())
if err != nil {
t.Errorf("expect no error, but got %v", err)
}
c.validateActions(t, dynamicClient.Actions())
})
}
}
func TestUpdateApplyKube(t *testing.T) {
cases := []struct {
name string
owner metav1.OwnerReference
existing *corev1.Secret
required *unstructured.Unstructured
gvr schema.GroupVersionResource
validateActions func(t *testing.T, actions []clienttesting.Action)
}{
{
name: "apply non exist object using kube client",
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], "get")
spoketesting.AssertAction(t, actions[1], "create")
},
},
{
name: "apply existing object using kube client",
owner: metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: "testowner"},
existing: spoketesting.NewSecretWithType("test", "ns1", "foo", corev1.SecretTypeOpaque),
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], "get")
spoketesting.AssertAction(t, actions[1], "update")
obj := actions[1].(clienttesting.UpdateActionImpl).Object.(*corev1.Secret)
data, ok := obj.Data["test"]
if ok {
t.Errorf("Expect no secret data, but have %v", data)
}
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
objects := []runtime.Object{}
if c.existing != nil {
objects = append(objects, c.existing)
}
kubeclient := fake.NewSimpleClientset(objects...)
applier := NewUpdateApply(nil, kubeclient, nil)
syncContext := spoketesting.NewFakeSyncContext(t, "test")
obj, err := applier.Apply(
context.TODO(), c.gvr, c.required, c.owner, nil, syncContext.Recorder())
if err != nil {
t.Errorf("expect no error, but got %v", err)
}
accessor, err := meta.Accessor(obj)
if err != nil {
t.Errorf("type %t cannot be accessed: %v", obj, err)
}
if accessor.GetNamespace() != c.required.GetNamespace() || accessor.GetName() != c.required.GetName() {
t.Errorf("Expect resource %s/%s, but %s/%s",
c.required.GetNamespace(), c.required.GetName(), accessor.GetNamespace(), accessor.GetName())
}
owners := accessor.GetOwnerReferences()
if len(owners) != 1 {
t.Errorf("Expect 1 owners, but have %d", len(owners))
}
if owners[0].UID != c.owner.UID {
t.Errorf("Owner UId is not correct, got %s", owners[0].UID)
}
c.validateActions(t, kubeclient.Actions())
})
}
}
func TestUpdateApplyDynamic(t *testing.T) {
cases := []struct {
name string
owner metav1.OwnerReference
existing *unstructured.Unstructured
required *unstructured.Unstructured
gvr schema.GroupVersionResource
ownerApplied bool
}{
{
name: "apply non exist object using dynamic client",
owner: metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: "testowner"},
required: spoketesting.NewUnstructured("monitoring.coreos.com/v1", "ServiceMonitor", "ns1", "test"),
gvr: schema.GroupVersionResource{Group: "monitoring.coreos.com", Version: "v1", Resource: "servicemonitors"},
ownerApplied: true,
},
{
name: "apply existing object using dynamic client",
owner: metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: "testowner"},
existing: spoketesting.NewUnstructured("monitoring.coreos.com/v1", "ServiceMonitor", "ns1", "test"),
required: spoketesting.NewUnstructured("monitoring.coreos.com/v1", "ServiceMonitor", "ns1", "test"),
gvr: schema.GroupVersionResource{Group: "monitoring.coreos.com", Version: "v1", Resource: "servicemonitors"},
ownerApplied: false,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
objects := []runtime.Object{}
if c.existing != nil {
objects = append(objects, c.existing)
}
scheme := runtime.NewScheme()
dynamicclient := fakedynamic.NewSimpleDynamicClient(scheme, objects...)
applier := NewUpdateApply(dynamicclient, nil, nil)
syncContext := spoketesting.NewFakeSyncContext(t, "test")
obj, err := applier.Apply(
context.TODO(), c.gvr, c.required, c.owner, nil, syncContext.Recorder())
if err != nil {
t.Errorf("expect no error, but got %v", err)
}
accessor, err := meta.Accessor(obj)
if err != nil {
t.Errorf("type %t cannot be accessed: %v", obj, err)
}
if accessor.GetNamespace() != c.required.GetNamespace() || accessor.GetName() != c.required.GetName() {
t.Errorf("Expect resource %s/%s, but %s/%s",
c.required.GetNamespace(), c.required.GetName(), accessor.GetNamespace(), accessor.GetName())
}
owners := accessor.GetOwnerReferences()
if c.ownerApplied {
if len(owners) != 1 {
t.Errorf("Expect 1 owners, but have %d", len(owners))
}
if owners[0].UID != c.owner.UID {
t.Errorf("Owner UId is not correct, got %s", owners[0].UID)
}
}
})
}
}
func TestUpdateApplyApiExtension(t *testing.T) {
cases := []struct {
name string
owner metav1.OwnerReference
existing *apiextensionsv1.CustomResourceDefinition
required *unstructured.Unstructured
gvr schema.GroupVersionResource
validateActions func(t *testing.T, actions []clienttesting.Action)
}{
{
name: "apply non exist object using api extension client",
owner: metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: "testowner"},
required: spoketesting.NewUnstructured("apiextensions.k8s.io/v1", "CustomResourceDefinition", "", "testcrd"),
gvr: schema.GroupVersionResource{Group: "apiextensions.k8s.io", Version: "v1", Resource: "customresourcedefinition"},
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], "get")
spoketesting.AssertAction(t, actions[1], "create")
},
},
{
name: "apply existing object using api extension client",
owner: metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: "testowner"},
existing: newCRD("testcrd"),
required: spoketesting.NewUnstructured("apiextensions.k8s.io/v1", "CustomResourceDefinition", "", "testcrd"),
gvr: schema.GroupVersionResource{Group: "apiextensions.k8s.io", Version: "v1", Resource: "customresourcedefinition"},
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], "get")
spoketesting.AssertAction(t, actions[1], "update")
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
objects := []runtime.Object{}
if c.existing != nil {
objects = append(objects, c.existing)
}
apiextensionClient := fakeapiextensions.NewSimpleClientset(objects...)
applier := NewUpdateApply(nil, nil, apiextensionClient)
syncContext := spoketesting.NewFakeSyncContext(t, "test")
obj, err := applier.Apply(
context.TODO(), c.gvr, c.required, c.owner, nil, syncContext.Recorder())
if err != nil {
t.Errorf("expect no error, but got %v", err)
}
accessor, err := meta.Accessor(obj)
if err != nil {
t.Errorf("type %t cannot be accessed: %v", obj, err)
}
if accessor.GetNamespace() != c.required.GetNamespace() || accessor.GetName() != c.required.GetName() {
t.Errorf("Expect resource %s/%s, but %s/%s",
c.required.GetNamespace(), c.required.GetName(), accessor.GetNamespace(), accessor.GetName())
}
owners := accessor.GetOwnerReferences()
if len(owners) != 1 {
t.Errorf("Expect 1 owners, but have %d", len(owners))
}
if owners[0].UID != c.owner.UID {
t.Errorf("Owner UId is not correct, got %s", owners[0].UID)
}
c.validateActions(t, apiextensionClient.Actions())
})
}
}
func newCRD(name string) *apiextensionsv1.CustomResourceDefinition {
return &apiextensionsv1.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
}
}

View File

@@ -2,14 +2,11 @@ package manifestcontroller
import (
"context"
"encoding/json"
"fmt"
"reflect"
"strings"
"time"
apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apimachinery/pkg/api/equality"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -20,15 +17,11 @@ 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"
@@ -36,6 +29,7 @@ import (
workapiv1 "open-cluster-management.io/api/work/v1"
"open-cluster-management.io/work/pkg/helper"
"open-cluster-management.io/work/pkg/spoke/apply"
"open-cluster-management.io/work/pkg/spoke/controllers"
)
@@ -49,27 +43,18 @@ type ManifestWorkController struct {
appliedManifestWorkClient workv1client.AppliedManifestWorkInterface
appliedManifestWorkLister worklister.AppliedManifestWorkLister
spokeDynamicClient dynamic.Interface
spokeKubeclient kubernetes.Interface
spokeAPIExtensionClient apiextensionsclient.Interface
hubHash string
restMapper meta.RESTMapper
staticResourceCache resourceapply.ResourceCache
appliers *apply.Appliers
}
type applyResult struct {
resourceapply.ApplyResult
Result runtime.Object
Error error
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,
@@ -91,13 +76,10 @@ func NewManifestWorkController(
appliedManifestWorkClient: appliedManifestWorkClient,
appliedManifestWorkLister: appliedManifestWorkInformer.Lister(),
spokeDynamicClient: spokeDynamicClient,
spokeKubeclient: spokeKubeClient,
spokeAPIExtensionClient: spokeAPIExtensionClient,
hubHash: hubHash,
restMapper: restMapper,
// TODO we did not gc resources in cache, which may cause more memory usage. It
// should be refactored using own cache implementation in the future.
staticResourceCache: resourceapply.NewResourceCache(),
appliers: apply.NewAppliers(spokeDynamicClient, spokeKubeClient, spokeAPIExtensionClient),
}
return factory.New().
@@ -191,7 +173,7 @@ func (m *ManifestWorkController) sync(ctx context.Context, controllerContext fac
newManifestConditions := []workapiv1.ManifestCondition{}
for _, result := range resourceResults {
// ignore server side apply conflict error since it cannot be resolved by error fallback.
var ssaConflict *serverSideApplyConflictError
var ssaConflict *apply.ServerSideApplyConflictError
if result.Error != nil && !errors.As(result.Error, &ssaConflict) {
errs = append(errs, result.Error)
}
@@ -231,7 +213,7 @@ func (m *ManifestWorkController) applyManifests(
for index, manifest := range manifests {
switch {
case existingResults[index].Result == nil:
// Apply if there is not result.
// Apply if there is no result.
existingResults[index] = m.applyOneManifest(ctx, index, manifest, workSpec, recorder, owner)
case apierrors.IsConflict(existingResults[index].Error):
// Apply if there is a resource confilct error.
@@ -250,11 +232,6 @@ func (m *ManifestWorkController) applyOneManifest(
recorder events.Recorder,
owner metav1.OwnerReference) applyResult {
clientHolder := resourceapply.NewClientHolder().
WithAPIExtensionsClient(m.spokeAPIExtensionClient).
WithKubernetes(m.spokeKubeclient).
WithDynamicClient(m.spokeDynamicClient)
result := applyResult{}
// parse the required and set resource meta
@@ -271,16 +248,6 @@ func (m *ManifestWorkController) applyOneManifest(
return result
}
// 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
}
// compute required ownerrefs based on delete option
requiredOwner := manageOwnerRef(gvr, resMeta.Namespace, resMeta.Name, workSpec.DeleteOption, owner)
@@ -292,33 +259,10 @@ func (m *ManifestWorkController) applyOneManifest(
strategy = *option.UpdateStrategy
}
// 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")
applier := m.appliers.GetApplier(strategy.Type)
result.Result, result.Error = applier.Apply(ctx, gvr, required, requiredOwner, option, recorder)
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.
// patch the ownerref
if result.Error == nil {
result.Error = helper.ApplyOwnerReferences(ctx, m.spokeDynamicClient, gvr, result.Result, requiredOwner)
}
@@ -326,86 +270,6 @@ func (m *ManifestWorkController) applyOneManifest(
return result
}
func (m *ManifestWorkController) serverSideApply(
ctx context.Context,
gvr schema.GroupVersionResource,
required *unstructured.Unstructured,
config *workapiv1.ServerSideApplyConfig,
recorder events.Recorder) (*unstructured.Unstructured, bool, error) {
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()).
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)
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
}
// Merge OwnerRefs, Labels, and Annotations.
existingOwners := existing.GetOwnerReferences()
existingLabels := existing.GetLabels()
existingAnnotations := existing.GetAnnotations()
modified := resourcemerge.BoolPtr(false)
resourcemerge.MergeMap(modified, &existingLabels, required.GetLabels())
resourcemerge.MergeMap(modified, &existingAnnotations, required.GetAnnotations())
resourcemerge.MergeOwnerRefs(modified, &existingOwners, required.GetOwnerReferences())
// Always overwrite required from existing, since required has been merged to existing
required.SetOwnerReferences(existingOwners)
required.SetLabels(existingLabels)
required.SetAnnotations(existingAnnotations)
// Keep the finalizers unchanged
required.SetFinalizers(existing.GetFinalizers())
// Compare and update the unstrcuctured.
if !*modified && isSameUnstructured(required, existing) {
return existing, false, nil
}
required.SetResourceVersion(existing.GetResourceVersion())
actual, err := m.spokeDynamicClient.Resource(gvr).Namespace(required.GetNamespace()).Update(
ctx, required, metav1.UpdateOptions{})
recorder.Eventf(fmt.Sprintf(
"%s Updated", required.GetKind()), "Updated %s/%s", required.GetNamespace(), required.GetName())
return actual, true, err
}
// manageOwnerRef return a ownerref based on the resource and the deleteOption indicating whether the owneref
// should be removed or added. If the resource is orphaned, the owner's UID is updated for removal.
func manageOwnerRef(
@@ -494,50 +358,6 @@ func (m *ManifestWorkController) generateUpdateStatusFunc(generation int64, newM
}
}
// isDecodeError is to check if the error returned from resourceapply is due to that the object cannot
// be decoded or no typed client can handle the object.
func isDecodeError(err error) bool {
return err != nil && strings.HasPrefix(err.Error(), "cannot decode")
}
// isUnhandledError is to check if the error returned from resourceapply is due to that no typed
// client can handle the object
func isUnhandledError(err error) bool {
return err != nil && strings.HasPrefix(err.Error(), "unhandled type")
}
// isUnsupportedError is to check if the error returned from resourceapply is due to
// the PR https://github.com/openshift/library-go/pull/1042
func isUnsupportedError(err error) bool {
return err != nil && strings.HasPrefix(err.Error(), "unsupported object type")
}
// isSameUnstructured compares the two unstructured object.
// The comparison ignores the metadata and status field, and check if the two objects are semantically equal.
func isSameUnstructured(obj1, obj2 *unstructured.Unstructured) bool {
obj1Copy := obj1.DeepCopy()
obj2Copy := obj2.DeepCopy()
// Compare gvk, name, namespace at first
if obj1Copy.GroupVersionKind() != obj2Copy.GroupVersionKind() {
return false
}
if obj1Copy.GetName() != obj2Copy.GetName() {
return false
}
if obj1Copy.GetNamespace() != obj2Copy.GetNamespace() {
return false
}
// Compare semantically after removing metadata and status field
delete(obj1Copy.Object, "metadata")
delete(obj2Copy.Object, "metadata")
delete(obj1Copy.Object, "status")
delete(obj2Copy.Object, "status")
return equality.Semantic.DeepEqual(obj1Copy.Object, obj2Copy.Object)
}
// allInCondition checks status of conditions with a particular type in ManifestCondition array.
// Return true only if conditions with the condition type exist and they are all in condition.
func allInCondition(conditionType string, manifests []workapiv1.ManifestCondition) (inCondition bool, exists bool) {

View File

@@ -6,7 +6,6 @@ import (
"testing"
"time"
"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"
@@ -22,6 +21,7 @@ import (
fakeworkclient "open-cluster-management.io/api/client/work/clientset/versioned/fake"
workinformers "open-cluster-management.io/api/client/work/informers/externalversions"
workapiv1 "open-cluster-management.io/api/work/v1"
"open-cluster-management.io/work/pkg/spoke/apply"
"open-cluster-management.io/work/pkg/spoke/controllers"
"open-cluster-management.io/work/pkg/spoke/spoketesting"
)
@@ -43,7 +43,6 @@ func newController(t *testing.T, work *workapiv1.ManifestWork, appliedWork *work
appliedManifestWorkClient: fakeWorkClient.WorkV1().AppliedManifestWorks(),
appliedManifestWorkLister: workInformerFactory.Work().V1().AppliedManifestWorks().Lister(),
restMapper: mapper,
staticResourceCache: resourceapply.NewResourceCache(),
}
if err := workInformerFactory.Work().V1().ManifestWorks().Informer().GetStore().Add(work); err != nil {
@@ -61,9 +60,13 @@ func newController(t *testing.T, work *workapiv1.ManifestWork, appliedWork *work
}
}
func (t *testController) toController() *ManifestWorkController {
t.controller.appliers = apply.NewAppliers(t.dynamicClient, t.kubeClient, nil)
return t.controller
}
func (t *testController) withKubeObject(objects ...runtime.Object) *testController {
kubeClient := fakekube.NewSimpleClientset(objects...)
t.controller.spokeKubeclient = kubeClient
t.kubeClient = kubeClient
return t
}
@@ -196,13 +199,13 @@ func (t *testCase) validate(
}
}
if len(actualWorkActions) != len(t.expectedWorkAction) {
ts.Errorf("Expected %d action but got %#v", len(t.expectedWorkAction), actualWorkActions)
ts.Errorf("Expected work client has %d action but got %#v", len(t.expectedWorkAction), actualWorkActions)
}
for index := range actualWorkActions {
spoketesting.AssertAction(ts, actualWorkActions[index], t.expectedWorkAction[index])
}
if len(actualAppliedWorkActions) != len(t.expectedAppliedWorkAction) {
ts.Errorf("Expected %d action but got %#v", len(t.expectedAppliedWorkAction), actualAppliedWorkActions)
ts.Errorf("Expected applied work client has %d action but got %#v", len(t.expectedAppliedWorkAction), actualAppliedWorkActions)
}
for index := range actualAppliedWorkActions {
spoketesting.AssertAction(ts, actualAppliedWorkActions[index], t.expectedAppliedWorkAction[index])
@@ -210,14 +213,14 @@ func (t *testCase) validate(
spokeDynamicActions := dynamicClient.Actions()
if len(spokeDynamicActions) != len(t.expectedDynamicAction) {
ts.Errorf("Expected %d action but got %#v", len(t.expectedDynamicAction), spokeDynamicActions)
ts.Errorf("Expected dynamic client has %d action but got %#v", len(t.expectedDynamicAction), spokeDynamicActions)
}
for index := range spokeDynamicActions {
spoketesting.AssertAction(ts, spokeDynamicActions[index], t.expectedDynamicAction[index])
}
spokeKubeActions := kubeClient.Actions()
if len(spokeKubeActions) != len(t.expectedKubeAction) {
ts.Errorf("Expected %d action but got %#v", len(t.expectedKubeAction), spokeKubeActions)
ts.Errorf("Expected kube client has %d action but got %#v", len(t.expectedKubeAction), spokeKubeActions)
}
for index := range spokeKubeActions {
spoketesting.AssertAction(ts, spokeKubeActions[index], t.expectedKubeAction[index])
@@ -278,7 +281,6 @@ 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}),
@@ -292,7 +294,6 @@ 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").
@@ -318,7 +319,6 @@ 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}),
@@ -332,7 +332,7 @@ func TestSync(t *testing.T) {
withKubeObject(c.spokeObject...).
withUnstructuredObject(c.spokeDynamicObject...)
syncContext := spoketesting.NewFakeSyncContext(t, workKey)
err := controller.controller.sync(context.TODO(), syncContext)
err := controller.toController().sync(context.TODO(), syncContext)
if err != nil {
t.Errorf("Should be success with no err: %v", err)
}
@@ -349,7 +349,6 @@ 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})
@@ -373,7 +372,7 @@ func TestFailedToApplyResource(t *testing.T) {
return true, &corev1.Secret{}, fmt.Errorf("Fake error")
})
syncContext := spoketesting.NewFakeSyncContext(t, workKey)
err := controller.controller.sync(context.TODO(), syncContext)
err := controller.toController().sync(context.TODO(), syncContext)
if err == nil {
t.Errorf("Should return an err")
}
@@ -415,7 +414,7 @@ func TestUpdateStrategy(t *testing.T) {
withManifestConfig(newManifestConfigOption("", "newobjects", "ns1", "n1", &workapiv1.UpdateStrategy{Type: workapiv1.UpdateStrategyTypeServerSideApply})).
withExpectedWorkAction("update").
withAppliedWorkAction("create").
withExpectedDynamicAction("get", "patch", "patch").
withExpectedDynamicAction("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").
@@ -424,7 +423,7 @@ func TestUpdateStrategy(t *testing.T) {
withManifestConfig(newManifestConfigOption("", "newobjects", "ns1", "n1", &workapiv1.UpdateStrategy{Type: workapiv1.UpdateStrategyTypeServerSideApply})).
withExpectedWorkAction("update").
withAppliedWorkAction("create").
withExpectedDynamicAction("get", "patch", "patch").
withExpectedDynamicAction("patch", "patch").
withExpectedManifestCondition(expectedCondition{string(workapiv1.ManifestApplied), metav1.ConditionTrue}).
withExpectedWorkCondition(expectedCondition{string(workapiv1.WorkApplied), metav1.ConditionTrue}),
newTestCase("update single resource with create only updateStrategy").
@@ -452,7 +451,7 @@ func TestUpdateStrategy(t *testing.T) {
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)
err := controller.toController().sync(context.TODO(), syncContext)
if err != nil {
t.Errorf("Should be success with no err: %v", err)
}
@@ -469,7 +468,7 @@ func TestServerSideApplyConflict(t *testing.T) {
withManifestConfig(newManifestConfigOption("", "newobjects", "ns1", "n1", &workapiv1.UpdateStrategy{Type: workapiv1.UpdateStrategyTypeServerSideApply})).
withExpectedWorkAction("update").
withAppliedWorkAction("create").
withExpectedDynamicAction("get", "patch").
withExpectedDynamicAction("patch").
withExpectedManifestCondition(expectedCondition{string(workapiv1.ManifestApplied), metav1.ConditionFalse}).
withExpectedWorkCondition(expectedCondition{string(workapiv1.WorkApplied), metav1.ConditionFalse})
@@ -485,7 +484,7 @@ func TestServerSideApplyConflict(t *testing.T) {
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)
err := controller.toController().sync(context.TODO(), syncContext)
if err != nil {
t.Errorf("Should be success with no err: %v", err)
}
@@ -505,56 +504,6 @@ func newManifestConfigOption(group, resource, namespace, name string, strategy *
}
}
// Test unstructured compare
func TestIsSameUnstructured(t *testing.T) {
cases := []struct {
name string
obj1 *unstructured.Unstructured
obj2 *unstructured.Unstructured
expected bool
}{
{
name: "different kind",
obj1: spoketesting.NewUnstructured("v1", "Kind1", "ns1", "n1"),
obj2: spoketesting.NewUnstructured("v1", "Kind2", "ns1", "n1"),
expected: false,
},
{
name: "different namespace",
obj1: spoketesting.NewUnstructured("v1", "Kind1", "ns1", "n1"),
obj2: spoketesting.NewUnstructured("v1", "Kind1", "ns2", "n1"),
expected: false,
},
{
name: "different name",
obj1: spoketesting.NewUnstructured("v1", "Kind1", "ns1", "n1"),
obj2: spoketesting.NewUnstructured("v1", "Kind1", "ns1", "n2"),
expected: false,
},
{
name: "different spec",
obj1: spoketesting.NewUnstructuredWithContent("v1", "Kind1", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val1"}}),
obj2: spoketesting.NewUnstructuredWithContent("v1", "Kind1", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val2"}}),
expected: false,
},
{
name: "same spec, different status",
obj1: spoketesting.NewUnstructuredWithContent("v1", "Kind1", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val1"}, "status": "status1"}),
obj2: spoketesting.NewUnstructuredWithContent("v1", "Kind1", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val1"}, "status": "status2"}),
expected: true,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
actual := isSameUnstructured(c.obj1, c.obj2)
if c.expected != actual {
t.Errorf("expected %t, but %t", c.expected, actual)
}
})
}
}
func TestGenerateUpdateStatusFunc(t *testing.T) {
transitionTime := metav1.Now()
@@ -873,248 +822,3 @@ func TestManageOwner(t *testing.T) {
})
}
}
func TestApplyUnstructred(t *testing.T) {
cases := []struct {
name string
owner metav1.OwnerReference
existing *unstructured.Unstructured
required *unstructured.Unstructured
gvr schema.GroupVersionResource
validateActions func(t *testing.T, actions []clienttesting.Action)
}{
{
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) != 1 {
t.Errorf("Expect 1 actions, but have %d", len(actions))
}
spoketesting.AssertAction(t, actions[0], "create")
obj := actions[0].(clienttesting.CreateActionImpl).Object.(*unstructured.Unstructured)
owners := obj.GetOwnerReferences()
if len(owners) != 1 {
t.Errorf("Expect 1 owners, but have %d", len(owners))
}
if owners[0].UID != "testowner" {
t.Errorf("Owner UId is not correct, got %s", owners[0].UID)
}
},
},
{
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) != 1 {
t.Errorf("Expect 1 actions, but have %d", len(actions))
}
spoketesting.AssertAction(t, actions[0], "create")
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))
}
},
},
{
name: "update an object owner",
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) != 1 {
t.Errorf("Expect 1 actions, but have %d", len(actions))
}
spoketesting.AssertAction(t, actions[0], "update")
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))
}
if owners[0].UID != "testowner1" {
t.Errorf("Owner UId is not correct, got %s", owners[0].UID)
}
if owners[1].UID != "testowner" {
t.Errorf("Owner UId is not correct, got %s", owners[1].UID)
}
},
},
{
name: "update an object without owner",
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], "update")
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))
}
},
},
{
name: "merge labels",
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(
"v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"})
obj.SetLabels(map[string]string{"foo1": "bar1"})
return obj
}(),
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], "update")
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))
}
},
},
{
name: "merge annotation",
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(
"v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"})
obj.SetAnnotations(map[string]string{"foo1": "bar1"})
return obj
}(),
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], "update")
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))
}
},
},
{
name: "set existing finalizer",
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(
"v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"})
obj.SetFinalizers([]string{"foo1"})
return obj
}(),
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: "nothing to update",
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(
"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
}(),
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 %v", actions)
}
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
work, workKey := spoketesting.NewManifestWork(0)
work.Finalizers = []string{controllers.ManifestWorkFinalizer}
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.existing, c.required, c.gvr, syncContext.Recorder())
if err != nil {
t.Errorf("expect no error, but got %v", err)
}
c.validateActions(t, controller.dynamicClient.Actions())
})
}
}

View File

@@ -51,6 +51,12 @@ func NewSecret(name, namespace string, content string) *corev1.Secret {
}
}
func NewSecretWithType(name, namespace string, content string, t corev1.SecretType) *corev1.Secret {
secret := NewSecret(name, namespace, content)
secret.Type = t
return secret
}
func NewUnstructuredSecretBySize(namespace, name string, size int32) *unstructured.Unstructured {
data := ""
for i := int32(0); i < size; i++ {

View File

@@ -0,0 +1,92 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
package fake
import (
clientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1"
fakeapiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1"
fakeapiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/discovery"
fakediscovery "k8s.io/client-go/discovery/fake"
"k8s.io/client-go/testing"
)
// NewSimpleClientset returns a clientset that will respond with the provided objects.
// It's backed by a very simple object tracker that processes creates, updates and deletions as-is,
// without applying any validations and/or defaults. It shouldn't be considered a replacement
// for a real clientset and is mostly useful in simple unit tests.
func NewSimpleClientset(objects ...runtime.Object) *Clientset {
o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder())
for _, obj := range objects {
if err := o.Add(obj); err != nil {
panic(err)
}
}
cs := &Clientset{tracker: o}
cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake}
cs.AddReactor("*", "*", testing.ObjectReaction(o))
cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) {
gvr := action.GetResource()
ns := action.GetNamespace()
watch, err := o.Watch(gvr, ns)
if err != nil {
return false, nil, err
}
return true, watch, nil
})
return cs
}
// Clientset implements clientset.Interface. Meant to be embedded into a
// struct to get a default implementation. This makes faking out just the method
// you want to test easier.
type Clientset struct {
testing.Fake
discovery *fakediscovery.FakeDiscovery
tracker testing.ObjectTracker
}
func (c *Clientset) Discovery() discovery.DiscoveryInterface {
return c.discovery
}
func (c *Clientset) Tracker() testing.ObjectTracker {
return c.tracker
}
var (
_ clientset.Interface = &Clientset{}
_ testing.FakeClient = &Clientset{}
)
// ApiextensionsV1beta1 retrieves the ApiextensionsV1beta1Client
func (c *Clientset) ApiextensionsV1beta1() apiextensionsv1beta1.ApiextensionsV1beta1Interface {
return &fakeapiextensionsv1beta1.FakeApiextensionsV1beta1{Fake: &c.Fake}
}
// ApiextensionsV1 retrieves the ApiextensionsV1Client
func (c *Clientset) ApiextensionsV1() apiextensionsv1.ApiextensionsV1Interface {
return &fakeapiextensionsv1.FakeApiextensionsV1{Fake: &c.Fake}
}

View File

@@ -0,0 +1,20 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
// This package has the automatically generated fake clientset.
package fake

View File

@@ -0,0 +1,58 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
package fake
import (
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
serializer "k8s.io/apimachinery/pkg/runtime/serializer"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
)
var scheme = runtime.NewScheme()
var codecs = serializer.NewCodecFactory(scheme)
var localSchemeBuilder = runtime.SchemeBuilder{
apiextensionsv1beta1.AddToScheme,
apiextensionsv1.AddToScheme,
}
// AddToScheme adds all types of this clientset into the given scheme. This allows composition
// of clientsets, like in:
//
// import (
// "k8s.io/client-go/kubernetes"
// clientsetscheme "k8s.io/client-go/kubernetes/scheme"
// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
// )
//
// kclientset, _ := kubernetes.NewForConfig(c)
// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
//
// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
// correctly.
var AddToScheme = localSchemeBuilder.AddToScheme
func init() {
v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"})
utilruntime.Must(AddToScheme(scheme))
}

View File

@@ -0,0 +1,20 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
// Package fake has the automatically generated clients.
package fake

View File

@@ -0,0 +1,40 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
package fake
import (
v1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1"
rest "k8s.io/client-go/rest"
testing "k8s.io/client-go/testing"
)
type FakeApiextensionsV1 struct {
*testing.Fake
}
func (c *FakeApiextensionsV1) CustomResourceDefinitions() v1.CustomResourceDefinitionInterface {
return &FakeCustomResourceDefinitions{c}
}
// RESTClient returns a RESTClient that is used to communicate
// with API server by this client implementation.
func (c *FakeApiextensionsV1) RESTClient() rest.Interface {
var ret *rest.RESTClient
return ret
}

View File

@@ -0,0 +1,133 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
package fake
import (
"context"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
labels "k8s.io/apimachinery/pkg/labels"
schema "k8s.io/apimachinery/pkg/runtime/schema"
types "k8s.io/apimachinery/pkg/types"
watch "k8s.io/apimachinery/pkg/watch"
testing "k8s.io/client-go/testing"
)
// FakeCustomResourceDefinitions implements CustomResourceDefinitionInterface
type FakeCustomResourceDefinitions struct {
Fake *FakeApiextensionsV1
}
var customresourcedefinitionsResource = schema.GroupVersionResource{Group: "apiextensions.k8s.io", Version: "v1", Resource: "customresourcedefinitions"}
var customresourcedefinitionsKind = schema.GroupVersionKind{Group: "apiextensions.k8s.io", Version: "v1", Kind: "CustomResourceDefinition"}
// Get takes name of the customResourceDefinition, and returns the corresponding customResourceDefinition object, and an error if there is any.
func (c *FakeCustomResourceDefinitions) Get(ctx context.Context, name string, options v1.GetOptions) (result *apiextensionsv1.CustomResourceDefinition, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootGetAction(customresourcedefinitionsResource, name), &apiextensionsv1.CustomResourceDefinition{})
if obj == nil {
return nil, err
}
return obj.(*apiextensionsv1.CustomResourceDefinition), err
}
// List takes label and field selectors, and returns the list of CustomResourceDefinitions that match those selectors.
func (c *FakeCustomResourceDefinitions) List(ctx context.Context, opts v1.ListOptions) (result *apiextensionsv1.CustomResourceDefinitionList, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootListAction(customresourcedefinitionsResource, customresourcedefinitionsKind, opts), &apiextensionsv1.CustomResourceDefinitionList{})
if obj == nil {
return nil, err
}
label, _, _ := testing.ExtractFromListOptions(opts)
if label == nil {
label = labels.Everything()
}
list := &apiextensionsv1.CustomResourceDefinitionList{ListMeta: obj.(*apiextensionsv1.CustomResourceDefinitionList).ListMeta}
for _, item := range obj.(*apiextensionsv1.CustomResourceDefinitionList).Items {
if label.Matches(labels.Set(item.Labels)) {
list.Items = append(list.Items, item)
}
}
return list, err
}
// Watch returns a watch.Interface that watches the requested customResourceDefinitions.
func (c *FakeCustomResourceDefinitions) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
return c.Fake.
InvokesWatch(testing.NewRootWatchAction(customresourcedefinitionsResource, opts))
}
// Create takes the representation of a customResourceDefinition and creates it. Returns the server's representation of the customResourceDefinition, and an error, if there is any.
func (c *FakeCustomResourceDefinitions) Create(ctx context.Context, customResourceDefinition *apiextensionsv1.CustomResourceDefinition, opts v1.CreateOptions) (result *apiextensionsv1.CustomResourceDefinition, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootCreateAction(customresourcedefinitionsResource, customResourceDefinition), &apiextensionsv1.CustomResourceDefinition{})
if obj == nil {
return nil, err
}
return obj.(*apiextensionsv1.CustomResourceDefinition), err
}
// Update takes the representation of a customResourceDefinition and updates it. Returns the server's representation of the customResourceDefinition, and an error, if there is any.
func (c *FakeCustomResourceDefinitions) Update(ctx context.Context, customResourceDefinition *apiextensionsv1.CustomResourceDefinition, opts v1.UpdateOptions) (result *apiextensionsv1.CustomResourceDefinition, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootUpdateAction(customresourcedefinitionsResource, customResourceDefinition), &apiextensionsv1.CustomResourceDefinition{})
if obj == nil {
return nil, err
}
return obj.(*apiextensionsv1.CustomResourceDefinition), err
}
// UpdateStatus was generated because the type contains a Status member.
// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
func (c *FakeCustomResourceDefinitions) UpdateStatus(ctx context.Context, customResourceDefinition *apiextensionsv1.CustomResourceDefinition, opts v1.UpdateOptions) (*apiextensionsv1.CustomResourceDefinition, error) {
obj, err := c.Fake.
Invokes(testing.NewRootUpdateSubresourceAction(customresourcedefinitionsResource, "status", customResourceDefinition), &apiextensionsv1.CustomResourceDefinition{})
if obj == nil {
return nil, err
}
return obj.(*apiextensionsv1.CustomResourceDefinition), err
}
// Delete takes name of the customResourceDefinition and deletes it. Returns an error if one occurs.
func (c *FakeCustomResourceDefinitions) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
_, err := c.Fake.
Invokes(testing.NewRootDeleteActionWithOptions(customresourcedefinitionsResource, name, opts), &apiextensionsv1.CustomResourceDefinition{})
return err
}
// DeleteCollection deletes a collection of objects.
func (c *FakeCustomResourceDefinitions) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
action := testing.NewRootDeleteCollectionAction(customresourcedefinitionsResource, listOpts)
_, err := c.Fake.Invokes(action, &apiextensionsv1.CustomResourceDefinitionList{})
return err
}
// Patch applies the patch and returns the patched customResourceDefinition.
func (c *FakeCustomResourceDefinitions) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *apiextensionsv1.CustomResourceDefinition, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootPatchSubresourceAction(customresourcedefinitionsResource, name, pt, data, subresources...), &apiextensionsv1.CustomResourceDefinition{})
if obj == nil {
return nil, err
}
return obj.(*apiextensionsv1.CustomResourceDefinition), err
}

View File

@@ -0,0 +1,20 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
// Package fake has the automatically generated clients.
package fake

View File

@@ -0,0 +1,40 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
package fake
import (
v1beta1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1"
rest "k8s.io/client-go/rest"
testing "k8s.io/client-go/testing"
)
type FakeApiextensionsV1beta1 struct {
*testing.Fake
}
func (c *FakeApiextensionsV1beta1) CustomResourceDefinitions() v1beta1.CustomResourceDefinitionInterface {
return &FakeCustomResourceDefinitions{c}
}
// RESTClient returns a RESTClient that is used to communicate
// with API server by this client implementation.
func (c *FakeApiextensionsV1beta1) RESTClient() rest.Interface {
var ret *rest.RESTClient
return ret
}

View File

@@ -0,0 +1,133 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
package fake
import (
"context"
v1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
labels "k8s.io/apimachinery/pkg/labels"
schema "k8s.io/apimachinery/pkg/runtime/schema"
types "k8s.io/apimachinery/pkg/types"
watch "k8s.io/apimachinery/pkg/watch"
testing "k8s.io/client-go/testing"
)
// FakeCustomResourceDefinitions implements CustomResourceDefinitionInterface
type FakeCustomResourceDefinitions struct {
Fake *FakeApiextensionsV1beta1
}
var customresourcedefinitionsResource = schema.GroupVersionResource{Group: "apiextensions.k8s.io", Version: "v1beta1", Resource: "customresourcedefinitions"}
var customresourcedefinitionsKind = schema.GroupVersionKind{Group: "apiextensions.k8s.io", Version: "v1beta1", Kind: "CustomResourceDefinition"}
// Get takes name of the customResourceDefinition, and returns the corresponding customResourceDefinition object, and an error if there is any.
func (c *FakeCustomResourceDefinitions) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta1.CustomResourceDefinition, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootGetAction(customresourcedefinitionsResource, name), &v1beta1.CustomResourceDefinition{})
if obj == nil {
return nil, err
}
return obj.(*v1beta1.CustomResourceDefinition), err
}
// List takes label and field selectors, and returns the list of CustomResourceDefinitions that match those selectors.
func (c *FakeCustomResourceDefinitions) List(ctx context.Context, opts v1.ListOptions) (result *v1beta1.CustomResourceDefinitionList, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootListAction(customresourcedefinitionsResource, customresourcedefinitionsKind, opts), &v1beta1.CustomResourceDefinitionList{})
if obj == nil {
return nil, err
}
label, _, _ := testing.ExtractFromListOptions(opts)
if label == nil {
label = labels.Everything()
}
list := &v1beta1.CustomResourceDefinitionList{ListMeta: obj.(*v1beta1.CustomResourceDefinitionList).ListMeta}
for _, item := range obj.(*v1beta1.CustomResourceDefinitionList).Items {
if label.Matches(labels.Set(item.Labels)) {
list.Items = append(list.Items, item)
}
}
return list, err
}
// Watch returns a watch.Interface that watches the requested customResourceDefinitions.
func (c *FakeCustomResourceDefinitions) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
return c.Fake.
InvokesWatch(testing.NewRootWatchAction(customresourcedefinitionsResource, opts))
}
// Create takes the representation of a customResourceDefinition and creates it. Returns the server's representation of the customResourceDefinition, and an error, if there is any.
func (c *FakeCustomResourceDefinitions) Create(ctx context.Context, customResourceDefinition *v1beta1.CustomResourceDefinition, opts v1.CreateOptions) (result *v1beta1.CustomResourceDefinition, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootCreateAction(customresourcedefinitionsResource, customResourceDefinition), &v1beta1.CustomResourceDefinition{})
if obj == nil {
return nil, err
}
return obj.(*v1beta1.CustomResourceDefinition), err
}
// Update takes the representation of a customResourceDefinition and updates it. Returns the server's representation of the customResourceDefinition, and an error, if there is any.
func (c *FakeCustomResourceDefinitions) Update(ctx context.Context, customResourceDefinition *v1beta1.CustomResourceDefinition, opts v1.UpdateOptions) (result *v1beta1.CustomResourceDefinition, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootUpdateAction(customresourcedefinitionsResource, customResourceDefinition), &v1beta1.CustomResourceDefinition{})
if obj == nil {
return nil, err
}
return obj.(*v1beta1.CustomResourceDefinition), err
}
// UpdateStatus was generated because the type contains a Status member.
// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
func (c *FakeCustomResourceDefinitions) UpdateStatus(ctx context.Context, customResourceDefinition *v1beta1.CustomResourceDefinition, opts v1.UpdateOptions) (*v1beta1.CustomResourceDefinition, error) {
obj, err := c.Fake.
Invokes(testing.NewRootUpdateSubresourceAction(customresourcedefinitionsResource, "status", customResourceDefinition), &v1beta1.CustomResourceDefinition{})
if obj == nil {
return nil, err
}
return obj.(*v1beta1.CustomResourceDefinition), err
}
// Delete takes name of the customResourceDefinition and deletes it. Returns an error if one occurs.
func (c *FakeCustomResourceDefinitions) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
_, err := c.Fake.
Invokes(testing.NewRootDeleteActionWithOptions(customresourcedefinitionsResource, name, opts), &v1beta1.CustomResourceDefinition{})
return err
}
// DeleteCollection deletes a collection of objects.
func (c *FakeCustomResourceDefinitions) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
action := testing.NewRootDeleteCollectionAction(customresourcedefinitionsResource, listOpts)
_, err := c.Fake.Invokes(action, &v1beta1.CustomResourceDefinitionList{})
return err
}
// Patch applies the patch and returns the patched customResourceDefinition.
func (c *FakeCustomResourceDefinitions) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.CustomResourceDefinition, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootPatchSubresourceAction(customresourcedefinitionsResource, name, pt, data, subresources...), &v1beta1.CustomResourceDefinition{})
if obj == nil {
return nil, err
}
return obj.(*v1beta1.CustomResourceDefinition), err
}

3
vendor/modules.txt vendored
View File

@@ -658,9 +658,12 @@ k8s.io/apiextensions-apiserver/pkg/apis/apiextensions
k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1
k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1
k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset
k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake
k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme
k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1
k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake
k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1
k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake
# k8s.io/apimachinery v0.24.3
## explicit; go 1.16
k8s.io/apimachinery/pkg/api/equality