Skip to content
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ require (
k8s.io/client-go v0.37.0-alpha.3
k8s.io/klog/v2 v2.140.0
k8s.io/utils v0.0.0-20260626114624-be93311217bd
sigs.k8s.io/randfill v1.0.0
sigs.k8s.io/structured-merge-diff/v6 v6.4.2
sigs.k8s.io/yaml v1.6.0
)
Expand Down Expand Up @@ -105,5 +106,4 @@ require (
k8s.io/streaming v0.37.0-alpha.3 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.36.0 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
)
28 changes: 28 additions & 0 deletions pkg/client/fake/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -1526,6 +1526,24 @@ func getSingleOrZeroOptions[T any](opts []T) (opt T, err error) {

func extractScale(obj client.Object) (*autoscalingv1.Scale, error) {
switch obj := obj.(type) {
case *unstructured.Unstructured:
Comment thread
sbueringer marked this conversation as resolved.
var typed client.Object
switch obj.GroupVersionKind() {
case appsv1.SchemeGroupVersion.WithKind("Deployment"):
typed = &appsv1.Deployment{}
case appsv1.SchemeGroupVersion.WithKind("ReplicaSet"):
typed = &appsv1.ReplicaSet{}
case appsv1.SchemeGroupVersion.WithKind("StatefulSet"):
typed = &appsv1.StatefulSet{}
case corev1.SchemeGroupVersion.WithKind("ReplicationController"):
typed = &corev1.ReplicationController{}
default:
return nil, fmt.Errorf("scale subresource for resource %T is not implemented", obj)
}
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, typed); err != nil {
return nil, err
}
return extractScale(typed)
case *appsv1.Deployment:
var replicas int32 = 1
if obj.Spec.Replicas != nil {
Expand Down Expand Up @@ -1630,6 +1648,16 @@ func extractScale(obj client.Object) (*autoscalingv1.Scale, error) {

func applyScale(obj client.Object, scale *autoscalingv1.Scale) error {
switch obj := obj.(type) {
case *unstructured.Unstructured:
switch obj.GroupVersionKind() {
case appsv1.SchemeGroupVersion.WithKind("Deployment"),
appsv1.SchemeGroupVersion.WithKind("ReplicaSet"),
appsv1.SchemeGroupVersion.WithKind("StatefulSet"),
corev1.SchemeGroupVersion.WithKind("ReplicationController"):
return unstructured.SetNestedField(obj.Object, int64(scale.Spec.Replicas), "spec", "replicas")
default:
return fmt.Errorf("scale subresource for resource %T is not implemented", obj)
}
case *appsv1.Deployment:
obj.Spec.Replicas = new(scale.Spec.Replicas)
case *appsv1.ReplicaSet:
Expand Down
105 changes: 105 additions & 0 deletions pkg/client/fake/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/rand"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/watch"
clientgoapplyconfigurations "k8s.io/client-go/applyconfigurations"
Expand All @@ -54,6 +55,7 @@ import (

"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
"sigs.k8s.io/randfill"
)

const (
Expand Down Expand Up @@ -2624,6 +2626,109 @@ var _ = Describe("Fake client", func() {
Expect(cl.SubResource(subResourceScale).Get(ctx, obj, scale).Error()).To(Equal(expectedErr))
Expect(cl.SubResource(subResourceScale).Update(ctx, obj, client.WithSubResourceBody(scale)).Error()).To(Equal(expectedErr))
})
It("supports scale subresources on unstructured objects with spec.replicas", func(ctx SpecContext) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we please add a test using https://github.com/kubernetes-sigs/randfill that asserts consistent behavior between the structured/unstructured representations by doing something like:

  • Have two fake clients, one with the default scheme, one with an empty one
  • Randfill a typed object, create it as typed in the client with default scheme and as unstructured in the other
  • Get scale from both clients, validate for equality
  • Ranfill a scale resource, update the objects in both clients with it
  • Fetch the object and scale resource from both clients, validate for equality

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes a lot of sense. Before I proceed, would it be preferable to add this as a separate test or refactor the existing unstructured scale test to cover the structured/unstructured parity scenario?

obj := &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]any{
"name": "foo",
"namespace": "default",
},
"spec": map[string]any{
"replicas": int64(1),
},
"status": map[string]any{
"replicas": int64(1),
},
}}
cl := NewClientBuilder().WithScheme(runtime.NewScheme()).WithObjects(obj).Build()

scale := &autoscalingv1.Scale{}
Expect(cl.SubResource(subResourceScale).Get(ctx, obj, scale)).To(Succeed())
Expect(scale.Spec.Replicas).To(Equal(int32(1)))
Expect(scale.Status.Replicas).To(Equal(int32(1)))

scale.Spec.Replicas = 3
Expect(cl.SubResource(subResourceScale).Update(ctx, obj, client.WithSubResourceBody(scale))).To(Succeed())

updated := &unstructured.Unstructured{}
updated.SetAPIVersion("apps/v1")
updated.SetKind("Deployment")
updated.SetName("foo")
updated.SetNamespace("default")
Expect(cl.Get(ctx, client.ObjectKeyFromObject(updated), updated)).To(Succeed())
replicas, found, err := unstructured.NestedInt64(updated.Object, "spec", "replicas")
Expect(err).NotTo(HaveOccurred())
Expect(found).To(BeTrue())
Expect(int32(replicas)).To(Equal(int32(3)))
})

It("structured and unstructured scale subresources behave consistently", func(ctx SpecContext) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test looks good, but you need to run the fuzzing in a loop, a single iteration is not very likely to catch issues. Something simple like for range 100 { existing test body } is fine

seed := time.Now().UnixMicro()
GinkgoWriter.Printf("seed: %d\n", seed)
fuzzer := randfill.NewWithSeed(seed).Funcs(
func(d *appsv1.Deployment, c randfill.Continue) {
var replicas, statusReplicas int32
c.Fill(&replicas)
c.Fill(&statusReplicas)
d.TypeMeta = metav1.TypeMeta{APIVersion: "apps/v1", Kind: "Deployment"}
d.ObjectMeta = metav1.ObjectMeta{Name: "scale-" + rand.String(8), Namespace: "default"}
d.Spec.Replicas = &replicas
d.Status.Replicas = statusReplicas
},
func(scale *autoscalingv1.Scale, c randfill.Continue) {
c.Fill(&scale.Spec.Replicas)
},
)

for range 100 {
dep := &appsv1.Deployment{}
fuzzer.Fill(dep)

unstrMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(dep)
Expect(err).NotTo(HaveOccurred())
unstr := &unstructured.Unstructured{Object: unstrMap}
unstr.SetAPIVersion("apps/v1")
unstr.SetKind("Deployment")

structuredCl := NewClientBuilder().WithObjects(dep.DeepCopy()).Build()
unstructuredCl := NewClientBuilder().WithScheme(runtime.NewScheme()).WithObjects(unstr.DeepCopy()).Build()

depKey := dep.DeepCopy()
unstrKey := &unstructured.Unstructured{}
unstrKey.SetAPIVersion("apps/v1")
unstrKey.SetKind("Deployment")
unstrKey.SetName(dep.Name)
unstrKey.SetNamespace(dep.Namespace)

scaleTyped, scaleUnstr := &autoscalingv1.Scale{}, &autoscalingv1.Scale{}
Expect(structuredCl.SubResource(subResourceScale).Get(ctx, depKey, scaleTyped)).To(Succeed())
Expect(unstructuredCl.SubResource(subResourceScale).Get(ctx, unstrKey, scaleUnstr)).To(Succeed())
Expect(scaleTyped.Spec.Replicas).To(Equal(scaleUnstr.Spec.Replicas))
Expect(scaleTyped.Status.Replicas).To(Equal(scaleUnstr.Status.Replicas))

updateScale := &autoscalingv1.Scale{}
fuzzer.Fill(updateScale)
Expect(structuredCl.SubResource(subResourceScale).Update(ctx, depKey, client.WithSubResourceBody(updateScale.DeepCopy()))).To(Succeed())
Expect(unstructuredCl.SubResource(subResourceScale).Update(ctx, unstrKey, client.WithSubResourceBody(updateScale.DeepCopy()))).To(Succeed())

Expect(structuredCl.Get(ctx, client.ObjectKeyFromObject(dep), depKey)).To(Succeed())
Expect(depKey.Spec.Replicas).NotTo(BeNil())
Expect(*depKey.Spec.Replicas).To(Equal(updateScale.Spec.Replicas))

Expect(unstructuredCl.Get(ctx, client.ObjectKeyFromObject(dep), unstrKey)).To(Succeed())
replicas, found, err := unstructured.NestedInt64(unstrKey.Object, "spec", "replicas")
Expect(err).NotTo(HaveOccurred())
Expect(found).To(BeTrue())
Expect(int32(replicas)).To(Equal(updateScale.Spec.Replicas))

scaleTyped, scaleUnstr = &autoscalingv1.Scale{}, &autoscalingv1.Scale{}
Expect(structuredCl.SubResource(subResourceScale).Get(ctx, depKey, scaleTyped)).To(Succeed())
Expect(unstructuredCl.SubResource(subResourceScale).Get(ctx, unstrKey, scaleUnstr)).To(Succeed())
Expect(scaleTyped.Spec.Replicas).To(Equal(scaleUnstr.Spec.Replicas))
Expect(scaleTyped.Spec.Replicas).To(Equal(updateScale.Spec.Replicas))
}
})

It("disallows scale subresources on non-existing objects", func(ctx SpecContext) {
obj := &appsv1.Deployment{
Expand Down