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  }