diff --git a/.prow.yaml b/.prow.yaml index 405353eae..9773318a1 100644 --- a/.prow.yaml +++ b/.prow.yaml @@ -7,7 +7,7 @@ presubmits: preset-goproxy: "true" spec: containers: - - image: ghcr.io/kcp-dev/infra/build:1.23.4-1 + - image: ghcr.io/kcp-dev/infra/build:1.23.7-2 command: - make - verify @@ -20,7 +20,7 @@ presubmits: preset-goproxy: "true" spec: containers: - - image: ghcr.io/kcp-dev/infra/build:1.23.4-1 + - image: ghcr.io/kcp-dev/infra/build:1.23.7-2 command: - make - lint diff --git a/third_party/k8s.io/client-go/gentype/fake_cluster.go b/third_party/k8s.io/client-go/gentype/fake_cluster.go new file mode 100644 index 000000000..4ef682c67 --- /dev/null +++ b/third_party/k8s.io/client-go/gentype/fake_cluster.go @@ -0,0 +1,121 @@ +/* +Copyright 2024 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. +*/ + +package gentype + +import ( + "context" + + "github.com/kcp-dev/logicalcluster/v3" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + watch "k8s.io/apimachinery/pkg/watch" + + kcptesting "github.com/kcp-dev/client-go/third_party/k8s.io/client-go/testing" +) + +// FakeClusterClient represents a fake cluster client +type FakeClusterClient[T objectWithMeta] struct { + *kcptesting.Fake + resource schema.GroupVersionResource + kind schema.GroupVersionKind + newObject func() T +} + +// FakeClusterClientWithList represents a fake cluster client with support for lists. +type FakeClusterClientWithList[T objectWithMeta, L runtime.Object] struct { + *FakeClusterClient[T] + alsoFakeClusterLister[T, L] +} + +// Helper types for composition +type alsoFakeClusterLister[T objectWithMeta, L runtime.Object] struct { + client *FakeClusterClient[T] + newList func() L + copyListMeta func(L, L) + getItems func(L) []T + setItems func(L, []T) +} + +// NewFakeClient constructs a fake client, namespaced or not, with no support for lists or apply. +// Non-namespaced clients are constructed by passing an empty namespace (""). +func NewFakeClusterClient[T objectWithMeta]( + fake *kcptesting.Fake, resource schema.GroupVersionResource, kind schema.GroupVersionKind, emptyObjectCreator func() T, +) *FakeClusterClient[T] { + return &FakeClusterClient[T]{fake, resource, kind, emptyObjectCreator} +} + +// NewFakeClusterClientWithList constructs a namespaced client with support for lists. +func NewFakeClusterClientWithList[T objectWithMeta, L runtime.Object]( + fake *kcptesting.Fake, resource schema.GroupVersionResource, kind schema.GroupVersionKind, emptyObjectCreator func() T, + emptyListCreator func() L, listMetaCopier func(L, L), itemGetter func(L) []T, itemSetter func(L, []T), +) *FakeClusterClientWithList[T, L] { + fakeClusterClient := NewFakeClusterClient[T](fake, resource, kind, emptyObjectCreator) + return &FakeClusterClientWithList[T, L]{ + fakeClusterClient, + alsoFakeClusterLister[T, L]{fakeClusterClient, emptyListCreator, listMetaCopier, itemGetter, itemSetter}, + } +} + +// List takes label and field selectors, and returns the list of resources that match those selectors. +func (l *alsoFakeClusterLister[T, L]) List(ctx context.Context, opts metav1.ListOptions) (result L, err error) { + emptyResult := l.newList() + obj, err := l.client.Fake. + Invokes(kcptesting.NewListAction(l.client.resource, l.client.kind, logicalcluster.Wildcard, metav1.NamespaceAll, opts), emptyResult) + if obj == nil { + return emptyResult, err + } + + label, _, _ := kcptesting.ExtractFromListOptions(opts) + if label == nil { + // Everything matches + return obj.(L), nil + } + list := l.newList() + l.copyListMeta(list, obj.(L)) + var items []T + for _, item := range l.getItems(obj.(L)) { + itemMeta, err := meta.Accessor(item) + if err != nil { + // No ObjectMeta, nothing can match + continue + } + if label.Matches(labels.Set(itemMeta.GetLabels())) { + items = append(items, item) + } + } + l.setItems(list, items) + return list, err +} + +// Watch returns a watch.Interface that watches the requested resources. +func (c *FakeClusterClient[T]) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + opts.Watch = true + return c.Fake. + InvokesWatch(kcptesting.NewWatchAction(c.resource, logicalcluster.Wildcard, metav1.NamespaceAll, opts)) +} + +func (c *FakeClusterClient[T]) Kind() schema.GroupVersionKind { + return c.kind +} + +func (c *FakeClusterClient[T]) Resource() schema.GroupVersionResource { + return c.resource +} diff --git a/third_party/k8s.io/client-go/gentype/fake_single.go b/third_party/k8s.io/client-go/gentype/fake_single.go new file mode 100644 index 000000000..ded298a32 --- /dev/null +++ b/third_party/k8s.io/client-go/gentype/fake_single.go @@ -0,0 +1,305 @@ +/* +Copyright 2024 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. +*/ + +package gentype + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/kcp-dev/logicalcluster/v3" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + + kcptesting "github.com/kcp-dev/client-go/third_party/k8s.io/client-go/testing" +) + +// FakeClient represents a fake client +type FakeClient[T objectWithMeta] struct { + *kcptesting.Fake + cluster logicalcluster.Path + ns string + resource schema.GroupVersionResource + kind schema.GroupVersionKind + newObject func() T +} + +// FakeClientWithList represents a fake client with support for lists. +type FakeClientWithList[T objectWithMeta, L runtime.Object] struct { + *FakeClient[T] + alsoFakeLister[T, L] +} + +// FakeClientWithApply represents a fake client with support for apply declarative configurations. +type FakeClientWithApply[T objectWithMeta, C namedObject] struct { + *FakeClient[T] + alsoFakeApplier[T, C] +} + +// FakeClientWithListAndApply represents a fake client with support for lists and apply declarative configurations. +type FakeClientWithListAndApply[T objectWithMeta, L runtime.Object, C namedObject] struct { + *FakeClient[T] + alsoFakeLister[T, L] + alsoFakeApplier[T, C] +} + +// Helper types for composition +type alsoFakeLister[T objectWithMeta, L runtime.Object] struct { + client *FakeClient[T] + newList func() L + copyListMeta func(L, L) + getItems func(L) []T + setItems func(L, []T) +} + +type alsoFakeApplier[T objectWithMeta, C namedObject] struct { + client *FakeClient[T] +} + +// NewFakeClient constructs a fake client, namespaced or not, with no support for lists or apply. +// Non-namespaced clients are constructed by passing an empty namespace (""). +func NewFakeClient[T objectWithMeta]( + fake *kcptesting.Fake, cluster logicalcluster.Path, namespace string, resource schema.GroupVersionResource, kind schema.GroupVersionKind, emptyObjectCreator func() T, +) *FakeClient[T] { + return &FakeClient[T]{fake, cluster, namespace, resource, kind, emptyObjectCreator} +} + +// NewFakeClientWithList constructs a namespaced client with support for lists. +func NewFakeClientWithList[T objectWithMeta, L runtime.Object]( + fake *kcptesting.Fake, cluster logicalcluster.Path, namespace string, resource schema.GroupVersionResource, kind schema.GroupVersionKind, emptyObjectCreator func() T, + emptyListCreator func() L, listMetaCopier func(L, L), itemGetter func(L) []T, itemSetter func(L, []T), +) *FakeClientWithList[T, L] { + fakeClient := NewFakeClient[T](fake, cluster, namespace, resource, kind, emptyObjectCreator) + return &FakeClientWithList[T, L]{ + fakeClient, + alsoFakeLister[T, L]{fakeClient, emptyListCreator, listMetaCopier, itemGetter, itemSetter}, + } +} + +// NewFakeClientWithApply constructs a namespaced client with support for apply declarative configurations. +func NewFakeClientWithApply[T objectWithMeta, C namedObject]( + fake *kcptesting.Fake, cluster logicalcluster.Path, namespace string, resource schema.GroupVersionResource, kind schema.GroupVersionKind, emptyObjectCreator func() T, +) *FakeClientWithApply[T, C] { + fakeClient := NewFakeClient[T](fake, cluster, namespace, resource, kind, emptyObjectCreator) + return &FakeClientWithApply[T, C]{ + fakeClient, + alsoFakeApplier[T, C]{fakeClient}, + } +} + +// NewFakeClientWithListAndApply constructs a client with support for lists and applying declarative configurations. +func NewFakeClientWithListAndApply[T objectWithMeta, L runtime.Object, C namedObject]( + fake *kcptesting.Fake, cluster logicalcluster.Path, namespace string, resource schema.GroupVersionResource, kind schema.GroupVersionKind, emptyObjectCreator func() T, + emptyListCreator func() L, listMetaCopier func(L, L), itemGetter func(L) []T, itemSetter func(L, []T), +) *FakeClientWithListAndApply[T, L, C] { + fakeClient := NewFakeClient[T](fake, cluster, namespace, resource, kind, emptyObjectCreator) + return &FakeClientWithListAndApply[T, L, C]{ + fakeClient, + alsoFakeLister[T, L]{fakeClient, emptyListCreator, listMetaCopier, itemGetter, itemSetter}, + alsoFakeApplier[T, C]{fakeClient}, + } +} + +// Get takes name of a resource, and returns the corresponding object, and an error if there is any. +func (c *FakeClient[T]) Get(ctx context.Context, name string, _ metav1.GetOptions) (T, error) { + emptyResult := c.newObject() + + obj, err := c.Fake.Invokes(kcptesting.NewGetAction(c.resource, c.cluster, c.ns, name), emptyResult) + if obj == nil { + return emptyResult, err + } + return obj.(T), err +} + +func ToPointerSlice[T any](src []T) []*T { + if src == nil { + return nil + } + result := make([]*T, len(src)) + for i := range src { + result[i] = &src[i] + } + return result +} + +func FromPointerSlice[T any](src []*T) []T { + if src == nil { + return nil + } + result := make([]T, len(src)) + for i := range src { + result[i] = *src[i] + } + return result +} + +// List takes label and field selectors, and returns the list of resources that match those selectors. +func (l *alsoFakeLister[T, L]) List(ctx context.Context, opts metav1.ListOptions) (result L, err error) { + emptyResult := l.newList() + obj, err := l.client.Fake. + Invokes(kcptesting.NewListAction(l.client.resource, l.client.kind, l.client.cluster, l.client.ns, opts), emptyResult) + if obj == nil { + return emptyResult, err + } + + label, _, _ := kcptesting.ExtractFromListOptions(opts) + if label == nil { + // Everything matches + return obj.(L), nil + } + list := l.newList() + l.copyListMeta(list, obj.(L)) + var items []T + for _, item := range l.getItems(obj.(L)) { + itemMeta, err := meta.Accessor(item) + if err != nil { + // No ObjectMeta, nothing can match + continue + } + if label.Matches(labels.Set(itemMeta.GetLabels())) { + items = append(items, item) + } + } + l.setItems(list, items) + return list, err +} + +// Watch returns a watch.Interface that watches the requested resources. +func (c *FakeClient[T]) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + opts.Watch = true + return c.Fake.InvokesWatch(kcptesting.NewWatchAction(c.resource, c.cluster, c.ns, opts)) +} + +// Create takes the representation of a resource and creates it. Returns the server's representation of the resource, and an error, if there is any. +func (c *FakeClient[T]) Create(ctx context.Context, resource T, _ metav1.CreateOptions) (result T, err error) { + emptyResult := c.newObject() + obj, err := c.Fake.Invokes(kcptesting.NewCreateAction(c.resource, c.cluster, c.ns, resource), emptyResult) + if obj == nil { + return emptyResult, err + } + return obj.(T), err +} + +// Update takes the representation of a resource and updates it. Returns the server's representation of the resource, and an error, if there is any. +func (c *FakeClient[T]) Update(ctx context.Context, resource T, _ metav1.UpdateOptions) (result T, err error) { + emptyResult := c.newObject() + obj, err := c.Fake.Invokes(kcptesting.NewUpdateAction(c.resource, c.cluster, c.ns, resource), emptyResult) + if obj == nil { + return emptyResult, err + } + return obj.(T), err +} + +// UpdateStatus updates the resource's status and returns the updated resource. +func (c *FakeClient[T]) UpdateStatus(ctx context.Context, resource T, _ metav1.UpdateOptions) (result T, err error) { + emptyResult := c.newObject() + obj, err := c.Fake. + Invokes(kcptesting.NewUpdateSubresourceAction(c.resource, c.cluster, "status", c.ns, resource), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(T), err +} + +// Delete deletes the resource matching the given name. Returns an error if one occurs. +func (c *FakeClient[T]) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + _, err := c.Fake. + Invokes(kcptesting.NewDeleteActionWithOptions(c.resource, c.cluster, c.ns, name, opts), c.newObject()) + return err +} + +// DeleteCollection deletes a collection of objects. +func (l *alsoFakeLister[T, L]) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, _ metav1.ListOptions) error { + _, err := l.client.Fake. + Invokes(kcptesting.NewDeleteCollectionAction(l.client.resource, l.client.cluster, l.client.ns, opts), l.newList()) + return err +} + +// Patch applies the patch and returns the patched resource. +func (c *FakeClient[T]) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, _ metav1.PatchOptions, subresources ...string) (result T, err error) { + emptyResult := c.newObject() + obj, err := c.Fake. + Invokes(kcptesting.NewPatchSubresourceAction(c.resource, c.cluster, c.ns, name, pt, data, subresources...), emptyResult) + if obj == nil { + return emptyResult, err + } + return obj.(T), err +} + +// Apply takes the given apply declarative configuration, applies it and returns the applied resource. +func (a *alsoFakeApplier[T, C]) Apply(ctx context.Context, configuration C, _ metav1.ApplyOptions) (result T, err error) { + if configuration == *new(C) { + return *new(T), fmt.Errorf("configuration provided to Apply must not be nil") + } + data, err := json.Marshal(configuration) + if err != nil { + return *new(T), err + } + name := configuration.GetName() + if name == nil { + return *new(T), fmt.Errorf("configuration.Name must be provided to Apply") + } + emptyResult := a.client.newObject() + obj, err := a.client.Fake. + Invokes(kcptesting.NewPatchSubresourceAction(a.client.resource, a.client.cluster, a.client.ns, *name, types.ApplyPatchType, data), emptyResult) + if obj == nil { + return emptyResult, err + } + return obj.(T), err +} + +// ApplyStatus applies the given apply declarative configuration to the resource's status and returns the updated resource. +func (a *alsoFakeApplier[T, C]) ApplyStatus(ctx context.Context, configuration C, _ metav1.ApplyOptions) (result T, err error) { + if configuration == *new(C) { + return *new(T), fmt.Errorf("configuration provided to Apply must not be nil") + } + data, err := json.Marshal(configuration) + if err != nil { + return *new(T), err + } + name := configuration.GetName() + if name == nil { + return *new(T), fmt.Errorf("configuration.Name must be provided to Apply") + } + emptyResult := a.client.newObject() + obj, err := a.client.Fake. + Invokes(kcptesting.NewPatchSubresourceAction(a.client.resource, a.client.cluster, a.client.ns, *name, types.ApplyPatchType, data, "status"), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(T), err +} + +func (c *FakeClient[T]) Namespace() string { + return c.ns +} + +func (c *FakeClient[T]) Kind() schema.GroupVersionKind { + return c.kind +} + +func (c *FakeClient[T]) Resource() schema.GroupVersionResource { + return c.resource +} diff --git a/third_party/k8s.io/client-go/gentype/type.go b/third_party/k8s.io/client-go/gentype/type.go new file mode 100644 index 000000000..1a2400291 --- /dev/null +++ b/third_party/k8s.io/client-go/gentype/type.go @@ -0,0 +1,34 @@ +/* +Copyright 2024 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. +*/ + +package gentype + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// objectWithMeta matches objects implementing both runtime.Object and metav1.Object. +type objectWithMeta interface { + runtime.Object + metav1.Object +} + +// namedObject matches comparable objects implementing GetName(); it is intended for use with apply declarative configurations. +type namedObject interface { + comparable + GetName() *string +} diff --git a/third_party/k8s.io/client-go/listers/generic_helpers.go b/third_party/k8s.io/client-go/listers/generic_helpers.go new file mode 100644 index 000000000..576ca7e2c --- /dev/null +++ b/third_party/k8s.io/client-go/listers/generic_helpers.go @@ -0,0 +1,92 @@ +/* +Copyright 2023 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. +*/ + +package listers + +import ( + kcpcache "github.com/kcp-dev/apimachinery/v2/pkg/cache" + "github.com/kcp-dev/logicalcluster/v3" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/tools/cache" +) + +// ResourceIndexer wraps an indexer, resource, and optional namespace for a given type. +// This is intended for use by listers (generated by lister-gen) only. +type ResourceClusterIndexer[T runtime.Object] struct { + indexer cache.Indexer + resource schema.GroupResource +} + +// NewCluster returns a new instance of a lister (resource indexer) wrapping the given indexer +// and resource for the specified type. +// This is intended for use by listers (generated by lister-gen) only. +func NewCluster[T runtime.Object](indexer cache.Indexer, resource schema.GroupResource) ResourceClusterIndexer[T] { + return ResourceClusterIndexer[T]{indexer: indexer, resource: resource} +} + +// List lists all resources in the indexer matching the given selector. +func (l ResourceClusterIndexer[T]) List(selector labels.Selector) (ret []T, err error) { + err = cache.ListAll(l.indexer, selector, func(m interface{}) { + ret = append(ret, m.(T)) + }) + return ret, err +} + +// ResourceIndexer wraps an indexer, resource, and optional namespace for a given type. +// This is intended for use by listers (generated by lister-gen) only. +type ResourceIndexer[T runtime.Object] struct { + indexer cache.Indexer + cluster logicalcluster.Name + resource schema.GroupResource + namespace string // empty for non-namespaced types +} + +// New returns a new instance of a lister (resource indexer) wrapping the given indexer and resource for the specified type. +// This is intended for use by listers (generated by lister-gen) only. +func New[T runtime.Object](indexer cache.Indexer, cluster logicalcluster.Name, resource schema.GroupResource) ResourceIndexer[T] { + return ResourceIndexer[T]{indexer: indexer, cluster: cluster, resource: resource} +} + +// NewNamespaced returns a new instance of a namespaced lister (resource indexer) wrapping the given parent and namespace for the specified type. +// This is intended for use by listers (generated by lister-gen) only. +func NewNamespaced[T runtime.Object](parent ResourceIndexer[T], namespace string) ResourceIndexer[T] { + return ResourceIndexer[T]{indexer: parent.indexer, resource: parent.resource, namespace: namespace} +} + +// List lists all resources in the indexer matching the given selector. +func (l ResourceIndexer[T]) List(selector labels.Selector) (ret []T, err error) { + err = kcpcache.ListAllByClusterAndNamespace(l.indexer, l.cluster, l.namespace, selector, func(i interface{}) { + ret = append(ret, i.(T)) + }) + return ret, err +} + +// Get retrieves the resource from the index for a given name. +func (l ResourceIndexer[T]) Get(name string) (T, error) { + key := kcpcache.ToClusterAwareKey(l.cluster.String(), l.namespace, name) + obj, exists, err := l.indexer.GetByKey(key) + if err != nil { + return *new(T), err + } + if !exists { + return *new(T), errors.NewNotFound(l.resource, name) + } + return obj.(T), nil +} diff --git a/third_party/k8s.io/client-go/testing/actions.go b/third_party/k8s.io/client-go/testing/actions.go index 31d2785e0..c05ed11e6 100644 --- a/third_party/k8s.io/client-go/testing/actions.go +++ b/third_party/k8s.io/client-go/testing/actions.go @@ -32,28 +32,46 @@ import ( "k8s.io/apimachinery/pkg/types" ) +// All NewRoot... functions return non-namespaced actions, and are equivalent to +// calling the corresponding New... function with an empty namespace. +// This is assumed by the fake client generator. + func NewRootGetAction(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, name string) GetActionImpl { + return NewRootGetActionWithOptions(resource, clusterPath, name, metav1.GetOptions{}) +} + +func NewRootGetActionWithOptions(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, name string, opts metav1.GetOptions) GetActionImpl { action := GetActionImpl{} action.Verb = "get" action.Resource = resource action.Name = name action.ClusterPath = clusterPath + action.GetOptions = opts return action } func NewGetAction(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, namespace, name string) GetActionImpl { + return NewGetActionWithOptions(resource, clusterPath, namespace, name, metav1.GetOptions{}) +} + +func NewGetActionWithOptions(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, namespace, name string, opts metav1.GetOptions) GetActionImpl { action := GetActionImpl{} action.Verb = "get" action.Resource = resource action.Namespace = namespace action.Name = name action.ClusterPath = clusterPath + action.GetOptions = opts return action } func NewGetSubresourceAction(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, namespace, subresource, name string) GetActionImpl { + return NewGetSubresourceActionWithOptions(resource, clusterPath, namespace, subresource, name, metav1.GetOptions{}) +} + +func NewGetSubresourceActionWithOptions(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, namespace, subresource, name string, opts metav1.GetOptions) GetActionImpl { action := GetActionImpl{} action.Verb = "get" action.Resource = resource @@ -61,17 +79,23 @@ func NewGetSubresourceAction(resource schema.GroupVersionResource, clusterPath l action.Namespace = namespace action.Name = name action.ClusterPath = clusterPath + action.GetOptions = opts return action } func NewRootGetSubresourceAction(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, subresource, name string) GetActionImpl { + return NewRootGetSubresourceActionWithOptions(resource, clusterPath, subresource, name, metav1.GetOptions{}) +} + +func NewRootGetSubresourceActionWithOptions(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, subresource, name string, opts metav1.GetOptions) GetActionImpl { action := GetActionImpl{} action.Verb = "get" action.Resource = resource action.Subresource = subresource action.Name = name action.ClusterPath = clusterPath + action.GetOptions = opts return action } @@ -84,6 +108,22 @@ func NewRootListAction(resource schema.GroupVersionResource, kind schema.GroupVe action.Kind = kind labelSelector, fieldSelector, _ := ExtractFromListOptions(opts) action.ListRestrictions = ListRestrictions{labelSelector, fieldSelector} + action.ListOptions = metav1.ListOptions{LabelSelector: labelSelector.String(), FieldSelector: fieldSelector.String()} + + return action +} + +func NewRootListActionWithOptions(resource schema.GroupVersionResource, kind schema.GroupVersionKind, clusterPath logicalcluster.Path, opts metav1.ListOptions) ListActionImpl { + action := ListActionImpl{} + action.Verb = "list" + action.Resource = resource + action.ClusterPath = clusterPath + action.Kind = kind + action.ListOptions = opts + + labelSelector, fieldSelector, _ := ExtractFromListOptions(opts) + action.ListRestrictions = ListRestrictions{labelSelector, fieldSelector} + action.ListOptions = metav1.ListOptions{LabelSelector: labelSelector.String(), FieldSelector: fieldSelector.String()} return action } @@ -95,6 +135,22 @@ func NewListAction(resource schema.GroupVersionResource, kind schema.GroupVersio action.ClusterPath = clusterPath action.Kind = kind action.Namespace = namespace + labelSelector, fieldSelector, _ := ExtractFromListOptions(opts) + action.ListRestrictions = ListRestrictions{labelSelector, fieldSelector} + action.ListOptions = metav1.ListOptions{LabelSelector: labelSelector.String(), FieldSelector: fieldSelector.String()} + + return action +} + +func NewListActionWithOptions(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, kind schema.GroupVersionKind, namespace string, opts metav1.ListOptions) ListActionImpl { + action := ListActionImpl{} + action.Verb = "list" + action.Resource = resource + action.ClusterPath = clusterPath + action.Kind = kind + action.Namespace = namespace + action.ListOptions = opts + labelSelector, fieldSelector, _ := ExtractFromListOptions(opts) action.ListRestrictions = ListRestrictions{labelSelector, fieldSelector} @@ -102,27 +158,41 @@ func NewListAction(resource schema.GroupVersionResource, kind schema.GroupVersio } func NewRootCreateAction(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, object runtime.Object) CreateActionImpl { + return NewRootCreateActionWithOptions(resource, clusterPath, object, metav1.CreateOptions{}) +} + +func NewRootCreateActionWithOptions(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, object runtime.Object, opts metav1.CreateOptions) CreateActionImpl { action := CreateActionImpl{} action.Verb = "create" action.Resource = resource action.ClusterPath = clusterPath action.Object = object + action.CreateOptions = opts return action } func NewCreateAction(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, namespace string, object runtime.Object) CreateActionImpl { + return NewCreateActionWithOptions(resource, clusterPath, namespace, object, metav1.CreateOptions{}) +} + +func NewCreateActionWithOptions(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, namespace string, object runtime.Object, opts metav1.CreateOptions) CreateActionImpl { action := CreateActionImpl{} action.Verb = "create" action.Resource = resource action.ClusterPath = clusterPath action.Namespace = namespace action.Object = object + action.CreateOptions = opts return action } func NewRootCreateSubresourceAction(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, name, subresource string, object runtime.Object) CreateActionImpl { + return NewRootCreateSubresourceActionWithOptions(resource, clusterPath, name, subresource, object, metav1.CreateOptions{}) +} + +func NewRootCreateSubresourceActionWithOptions(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, name, subresource string, object runtime.Object, opts metav1.CreateOptions) CreateActionImpl { action := CreateActionImpl{} action.Verb = "create" action.Resource = resource @@ -130,11 +200,16 @@ func NewRootCreateSubresourceAction(resource schema.GroupVersionResource, cluste action.Name = name action.ClusterPath = clusterPath action.Object = object + action.CreateOptions = opts return action } func NewCreateSubresourceAction(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, name, subresource, namespace string, object runtime.Object) CreateActionImpl { + return NewCreateSubresourceActionWithOptions(resource, clusterPath, name, subresource, namespace, object, metav1.CreateOptions{}) +} + +func NewCreateSubresourceActionWithOptions(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, name, subresource, namespace string, object runtime.Object, opts metav1.CreateOptions) CreateActionImpl { action := CreateActionImpl{} action.Verb = "create" action.Resource = resource @@ -143,32 +218,47 @@ func NewCreateSubresourceAction(resource schema.GroupVersionResource, clusterPat action.Name = name action.ClusterPath = clusterPath action.Object = object + action.CreateOptions = opts return action } func NewRootUpdateAction(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, object runtime.Object) UpdateActionImpl { + return NewRootUpdateActionWithOptions(resource, clusterPath, object, metav1.UpdateOptions{}) +} + +func NewRootUpdateActionWithOptions(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, object runtime.Object, opts metav1.UpdateOptions) UpdateActionImpl { action := UpdateActionImpl{} action.Verb = "update" action.Resource = resource action.ClusterPath = clusterPath action.Object = object + action.UpdateOptions = opts return action } func NewUpdateAction(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, namespace string, object runtime.Object) UpdateActionImpl { + return NewUpdateActionWithOptions(resource, clusterPath, namespace, object, metav1.UpdateOptions{}) +} + +func NewUpdateActionWithOptions(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, namespace string, object runtime.Object, opts metav1.UpdateOptions) UpdateActionImpl { action := UpdateActionImpl{} action.Verb = "update" action.Resource = resource action.ClusterPath = clusterPath action.Namespace = namespace action.Object = object + action.UpdateOptions = opts return action } func NewRootPatchAction(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, name string, pt types.PatchType, patch []byte) PatchActionImpl { + return NewRootPatchActionWithOptions(resource, clusterPath, name, pt, patch, metav1.PatchOptions{}) +} + +func NewRootPatchActionWithOptions(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, name string, pt types.PatchType, patch []byte, opts metav1.PatchOptions) PatchActionImpl { action := PatchActionImpl{} action.Verb = "patch" action.Resource = resource @@ -176,11 +266,16 @@ func NewRootPatchAction(resource schema.GroupVersionResource, clusterPath logica action.ClusterPath = clusterPath action.PatchType = pt action.Patch = patch + action.PatchOptions = opts return action } func NewPatchAction(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, namespace string, name string, pt types.PatchType, patch []byte) PatchActionImpl { + return NewPatchActionWithOptions(resource, clusterPath, namespace, name, pt, patch, metav1.PatchOptions{}) +} + +func NewPatchActionWithOptions(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, namespace string, name string, pt types.PatchType, patch []byte, opts metav1.PatchOptions) PatchActionImpl { action := PatchActionImpl{} action.Verb = "patch" action.Resource = resource @@ -189,11 +284,16 @@ func NewPatchAction(resource schema.GroupVersionResource, clusterPath logicalclu action.ClusterPath = clusterPath action.PatchType = pt action.Patch = patch + action.PatchOptions = opts return action } func NewRootPatchSubresourceAction(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, name string, pt types.PatchType, patch []byte, subresources ...string) PatchActionImpl { + return NewRootPatchSubresourceActionWithOptions(resource, clusterPath, name, pt, patch, metav1.PatchOptions{}, subresources...) +} + +func NewRootPatchSubresourceActionWithOptions(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, name string, pt types.PatchType, patch []byte, opts metav1.PatchOptions, subresources ...string) PatchActionImpl { action := PatchActionImpl{} action.Verb = "patch" action.Resource = resource @@ -202,11 +302,16 @@ func NewRootPatchSubresourceAction(resource schema.GroupVersionResource, cluster action.ClusterPath = clusterPath action.PatchType = pt action.Patch = patch + action.PatchOptions = opts return action } func NewPatchSubresourceAction(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, namespace, name string, pt types.PatchType, patch []byte, subresources ...string) PatchActionImpl { + return NewPatchSubresourceActionWithOptions(resource, clusterPath, namespace, name, pt, patch, metav1.PatchOptions{}, subresources...) +} + +func NewPatchSubresourceActionWithOptions(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, namespace, name string, pt types.PatchType, patch []byte, opts metav1.PatchOptions, subresources ...string) PatchActionImpl { action := PatchActionImpl{} action.Verb = "patch" action.Resource = resource @@ -216,21 +321,32 @@ func NewPatchSubresourceAction(resource schema.GroupVersionResource, clusterPath action.ClusterPath = clusterPath action.PatchType = pt action.Patch = patch + action.PatchOptions = opts return action } func NewRootUpdateSubresourceAction(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, subresource string, object runtime.Object) UpdateActionImpl { + return NewRootUpdateSubresourceActionWithOptions(resource, clusterPath, subresource, object, metav1.UpdateOptions{}) +} + +func NewRootUpdateSubresourceActionWithOptions(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, subresource string, object runtime.Object, opts metav1.UpdateOptions) UpdateActionImpl { action := UpdateActionImpl{} action.Verb = "update" action.Resource = resource action.ClusterPath = clusterPath action.Subresource = subresource action.Object = object + action.UpdateOptions = opts return action } + func NewUpdateSubresourceAction(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, subresource string, namespace string, object runtime.Object) UpdateActionImpl { + return NewUpdateSubresourceActionWithOptions(resource, clusterPath, subresource, namespace, object, metav1.UpdateOptions{}) +} + +func NewUpdateSubresourceActionWithOptions(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, subresource string, namespace string, object runtime.Object, opts metav1.UpdateOptions) UpdateActionImpl { action := UpdateActionImpl{} action.Verb = "update" action.Resource = resource @@ -238,6 +354,7 @@ func NewUpdateSubresourceAction(resource schema.GroupVersionResource, clusterPat action.Subresource = subresource action.Namespace = namespace action.Object = object + action.UpdateOptions = opts return action } @@ -258,12 +375,17 @@ func NewRootDeleteActionWithOptions(resource schema.GroupVersionResource, cluste } func NewRootDeleteSubresourceAction(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, subresource string, name string) DeleteActionImpl { + return NewRootDeleteSubresourceActionWithOptions(resource, clusterPath, subresource, name, metav1.DeleteOptions{}) +} + +func NewRootDeleteSubresourceActionWithOptions(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, subresource string, name string, opts metav1.DeleteOptions) DeleteActionImpl { action := DeleteActionImpl{} action.Verb = "delete" action.Resource = resource action.Subresource = subresource action.Name = name action.ClusterPath = clusterPath + action.DeleteOptions = opts return action } @@ -285,6 +407,10 @@ func NewDeleteActionWithOptions(resource schema.GroupVersionResource, clusterPat } func NewDeleteSubresourceAction(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, subresource, namespace, name string) DeleteActionImpl { + return NewDeleteSubresourceActionWithOptions(resource, clusterPath, subresource, namespace, name, metav1.DeleteOptions{}) +} + +func NewDeleteSubresourceActionWithOptions(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, subresource, namespace, name string, opts metav1.DeleteOptions) DeleteActionImpl { action := DeleteActionImpl{} action.Verb = "delete" action.Resource = resource @@ -292,38 +418,62 @@ func NewDeleteSubresourceAction(resource schema.GroupVersionResource, clusterPat action.Namespace = namespace action.Name = name action.ClusterPath = clusterPath + action.DeleteOptions = opts return action } func NewRootDeleteCollectionAction(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, opts interface{}) DeleteCollectionActionImpl { + listOpts, _ := opts.(metav1.ListOptions) + return NewRootDeleteCollectionActionWithOptions(resource, clusterPath, metav1.DeleteOptions{}, listOpts) +} + +func NewRootDeleteCollectionActionWithOptions(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, deleteOpts metav1.DeleteOptions, listOpts metav1.ListOptions) DeleteCollectionActionImpl { action := DeleteCollectionActionImpl{} action.Verb = "delete-collection" action.Resource = resource action.ClusterPath = clusterPath - labelSelector, fieldSelector, _ := ExtractFromListOptions(opts) + action.DeleteOptions = deleteOpts + action.ListOptions = listOpts + + labelSelector, fieldSelector, _ := ExtractFromListOptions(listOpts) action.ListRestrictions = ListRestrictions{labelSelector, fieldSelector} return action } func NewDeleteCollectionAction(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, namespace string, opts interface{}) DeleteCollectionActionImpl { + listOpts, _ := opts.(metav1.ListOptions) + return NewDeleteCollectionActionWithOptions(resource, clusterPath, namespace, metav1.DeleteOptions{}, listOpts) +} + +func NewDeleteCollectionActionWithOptions(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, namespace string, deleteOpts metav1.DeleteOptions, listOpts metav1.ListOptions) DeleteCollectionActionImpl { action := DeleteCollectionActionImpl{} action.Verb = "delete-collection" action.Resource = resource action.ClusterPath = clusterPath action.Namespace = namespace - labelSelector, fieldSelector, _ := ExtractFromListOptions(opts) + action.DeleteOptions = deleteOpts + action.ListOptions = listOpts + + labelSelector, fieldSelector, _ := ExtractFromListOptions(listOpts) action.ListRestrictions = ListRestrictions{labelSelector, fieldSelector} return action } func NewRootWatchAction(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, opts interface{}) WatchActionImpl { + listOpts, _ := opts.(metav1.ListOptions) + return NewRootWatchActionWithOptions(resource, clusterPath, listOpts) +} + +func NewRootWatchActionWithOptions(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, opts metav1.ListOptions) WatchActionImpl { action := WatchActionImpl{} action.Verb = "watch" action.Resource = resource action.ClusterPath = clusterPath + action.ListOptions = opts + labelSelector, fieldSelector, resourceVersion := ExtractFromListOptions(opts) action.WatchRestrictions = WatchRestrictions{labelSelector, fieldSelector, resourceVersion} @@ -356,11 +506,18 @@ func ExtractFromListOptions(opts interface{}) (labelSelector labels.Selector, fi } func NewWatchAction(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, namespace string, opts interface{}) WatchActionImpl { + listOpts, _ := opts.(metav1.ListOptions) + return NewWatchActionWithOptions(resource, clusterPath, namespace, listOpts) +} + +func NewWatchActionWithOptions(resource schema.GroupVersionResource, clusterPath logicalcluster.Path, namespace string, opts metav1.ListOptions) WatchActionImpl { action := WatchActionImpl{} action.Verb = "watch" action.Resource = resource action.ClusterPath = clusterPath action.Namespace = namespace + action.ListOptions = opts + labelSelector, fieldSelector, resourceVersion := ExtractFromListOptions(opts) action.WatchRestrictions = WatchRestrictions{labelSelector, fieldSelector, resourceVersion} @@ -523,17 +680,23 @@ func (a GenericActionImpl) DeepCopy() Action { type GetActionImpl struct { ActionImpl - Name string + Name string + GetOptions metav1.GetOptions } func (a GetActionImpl) GetName() string { return a.Name } +func (a GetActionImpl) GetGetOptions() metav1.GetOptions { + return a.GetOptions +} + func (a GetActionImpl) DeepCopy() Action { return GetActionImpl{ ActionImpl: a.ActionImpl.DeepCopy().(ActionImpl), Name: a.Name, + GetOptions: *a.GetOptions.DeepCopy(), } } @@ -542,12 +705,17 @@ type ListActionImpl struct { Kind schema.GroupVersionKind Name string ListRestrictions ListRestrictions + ListOptions metav1.ListOptions } func (a ListActionImpl) GetKind() schema.GroupVersionKind { return a.Kind } +func (a ListActionImpl) GetListOptions() metav1.ListOptions { + return a.ListOptions +} + func (a ListActionImpl) GetListRestrictions() ListRestrictions { return a.ListRestrictions } @@ -561,54 +729,72 @@ func (a ListActionImpl) DeepCopy() Action { Labels: a.ListRestrictions.Labels.DeepCopySelector(), Fields: a.ListRestrictions.Fields.DeepCopySelector(), }, + ListOptions: *a.ListOptions.DeepCopy(), } } type CreateActionImpl struct { ActionImpl - Name string - Object runtime.Object + Name string + Object runtime.Object + CreateOptions metav1.CreateOptions } func (a CreateActionImpl) GetObject() runtime.Object { return a.Object } +func (a CreateActionImpl) GetCreateOptions() metav1.CreateOptions { + return a.CreateOptions +} + func (a CreateActionImpl) DeepCopy() Action { return CreateActionImpl{ - ActionImpl: a.ActionImpl.DeepCopy().(ActionImpl), - Name: a.Name, - Object: a.Object.DeepCopyObject(), + ActionImpl: a.ActionImpl.DeepCopy().(ActionImpl), + Name: a.Name, + Object: a.Object.DeepCopyObject(), + CreateOptions: *a.CreateOptions.DeepCopy(), } } type UpdateActionImpl struct { ActionImpl - Object runtime.Object + Object runtime.Object + UpdateOptions metav1.UpdateOptions } func (a UpdateActionImpl) GetObject() runtime.Object { return a.Object } +func (a UpdateActionImpl) GetUpdateOptions() metav1.UpdateOptions { + return a.UpdateOptions +} + func (a UpdateActionImpl) DeepCopy() Action { return UpdateActionImpl{ - ActionImpl: a.ActionImpl.DeepCopy().(ActionImpl), - Object: a.Object.DeepCopyObject(), + ActionImpl: a.ActionImpl.DeepCopy().(ActionImpl), + Object: a.Object.DeepCopyObject(), + UpdateOptions: *a.UpdateOptions.DeepCopy(), } } type PatchActionImpl struct { ActionImpl - Name string - PatchType types.PatchType - Patch []byte + Name string + PatchType types.PatchType + Patch []byte + PatchOptions metav1.PatchOptions } func (a PatchActionImpl) GetName() string { return a.Name } +func (a PatchActionImpl) GetPatchOptions() metav1.PatchOptions { + return a.PatchOptions +} + func (a PatchActionImpl) GetPatch() []byte { return a.Patch } @@ -621,10 +807,11 @@ func (a PatchActionImpl) DeepCopy() Action { patch := make([]byte, len(a.Patch)) copy(patch, a.Patch) return PatchActionImpl{ - ActionImpl: a.ActionImpl.DeepCopy().(ActionImpl), - Name: a.Name, - PatchType: a.PatchType, - Patch: patch, + ActionImpl: a.ActionImpl.DeepCopy().(ActionImpl), + Name: a.Name, + PatchType: a.PatchType, + Patch: patch, + PatchOptions: *a.PatchOptions.DeepCopy(), } } @@ -653,12 +840,22 @@ func (a DeleteActionImpl) DeepCopy() Action { type DeleteCollectionActionImpl struct { ActionImpl ListRestrictions ListRestrictions + DeleteOptions metav1.DeleteOptions + ListOptions metav1.ListOptions } func (a DeleteCollectionActionImpl) GetListRestrictions() ListRestrictions { return a.ListRestrictions } +func (a DeleteCollectionActionImpl) GetDeleteOptions() metav1.DeleteOptions { + return a.DeleteOptions +} + +func (a DeleteCollectionActionImpl) GetListOptions() metav1.ListOptions { + return a.ListOptions +} + func (a DeleteCollectionActionImpl) DeepCopy() Action { return DeleteCollectionActionImpl{ ActionImpl: a.ActionImpl.DeepCopy().(ActionImpl), @@ -666,18 +863,25 @@ func (a DeleteCollectionActionImpl) DeepCopy() Action { Labels: a.ListRestrictions.Labels.DeepCopySelector(), Fields: a.ListRestrictions.Fields.DeepCopySelector(), }, + DeleteOptions: *a.DeleteOptions.DeepCopy(), + ListOptions: *a.ListOptions.DeepCopy(), } } type WatchActionImpl struct { ActionImpl WatchRestrictions WatchRestrictions + ListOptions metav1.ListOptions } func (a WatchActionImpl) GetWatchRestrictions() WatchRestrictions { return a.WatchRestrictions } +func (a WatchActionImpl) GetListOptions() metav1.ListOptions { + return a.ListOptions +} + func (a WatchActionImpl) DeepCopy() Action { return WatchActionImpl{ ActionImpl: a.ActionImpl.DeepCopy().(ActionImpl), @@ -686,6 +890,7 @@ func (a WatchActionImpl) DeepCopy() Action { Fields: a.WatchRestrictions.Fields.DeepCopySelector(), ResourceVersion: a.WatchRestrictions.ResourceVersion, }, + ListOptions: *a.ListOptions.DeepCopy(), } } diff --git a/third_party/k8s.io/client-go/testing/fixture.go b/third_party/k8s.io/client-go/testing/fixture.go index 42b61929a..de50a3d62 100644 --- a/third_party/k8s.io/client-go/testing/fixture.go +++ b/third_party/k8s.io/client-go/testing/fixture.go @@ -18,25 +18,30 @@ limitations under the License. package testing import ( + "encoding/json" "fmt" "reflect" "sort" "strings" "sync" - jsonpatch "github.com/evanphx/json-patch" "github.com/kcp-dev/logicalcluster/v3" + jsonpatch "gopkg.in/evanphx/json-patch.v4" - "k8s.io/apimachinery/pkg/api/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/api/meta/testrestmapper" 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/apimachinery/pkg/util/json" + "k8s.io/apimachinery/pkg/util/managedfields" "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/apimachinery/pkg/watch" restclient "k8s.io/client-go/rest" + "sigs.k8s.io/structured-merge-diff/v4/typed" + "sigs.k8s.io/yaml" ) type ClusterNamespacedName struct { @@ -70,29 +75,35 @@ type ObjectTracker interface { type ScopedObjectTracker interface { // List retrieves all objects of a given kind in the given // namespace. Only non-List kinds are accepted. - List(gvr schema.GroupVersionResource, gvk schema.GroupVersionKind, ns string) (runtime.Object, error) + List(gvr schema.GroupVersionResource, gvk schema.GroupVersionKind, ns string, opts ...metav1.ListOptions) (runtime.Object, error) // Watch watches objects from the tracker. Watch returns a channel // which will push added / modified / deleted object. - Watch(gvr schema.GroupVersionResource, ns string) (watch.Interface, error) + Watch(gvr schema.GroupVersionResource, ns string, opts ...metav1.ListOptions) (watch.Interface, error) // Add adds an object to the tracker. If object being added // is a list, its items are added separately. Add(obj runtime.Object) error // Get retrieves the object by its kind, namespace and name. - Get(gvr schema.GroupVersionResource, ns, name string) (runtime.Object, error) + Get(gvr schema.GroupVersionResource, ns, name string, opts ...metav1.GetOptions) (runtime.Object, error) // Create adds an object to the tracker in the specified namespace. - Create(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error + Create(gvr schema.GroupVersionResource, obj runtime.Object, ns string, opts ...metav1.CreateOptions) error // Update updates an existing object in the tracker in the specified namespace. - Update(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error + Update(gvr schema.GroupVersionResource, obj runtime.Object, ns string, opts ...metav1.UpdateOptions) error + + // Patch patches an existing object in the tracker in the specified namespace. + Patch(gvr schema.GroupVersionResource, obj runtime.Object, ns string, opts ...metav1.PatchOptions) error + + // Apply applies an object in the tracker in the specified namespace. + Apply(gvr schema.GroupVersionResource, applyConfiguration runtime.Object, ns string, opts ...metav1.PatchOptions) error // Delete deletes an existing object from the tracker. If object // didn't exist in the tracker prior to deletion, Delete returns // no error. - Delete(gvr schema.GroupVersionResource, ns, name string) error + Delete(gvr schema.GroupVersionResource, ns, name string, opts ...metav1.DeleteOptions) error } // ObjectScheme abstracts the implementation of common operations on objects. @@ -103,140 +114,226 @@ type ObjectScheme interface { // ObjectReaction returns a ReactionFunc that applies core.Action to // the given tracker. +// +// If tracker also implements ManagedFieldObjectTracker, then managed fields +// will be handled by the tracker and apply patch actions will be evaluated +// using the field manager and will take field ownership into consideration. +// Without a ManagedFieldObjectTracker, apply patch actions do not consider +// field ownership. +// +// WARNING: There is no server side defaulting, validation, or conversion handled +// by the fake client and subresources are not handled accurately (fields in the +// root resource are not automatically updated when a scale resource is updated, for example). func ObjectReaction(tracker ObjectTracker) ReactionFunc { + reactor := objectTrackerReact{tracker: tracker} return func(action Action) (bool, runtime.Object, error) { - ns := action.GetNamespace() - gvr := action.GetResource() // Here and below we need to switch on implementation types, // not on interfaces, as some interfaces are identical // (e.g. UpdateAction and CreateAction), so if we use them, // updates and creates end up matching the same case branch. switch action := action.(type) { - case ListActionImpl: var obj runtime.Object var err error switch action.GetCluster() { case logicalcluster.Wildcard: - obj, err = tracker.List(gvr, action.GetKind(), ns) + obj, err = reactor.List(action) default: - obj, err = tracker.Cluster(action.GetCluster()).List(gvr, action.GetKind(), ns) + obj, err = reactor.Cluster(action.GetCluster()).List(action) } return true, obj, err case GetActionImpl: - obj, err := tracker.Cluster(action.GetCluster()).Get(gvr, ns, action.GetName()) + obj, err := reactor.Cluster(action.GetCluster()).Get(action) return true, obj, err case CreateActionImpl: - objMeta, err := meta.Accessor(action.GetObject()) - if err != nil { - return true, nil, err - } - if action.GetSubresource() == "" { - err = tracker.Cluster(action.GetCluster()).Create(gvr, action.GetObject(), ns) - } else { - oldObj, getOldObjErr := tracker.Cluster(action.GetCluster()).Get(gvr, ns, objMeta.GetName()) - if getOldObjErr != nil { - return true, nil, getOldObjErr - } - // Check whether the existing historical object type is the same as the current operation object type that needs to be updated, and if it is the same, perform the update operation. - if reflect.TypeOf(oldObj) == reflect.TypeOf(action.GetObject()) { - // TODO: Currently we're handling subresource creation as an update - // on the enclosing resource. This works for some subresources but - // might not be generic enough. - err = tracker.Cluster(action.GetCluster()).Update(gvr, action.GetObject(), ns) - } else { - // If the historical object type is different from the current object type, need to make sure we return the object submitted,don't persist the submitted object in the tracker. - return true, action.GetObject(), nil - } - } - if err != nil { - return true, nil, err - } - obj, err := tracker.Cluster(action.GetCluster()).Get(gvr, ns, objMeta.GetName()) + obj, err := reactor.Cluster(action.GetCluster()).Create(action) return true, obj, err case UpdateActionImpl: - objMeta, err := meta.Accessor(action.GetObject()) - if err != nil { - return true, nil, err - } - err = tracker.Cluster(action.GetCluster()).Update(gvr, action.GetObject(), ns) - if err != nil { - return true, nil, err - } - obj, err := tracker.Cluster(action.GetCluster()).Get(gvr, ns, objMeta.GetName()) + obj, err := reactor.Cluster(action.GetCluster()).Update(action) return true, obj, err case DeleteActionImpl: - err := tracker.Cluster(action.GetCluster()).Delete(gvr, ns, action.GetName()) - if err != nil { - return true, nil, err - } - return true, nil, nil + obj, err := reactor.Cluster(action.GetCluster()).Delete(action) + return true, obj, err case PatchActionImpl: - obj, err := tracker.Cluster(action.GetCluster()).Get(gvr, ns, action.GetName()) - if err != nil { - return true, nil, err + if action.GetPatchType() == types.ApplyPatchType { + obj, err := reactor.Cluster(action.GetCluster()).Apply(action) + return true, obj, err } + obj, err := reactor.Cluster(action.GetCluster()).Patch(action) + return true, obj, err - old, err := json.Marshal(obj) - if err != nil { - return true, nil, err - } + default: + return false, nil, fmt.Errorf("no reaction implemented for %s", action) + } + } +} - // reset the object in preparation to unmarshal, since unmarshal does not guarantee that fields - // in obj that are removed by patch are cleared - value := reflect.ValueOf(obj) - value.Elem().Set(reflect.New(value.Type().Elem()).Elem()) - - switch action.GetPatchType() { - case types.JSONPatchType: - patch, err := jsonpatch.DecodePatch(action.GetPatch()) - if err != nil { - return true, nil, err - } - modified, err := patch.Apply(old) - if err != nil { - return true, nil, err - } - - if err = json.Unmarshal(modified, obj); err != nil { - return true, nil, err - } - case types.MergePatchType: - modified, err := jsonpatch.MergePatch(old, action.GetPatch()) - if err != nil { - return true, nil, err - } - - if err := json.Unmarshal(modified, obj); err != nil { - return true, nil, err - } - case types.StrategicMergePatchType, types.ApplyPatchType: - mergedByte, err := strategicpatch.StrategicMergePatch(old, action.GetPatch(), obj) - if err != nil { - return true, nil, err - } - if err = json.Unmarshal(mergedByte, obj); err != nil { - return true, nil, err - } - default: - return true, nil, fmt.Errorf("PatchType is not supported") - } +type objectTrackerReact struct { + tracker ObjectTracker +} - if err = tracker.Cluster(action.GetCluster()).Update(gvr, obj, ns); err != nil { - return true, nil, err - } +func (o objectTrackerReact) List(action ListActionImpl) (runtime.Object, error) { + return o.tracker.List(action.GetResource(), action.GetKind(), action.GetNamespace()) +} - return true, obj, nil +func (o objectTrackerReact) Cluster(clusterPath logicalcluster.Path) scopedObjectTrackerReact { + return scopedObjectTrackerReact{o.tracker.Cluster(clusterPath)} +} - default: - return false, nil, fmt.Errorf("no reaction implemented for %s", action) +type scopedObjectTrackerReact struct { + tracker ScopedObjectTracker +} + +func (o scopedObjectTrackerReact) List(action ListActionImpl) (runtime.Object, error) { + return o.tracker.List(action.GetResource(), action.GetKind(), action.GetNamespace(), action.ListOptions) +} + +func (o scopedObjectTrackerReact) Get(action GetActionImpl) (runtime.Object, error) { + return o.tracker.Get(action.GetResource(), action.GetNamespace(), action.GetName(), action.GetOptions) +} + +func (o scopedObjectTrackerReact) Create(action CreateActionImpl) (runtime.Object, error) { + ns := action.GetNamespace() + gvr := action.GetResource() + objMeta, err := meta.Accessor(action.GetObject()) + if err != nil { + return nil, err + } + if action.GetSubresource() == "" { + err = o.tracker.Create(gvr, action.GetObject(), ns, action.CreateOptions) + if err != nil { + return nil, err + } + } else { + oldObj, getOldObjErr := o.tracker.Get(gvr, ns, objMeta.GetName(), metav1.GetOptions{}) + if getOldObjErr != nil { + return nil, getOldObjErr + } + // Check whether the existing historical object type is the same as the current operation object type that needs to be updated, and if it is the same, perform the update operation. + if reflect.TypeOf(oldObj) == reflect.TypeOf(action.GetObject()) { + // TODO: Currently we're handling subresource creation as an update + // on the enclosing resource. This works for some subresources but + // might not be generic enough. + err = o.tracker.Update(gvr, action.GetObject(), ns, metav1.UpdateOptions{ + DryRun: action.CreateOptions.DryRun, + FieldManager: action.CreateOptions.FieldManager, + FieldValidation: action.CreateOptions.FieldValidation, + }) + } else { + // If the historical object type is different from the current object type, need to make sure we return the object submitted,don't persist the submitted object in the tracker. + return action.GetObject(), nil + } + } + if err != nil { + return nil, err + } + obj, err := o.tracker.Get(gvr, ns, objMeta.GetName(), metav1.GetOptions{}) + return obj, err +} + +func (o scopedObjectTrackerReact) Update(action UpdateActionImpl) (runtime.Object, error) { + ns := action.GetNamespace() + gvr := action.GetResource() + objMeta, err := meta.Accessor(action.GetObject()) + if err != nil { + return nil, err + } + + err = o.tracker.Update(gvr, action.GetObject(), ns, action.UpdateOptions) + if err != nil { + return nil, err + } + + obj, err := o.tracker.Get(gvr, ns, objMeta.GetName(), metav1.GetOptions{}) + return obj, err +} + +func (o scopedObjectTrackerReact) Delete(action DeleteActionImpl) (runtime.Object, error) { + err := o.tracker.Delete(action.GetResource(), action.GetNamespace(), action.GetName(), action.DeleteOptions) + return nil, err +} + +func (o scopedObjectTrackerReact) Apply(action PatchActionImpl) (runtime.Object, error) { + ns := action.GetNamespace() + gvr := action.GetResource() + + patchObj := &unstructured.Unstructured{Object: map[string]interface{}{}} + if err := yaml.Unmarshal(action.GetPatch(), &patchObj.Object); err != nil { + return nil, err + } + patchObj.SetName(action.GetName()) + err := o.tracker.Apply(gvr, patchObj, ns, action.PatchOptions) + if err != nil { + return nil, err + } + obj, err := o.tracker.Get(gvr, ns, action.GetName(), metav1.GetOptions{}) + return obj, err +} + +func (o scopedObjectTrackerReact) Patch(action PatchActionImpl) (runtime.Object, error) { + ns := action.GetNamespace() + gvr := action.GetResource() + + obj, err := o.tracker.Get(gvr, ns, action.GetName(), metav1.GetOptions{}) + if err != nil { + return nil, err + } + + old, err := json.Marshal(obj) + if err != nil { + return nil, err + } + + // reset the object in preparation to unmarshal, since unmarshal does not guarantee that fields + // in obj that are removed by patch are cleared + value := reflect.ValueOf(obj) + value.Elem().Set(reflect.New(value.Type().Elem()).Elem()) + + switch action.GetPatchType() { + case types.JSONPatchType: + patch, err := jsonpatch.DecodePatch(action.GetPatch()) + if err != nil { + return nil, err + } + modified, err := patch.Apply(old) + if err != nil { + return nil, err } + + if err = json.Unmarshal(modified, obj); err != nil { + return nil, err + } + case types.MergePatchType: + modified, err := jsonpatch.MergePatch(old, action.GetPatch()) + if err != nil { + return nil, err + } + + if err := json.Unmarshal(modified, obj); err != nil { + return nil, err + } + case types.StrategicMergePatchType: + mergedByte, err := strategicpatch.StrategicMergePatch(old, action.GetPatch(), obj) + if err != nil { + return nil, err + } + if err = json.Unmarshal(mergedByte, obj); err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("PatchType %s is not supported", action.GetPatchType()) } + + if err = o.tracker.Patch(gvr, obj, ns, action.PatchOptions); err != nil { + return nil, err + } + + return obj, nil } // WatchReaction returns a WatchReactionFunc that applies core.Action to @@ -333,8 +430,8 @@ func (t *tracker) AddAll(objects ...runtime.Object) error { return nil } -func (t *scopedTracker) List(gvr schema.GroupVersionResource, gvk schema.GroupVersionKind, ns string) (runtime.Object, error) { - list, err := t.tracker.List(gvr, gvk, ns) +func (t *scopedTracker) List(gvr schema.GroupVersionResource, gvk schema.GroupVersionKind, ns string, opts ...metav1.ListOptions) (runtime.Object, error) { + list, err := t.list(gvr, gvk, ns, opts...) if err != nil { return list, err } @@ -353,6 +450,15 @@ func (t *scopedTracker) List(gvr schema.GroupVersionResource, gvk schema.GroupVe } func (t *tracker) List(gvr schema.GroupVersionResource, gvk schema.GroupVersionKind, ns string) (runtime.Object, error) { + return t.list(gvr, gvk, ns) +} + +func (t *tracker) list(gvr schema.GroupVersionResource, gvk schema.GroupVersionKind, ns string, opts ...metav1.ListOptions) (runtime.Object, error) { + _, err := assertOptionalSingleArgument(opts) + if err != nil { + return nil, err + } + // Heuristic for list kind: original kind + List suffix. Might // not always be true but this tracker has a pretty limited // understanding of the actual API model. @@ -391,15 +497,20 @@ func (t *tracker) List(gvr schema.GroupVersionResource, gvk schema.GroupVersionK return list.DeepCopyObject(), nil } -func (t *scopedTracker) Watch(gvr schema.GroupVersionResource, ns string) (watch.Interface, error) { - return t.tracker.watch(gvr, t.clusterPath, ns) +func (t *scopedTracker) Watch(gvr schema.GroupVersionResource, ns string, opts ...metav1.ListOptions) (watch.Interface, error) { + return t.tracker.watch(gvr, t.clusterPath, ns, opts...) } func (t *tracker) Watch(gvr schema.GroupVersionResource, ns string) (watch.Interface, error) { return t.watch(gvr, logicalcluster.Wildcard, ns) } -func (t *tracker) watch(gvr schema.GroupVersionResource, cluster logicalcluster.Path, ns string) (watch.Interface, error) { +func (t *tracker) watch(gvr schema.GroupVersionResource, cluster logicalcluster.Path, ns string, opts ...metav1.ListOptions) (watch.Interface, error) { + _, err := assertOptionalSingleArgument(opts) + if err != nil { + return nil, err + } + t.lock.Lock() defer t.lock.Unlock() @@ -415,8 +526,13 @@ func (t *tracker) watch(gvr schema.GroupVersionResource, cluster logicalcluster. return fakewatcher, nil } -func (t *scopedTracker) Get(gvr schema.GroupVersionResource, ns, name string) (runtime.Object, error) { - errNotFound := errors.NewNotFound(gvr.GroupResource(), name) +func (t *scopedTracker) Get(gvr schema.GroupVersionResource, ns, name string, opts ...metav1.GetOptions) (runtime.Object, error) { + _, err := assertOptionalSingleArgument(opts) + if err != nil { + return nil, err + } + + errNotFound := apierrors.NewNotFound(gvr.GroupResource(), name) t.lock.RLock() defer t.lock.RUnlock() @@ -437,7 +553,7 @@ func (t *scopedTracker) Get(gvr schema.GroupVersionResource, ns, name string) (r obj := matchingObj.DeepCopyObject() if status, ok := obj.(*metav1.Status); ok { if status.Status != metav1.StatusSuccess { - return nil, &errors.StatusError{ErrStatus: *status} + return nil, &apierrors.StatusError{ErrStatus: *status} } } @@ -484,11 +600,73 @@ func (t *scopedTracker) Add(obj runtime.Object) error { return nil } -func (t *scopedTracker) Create(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error { +func (t *scopedTracker) Create(gvr schema.GroupVersionResource, obj runtime.Object, ns string, opts ...metav1.CreateOptions) error { + _, err := assertOptionalSingleArgument(opts) + if err != nil { + return err + } + return t.add(gvr, obj, ns, false) } -func (t *scopedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error { +func (t *scopedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Object, ns string, opts ...metav1.UpdateOptions) error { + _, err := assertOptionalSingleArgument(opts) + if err != nil { + return err + } + + return t.add(gvr, obj, ns, true) +} + +func (t *scopedTracker) Patch(gvr schema.GroupVersionResource, patchedObject runtime.Object, ns string, opts ...metav1.PatchOptions) error { + _, err := assertOptionalSingleArgument(opts) + if err != nil { + return err + } + + return t.add(gvr, patchedObject, ns, true) +} + +func (t *scopedTracker) Apply(gvr schema.GroupVersionResource, applyConfiguration runtime.Object, ns string, opts ...metav1.PatchOptions) error { + _, err := assertOptionalSingleArgument(opts) + if err != nil { + return err + } + applyConfigurationMeta, err := meta.Accessor(applyConfiguration) + if err != nil { + return err + } + + obj, err := t.Get(gvr, ns, applyConfigurationMeta.GetName(), metav1.GetOptions{}) + if err != nil { + return err + } + + old, err := json.Marshal(obj) + if err != nil { + return err + } + + // reset the object in preparation to unmarshal, since unmarshal does not guarantee that fields + // in obj that are removed by patch are cleared + value := reflect.ValueOf(obj) + value.Elem().Set(reflect.New(value.Type().Elem()).Elem()) + + // For backward compatibility with behavior 1.30 and earlier, continue to handle apply + // via strategic merge patch (clients may use fake.NewClientset and ManagedFieldObjectTracker + // for full field manager support). + patch, err := json.Marshal(applyConfiguration) + if err != nil { + return err + } + mergedByte, err := strategicpatch.StrategicMergePatch(old, patch, obj) + if err != nil { + return err + } + if err = json.Unmarshal(mergedByte, obj); err != nil { + return err + } + return t.add(gvr, obj, ns, true) } @@ -542,7 +720,7 @@ func (t *scopedTracker) add(gvr schema.GroupVersionResource, obj runtime.Object, if ns != newMeta.GetNamespace() { msg := fmt.Sprintf("request namespace does not match object namespace, request: %q object: %q", ns, newMeta.GetNamespace()) - return errors.NewBadRequest(msg) + return apierrors.NewBadRequest(msg) } _, ok := t.objects[gvr] @@ -565,12 +743,12 @@ func (t *scopedTracker) add(gvr schema.GroupVersionResource, obj runtime.Object, t.objects[gvr][namespacedName] = obj return nil } - return errors.NewAlreadyExists(gr, newMeta.GetName()) + return apierrors.NewAlreadyExists(gr, newMeta.GetName()) } if replaceExisting { // Tried to update but no matching object was found. - return errors.NewNotFound(gr, newMeta.GetName()) + return apierrors.NewNotFound(gr, newMeta.GetName()) } t.objects[gvr][namespacedName] = obj @@ -600,19 +778,24 @@ func (t *scopedTracker) addList(obj runtime.Object, replaceExisting bool) error return nil } -func (t *scopedTracker) Delete(gvr schema.GroupVersionResource, ns, name string) error { +func (t *scopedTracker) Delete(gvr schema.GroupVersionResource, ns, name string, opts ...metav1.DeleteOptions) error { + _, err := assertOptionalSingleArgument(opts) + if err != nil { + return err + } + t.lock.Lock() defer t.lock.Unlock() objs, ok := t.objects[gvr] if !ok { - return errors.NewNotFound(gvr.GroupResource(), name) + return apierrors.NewNotFound(gvr.GroupResource(), name) } namespacedName := ClusterNamespacedName{Cluster: t.clusterPath, NamespacedName: types.NamespacedName{Namespace: ns, Name: name}} obj, ok := objs[namespacedName] if !ok { - return errors.NewNotFound(gvr.GroupResource(), name) + return apierrors.NewNotFound(gvr.GroupResource(), name) } delete(objs, namespacedName) @@ -622,6 +805,223 @@ func (t *scopedTracker) Delete(gvr schema.GroupVersionResource, ns, name string) return nil } +type managedFieldObjectTracker struct { + ObjectTracker + scheme ObjectScheme + objectConverter runtime.ObjectConvertor + mapper meta.RESTMapper + typeConverter managedfields.TypeConverter +} + +var _ ObjectTracker = &managedFieldObjectTracker{} + +// NewFieldManagedObjectTracker returns an ObjectTracker that can be used to keep track +// of objects and managed fields for the fake clientset. Mostly useful for unit tests. +func NewFieldManagedObjectTracker(scheme *runtime.Scheme, decoder runtime.Decoder, typeConverter managedfields.TypeConverter) ObjectTracker { + return &managedFieldObjectTracker{ + ObjectTracker: NewObjectTracker(scheme, decoder), + scheme: scheme, + objectConverter: scheme, + mapper: testrestmapper.TestOnlyStaticRESTMapper(scheme), + typeConverter: typeConverter, + } +} + +func (t *managedFieldObjectTracker) Cluster(clusterPath logicalcluster.Path) ScopedObjectTracker { + return &scopedManagedFieldObjectTracker{ + ScopedObjectTracker: t.ObjectTracker.Cluster(clusterPath), + scheme: t.scheme, + objectConverter: t.objectConverter, + mapper: t.mapper, + typeConverter: t.typeConverter, + } +} + +type scopedManagedFieldObjectTracker struct { + ScopedObjectTracker + scheme ObjectScheme + objectConverter runtime.ObjectConvertor + mapper meta.RESTMapper + typeConverter managedfields.TypeConverter +} + +var _ ScopedObjectTracker = &scopedManagedFieldObjectTracker{} + +func (t *scopedManagedFieldObjectTracker) Create(gvr schema.GroupVersionResource, obj runtime.Object, ns string, vopts ...metav1.CreateOptions) error { + opts, err := assertOptionalSingleArgument(vopts) + if err != nil { + return err + } + gvk, err := t.mapper.KindFor(gvr) + if err != nil { + return err + } + mgr, err := t.fieldManagerFor(gvk) + if err != nil { + return err + } + + objType, err := meta.TypeAccessor(obj) + if err != nil { + return err + } + // Stamp GVK + apiVersion, kind := gvk.ToAPIVersionAndKind() + objType.SetAPIVersion(apiVersion) + objType.SetKind(kind) + + objMeta, err := meta.Accessor(obj) + if err != nil { + return err + } + liveObject, err := t.ScopedObjectTracker.Get(gvr, ns, objMeta.GetName(), metav1.GetOptions{}) + if apierrors.IsNotFound(err) { + liveObject, err = t.scheme.New(gvk) + if err != nil { + return err + } + liveObject.GetObjectKind().SetGroupVersionKind(gvk) + } else if err != nil { + return err + } + objWithManagedFields, err := mgr.Update(liveObject, obj, opts.FieldManager) + if err != nil { + return err + } + return t.ScopedObjectTracker.Create(gvr, objWithManagedFields, ns, opts) +} + +func (t *scopedManagedFieldObjectTracker) Update(gvr schema.GroupVersionResource, obj runtime.Object, ns string, vopts ...metav1.UpdateOptions) error { + opts, err := assertOptionalSingleArgument(vopts) + if err != nil { + return err + } + gvk, err := t.mapper.KindFor(gvr) + if err != nil { + return err + } + mgr, err := t.fieldManagerFor(gvk) + if err != nil { + return err + } + + objMeta, err := meta.Accessor(obj) + if err != nil { + return err + } + oldObj, err := t.ScopedObjectTracker.Get(gvr, ns, objMeta.GetName(), metav1.GetOptions{}) + if err != nil { + return err + } + objWithManagedFields, err := mgr.Update(oldObj, obj, opts.FieldManager) + if err != nil { + return err + } + + return t.ScopedObjectTracker.Update(gvr, objWithManagedFields, ns, opts) +} + +func (t *scopedManagedFieldObjectTracker) Patch(gvr schema.GroupVersionResource, patchedObject runtime.Object, ns string, vopts ...metav1.PatchOptions) error { + opts, err := assertOptionalSingleArgument(vopts) + if err != nil { + return err + } + gvk, err := t.mapper.KindFor(gvr) + if err != nil { + return err + } + mgr, err := t.fieldManagerFor(gvk) + if err != nil { + return err + } + + objMeta, err := meta.Accessor(patchedObject) + if err != nil { + return err + } + oldObj, err := t.ScopedObjectTracker.Get(gvr, ns, objMeta.GetName(), metav1.GetOptions{}) + if err != nil { + return err + } + objWithManagedFields, err := mgr.Update(oldObj, patchedObject, opts.FieldManager) + if err != nil { + return err + } + return t.ScopedObjectTracker.Patch(gvr, objWithManagedFields, ns, vopts...) +} + +func (t *scopedManagedFieldObjectTracker) Apply(gvr schema.GroupVersionResource, applyConfiguration runtime.Object, ns string, vopts ...metav1.PatchOptions) error { + opts, err := assertOptionalSingleArgument(vopts) + if err != nil { + return err + } + gvk, err := t.mapper.KindFor(gvr) + if err != nil { + return err + } + applyConfigurationMeta, err := meta.Accessor(applyConfiguration) + if err != nil { + return err + } + + exists := true + liveObject, err := t.ScopedObjectTracker.Get(gvr, ns, applyConfigurationMeta.GetName(), metav1.GetOptions{}) + if apierrors.IsNotFound(err) { + exists = false + liveObject, err = t.scheme.New(gvk) + if err != nil { + return err + } + liveObject.GetObjectKind().SetGroupVersionKind(gvk) + } else if err != nil { + return err + } + mgr, err := t.fieldManagerFor(gvk) + if err != nil { + return err + } + force := false + if opts.Force != nil { + force = *opts.Force + } + objWithManagedFields, err := mgr.Apply(liveObject, applyConfiguration, opts.FieldManager, force) + if err != nil { + return err + } + + if !exists { + return t.ScopedObjectTracker.Create(gvr, objWithManagedFields, ns, metav1.CreateOptions{ + DryRun: opts.DryRun, + FieldManager: opts.FieldManager, + FieldValidation: opts.FieldValidation, + }) + } else { + return t.ScopedObjectTracker.Update(gvr, objWithManagedFields, ns, metav1.UpdateOptions{ + DryRun: opts.DryRun, + FieldManager: opts.FieldManager, + FieldValidation: opts.FieldValidation, + }) + } +} + +func (t *scopedManagedFieldObjectTracker) fieldManagerFor(gvk schema.GroupVersionKind) (*managedfields.FieldManager, error) { + return managedfields.NewDefaultFieldManager( + t.typeConverter, + t.objectConverter, + &objectDefaulter{}, + t.scheme, + gvk, + gvk.GroupVersion(), + "", + nil, + ) +} + +// objectDefaulter implements runtime.Defaulter, but it actually does nothing. +type objectDefaulter struct{} + +func (d *objectDefaulter) Default(_ runtime.Object) {} + // filterByNamespace returns all objects in the collection that // match provided namespace. Empty namespace matches // non-namespaced objects. @@ -757,3 +1157,76 @@ func resourceCovers(resource string, action Action) bool { return false } + +// assertOptionalSingleArgument returns an error if there is more than one variadic argument. +// Otherwise, it returns the first variadic argument, or zero value if there are no arguments. +func assertOptionalSingleArgument[T any](arguments []T) (T, error) { + var a T + switch len(arguments) { + case 0: + return a, nil + case 1: + return arguments[0], nil + default: + return a, fmt.Errorf("expected only one option argument but got %d", len(arguments)) + } +} + +type TypeResolver interface { + Type(openAPIName string) typed.ParseableType +} + +type TypeConverter struct { + Scheme *runtime.Scheme + TypeResolver TypeResolver +} + +func (tc TypeConverter) ObjectToTyped(obj runtime.Object, opts ...typed.ValidationOptions) (*typed.TypedValue, error) { + gvk := obj.GetObjectKind().GroupVersionKind() + name, err := tc.openAPIName(gvk) + if err != nil { + return nil, err + } + t := tc.TypeResolver.Type(name) + switch o := obj.(type) { + case *unstructured.Unstructured: + return t.FromUnstructured(o.UnstructuredContent(), opts...) + default: + return t.FromStructured(obj, opts...) + } +} + +func (tc TypeConverter) TypedToObject(value *typed.TypedValue) (runtime.Object, error) { + vu := value.AsValue().Unstructured() + switch o := vu.(type) { + case map[string]interface{}: + return &unstructured.Unstructured{Object: o}, nil + default: + return nil, fmt.Errorf("failed to convert value to unstructured for type %T", vu) + } +} + +func (tc TypeConverter) openAPIName(kind schema.GroupVersionKind) (string, error) { + example, err := tc.Scheme.New(kind) + if err != nil { + return "", err + } + rtype := reflect.TypeOf(example).Elem() + name := friendlyName(rtype.PkgPath() + "." + rtype.Name()) + return name, nil +} + +// This is a copy of openapi.friendlyName. +// TODO: consider introducing a shared version of this function in apimachinery. +func friendlyName(name string) string { + nameParts := strings.Split(name, "/") + // Reverse first part. e.g., io.k8s... instead of k8s.io... + if len(nameParts) > 0 && strings.Contains(nameParts[0], ".") { + parts := strings.Split(nameParts[0], ".") + for i, j := 0, len(parts)-1; i < j; i, j = i+1, j-1 { + parts[i], parts[j] = parts[j], parts[i] + } + nameParts[0] = strings.Join(parts, ".") + } + return strings.Join(nameParts, ".") +}