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:
KubeSphere CI Bot
2024-09-06 11:05:52 +08:00
committed by GitHub
parent b5015ec7b9
commit 447a51f08b
8557 changed files with 546695 additions and 1146174 deletions

View File

@@ -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 {

View File

@@ -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.