k8s.io/client-go@v0.31.1/scale/client_test.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 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 "gopkg.in/evanphx/json-patch.v4" 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 // test a resource that doesn't exist anywere to make sure we're not accidentally depending 90 // on a static RESTMapper anywhere. 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 // Group cheese.testing.k8s.io 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 }