Skip to content

Commit 7394f7b

Browse files
authored
Merge pull request #2855 from TheSpiritXIII/fake-scale-subresource
✨ Add scale subresource logic to fake client
2 parents a39ace3 + 00883f7 commit 7394f7b

File tree

2 files changed

+296
-5
lines changed

2 files changed

+296
-5
lines changed

pkg/client/fake/client.go

Lines changed: 169 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import (
3131

3232
// Using v4 to match upstream
3333
jsonpatch "gopkg.in/evanphx/json-patch.v4"
34+
appsv1 "k8s.io/api/apps/v1"
35+
autoscalingv1 "k8s.io/api/autoscaling/v1"
3436
corev1 "k8s.io/api/core/v1"
3537
policyv1 "k8s.io/api/policy/v1"
3638
policyv1beta1 "k8s.io/api/policy/v1beta1"
@@ -50,6 +52,7 @@ import (
5052
"k8s.io/apimachinery/pkg/watch"
5153
"k8s.io/client-go/kubernetes/scheme"
5254
"k8s.io/client-go/testing"
55+
"k8s.io/utils/ptr"
5356

5457
"sigs.k8s.io/controller-runtime/pkg/client"
5558
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
@@ -83,6 +86,8 @@ const (
8386
maxNameLength = 63
8487
randomLength = 5
8588
maxGeneratedNameLength = maxNameLength - randomLength
89+
90+
subResourceScale = "scale"
8691
)
8792

8893
// NewFakeClient creates a new fake client for testing.
@@ -1111,7 +1116,26 @@ type fakeSubResourceClient struct {
11111116
}
11121117

11131118
func (sw *fakeSubResourceClient) Get(ctx context.Context, obj, subResource client.Object, opts ...client.SubResourceGetOption) error {
1114-
panic("fakeSubResourceClient does not support get")
1119+
switch sw.subResource {
1120+
case subResourceScale:
1121+
// Actual client looks up resource, then extracts the scale sub-resource:
1122+
// https://github.com/kubernetes/kubernetes/blob/fb6bbc9781d11a87688c398778525c4e1dcb0f08/pkg/registry/apps/deployment/storage/storage.go#L307
1123+
if err := sw.client.Get(ctx, client.ObjectKeyFromObject(obj), obj); err != nil {
1124+
return err
1125+
}
1126+
scale, isScale := subResource.(*autoscalingv1.Scale)
1127+
if !isScale {
1128+
return apierrors.NewBadRequest(fmt.Sprintf("expected Scale, got %t", subResource))
1129+
}
1130+
scaleOut, err := extractScale(obj)
1131+
if err != nil {
1132+
return err
1133+
}
1134+
*scale = *scaleOut
1135+
return nil
1136+
default:
1137+
return fmt.Errorf("fakeSubResourceClient does not support get for %s", sw.subResource)
1138+
}
11151139
}
11161140

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

1141-
body := obj
1142-
if updateOptions.SubResourceBody != nil {
1143-
body = updateOptions.SubResourceBody
1165+
switch sw.subResource {
1166+
case subResourceScale:
1167+
if err := sw.client.Get(ctx, client.ObjectKeyFromObject(obj), obj); err != nil {
1168+
return err
1169+
}
1170+
if updateOptions.SubResourceBody == nil {
1171+
return apierrors.NewBadRequest("missing SubResourceBody")
1172+
}
1173+
1174+
scale, isScale := updateOptions.SubResourceBody.(*autoscalingv1.Scale)
1175+
if !isScale {
1176+
return apierrors.NewBadRequest(fmt.Sprintf("expected Scale, got %t", updateOptions.SubResourceBody))
1177+
}
1178+
if err := applyScale(obj, scale); err != nil {
1179+
return err
1180+
}
1181+
return sw.client.update(obj, false, &updateOptions.UpdateOptions)
1182+
default:
1183+
body := obj
1184+
if updateOptions.SubResourceBody != nil {
1185+
body = updateOptions.SubResourceBody
1186+
}
1187+
return sw.client.update(body, true, &updateOptions.UpdateOptions)
11441188
}
1145-
return sw.client.update(body, true, &updateOptions.UpdateOptions)
11461189
}
11471190

11481191
func (sw *fakeSubResourceClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.SubResourcePatchOption) error {
@@ -1323,3 +1366,124 @@ func getSingleOrZeroOptions[T any](opts []T) (opt T, err error) {
13231366
}
13241367
return
13251368
}
1369+
1370+
func extractScale(obj client.Object) (*autoscalingv1.Scale, error) {
1371+
switch obj := obj.(type) {
1372+
case *appsv1.Deployment:
1373+
var replicas int32 = 1
1374+
if obj.Spec.Replicas != nil {
1375+
replicas = *obj.Spec.Replicas
1376+
}
1377+
var selector string
1378+
if obj.Spec.Selector != nil {
1379+
selector = obj.Spec.Selector.String()
1380+
}
1381+
return &autoscalingv1.Scale{
1382+
ObjectMeta: metav1.ObjectMeta{
1383+
Namespace: obj.Namespace,
1384+
Name: obj.Name,
1385+
UID: obj.UID,
1386+
ResourceVersion: obj.ResourceVersion,
1387+
CreationTimestamp: obj.CreationTimestamp,
1388+
},
1389+
Spec: autoscalingv1.ScaleSpec{
1390+
Replicas: replicas,
1391+
},
1392+
Status: autoscalingv1.ScaleStatus{
1393+
Replicas: obj.Status.Replicas,
1394+
Selector: selector,
1395+
},
1396+
}, nil
1397+
case *appsv1.ReplicaSet:
1398+
var replicas int32 = 1
1399+
if obj.Spec.Replicas != nil {
1400+
replicas = *obj.Spec.Replicas
1401+
}
1402+
var selector string
1403+
if obj.Spec.Selector != nil {
1404+
selector = obj.Spec.Selector.String()
1405+
}
1406+
return &autoscalingv1.Scale{
1407+
ObjectMeta: metav1.ObjectMeta{
1408+
Namespace: obj.Namespace,
1409+
Name: obj.Name,
1410+
UID: obj.UID,
1411+
ResourceVersion: obj.ResourceVersion,
1412+
CreationTimestamp: obj.CreationTimestamp,
1413+
},
1414+
Spec: autoscalingv1.ScaleSpec{
1415+
Replicas: replicas,
1416+
},
1417+
Status: autoscalingv1.ScaleStatus{
1418+
Replicas: obj.Status.Replicas,
1419+
Selector: selector,
1420+
},
1421+
}, nil
1422+
case *corev1.ReplicationController:
1423+
var replicas int32 = 1
1424+
if obj.Spec.Replicas != nil {
1425+
replicas = *obj.Spec.Replicas
1426+
}
1427+
return &autoscalingv1.Scale{
1428+
ObjectMeta: metav1.ObjectMeta{
1429+
Namespace: obj.Namespace,
1430+
Name: obj.Name,
1431+
UID: obj.UID,
1432+
ResourceVersion: obj.ResourceVersion,
1433+
CreationTimestamp: obj.CreationTimestamp,
1434+
},
1435+
Spec: autoscalingv1.ScaleSpec{
1436+
Replicas: replicas,
1437+
},
1438+
Status: autoscalingv1.ScaleStatus{
1439+
Replicas: obj.Status.Replicas,
1440+
Selector: labels.Set(obj.Spec.Selector).String(),
1441+
},
1442+
}, nil
1443+
case *appsv1.StatefulSet:
1444+
var replicas int32 = 1
1445+
if obj.Spec.Replicas != nil {
1446+
replicas = *obj.Spec.Replicas
1447+
}
1448+
var selector string
1449+
if obj.Spec.Selector != nil {
1450+
selector = obj.Spec.Selector.String()
1451+
}
1452+
return &autoscalingv1.Scale{
1453+
ObjectMeta: metav1.ObjectMeta{
1454+
Namespace: obj.Namespace,
1455+
Name: obj.Name,
1456+
UID: obj.UID,
1457+
ResourceVersion: obj.ResourceVersion,
1458+
CreationTimestamp: obj.CreationTimestamp,
1459+
},
1460+
Spec: autoscalingv1.ScaleSpec{
1461+
Replicas: replicas,
1462+
},
1463+
Status: autoscalingv1.ScaleStatus{
1464+
Replicas: obj.Status.Replicas,
1465+
Selector: selector,
1466+
},
1467+
}, nil
1468+
default:
1469+
// TODO: CRDs https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#scale-subresource
1470+
return nil, fmt.Errorf("unimplemented scale subresource for resource %T", obj)
1471+
}
1472+
}
1473+
1474+
func applyScale(obj client.Object, scale *autoscalingv1.Scale) error {
1475+
switch obj := obj.(type) {
1476+
case *appsv1.Deployment:
1477+
obj.Spec.Replicas = ptr.To(scale.Spec.Replicas)
1478+
case *appsv1.ReplicaSet:
1479+
obj.Spec.Replicas = ptr.To(scale.Spec.Replicas)
1480+
case *corev1.ReplicationController:
1481+
obj.Spec.Replicas = ptr.To(scale.Spec.Replicas)
1482+
case *appsv1.StatefulSet:
1483+
obj.Spec.Replicas = ptr.To(scale.Spec.Replicas)
1484+
default:
1485+
// TODO: CRDs https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#scale-subresource
1486+
return fmt.Errorf("unimplemented scale subresource for resource %T", obj)
1487+
}
1488+
return nil
1489+
}

pkg/client/fake/client_test.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@ import (
2727
. "github.com/onsi/ginkgo/v2"
2828
. "github.com/onsi/gomega"
2929
appsv1 "k8s.io/api/apps/v1"
30+
autoscalingv1 "k8s.io/api/autoscaling/v1"
3031
coordinationv1 "k8s.io/api/coordination/v1"
3132
corev1 "k8s.io/api/core/v1"
3233
policyv1 "k8s.io/api/policy/v1"
3334
policyv1beta1 "k8s.io/api/policy/v1beta1"
35+
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
3436
apierrors "k8s.io/apimachinery/pkg/api/errors"
3537
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3638
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -2068,6 +2070,131 @@ var _ = Describe("Fake client", func() {
20682070
err := cl.Get(context.Background(), client.ObjectKey{Name: "foo"}, obj)
20692071
Expect(apierrors.IsNotFound(err)).To(BeTrue())
20702072
})
2073+
2074+
It("disallows scale subresources on unsupported built-in types", func() {
2075+
scheme := runtime.NewScheme()
2076+
Expect(corev1.AddToScheme(scheme)).To(Succeed())
2077+
Expect(apiextensions.AddToScheme(scheme)).To(Succeed())
2078+
2079+
obj := &corev1.Pod{
2080+
ObjectMeta: metav1.ObjectMeta{
2081+
Name: "foo",
2082+
},
2083+
}
2084+
cl := NewClientBuilder().WithScheme(scheme).WithObjects(obj).Build()
2085+
2086+
scale := &autoscalingv1.Scale{Spec: autoscalingv1.ScaleSpec{Replicas: 2}}
2087+
expectedErr := "unimplemented scale subresource for resource *v1.Pod"
2088+
Expect(cl.SubResource(subResourceScale).Get(context.Background(), obj, scale).Error()).To(Equal(expectedErr))
2089+
Expect(cl.SubResource(subResourceScale).Update(context.Background(), obj, client.WithSubResourceBody(scale)).Error()).To(Equal(expectedErr))
2090+
})
2091+
2092+
It("disallows scale subresources on non-existing objects", func() {
2093+
obj := &appsv1.Deployment{
2094+
ObjectMeta: metav1.ObjectMeta{
2095+
Name: "foo",
2096+
},
2097+
Spec: appsv1.DeploymentSpec{
2098+
Replicas: ptr.To[int32](2),
2099+
},
2100+
}
2101+
cl := NewClientBuilder().Build()
2102+
2103+
scale := &autoscalingv1.Scale{Spec: autoscalingv1.ScaleSpec{Replicas: 2}}
2104+
expectedErr := "deployments.apps \"foo\" not found"
2105+
Expect(cl.SubResource(subResourceScale).Get(context.Background(), obj, scale).Error()).To(Equal(expectedErr))
2106+
Expect(cl.SubResource(subResourceScale).Update(context.Background(), obj, client.WithSubResourceBody(scale)).Error()).To(Equal(expectedErr))
2107+
})
2108+
2109+
scalableObjs := []client.Object{
2110+
&appsv1.Deployment{
2111+
ObjectMeta: metav1.ObjectMeta{
2112+
Name: "foo",
2113+
},
2114+
Spec: appsv1.DeploymentSpec{
2115+
Replicas: ptr.To[int32](2),
2116+
},
2117+
},
2118+
&appsv1.ReplicaSet{
2119+
ObjectMeta: metav1.ObjectMeta{
2120+
Name: "foo",
2121+
},
2122+
Spec: appsv1.ReplicaSetSpec{
2123+
Replicas: ptr.To[int32](2),
2124+
},
2125+
},
2126+
&corev1.ReplicationController{
2127+
ObjectMeta: metav1.ObjectMeta{
2128+
Name: "foo",
2129+
},
2130+
Spec: corev1.ReplicationControllerSpec{
2131+
Replicas: ptr.To[int32](2),
2132+
},
2133+
},
2134+
&appsv1.StatefulSet{
2135+
ObjectMeta: metav1.ObjectMeta{
2136+
Name: "foo",
2137+
},
2138+
Spec: appsv1.StatefulSetSpec{
2139+
Replicas: ptr.To[int32](2),
2140+
},
2141+
},
2142+
}
2143+
for _, obj := range scalableObjs {
2144+
It(fmt.Sprintf("should be able to Get scale subresources for resource %T", obj), func() {
2145+
cl := NewClientBuilder().WithObjects(obj).Build()
2146+
2147+
scaleActual := &autoscalingv1.Scale{}
2148+
Expect(cl.SubResource(subResourceScale).Get(context.Background(), obj, scaleActual)).NotTo(HaveOccurred())
2149+
2150+
scaleExpected := &autoscalingv1.Scale{
2151+
ObjectMeta: metav1.ObjectMeta{
2152+
Name: obj.GetName(),
2153+
UID: obj.GetUID(),
2154+
ResourceVersion: obj.GetResourceVersion(),
2155+
},
2156+
Spec: autoscalingv1.ScaleSpec{
2157+
Replicas: 2,
2158+
},
2159+
}
2160+
Expect(cmp.Diff(scaleExpected, scaleActual)).To(BeEmpty())
2161+
})
2162+
2163+
It(fmt.Sprintf("should be able to Update scale subresources for resource %T", obj), func() {
2164+
cl := NewClientBuilder().WithObjects(obj).Build()
2165+
2166+
scaleExpected := &autoscalingv1.Scale{Spec: autoscalingv1.ScaleSpec{Replicas: 3}}
2167+
Expect(cl.SubResource(subResourceScale).Update(context.Background(), obj, client.WithSubResourceBody(scaleExpected))).NotTo(HaveOccurred())
2168+
2169+
objActual := obj.DeepCopyObject().(client.Object)
2170+
Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(objActual), objActual)).To(Succeed())
2171+
2172+
objExpected := obj.DeepCopyObject().(client.Object)
2173+
switch expected := objExpected.(type) {
2174+
case *appsv1.Deployment:
2175+
expected.ResourceVersion = objActual.GetResourceVersion()
2176+
expected.Spec.Replicas = ptr.To(int32(3))
2177+
case *appsv1.ReplicaSet:
2178+
expected.ResourceVersion = objActual.GetResourceVersion()
2179+
expected.Spec.Replicas = ptr.To(int32(3))
2180+
case *corev1.ReplicationController:
2181+
expected.ResourceVersion = objActual.GetResourceVersion()
2182+
expected.Spec.Replicas = ptr.To(int32(3))
2183+
case *appsv1.StatefulSet:
2184+
expected.ResourceVersion = objActual.GetResourceVersion()
2185+
expected.Spec.Replicas = ptr.To(int32(3))
2186+
}
2187+
Expect(cmp.Diff(objExpected, objActual)).To(BeEmpty())
2188+
2189+
scaleActual := &autoscalingv1.Scale{}
2190+
Expect(cl.SubResource(subResourceScale).Get(context.Background(), obj, scaleActual)).NotTo(HaveOccurred())
2191+
2192+
// When we called Update, these were derived but we need them now to compare.
2193+
scaleExpected.Name = scaleActual.Name
2194+
scaleExpected.ResourceVersion = scaleActual.ResourceVersion
2195+
Expect(cmp.Diff(scaleExpected, scaleActual)).To(BeEmpty())
2196+
})
2197+
}
20712198
})
20722199

20732200
type WithPointerMetaList struct {

0 commit comments

Comments
 (0)