mirror of
https://github.com/open-cluster-management-io/ocm.git
synced 2026-05-17 14:48:49 +00:00
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:
42
pkg/spoke/apply/apply.go
Normal file
42
pkg/spoke/apply/apply.go
Normal 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]
|
||||
}
|
||||
48
pkg/spoke/apply/create_only_apply.go
Normal file
48
pkg/spoke/apply/create_only_apply.go
Normal 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
|
||||
}
|
||||
101
pkg/spoke/apply/create_only_apply_test.go
Normal file
101
pkg/spoke/apply/create_only_apply_test.go
Normal 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())
|
||||
})
|
||||
}
|
||||
}
|
||||
77
pkg/spoke/apply/server_side_apply.go
Normal file
77
pkg/spoke/apply/server_side_apply.go
Normal 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
|
||||
|
||||
}
|
||||
141
pkg/spoke/apply/server_side_apply_test.go
Normal file
141
pkg/spoke/apply/server_side_apply_test.go
Normal 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")
|
||||
}
|
||||
172
pkg/spoke/apply/update_apply.go
Normal file
172
pkg/spoke/apply/update_apply.go
Normal 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)
|
||||
}
|
||||
564
pkg/spoke/apply/update_apply_test.go
Normal file
564
pkg/spoke/apply/update_apply_test.go
Normal 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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++ {
|
||||
|
||||
92
vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake/clientset_generated.go
generated
vendored
Normal file
92
vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake/clientset_generated.go
generated
vendored
Normal 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}
|
||||
}
|
||||
20
vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake/doc.go
generated
vendored
Normal file
20
vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake/doc.go
generated
vendored
Normal 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
|
||||
58
vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake/register.go
generated
vendored
Normal file
58
vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake/register.go
generated
vendored
Normal 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))
|
||||
}
|
||||
20
vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake/doc.go
generated
vendored
Normal file
20
vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake/doc.go
generated
vendored
Normal 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
|
||||
40
vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake/fake_apiextensions_client.go
generated
vendored
Normal file
40
vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake/fake_apiextensions_client.go
generated
vendored
Normal 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
|
||||
}
|
||||
133
vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake/fake_customresourcedefinition.go
generated
vendored
Normal file
133
vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake/fake_customresourcedefinition.go
generated
vendored
Normal 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
|
||||
}
|
||||
20
vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake/doc.go
generated
vendored
Normal file
20
vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake/doc.go
generated
vendored
Normal 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
|
||||
40
vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake/fake_apiextensions_client.go
generated
vendored
Normal file
40
vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake/fake_apiextensions_client.go
generated
vendored
Normal 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
|
||||
}
|
||||
133
vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake/fake_customresourcedefinition.go
generated
vendored
Normal file
133
vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake/fake_customresourcedefinition.go
generated
vendored
Normal 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
3
vendor/modules.txt
vendored
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user