diff --git a/pkg/spoke/apply/apply.go b/pkg/spoke/apply/apply.go new file mode 100644 index 000000000..6a4c80ce2 --- /dev/null +++ b/pkg/spoke/apply/apply.go @@ -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] +} diff --git a/pkg/spoke/apply/create_only_apply.go b/pkg/spoke/apply/create_only_apply.go new file mode 100644 index 000000000..8ee36cc10 --- /dev/null +++ b/pkg/spoke/apply/create_only_apply.go @@ -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 +} diff --git a/pkg/spoke/apply/create_only_apply_test.go b/pkg/spoke/apply/create_only_apply_test.go new file mode 100644 index 000000000..129a7d43c --- /dev/null +++ b/pkg/spoke/apply/create_only_apply_test.go @@ -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()) + }) + } +} diff --git a/pkg/spoke/apply/server_side_apply.go b/pkg/spoke/apply/server_side_apply.go new file mode 100644 index 000000000..daae89b26 --- /dev/null +++ b/pkg/spoke/apply/server_side_apply.go @@ -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 + +} diff --git a/pkg/spoke/apply/server_side_apply_test.go b/pkg/spoke/apply/server_side_apply_test.go new file mode 100644 index 000000000..18e009f75 --- /dev/null +++ b/pkg/spoke/apply/server_side_apply_test.go @@ -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") +} diff --git a/pkg/spoke/apply/update_apply.go b/pkg/spoke/apply/update_apply.go new file mode 100644 index 000000000..d4c0d7c60 --- /dev/null +++ b/pkg/spoke/apply/update_apply.go @@ -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) +} diff --git a/pkg/spoke/apply/update_apply_test.go b/pkg/spoke/apply/update_apply_test.go new file mode 100644 index 000000000..1cb7a81dd --- /dev/null +++ b/pkg/spoke/apply/update_apply_test.go @@ -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, + }, + } +} diff --git a/pkg/spoke/controllers/manifestcontroller/manifestwork_controller.go b/pkg/spoke/controllers/manifestcontroller/manifestwork_controller.go index b5805f2b1..52c95a027 100644 --- a/pkg/spoke/controllers/manifestcontroller/manifestwork_controller.go +++ b/pkg/spoke/controllers/manifestcontroller/manifestwork_controller.go @@ -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) { diff --git a/pkg/spoke/controllers/manifestcontroller/manifestwork_controller_test.go b/pkg/spoke/controllers/manifestcontroller/manifestwork_controller_test.go index 01ed3e408..b362b3599 100644 --- a/pkg/spoke/controllers/manifestcontroller/manifestwork_controller_test.go +++ b/pkg/spoke/controllers/manifestcontroller/manifestwork_controller_test.go @@ -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()) - }) - } -} diff --git a/pkg/spoke/spoketesting/manifestwork_helpers.go b/pkg/spoke/spoketesting/manifestwork_helpers.go index 6bcf828df..63e3fe547 100644 --- a/pkg/spoke/spoketesting/manifestwork_helpers.go +++ b/pkg/spoke/spoketesting/manifestwork_helpers.go @@ -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++ { diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake/clientset_generated.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake/clientset_generated.go new file mode 100644 index 000000000..14acba99a --- /dev/null +++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake/clientset_generated.go @@ -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} +} diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake/doc.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake/doc.go new file mode 100644 index 000000000..9b99e7167 --- /dev/null +++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake/doc.go @@ -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 diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake/register.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake/register.go new file mode 100644 index 000000000..9aba8e85e --- /dev/null +++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake/register.go @@ -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)) +} diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake/doc.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake/doc.go new file mode 100644 index 000000000..16f443990 --- /dev/null +++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake/doc.go @@ -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 diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake/fake_apiextensions_client.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake/fake_apiextensions_client.go new file mode 100644 index 000000000..43b4ee7a2 --- /dev/null +++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake/fake_apiextensions_client.go @@ -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 +} diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake/fake_customresourcedefinition.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake/fake_customresourcedefinition.go new file mode 100644 index 000000000..2cc2fc940 --- /dev/null +++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake/fake_customresourcedefinition.go @@ -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 +} diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake/doc.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake/doc.go new file mode 100644 index 000000000..16f443990 --- /dev/null +++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake/doc.go @@ -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 diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake/fake_apiextensions_client.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake/fake_apiextensions_client.go new file mode 100644 index 000000000..288683ef9 --- /dev/null +++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake/fake_apiextensions_client.go @@ -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 +} diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake/fake_customresourcedefinition.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake/fake_customresourcedefinition.go new file mode 100644 index 000000000..d67d7928f --- /dev/null +++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake/fake_customresourcedefinition.go @@ -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 +} diff --git a/vendor/modules.txt b/vendor/modules.txt index affc4037a..0f5b9cf36 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -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