Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add scale subresource logic to fake client #2855

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 169 additions & 5 deletions pkg/client/fake/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import (

// Using v4 to match upstream
jsonpatch "gopkg.in/evanphx/json-patch.v4"
appsv1 "k8s.io/api/apps/v1"
autoscalingv1 "k8s.io/api/autoscaling/v1"
corev1 "k8s.io/api/core/v1"
policyv1 "k8s.io/api/policy/v1"
policyv1beta1 "k8s.io/api/policy/v1beta1"
Expand All @@ -50,6 +52,7 @@ import (
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/testing"
"k8s.io/utils/ptr"

"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
Expand Down Expand Up @@ -83,6 +86,8 @@ const (
maxNameLength = 63
randomLength = 5
maxGeneratedNameLength = maxNameLength - randomLength

subResourceScale = "scale"
)

// NewFakeClient creates a new fake client for testing.
Expand Down Expand Up @@ -1111,7 +1116,26 @@ type fakeSubResourceClient struct {
}

func (sw *fakeSubResourceClient) Get(ctx context.Context, obj, subResource client.Object, opts ...client.SubResourceGetOption) error {
panic("fakeSubResourceClient does not support get")
switch sw.subResource {
case subResourceScale:
// Actual client looks up resource, then extracts the scale sub-resource:
// https://github.com/kubernetes/kubernetes/blob/fb6bbc9781d11a87688c398778525c4e1dcb0f08/pkg/registry/apps/deployment/storage/storage.go#L307
if err := sw.client.Get(ctx, client.ObjectKeyFromObject(obj), obj); err != nil {
return err
}
scale, isScale := subResource.(*autoscalingv1.Scale)
if !isScale {
return apierrors.NewBadRequest(fmt.Sprintf("expected Scale, got %t", subResource))
}
scaleOut, err := extractScale(obj)
if err != nil {
return err
}
*scale = *scaleOut
return nil
default:
return fmt.Errorf("fakeSubResourceClient does not support get for %s", sw.subResource)
}
}

func (sw *fakeSubResourceClient) Create(ctx context.Context, obj client.Object, subResource client.Object, opts ...client.SubResourceCreateOption) error {
Expand All @@ -1138,11 +1162,30 @@ func (sw *fakeSubResourceClient) Update(ctx context.Context, obj client.Object,
updateOptions := client.SubResourceUpdateOptions{}
updateOptions.ApplyOptions(opts)

body := obj
if updateOptions.SubResourceBody != nil {
body = updateOptions.SubResourceBody
switch sw.subResource {
case subResourceScale:
if err := sw.client.Get(ctx, client.ObjectKeyFromObject(obj), obj); err != nil {
return err
}
if updateOptions.SubResourceBody == nil {
return apierrors.NewBadRequest("missing SubResourceBody")
}

scale, isScale := updateOptions.SubResourceBody.(*autoscalingv1.Scale)
if !isScale {
return apierrors.NewBadRequest(fmt.Sprintf("expected Scale, got %t", updateOptions.SubResourceBody))
}
if err := applyScale(obj, scale); err != nil {
return err
}
return sw.client.update(obj, false, &updateOptions.UpdateOptions)
default:
body := obj
if updateOptions.SubResourceBody != nil {
body = updateOptions.SubResourceBody
}
return sw.client.update(body, true, &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 {
Expand Down Expand Up @@ -1323,3 +1366,124 @@ func getSingleOrZeroOptions[T any](opts []T) (opt T, err error) {
}
return
}

func extractScale(obj client.Object) (*autoscalingv1.Scale, error) {
switch obj := obj.(type) {
case *appsv1.Deployment:
alvaroaleman marked this conversation as resolved.
Show resolved Hide resolved
var replicas int32 = 1
if obj.Spec.Replicas != nil {
replicas = *obj.Spec.Replicas
}
var selector string
if obj.Spec.Selector != nil {
selector = obj.Spec.Selector.String()
}
return &autoscalingv1.Scale{
ObjectMeta: metav1.ObjectMeta{
Namespace: obj.Namespace,
Name: obj.Name,
UID: obj.UID,
ResourceVersion: obj.ResourceVersion,
CreationTimestamp: obj.CreationTimestamp,
},
Spec: autoscalingv1.ScaleSpec{
Replicas: replicas,
},
Status: autoscalingv1.ScaleStatus{
Replicas: obj.Status.Replicas,
Selector: selector,
},
}, nil
case *appsv1.ReplicaSet:
var replicas int32 = 1
if obj.Spec.Replicas != nil {
replicas = *obj.Spec.Replicas
}
var selector string
if obj.Spec.Selector != nil {
selector = obj.Spec.Selector.String()
}
return &autoscalingv1.Scale{
ObjectMeta: metav1.ObjectMeta{
Namespace: obj.Namespace,
Name: obj.Name,
UID: obj.UID,
ResourceVersion: obj.ResourceVersion,
CreationTimestamp: obj.CreationTimestamp,
},
Spec: autoscalingv1.ScaleSpec{
Replicas: replicas,
},
Status: autoscalingv1.ScaleStatus{
Replicas: obj.Status.Replicas,
Selector: selector,
},
}, nil
case *corev1.ReplicationController:
var replicas int32 = 1
if obj.Spec.Replicas != nil {
replicas = *obj.Spec.Replicas
}
return &autoscalingv1.Scale{
ObjectMeta: metav1.ObjectMeta{
Namespace: obj.Namespace,
Name: obj.Name,
UID: obj.UID,
ResourceVersion: obj.ResourceVersion,
CreationTimestamp: obj.CreationTimestamp,
},
Spec: autoscalingv1.ScaleSpec{
Replicas: replicas,
},
Status: autoscalingv1.ScaleStatus{
Replicas: obj.Status.Replicas,
Selector: labels.Set(obj.Spec.Selector).String(),
},
}, nil
case *appsv1.StatefulSet:
var replicas int32 = 1
if obj.Spec.Replicas != nil {
replicas = *obj.Spec.Replicas
}
var selector string
if obj.Spec.Selector != nil {
selector = obj.Spec.Selector.String()
}
return &autoscalingv1.Scale{
ObjectMeta: metav1.ObjectMeta{
Namespace: obj.Namespace,
Name: obj.Name,
UID: obj.UID,
ResourceVersion: obj.ResourceVersion,
CreationTimestamp: obj.CreationTimestamp,
},
Spec: autoscalingv1.ScaleSpec{
Replicas: replicas,
},
Status: autoscalingv1.ScaleStatus{
Replicas: obj.Status.Replicas,
Selector: selector,
},
}, nil
default:
// TODO: CRDs https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#scale-subresource
return nil, fmt.Errorf("unimplemented scale subresource for resource %T", obj)
}
}

func applyScale(obj client.Object, scale *autoscalingv1.Scale) error {
switch obj := obj.(type) {
case *appsv1.Deployment:
obj.Spec.Replicas = ptr.To(scale.Spec.Replicas)
case *appsv1.ReplicaSet:
obj.Spec.Replicas = ptr.To(scale.Spec.Replicas)
case *corev1.ReplicationController:
obj.Spec.Replicas = ptr.To(scale.Spec.Replicas)
case *appsv1.StatefulSet:
obj.Spec.Replicas = ptr.To(scale.Spec.Replicas)
default:
// TODO: CRDs https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#scale-subresource
return fmt.Errorf("unimplemented scale subresource for resource %T", obj)
}
return nil
}
127 changes: 127 additions & 0 deletions pkg/client/fake/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
appsv1 "k8s.io/api/apps/v1"
autoscalingv1 "k8s.io/api/autoscaling/v1"
coordinationv1 "k8s.io/api/coordination/v1"
corev1 "k8s.io/api/core/v1"
policyv1 "k8s.io/api/policy/v1"
policyv1beta1 "k8s.io/api/policy/v1beta1"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
Expand Down Expand Up @@ -2068,6 +2070,131 @@ var _ = Describe("Fake client", func() {
err := cl.Get(context.Background(), client.ObjectKey{Name: "foo"}, obj)
Expect(apierrors.IsNotFound(err)).To(BeTrue())
})

It("disallows scale subresources on unsupported built-in types", func() {
scheme := runtime.NewScheme()
Expect(corev1.AddToScheme(scheme)).To(Succeed())
Expect(apiextensions.AddToScheme(scheme)).To(Succeed())

obj := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
}
cl := NewClientBuilder().WithScheme(scheme).WithObjects(obj).Build()

scale := &autoscalingv1.Scale{Spec: autoscalingv1.ScaleSpec{Replicas: 2}}
expectedErr := "unimplemented scale subresource for resource *v1.Pod"
Expect(cl.SubResource(subResourceScale).Get(context.Background(), obj, scale).Error()).To(Equal(expectedErr))
Expect(cl.SubResource(subResourceScale).Update(context.Background(), obj, client.WithSubResourceBody(scale)).Error()).To(Equal(expectedErr))
})

It("disallows scale subresources on non-existing objects", func() {
obj := &appsv1.Deployment{
TheSpiritXIII marked this conversation as resolved.
Show resolved Hide resolved
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: appsv1.DeploymentSpec{
Replicas: ptr.To[int32](2),
},
}
cl := NewClientBuilder().Build()

scale := &autoscalingv1.Scale{Spec: autoscalingv1.ScaleSpec{Replicas: 2}}
expectedErr := "deployments.apps \"foo\" not found"
Expect(cl.SubResource(subResourceScale).Get(context.Background(), obj, scale).Error()).To(Equal(expectedErr))
Expect(cl.SubResource(subResourceScale).Update(context.Background(), obj, client.WithSubResourceBody(scale)).Error()).To(Equal(expectedErr))
})

scalableObjs := []client.Object{
&appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: appsv1.DeploymentSpec{
Replicas: ptr.To[int32](2),
},
},
&appsv1.ReplicaSet{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: appsv1.ReplicaSetSpec{
Replicas: ptr.To[int32](2),
},
},
&corev1.ReplicationController{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: corev1.ReplicationControllerSpec{
Replicas: ptr.To[int32](2),
},
},
&appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: appsv1.StatefulSetSpec{
Replicas: ptr.To[int32](2),
},
},
}
for _, obj := range scalableObjs {
It(fmt.Sprintf("should be able to Get scale subresources for resource %T", obj), func() {
cl := NewClientBuilder().WithObjects(obj).Build()

scaleActual := &autoscalingv1.Scale{}
Expect(cl.SubResource(subResourceScale).Get(context.Background(), obj, scaleActual)).NotTo(HaveOccurred())

scaleExpected := &autoscalingv1.Scale{
ObjectMeta: metav1.ObjectMeta{
Name: obj.GetName(),
UID: obj.GetUID(),
ResourceVersion: obj.GetResourceVersion(),
},
Spec: autoscalingv1.ScaleSpec{
Replicas: 2,
},
}
Expect(cmp.Diff(scaleExpected, scaleActual)).To(BeEmpty())
})

It(fmt.Sprintf("should be able to Update scale subresources for resource %T", obj), func() {
cl := NewClientBuilder().WithObjects(obj).Build()

scaleExpected := &autoscalingv1.Scale{Spec: autoscalingv1.ScaleSpec{Replicas: 3}}
Expect(cl.SubResource(subResourceScale).Update(context.Background(), obj, client.WithSubResourceBody(scaleExpected))).NotTo(HaveOccurred())

objActual := obj.DeepCopyObject().(client.Object)
Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(objActual), objActual)).To(Succeed())

objExpected := obj.DeepCopyObject().(client.Object)
switch expected := objExpected.(type) {
case *appsv1.Deployment:
expected.ResourceVersion = objActual.GetResourceVersion()
expected.Spec.Replicas = ptr.To(int32(3))
case *appsv1.ReplicaSet:
expected.ResourceVersion = objActual.GetResourceVersion()
expected.Spec.Replicas = ptr.To(int32(3))
case *corev1.ReplicationController:
expected.ResourceVersion = objActual.GetResourceVersion()
expected.Spec.Replicas = ptr.To(int32(3))
case *appsv1.StatefulSet:
expected.ResourceVersion = objActual.GetResourceVersion()
expected.Spec.Replicas = ptr.To(int32(3))
}
Expect(cmp.Diff(objExpected, objActual)).To(BeEmpty())

scaleActual := &autoscalingv1.Scale{}
Expect(cl.SubResource(subResourceScale).Get(context.Background(), obj, scaleActual)).NotTo(HaveOccurred())

// When we called Update, these were derived but we need them now to compare.
scaleExpected.Name = scaleActual.Name
scaleExpected.ResourceVersion = scaleActual.ResourceVersion
Expect(cmp.Diff(scaleExpected, scaleActual)).To(BeEmpty())
})
}
})

type WithPointerMetaList struct {
Expand Down