Skip to content

Commit

Permalink
Merge pull request #2855 from TheSpiritXIII/fake-scale-subresource
Browse files Browse the repository at this point in the history
✨ Add scale subresource logic to fake client
  • Loading branch information
k8s-ci-robot authored Aug 3, 2024
2 parents a39ace3 + 00883f7 commit 7394f7b
Show file tree
Hide file tree
Showing 2 changed files with 296 additions and 5 deletions.
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:
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{
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

0 comments on commit 7394f7b

Please sign in to comment.