...

Source file src/k8s.io/client-go/scale/client_test.go

Documentation: k8s.io/client-go/scale

     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 "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  		// 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  }
   365  

View as plain text