1
16
17 package scale
18
19 import (
20 "bytes"
21 "context"
22 "encoding/json"
23 "fmt"
24 "io"
25 "net/http"
26 "testing"
27
28 jsonpatch "github.com/evanphx/json-patch"
29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30 "k8s.io/apimachinery/pkg/runtime"
31 "k8s.io/apimachinery/pkg/runtime/schema"
32 "k8s.io/apimachinery/pkg/types"
33 fakedisco "k8s.io/client-go/discovery/fake"
34 "k8s.io/client-go/dynamic"
35 fakerest "k8s.io/client-go/rest/fake"
36
37 "github.com/stretchr/testify/assert"
38 appsv1beta1 "k8s.io/api/apps/v1beta1"
39 appsv1beta2 "k8s.io/api/apps/v1beta2"
40 autoscalingv1 "k8s.io/api/autoscaling/v1"
41 corev1 "k8s.io/api/core/v1"
42 extv1beta1 "k8s.io/api/extensions/v1beta1"
43 "k8s.io/client-go/restmapper"
44 coretesting "k8s.io/client-go/testing"
45 )
46
47 func bytesBody(bodyBytes []byte) io.ReadCloser {
48 return io.NopCloser(bytes.NewReader(bodyBytes))
49 }
50
51 func defaultHeaders() http.Header {
52 header := http.Header{}
53 header.Set("Content-Type", runtime.ContentTypeJSON)
54 return header
55 }
56
57 func fakeScaleClient(t *testing.T) (ScalesGetter, []schema.GroupResource) {
58 fakeDiscoveryClient := &fakedisco.FakeDiscovery{Fake: &coretesting.Fake{}}
59 fakeDiscoveryClient.Resources = []*metav1.APIResourceList{
60 {
61 GroupVersion: corev1.SchemeGroupVersion.String(),
62 APIResources: []metav1.APIResource{
63 {Name: "pods", Namespaced: true, Kind: "Pod"},
64 {Name: "replicationcontrollers", Namespaced: true, Kind: "ReplicationController"},
65 {Name: "replicationcontrollers/scale", Namespaced: true, Kind: "Scale", Group: "autoscaling", Version: "v1"},
66 },
67 },
68 {
69 GroupVersion: extv1beta1.SchemeGroupVersion.String(),
70 APIResources: []metav1.APIResource{
71 {Name: "replicasets", Namespaced: true, Kind: "ReplicaSet"},
72 {Name: "replicasets/scale", Namespaced: true, Kind: "Scale"},
73 },
74 },
75 {
76 GroupVersion: appsv1beta2.SchemeGroupVersion.String(),
77 APIResources: []metav1.APIResource{
78 {Name: "deployments", Namespaced: true, Kind: "Deployment"},
79 {Name: "deployments/scale", Namespaced: true, Kind: "Scale", Group: "apps", Version: "v1beta2"},
80 },
81 },
82 {
83 GroupVersion: appsv1beta1.SchemeGroupVersion.String(),
84 APIResources: []metav1.APIResource{
85 {Name: "statefulsets", Namespaced: true, Kind: "StatefulSet"},
86 {Name: "statefulsets/scale", Namespaced: true, Kind: "Scale", Group: "apps", Version: "v1beta1"},
87 },
88 },
89
90
91 {
92 GroupVersion: "cheese.testing.k8s.io/v27alpha15",
93 APIResources: []metav1.APIResource{
94 {Name: "cheddars", Namespaced: true, Kind: "Cheddar"},
95 {Name: "cheddars/scale", Namespaced: true, Kind: "Scale", Group: "extensions", Version: "v1beta1"},
96 },
97 },
98 }
99
100 restMapperRes, err := restmapper.GetAPIGroupResources(fakeDiscoveryClient)
101 if err != nil {
102 t.Fatalf("unexpected error while constructing resource list from fake discovery client: %v", err)
103 }
104 restMapper := restmapper.NewDiscoveryRESTMapper(restMapperRes)
105
106 autoscalingScale := &autoscalingv1.Scale{
107 TypeMeta: metav1.TypeMeta{
108 Kind: "Scale",
109 APIVersion: autoscalingv1.SchemeGroupVersion.String(),
110 },
111 ObjectMeta: metav1.ObjectMeta{
112 Name: "foo",
113 },
114 Spec: autoscalingv1.ScaleSpec{Replicas: 10},
115 Status: autoscalingv1.ScaleStatus{
116 Replicas: 10,
117 Selector: "foo=bar",
118 },
119 }
120 extScale := &extv1beta1.Scale{
121 TypeMeta: metav1.TypeMeta{
122 Kind: "Scale",
123 APIVersion: extv1beta1.SchemeGroupVersion.String(),
124 },
125 ObjectMeta: metav1.ObjectMeta{
126 Name: "foo",
127 },
128 Spec: extv1beta1.ScaleSpec{Replicas: 10},
129 Status: extv1beta1.ScaleStatus{
130 Replicas: 10,
131 TargetSelector: "foo=bar",
132 },
133 }
134 appsV1beta2Scale := &appsv1beta2.Scale{
135 TypeMeta: metav1.TypeMeta{
136 Kind: "Scale",
137 APIVersion: appsv1beta2.SchemeGroupVersion.String(),
138 },
139 ObjectMeta: metav1.ObjectMeta{
140 Name: "foo",
141 },
142 Spec: appsv1beta2.ScaleSpec{Replicas: 10},
143 Status: appsv1beta2.ScaleStatus{
144 Replicas: 10,
145 TargetSelector: "foo=bar",
146 },
147 }
148 appsV1beta1Scale := &appsv1beta1.Scale{
149 TypeMeta: metav1.TypeMeta{
150 Kind: "Scale",
151 APIVersion: appsv1beta1.SchemeGroupVersion.String(),
152 },
153 ObjectMeta: metav1.ObjectMeta{
154 Name: "foo",
155 },
156 Spec: appsv1beta1.ScaleSpec{Replicas: 10},
157 Status: appsv1beta1.ScaleStatus{
158 Replicas: 10,
159 TargetSelector: "foo=bar",
160 },
161 }
162
163 resourcePaths := map[string]runtime.Object{
164 "/api/v1/namespaces/default/replicationcontrollers/foo/scale": autoscalingScale,
165 "/apis/extensions/v1beta1/namespaces/default/replicasets/foo/scale": extScale,
166 "/apis/apps/v1beta1/namespaces/default/statefulsets/foo/scale": appsV1beta1Scale,
167 "/apis/apps/v1beta2/namespaces/default/deployments/foo/scale": appsV1beta2Scale,
168 "/apis/cheese.testing.k8s.io/v27alpha15/namespaces/default/cheddars/foo/scale": extScale,
169 }
170
171 fakeReqHandler := func(req *http.Request) (*http.Response, error) {
172 scale, isScalePath := resourcePaths[req.URL.Path]
173 if !isScalePath {
174 return nil, fmt.Errorf("unexpected request for URL %q with method %q", req.URL.String(), req.Method)
175 }
176
177 switch req.Method {
178 case "GET":
179 res, err := json.Marshal(scale)
180 if err != nil {
181 return nil, err
182 }
183 return &http.Response{StatusCode: http.StatusOK, Header: defaultHeaders(), Body: bytesBody(res)}, nil
184 case "PUT":
185 decoder := codecs.UniversalDeserializer()
186 body, err := io.ReadAll(req.Body)
187 if err != nil {
188 return nil, err
189 }
190 newScale, newScaleGVK, err := decoder.Decode(body, nil, nil)
191 if err != nil {
192 return nil, fmt.Errorf("unexpected request body: %v", err)
193 }
194 if *newScaleGVK != scale.GetObjectKind().GroupVersionKind() {
195 return nil, fmt.Errorf("unexpected scale API version %s (expected %s)", newScaleGVK.String(), scale.GetObjectKind().GroupVersionKind().String())
196 }
197 res, err := json.Marshal(newScale)
198 if err != nil {
199 return nil, err
200 }
201 return &http.Response{StatusCode: http.StatusOK, Header: defaultHeaders(), Body: bytesBody(res)}, nil
202 case "PATCH":
203 body, err := io.ReadAll(req.Body)
204 if err != nil {
205 return nil, err
206 }
207 originScale, err := json.Marshal(scale)
208 if err != nil {
209 return nil, err
210 }
211 var res []byte
212 contentType := req.Header.Get("Content-Type")
213 pt := types.PatchType(contentType)
214 switch pt {
215 case types.MergePatchType:
216 res, err = jsonpatch.MergePatch(originScale, body)
217 if err != nil {
218 return nil, err
219 }
220 case types.JSONPatchType:
221 patch, err := jsonpatch.DecodePatch(body)
222 if err != nil {
223 return nil, err
224 }
225 res, err = patch.Apply(originScale)
226 if err != nil {
227 return nil, err
228 }
229 default:
230 return nil, fmt.Errorf("invalid patch type")
231 }
232 return &http.Response{StatusCode: http.StatusOK, Header: defaultHeaders(), Body: bytesBody(res)}, nil
233 default:
234 return nil, fmt.Errorf("unexpected request for URL %q with method %q", req.URL.String(), req.Method)
235 }
236 }
237
238 fakeClient := &fakerest.RESTClient{
239 Client: fakerest.CreateHTTPClient(fakeReqHandler),
240 NegotiatedSerializer: codecs.WithoutConversion(),
241 GroupVersion: schema.GroupVersion{},
242 VersionedAPIPath: "/not/a/real/path",
243 }
244
245 resolver := NewDiscoveryScaleKindResolver(fakeDiscoveryClient)
246 client := New(fakeClient, restMapper, dynamic.LegacyAPIPathResolverFunc, resolver)
247
248 groupResources := []schema.GroupResource{
249 {Group: corev1.GroupName, Resource: "replicationcontrollers"},
250 {Group: extv1beta1.GroupName, Resource: "replicasets"},
251 {Group: appsv1beta2.GroupName, Resource: "deployments"},
252 {Group: "cheese.testing.k8s.io", Resource: "cheddars"},
253 }
254
255 return client, groupResources
256 }
257
258 func TestGetScale(t *testing.T) {
259 scaleClient, groupResources := fakeScaleClient(t)
260 expectedScale := &autoscalingv1.Scale{
261 TypeMeta: metav1.TypeMeta{
262 Kind: "Scale",
263 APIVersion: autoscalingv1.SchemeGroupVersion.String(),
264 },
265 ObjectMeta: metav1.ObjectMeta{
266 Name: "foo",
267 },
268 Spec: autoscalingv1.ScaleSpec{Replicas: 10},
269 Status: autoscalingv1.ScaleStatus{
270 Replicas: 10,
271 Selector: "foo=bar",
272 },
273 }
274
275 for _, groupResource := range groupResources {
276 scale, err := scaleClient.Scales("default").Get(context.TODO(), groupResource, "foo", metav1.GetOptions{})
277 if !assert.NoError(t, err, "should have been able to fetch a scale for %s", groupResource.String()) {
278 continue
279 }
280 assert.NotNil(t, scale, "should have returned a non-nil scale for %s", groupResource.String())
281
282 assert.Equal(t, expectedScale, scale, "should have returned the expected scale for %s", groupResource.String())
283 }
284 }
285
286 func TestUpdateScale(t *testing.T) {
287 scaleClient, groupResources := fakeScaleClient(t)
288 expectedScale := &autoscalingv1.Scale{
289 TypeMeta: metav1.TypeMeta{
290 Kind: "Scale",
291 APIVersion: autoscalingv1.SchemeGroupVersion.String(),
292 },
293 ObjectMeta: metav1.ObjectMeta{
294 Name: "foo",
295 },
296 Spec: autoscalingv1.ScaleSpec{Replicas: 10},
297 Status: autoscalingv1.ScaleStatus{
298 Replicas: 10,
299 Selector: "foo=bar",
300 },
301 }
302
303 for _, groupResource := range groupResources {
304 scale, err := scaleClient.Scales("default").Update(context.TODO(), groupResource, expectedScale, metav1.UpdateOptions{})
305 if !assert.NoError(t, err, "should have been able to fetch a scale for %s", groupResource.String()) {
306 continue
307 }
308 assert.NotNil(t, scale, "should have returned a non-nil scale for %s", groupResource.String())
309
310 assert.Equal(t, expectedScale, scale, "should have returned the expected scale for %s", groupResource.String())
311 }
312 }
313
314 func TestPatchScale(t *testing.T) {
315 scaleClient, groupResources := fakeScaleClient(t)
316 expectedScale := &autoscalingv1.Scale{
317 TypeMeta: metav1.TypeMeta{
318 Kind: "Scale",
319 APIVersion: autoscalingv1.SchemeGroupVersion.String(),
320 },
321 ObjectMeta: metav1.ObjectMeta{
322 Name: "foo",
323 },
324 Spec: autoscalingv1.ScaleSpec{Replicas: 5},
325 Status: autoscalingv1.ScaleStatus{
326 Replicas: 10,
327 Selector: "foo=bar",
328 },
329 }
330 gvrs := make([]schema.GroupVersionResource, 0, len(groupResources))
331 for _, gr := range groupResources {
332 switch gr.Group {
333 case corev1.GroupName:
334 gvrs = append(gvrs, gr.WithVersion(corev1.SchemeGroupVersion.Version))
335 case extv1beta1.GroupName:
336 gvrs = append(gvrs, gr.WithVersion(extv1beta1.SchemeGroupVersion.Version))
337 case appsv1beta2.GroupName:
338 gvrs = append(gvrs, gr.WithVersion(appsv1beta2.SchemeGroupVersion.Version))
339 default:
340
341 gvrs = append(gvrs, gr.WithVersion("v27alpha15"))
342 }
343 }
344
345 patch := []byte(`{"spec":{"replicas":5}}`)
346 for _, gvr := range gvrs {
347 scale, err := scaleClient.Scales("default").Patch(context.TODO(), gvr, "foo", types.MergePatchType, patch, metav1.PatchOptions{})
348 if !assert.NoError(t, err, "should have been able to fetch a scale for %s", gvr.String()) {
349 continue
350 }
351 assert.NotNil(t, scale, "should have returned a non-nil scale for %s", gvr.String())
352 assert.Equal(t, expectedScale, scale, "should have returned the expected scale for %s", gvr.String())
353 }
354
355 patch = []byte(`[{"op":"replace","path":"/spec/replicas","value":5}]`)
356 for _, gvr := range gvrs {
357 scale, err := scaleClient.Scales("default").Patch(context.TODO(), gvr, "foo", types.JSONPatchType, patch, metav1.PatchOptions{})
358 if !assert.NoError(t, err, "should have been able to fetch a scale for %s", gvr.String()) {
359 continue
360 }
361 assert.NotNil(t, scale, "should have returned a non-nil scale for %s", gvr.String())
362 assert.Equal(t, expectedScale, scale, "should have returned the expected scale for %s", gvr.String())
363 }
364 }
365
View as plain text