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