feat: kubesphere 4.0 (#6115)
* feat: kubesphere 4.0 Signed-off-by: ci-bot <ci-bot@kubesphere.io> * feat: kubesphere 4.0 Signed-off-by: ci-bot <ci-bot@kubesphere.io> --------- Signed-off-by: ci-bot <ci-bot@kubesphere.io> Co-authored-by: ks-ci-bot <ks-ci-bot@example.com> Co-authored-by: joyceliu <joyceliu@yunify.com>
This commit is contained in:
committed by
GitHub
parent
b5015ec7b9
commit
447a51f08b
528
vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/client.go
generated
vendored
528
vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/client.go
generated
vendored
@@ -17,15 +17,23 @@ limitations under the License.
|
||||
package fake
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
// Using v4 to match upstream
|
||||
jsonpatch "github.com/evanphx/json-patch"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
policyv1 "k8s.io/api/policy/v1"
|
||||
policyv1beta1 "k8s.io/api/policy/v1beta1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -34,27 +42,33 @@ import (
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
utilrand "k8s.io/apimachinery/pkg/util/rand"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/testing"
|
||||
"sigs.k8s.io/controller-runtime/pkg/internal/field/selector"
|
||||
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
|
||||
"sigs.k8s.io/controller-runtime/pkg/internal/field/selector"
|
||||
"sigs.k8s.io/controller-runtime/pkg/internal/objectutil"
|
||||
)
|
||||
|
||||
type versionedTracker struct {
|
||||
testing.ObjectTracker
|
||||
scheme *runtime.Scheme
|
||||
scheme *runtime.Scheme
|
||||
withStatusSubresource sets.Set[schema.GroupVersionKind]
|
||||
}
|
||||
|
||||
type fakeClient struct {
|
||||
tracker versionedTracker
|
||||
scheme *runtime.Scheme
|
||||
restMapper meta.RESTMapper
|
||||
tracker versionedTracker
|
||||
scheme *runtime.Scheme
|
||||
restMapper meta.RESTMapper
|
||||
withStatusSubresource sets.Set[schema.GroupVersionKind]
|
||||
|
||||
// indexes maps each GroupVersionKind (GVK) to the indexes registered for that GVK.
|
||||
// The inner map maps from index name to IndexerFunc.
|
||||
@@ -73,21 +87,10 @@ const (
|
||||
|
||||
// NewFakeClient creates a new fake client for testing.
|
||||
// You can choose to initialize it with a slice of runtime.Object.
|
||||
//
|
||||
// Deprecated: Please use NewClientBuilder instead.
|
||||
func NewFakeClient(initObjs ...runtime.Object) client.WithWatch {
|
||||
return NewClientBuilder().WithRuntimeObjects(initObjs...).Build()
|
||||
}
|
||||
|
||||
// NewFakeClientWithScheme creates a new fake client with the given scheme
|
||||
// for testing.
|
||||
// You can choose to initialize it with a slice of runtime.Object.
|
||||
//
|
||||
// Deprecated: Please use NewClientBuilder instead.
|
||||
func NewFakeClientWithScheme(clientScheme *runtime.Scheme, initObjs ...runtime.Object) client.WithWatch {
|
||||
return NewClientBuilder().WithScheme(clientScheme).WithRuntimeObjects(initObjs...).Build()
|
||||
}
|
||||
|
||||
// NewClientBuilder returns a new builder to create a fake client.
|
||||
func NewClientBuilder() *ClientBuilder {
|
||||
return &ClientBuilder{}
|
||||
@@ -95,12 +98,14 @@ func NewClientBuilder() *ClientBuilder {
|
||||
|
||||
// ClientBuilder builds a fake client.
|
||||
type ClientBuilder struct {
|
||||
scheme *runtime.Scheme
|
||||
restMapper meta.RESTMapper
|
||||
initObject []client.Object
|
||||
initLists []client.ObjectList
|
||||
initRuntimeObjects []runtime.Object
|
||||
objectTracker testing.ObjectTracker
|
||||
scheme *runtime.Scheme
|
||||
restMapper meta.RESTMapper
|
||||
initObject []client.Object
|
||||
initLists []client.ObjectList
|
||||
initRuntimeObjects []runtime.Object
|
||||
withStatusSubresource []client.Object
|
||||
objectTracker testing.ObjectTracker
|
||||
interceptorFuncs *interceptor.Funcs
|
||||
|
||||
// indexes maps each GroupVersionKind (GVK) to the indexes registered for that GVK.
|
||||
// The inner map maps from index name to IndexerFunc.
|
||||
@@ -185,6 +190,19 @@ func (f *ClientBuilder) WithIndex(obj runtime.Object, field string, extractValue
|
||||
return f
|
||||
}
|
||||
|
||||
// WithStatusSubresource configures the passed object with a status subresource, which means
|
||||
// calls to Update and Patch will not alter its status.
|
||||
func (f *ClientBuilder) WithStatusSubresource(o ...client.Object) *ClientBuilder {
|
||||
f.withStatusSubresource = append(f.withStatusSubresource, o...)
|
||||
return f
|
||||
}
|
||||
|
||||
// WithInterceptorFuncs configures the client methods to be intercepted using the provided interceptor.Funcs.
|
||||
func (f *ClientBuilder) WithInterceptorFuncs(interceptorFuncs interceptor.Funcs) *ClientBuilder {
|
||||
f.interceptorFuncs = &interceptorFuncs
|
||||
return f
|
||||
}
|
||||
|
||||
// Build builds and returns a new fake client.
|
||||
func (f *ClientBuilder) Build() client.WithWatch {
|
||||
if f.scheme == nil {
|
||||
@@ -196,10 +214,19 @@ func (f *ClientBuilder) Build() client.WithWatch {
|
||||
|
||||
var tracker versionedTracker
|
||||
|
||||
withStatusSubResource := sets.New(inTreeResourcesWithStatus()...)
|
||||
for _, o := range f.withStatusSubresource {
|
||||
gvk, err := apiutil.GVKForObject(o, f.scheme)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to get gvk for object %T: %w", withStatusSubResource, err))
|
||||
}
|
||||
withStatusSubResource.Insert(gvk)
|
||||
}
|
||||
|
||||
if f.objectTracker == nil {
|
||||
tracker = versionedTracker{ObjectTracker: testing.NewObjectTracker(f.scheme, scheme.Codecs.UniversalDecoder()), scheme: f.scheme}
|
||||
tracker = versionedTracker{ObjectTracker: testing.NewObjectTracker(f.scheme, scheme.Codecs.UniversalDecoder()), scheme: f.scheme, withStatusSubresource: withStatusSubResource}
|
||||
} else {
|
||||
tracker = versionedTracker{ObjectTracker: f.objectTracker, scheme: f.scheme}
|
||||
tracker = versionedTracker{ObjectTracker: f.objectTracker, scheme: f.scheme, withStatusSubresource: withStatusSubResource}
|
||||
}
|
||||
|
||||
for _, obj := range f.initObject {
|
||||
@@ -217,12 +244,20 @@ func (f *ClientBuilder) Build() client.WithWatch {
|
||||
panic(fmt.Errorf("failed to add runtime object %v to fake client: %w", obj, err))
|
||||
}
|
||||
}
|
||||
return &fakeClient{
|
||||
tracker: tracker,
|
||||
scheme: f.scheme,
|
||||
restMapper: f.restMapper,
|
||||
indexes: f.indexes,
|
||||
|
||||
var result client.WithWatch = &fakeClient{
|
||||
tracker: tracker,
|
||||
scheme: f.scheme,
|
||||
restMapper: f.restMapper,
|
||||
indexes: f.indexes,
|
||||
withStatusSubresource: withStatusSubResource,
|
||||
}
|
||||
|
||||
if f.interceptorFuncs != nil {
|
||||
result = interceptor.NewClient(result, *f.interceptorFuncs)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const trackerAddResourceVersion = "999"
|
||||
@@ -243,6 +278,9 @@ func (t versionedTracker) Add(obj runtime.Object) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get accessor for object: %w", err)
|
||||
}
|
||||
if accessor.GetDeletionTimestamp() != nil && len(accessor.GetFinalizers()) == 0 {
|
||||
return fmt.Errorf("refusing to create obj %s with metadata.deletionTimestamp but no finalizers", accessor.GetName())
|
||||
}
|
||||
if accessor.GetResourceVersion() == "" {
|
||||
// We use a "magic" value of 999 here because this field
|
||||
// is parsed as uint and and 0 is already used in Update.
|
||||
@@ -290,20 +328,24 @@ func (t versionedTracker) Create(gvr schema.GroupVersionResource, obj runtime.Ob
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertFromUnstructuredIfNecessary will convert *unstructured.Unstructured for a GVK that is recocnized
|
||||
// convertFromUnstructuredIfNecessary will convert runtime.Unstructured for a GVK that is recognized
|
||||
// by the schema into the whatever the schema produces with New() for said GVK.
|
||||
// This is required because the tracker unconditionally saves on manipulations, but its List() implementation
|
||||
// tries to assign whatever it finds into a ListType it gets from schema.New() - Thus we have to ensure
|
||||
// we save as the very same type, otherwise subsequent List requests will fail.
|
||||
func convertFromUnstructuredIfNecessary(s *runtime.Scheme, o runtime.Object) (runtime.Object, error) {
|
||||
u, isUnstructured := o.(*unstructured.Unstructured)
|
||||
if !isUnstructured || !s.Recognizes(u.GroupVersionKind()) {
|
||||
u, isUnstructured := o.(runtime.Unstructured)
|
||||
if !isUnstructured {
|
||||
return o, nil
|
||||
}
|
||||
gvk := o.GetObjectKind().GroupVersionKind()
|
||||
if !s.Recognizes(gvk) {
|
||||
return o, nil
|
||||
}
|
||||
|
||||
typed, err := s.New(u.GroupVersionKind())
|
||||
typed, err := s.New(gvk)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scheme recognizes %s but failed to produce an object for it: %w", u.GroupVersionKind().String(), err)
|
||||
return nil, fmt.Errorf("scheme recognizes %s but failed to produce an object for it: %w", gvk, err)
|
||||
}
|
||||
|
||||
unstructuredSerialized, err := json.Marshal(u)
|
||||
@@ -318,6 +360,16 @@ func convertFromUnstructuredIfNecessary(s *runtime.Scheme, o runtime.Object) (ru
|
||||
}
|
||||
|
||||
func (t versionedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error {
|
||||
isStatus := false
|
||||
// We apply patches using a client-go reaction that ends up calling the trackers Update. As we can't change
|
||||
// that reaction, we use the callstack to figure out if this originated from the status client.
|
||||
if bytes.Contains(debug.Stack(), []byte("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeSubResourceClient).statusPatch")) {
|
||||
isStatus = true
|
||||
}
|
||||
return t.update(gvr, obj, ns, isStatus, false)
|
||||
}
|
||||
|
||||
func (t versionedTracker) update(gvr schema.GroupVersionResource, obj runtime.Object, ns string, isStatus bool, deleting bool) error {
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get accessor for object: %w", err)
|
||||
@@ -330,12 +382,9 @@ func (t versionedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Ob
|
||||
field.ErrorList{field.Required(field.NewPath("metadata.name"), "name is required")})
|
||||
}
|
||||
|
||||
gvk := obj.GetObjectKind().GroupVersionKind()
|
||||
if gvk.Empty() {
|
||||
gvk, err = apiutil.GVKForObject(obj, t.scheme)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gvk, err := apiutil.GVKForObject(obj, t.scheme)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
oldObject, err := t.ObjectTracker.Get(gvr, ns, accessor.GetName())
|
||||
@@ -348,6 +397,25 @@ func (t versionedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Ob
|
||||
return err
|
||||
}
|
||||
|
||||
if t.withStatusSubresource.Has(gvk) {
|
||||
if isStatus { // copy everything but status and metadata.ResourceVersion from original object
|
||||
if err := copyStatusFrom(obj, oldObject); err != nil {
|
||||
return fmt.Errorf("failed to copy non-status field for object with status subresouce: %w", err)
|
||||
}
|
||||
passedRV := accessor.GetResourceVersion()
|
||||
if err := copyFrom(oldObject, obj); err != nil {
|
||||
return fmt.Errorf("failed to restore non-status fields: %w", err)
|
||||
}
|
||||
accessor.SetResourceVersion(passedRV)
|
||||
} else { // copy status from original object
|
||||
if err := copyStatusFrom(oldObject, obj); err != nil {
|
||||
return fmt.Errorf("failed to copy the status for object with status subresource: %w", err)
|
||||
}
|
||||
}
|
||||
} else if isStatus {
|
||||
return apierrors.NewNotFound(gvr.GroupResource(), accessor.GetName())
|
||||
}
|
||||
|
||||
oldAccessor, err := meta.Accessor(oldObject)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -370,6 +438,11 @@ func (t versionedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Ob
|
||||
}
|
||||
intResourceVersion++
|
||||
accessor.SetResourceVersion(strconv.FormatUint(intResourceVersion, 10))
|
||||
|
||||
if !deleting && !deletionTimestampEqual(accessor, oldAccessor) {
|
||||
return fmt.Errorf("error: Unable to edit %s: metadata.deletionTimestamp field is immutable", accessor.GetName())
|
||||
}
|
||||
|
||||
if !accessor.GetDeletionTimestamp().IsZero() && len(accessor.GetFinalizers()) == 0 {
|
||||
return t.ObjectTracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName())
|
||||
}
|
||||
@@ -390,25 +463,25 @@ func (c *fakeClient) Get(ctx context.Context, key client.ObjectKey, obj client.O
|
||||
return err
|
||||
}
|
||||
|
||||
gvk, err := apiutil.GVKForObject(obj, c.scheme)
|
||||
if err != nil {
|
||||
return err
|
||||
if _, isUnstructured := obj.(runtime.Unstructured); isUnstructured {
|
||||
gvk, err := apiutil.GVKForObject(obj, c.scheme)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ta, err := meta.TypeAccessor(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ta.SetKind(gvk.Kind)
|
||||
ta.SetAPIVersion(gvk.GroupVersion().String())
|
||||
}
|
||||
ta, err := meta.TypeAccessor(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ta.SetKind(gvk.Kind)
|
||||
ta.SetAPIVersion(gvk.GroupVersion().String())
|
||||
|
||||
j, err := json.Marshal(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decoder := scheme.Codecs.UniversalDecoder()
|
||||
zero(obj)
|
||||
_, _, err = decoder.Decode(j, nil, obj)
|
||||
return err
|
||||
return json.Unmarshal(j, obj)
|
||||
}
|
||||
|
||||
func (c *fakeClient) Watch(ctx context.Context, list client.ObjectList, opts ...client.ListOption) (watch.Interface, error) {
|
||||
@@ -436,7 +509,7 @@ func (c *fakeClient) List(ctx context.Context, obj client.ObjectList, opts ...cl
|
||||
|
||||
gvk.Kind = strings.TrimSuffix(gvk.Kind, "List")
|
||||
|
||||
if _, isUnstructuredList := obj.(*unstructured.UnstructuredList); isUnstructuredList && !c.scheme.Recognizes(gvk) {
|
||||
if _, isUnstructuredList := obj.(runtime.Unstructured); isUnstructuredList && !c.scheme.Recognizes(gvk) {
|
||||
// We need to register the ListKind with UnstructuredList:
|
||||
// https://github.com/kubernetes/kubernetes/blob/7b2776b89fb1be28d4e9203bdeec079be903c103/staging/src/k8s.io/client-go/dynamic/fake/simple.go#L44-L51
|
||||
c.schemeWriteLock.Lock()
|
||||
@@ -453,21 +526,21 @@ func (c *fakeClient) List(ctx context.Context, obj client.ObjectList, opts ...cl
|
||||
return err
|
||||
}
|
||||
|
||||
ta, err := meta.TypeAccessor(o)
|
||||
if err != nil {
|
||||
return err
|
||||
if _, isUnstructured := obj.(runtime.Unstructured); isUnstructured {
|
||||
ta, err := meta.TypeAccessor(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ta.SetKind(originalKind)
|
||||
ta.SetAPIVersion(gvk.GroupVersion().String())
|
||||
}
|
||||
ta.SetKind(originalKind)
|
||||
ta.SetAPIVersion(gvk.GroupVersion().String())
|
||||
|
||||
j, err := json.Marshal(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decoder := scheme.Codecs.UniversalDecoder()
|
||||
zero(obj)
|
||||
_, _, err = decoder.Decode(j, nil, obj)
|
||||
if err != nil {
|
||||
if err := json.Unmarshal(j, obj); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -514,9 +587,7 @@ func (c *fakeClient) filterList(list []runtime.Object, gvk schema.GroupVersionKi
|
||||
}
|
||||
|
||||
func (c *fakeClient) filterWithFields(list []runtime.Object, gvk schema.GroupVersionKind, fs fields.Selector) ([]runtime.Object, error) {
|
||||
// We only allow filtering on the basis of a single field to ensure consistency with the
|
||||
// behavior of the cache reader (which we're faking here).
|
||||
fieldKey, fieldVal, requiresExact := selector.RequiresExactMatch(fs)
|
||||
requiresExact := selector.RequiresExactMatch(fs)
|
||||
if !requiresExact {
|
||||
return nil, fmt.Errorf("field selector %s is not in one of the two supported forms \"key==val\" or \"key=val\"",
|
||||
fs)
|
||||
@@ -525,15 +596,24 @@ func (c *fakeClient) filterWithFields(list []runtime.Object, gvk schema.GroupVer
|
||||
// Field selection is mimicked via indexes, so there's no sane answer this function can give
|
||||
// if there are no indexes registered for the GroupVersionKind of the objects in the list.
|
||||
indexes := c.indexes[gvk]
|
||||
if len(indexes) == 0 || indexes[fieldKey] == nil {
|
||||
return nil, fmt.Errorf("List on GroupVersionKind %v specifies selector on field %s, but no "+
|
||||
"index with name %s has been registered for GroupVersionKind %v", gvk, fieldKey, fieldKey, gvk)
|
||||
for _, req := range fs.Requirements() {
|
||||
if len(indexes) == 0 || indexes[req.Field] == nil {
|
||||
return nil, fmt.Errorf("List on GroupVersionKind %v specifies selector on field %s, but no "+
|
||||
"index with name %s has been registered for GroupVersionKind %v", gvk, req.Field, req.Field, gvk)
|
||||
}
|
||||
}
|
||||
|
||||
indexExtractor := indexes[fieldKey]
|
||||
filteredList := make([]runtime.Object, 0, len(list))
|
||||
for _, obj := range list {
|
||||
if c.objMatchesFieldSelector(obj, indexExtractor, fieldVal) {
|
||||
matches := true
|
||||
for _, req := range fs.Requirements() {
|
||||
indexExtractor := indexes[req.Field]
|
||||
if !c.objMatchesFieldSelector(obj, indexExtractor, req.Value) {
|
||||
matches = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if matches {
|
||||
filteredList = append(filteredList, obj)
|
||||
}
|
||||
}
|
||||
@@ -563,6 +643,16 @@ func (c *fakeClient) RESTMapper() meta.RESTMapper {
|
||||
return c.restMapper
|
||||
}
|
||||
|
||||
// GroupVersionKindFor returns the GroupVersionKind for the given object.
|
||||
func (c *fakeClient) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) {
|
||||
return apiutil.GVKForObject(obj, c.scheme)
|
||||
}
|
||||
|
||||
// IsObjectNamespaced returns true if the GroupVersionKind of the object is namespaced.
|
||||
func (c *fakeClient) IsObjectNamespaced(obj runtime.Object) (bool, error) {
|
||||
return apiutil.IsObjectNamespaced(obj, c.scheme, c.restMapper)
|
||||
}
|
||||
|
||||
func (c *fakeClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
|
||||
createOptions := &client.CreateOptions{}
|
||||
createOptions.ApplyOptions(opts)
|
||||
@@ -589,6 +679,10 @@ func (c *fakeClient) Create(ctx context.Context, obj client.Object, opts ...clie
|
||||
}
|
||||
accessor.SetName(fmt.Sprintf("%s%s", base, utilrand.String(randomLength)))
|
||||
}
|
||||
// Ignore attempts to set deletion timestamp
|
||||
if !accessor.GetDeletionTimestamp().IsZero() {
|
||||
accessor.SetDeletionTimestamp(nil)
|
||||
}
|
||||
|
||||
return c.tracker.Create(gvr, obj, accessor.GetNamespace())
|
||||
}
|
||||
@@ -679,6 +773,10 @@ func (c *fakeClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ..
|
||||
}
|
||||
|
||||
func (c *fakeClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
|
||||
return c.update(obj, false, opts...)
|
||||
}
|
||||
|
||||
func (c *fakeClient) update(obj client.Object, isStatus bool, opts ...client.UpdateOption) error {
|
||||
updateOptions := &client.UpdateOptions{}
|
||||
updateOptions.ApplyOptions(opts)
|
||||
|
||||
@@ -696,10 +794,14 @@ func (c *fakeClient) Update(ctx context.Context, obj client.Object, opts ...clie
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.tracker.Update(gvr, obj, accessor.GetNamespace())
|
||||
return c.tracker.update(gvr, obj, accessor.GetNamespace(), isStatus, false)
|
||||
}
|
||||
|
||||
func (c *fakeClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
|
||||
return c.patch(obj, patch, opts...)
|
||||
}
|
||||
|
||||
func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
|
||||
patchOptions := &client.PatchOptions{}
|
||||
patchOptions.ApplyOptions(opts)
|
||||
|
||||
@@ -722,8 +824,44 @@ func (c *fakeClient) Patch(ctx context.Context, obj client.Object, patch client.
|
||||
return err
|
||||
}
|
||||
|
||||
gvk, err := apiutil.GVKForObject(obj, c.scheme)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
oldObj, err := c.tracker.Get(gvr, accessor.GetNamespace(), accessor.GetName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
oldAccessor, err := meta.Accessor(oldObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Apply patch without updating object.
|
||||
// To remain in accordance with the behavior of k8s api behavior,
|
||||
// a patch must not allow for changes to the deletionTimestamp of an object.
|
||||
// The reaction() function applies the patch to the object and calls Update(),
|
||||
// whereas dryPatch() replicates this behavior but skips the call to Update().
|
||||
// This ensures that the patch may be rejected if a deletionTimestamp is modified, prior
|
||||
// to updating the object.
|
||||
action := testing.NewPatchAction(gvr, accessor.GetNamespace(), accessor.GetName(), patch.Type(), data)
|
||||
o, err := dryPatch(action, c.tracker)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newObj, err := meta.Accessor(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate that deletionTimestamp has not been changed
|
||||
if !deletionTimestampEqual(newObj, oldAccessor) {
|
||||
return fmt.Errorf("rejected patch, metadata.deletionTimestamp immutable")
|
||||
}
|
||||
|
||||
reaction := testing.ObjectReaction(c.tracker)
|
||||
handled, o, err := reaction(testing.NewPatchAction(gvr, accessor.GetNamespace(), accessor.GetName(), patch.Type(), data))
|
||||
handled, o, err := reaction(action)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -731,25 +869,164 @@ func (c *fakeClient) Patch(ctx context.Context, obj client.Object, patch client.
|
||||
panic("tracker could not handle patch method")
|
||||
}
|
||||
|
||||
gvk, err := apiutil.GVKForObject(obj, c.scheme)
|
||||
if err != nil {
|
||||
return err
|
||||
if _, isUnstructured := obj.(runtime.Unstructured); isUnstructured {
|
||||
ta, err := meta.TypeAccessor(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ta.SetKind(gvk.Kind)
|
||||
ta.SetAPIVersion(gvk.GroupVersion().String())
|
||||
}
|
||||
ta, err := meta.TypeAccessor(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ta.SetKind(gvk.Kind)
|
||||
ta.SetAPIVersion(gvk.GroupVersion().String())
|
||||
|
||||
j, err := json.Marshal(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decoder := scheme.Codecs.UniversalDecoder()
|
||||
zero(obj)
|
||||
_, _, err = decoder.Decode(j, nil, obj)
|
||||
return err
|
||||
return json.Unmarshal(j, obj)
|
||||
}
|
||||
|
||||
// Applying a patch results in a deletionTimestamp that is truncated to the nearest second.
|
||||
// Check that the diff between a new and old deletion timestamp is within a reasonable threshold
|
||||
// to be considered unchanged.
|
||||
func deletionTimestampEqual(newObj metav1.Object, obj metav1.Object) bool {
|
||||
newTime := newObj.GetDeletionTimestamp()
|
||||
oldTime := obj.GetDeletionTimestamp()
|
||||
|
||||
if newTime == nil || oldTime == nil {
|
||||
return newTime == oldTime
|
||||
}
|
||||
return newTime.Time.Sub(oldTime.Time).Abs() < time.Second
|
||||
}
|
||||
|
||||
// The behavior of applying the patch is pulled out into dryPatch(),
|
||||
// which applies the patch and returns an object, but does not Update() the object.
|
||||
// This function returns a patched runtime object that may then be validated before a call to Update() is executed.
|
||||
// This results in some code duplication, but was found to be a cleaner alternative than unmarshalling and introspecting the patch data
|
||||
// and easier than refactoring the k8s client-go method upstream.
|
||||
// Duplicate of upstream: https://github.com/kubernetes/client-go/blob/783d0d33626e59d55d52bfd7696b775851f92107/testing/fixture.go#L146-L194
|
||||
func dryPatch(action testing.PatchActionImpl, tracker testing.ObjectTracker) (runtime.Object, error) {
|
||||
ns := action.GetNamespace()
|
||||
gvr := action.GetResource()
|
||||
|
||||
obj, err := tracker.Get(gvr, ns, action.GetName())
|
||||
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
|
||||
}
|
||||
case types.ApplyPatchType:
|
||||
return nil, errors.New("apply patches are not supported in the fake client. Follow https://github.com/kubernetes/kubernetes/issues/115598 for the current status")
|
||||
default:
|
||||
return nil, fmt.Errorf("%s PatchType is not supported", action.GetPatchType())
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
// copyStatusFrom copies the status from old into new
|
||||
func copyStatusFrom(old, new runtime.Object) error {
|
||||
oldMapStringAny, err := toMapStringAny(old)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert old to *unstructured.Unstructured: %w", err)
|
||||
}
|
||||
newMapStringAny, err := toMapStringAny(new)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert new to *unststructured.Unstructured: %w", err)
|
||||
}
|
||||
|
||||
newMapStringAny["status"] = oldMapStringAny["status"]
|
||||
|
||||
if err := fromMapStringAny(newMapStringAny, new); err != nil {
|
||||
return fmt.Errorf("failed to convert back from map[string]any: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// copyFrom copies from old into new
|
||||
func copyFrom(old, new runtime.Object) error {
|
||||
oldMapStringAny, err := toMapStringAny(old)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert old to *unstructured.Unstructured: %w", err)
|
||||
}
|
||||
if err := fromMapStringAny(oldMapStringAny, new); err != nil {
|
||||
return fmt.Errorf("failed to convert back from map[string]any: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func toMapStringAny(obj runtime.Object) (map[string]any, error) {
|
||||
if unstructured, isUnstructured := obj.(*unstructured.Unstructured); isUnstructured {
|
||||
return unstructured.Object, nil
|
||||
}
|
||||
|
||||
serialized, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u := map[string]any{}
|
||||
return u, json.Unmarshal(serialized, &u)
|
||||
}
|
||||
|
||||
func fromMapStringAny(u map[string]any, target runtime.Object) error {
|
||||
if targetUnstructured, isUnstructured := target.(*unstructured.Unstructured); isUnstructured {
|
||||
targetUnstructured.Object = u
|
||||
return nil
|
||||
}
|
||||
|
||||
serialized, err := json.Marshal(u)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to serialize: %w", err)
|
||||
}
|
||||
|
||||
zero(target)
|
||||
if err := json.Unmarshal(serialized, &target); err != nil {
|
||||
return fmt.Errorf("failed to deserialize: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) Status() client.SubResourceWriter {
|
||||
@@ -757,7 +1034,7 @@ func (c *fakeClient) Status() client.SubResourceWriter {
|
||||
}
|
||||
|
||||
func (c *fakeClient) SubResource(subResource string) client.SubResourceClient {
|
||||
return &fakeSubResourceClient{client: c}
|
||||
return &fakeSubResourceClient{client: c, subResource: subResource}
|
||||
}
|
||||
|
||||
func (c *fakeClient) deleteObject(gvr schema.GroupVersionResource, accessor metav1.Object) error {
|
||||
@@ -768,7 +1045,9 @@ func (c *fakeClient) deleteObject(gvr schema.GroupVersionResource, accessor meta
|
||||
if len(oldAccessor.GetFinalizers()) > 0 {
|
||||
now := metav1.Now()
|
||||
oldAccessor.SetDeletionTimestamp(&now)
|
||||
return c.tracker.Update(gvr, old, accessor.GetNamespace())
|
||||
// Call update directly with mutability parameter set to true to allow
|
||||
// changes to deletionTimestamp
|
||||
return c.tracker.update(gvr, old, accessor.GetNamespace(), false, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -787,7 +1066,8 @@ func getGVRFromObject(obj runtime.Object, scheme *runtime.Scheme) (schema.GroupV
|
||||
}
|
||||
|
||||
type fakeSubResourceClient struct {
|
||||
client *fakeClient
|
||||
client *fakeClient
|
||||
subResource string
|
||||
}
|
||||
|
||||
func (sw *fakeSubResourceClient) Get(ctx context.Context, obj, subResource client.Object, opts ...client.SubResourceGetOption) error {
|
||||
@@ -795,12 +1075,26 @@ func (sw *fakeSubResourceClient) Get(ctx context.Context, obj, subResource clien
|
||||
}
|
||||
|
||||
func (sw *fakeSubResourceClient) Create(ctx context.Context, obj client.Object, subResource client.Object, opts ...client.SubResourceCreateOption) error {
|
||||
panic("fakeSubResourceWriter does not support create")
|
||||
switch sw.subResource {
|
||||
case "eviction":
|
||||
_, isEviction := subResource.(*policyv1beta1.Eviction)
|
||||
if !isEviction {
|
||||
_, isEviction = subResource.(*policyv1.Eviction)
|
||||
}
|
||||
if !isEviction {
|
||||
return apierrors.NewBadRequest(fmt.Sprintf("got invalid type %t, expected Eviction", subResource))
|
||||
}
|
||||
if _, isPod := obj.(*corev1.Pod); !isPod {
|
||||
return apierrors.NewNotFound(schema.GroupResource{}, "")
|
||||
}
|
||||
|
||||
return sw.client.Delete(ctx, obj)
|
||||
default:
|
||||
return fmt.Errorf("fakeSubResourceWriter does not support create for %s", sw.subResource)
|
||||
}
|
||||
}
|
||||
|
||||
func (sw *fakeSubResourceClient) Update(ctx context.Context, obj client.Object, opts ...client.SubResourceUpdateOption) error {
|
||||
// TODO(droot): This results in full update of the obj (spec + subresources). Need
|
||||
// a way to update subresource only.
|
||||
updateOptions := client.SubResourceUpdateOptions{}
|
||||
updateOptions.ApplyOptions(opts)
|
||||
|
||||
@@ -808,13 +1102,10 @@ func (sw *fakeSubResourceClient) Update(ctx context.Context, obj client.Object,
|
||||
if updateOptions.SubResourceBody != nil {
|
||||
body = updateOptions.SubResourceBody
|
||||
}
|
||||
return sw.client.Update(ctx, body, &updateOptions.UpdateOptions)
|
||||
return sw.client.update(body, true, &updateOptions.UpdateOptions)
|
||||
}
|
||||
|
||||
func (sw *fakeSubResourceClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.SubResourcePatchOption) error {
|
||||
// TODO(droot): This results in full update of the obj (spec + subresources). Need
|
||||
// a way to update subresource only.
|
||||
|
||||
patchOptions := client.SubResourcePatchOptions{}
|
||||
patchOptions.ApplyOptions(opts)
|
||||
|
||||
@@ -823,7 +1114,16 @@ func (sw *fakeSubResourceClient) Patch(ctx context.Context, obj client.Object, p
|
||||
body = patchOptions.SubResourceBody
|
||||
}
|
||||
|
||||
return sw.client.Patch(ctx, body, patch, &patchOptions.PatchOptions)
|
||||
// this is necessary to identify that last call was made for status patch, through stack trace.
|
||||
if sw.subResource == "status" {
|
||||
return sw.statusPatch(body, patch, patchOptions)
|
||||
}
|
||||
|
||||
return sw.client.patch(body, patch, &patchOptions.PatchOptions)
|
||||
}
|
||||
|
||||
func (sw *fakeSubResourceClient) statusPatch(body client.Object, patch client.Patch, patchOptions client.SubResourcePatchOptions) error {
|
||||
return sw.client.patch(body, patch, &patchOptions.PatchOptions)
|
||||
}
|
||||
|
||||
func allowsUnconditionalUpdate(gvk schema.GroupVersionKind) bool {
|
||||
@@ -863,7 +1163,7 @@ func allowsUnconditionalUpdate(gvk schema.GroupVersionKind) bool {
|
||||
case "PodSecurityPolicy":
|
||||
return true
|
||||
}
|
||||
case "rbac":
|
||||
case "rbac.authorization.k8s.io":
|
||||
switch gvk.Kind {
|
||||
case "ClusterRole", "ClusterRoleBinding", "Role", "RoleBinding":
|
||||
return true
|
||||
@@ -923,6 +1223,44 @@ func allowsCreateOnUpdate(gvk schema.GroupVersionKind) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func inTreeResourcesWithStatus() []schema.GroupVersionKind {
|
||||
return []schema.GroupVersionKind{
|
||||
{Version: "v1", Kind: "Namespace"},
|
||||
{Version: "v1", Kind: "Node"},
|
||||
{Version: "v1", Kind: "PersistentVolumeClaim"},
|
||||
{Version: "v1", Kind: "PersistentVolume"},
|
||||
{Version: "v1", Kind: "Pod"},
|
||||
{Version: "v1", Kind: "ReplicationController"},
|
||||
{Version: "v1", Kind: "Service"},
|
||||
|
||||
{Group: "apps", Version: "v1", Kind: "Deployment"},
|
||||
{Group: "apps", Version: "v1", Kind: "DaemonSet"},
|
||||
{Group: "apps", Version: "v1", Kind: "ReplicaSet"},
|
||||
{Group: "apps", Version: "v1", Kind: "StatefulSet"},
|
||||
|
||||
{Group: "autoscaling", Version: "v1", Kind: "HorizontalPodAutoscaler"},
|
||||
|
||||
{Group: "batch", Version: "v1", Kind: "CronJob"},
|
||||
{Group: "batch", Version: "v1", Kind: "Job"},
|
||||
|
||||
{Group: "certificates.k8s.io", Version: "v1", Kind: "CertificateSigningRequest"},
|
||||
|
||||
{Group: "networking.k8s.io", Version: "v1", Kind: "Ingress"},
|
||||
{Group: "networking.k8s.io", Version: "v1", Kind: "NetworkPolicy"},
|
||||
|
||||
{Group: "policy", Version: "v1", Kind: "PodDisruptionBudget"},
|
||||
|
||||
{Group: "storage.k8s.io", Version: "v1", Kind: "VolumeAttachment"},
|
||||
|
||||
{Group: "apiextensions.k8s.io", Version: "v1", Kind: "CustomResourceDefinition"},
|
||||
|
||||
{Group: "flowcontrol.apiserver.k8s.io", Version: "v1beta2", Kind: "FlowSchema"},
|
||||
{Group: "flowcontrol.apiserver.k8s.io", Version: "v1beta2", Kind: "PriorityLevelConfiguration"},
|
||||
{Group: "flowcontrol.apiserver.k8s.io", Version: "v1", Kind: "FlowSchema"},
|
||||
{Group: "flowcontrol.apiserver.k8s.io", Version: "v1", Kind: "PriorityLevelConfiguration"},
|
||||
}
|
||||
}
|
||||
|
||||
// zero zeros the value of a pointer.
|
||||
func zero(x interface{}) {
|
||||
if x == nil {
|
||||
|
||||
2
vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/doc.go
generated
vendored
2
vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/doc.go
generated
vendored
@@ -20,7 +20,7 @@ Package fake provides a fake client for testing.
|
||||
A fake client is backed by its simple object store indexed by GroupVersionResource.
|
||||
You can create a fake client with optional objects.
|
||||
|
||||
client := NewFakeClientWithScheme(scheme, initObjs...) // initObjs is a slice of runtime.Object
|
||||
client := NewClientBuilder().WithScheme(scheme).WithObj(initObjs...).Build()
|
||||
|
||||
You can invoke the methods defined in the Client interface.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user