From 70681df7b926f70b391c4412229bf663e07afd78 Mon Sep 17 00:00:00 2001 From: knight42 Date: Mon, 29 Jul 2019 12:33:24 +0800 Subject: [PATCH] feat(scale): add Patch method to ScaleInterface Signed-off-by: knight42 Kubernetes-commit: f020c9159869918c63e0aad56abd01e655e61e78 --- scale/client.go | 100 ++++++++++++++++++++++++------------------- scale/client_test.go | 93 ++++++++++++++++++++++++++++++++++++++-- scale/fake/client.go | 13 +++++- scale/interfaces.go | 4 ++ 4 files changed, 160 insertions(+), 50 deletions(-) diff --git a/scale/client.go b/scale/client.go index 00e59752..7084c1a4 100644 --- a/scale/client.go +++ b/scale/client.go @@ -23,6 +23,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" serializer "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/dynamic" restclient "k8s.io/client-go/rest" ) @@ -75,6 +76,19 @@ func New(baseClient restclient.Interface, mapper PreferredResourceMapper, resolv } } +// apiPathFor returns the absolute api path for the given GroupVersion +func (c *scaleClient) apiPathFor(groupVer schema.GroupVersion) string { + // we need to set the API path based on GroupVersion (defaulting to the legacy path if none is set) + // TODO: we "cheat" here since the API path really only depends on group ATM, but this should + // *probably* take GroupVersionResource and not GroupVersionKind. + apiPath := c.apiPathResolverFunc(groupVer.WithKind("")) + if apiPath == "" { + apiPath = "/api" + } + + return restclient.DefaultVersionedAPIPath(apiPath, groupVer) +} + // pathAndVersionFor returns the appropriate base path and the associated full GroupVersionResource // for the given GroupResource func (c *scaleClient) pathAndVersionFor(resource schema.GroupResource) (string, schema.GroupVersionResource, error) { @@ -85,17 +99,7 @@ func (c *scaleClient) pathAndVersionFor(resource schema.GroupResource) (string, groupVer := gvr.GroupVersion() - // we need to set the API path based on GroupVersion (defaulting to the legacy path if none is set) - // TODO: we "cheat" here since the API path really only depends on group ATM, but this should - // *probably* take GroupVersionResource and not GroupVersionKind. - apiPath := c.apiPathResolverFunc(groupVer.WithKind("")) - if apiPath == "" { - apiPath = "/api" - } - - path := restclient.DefaultVersionedAPIPath(apiPath, groupVer) - - return path, gvr, nil + return c.apiPathFor(groupVer), gvr, nil } // namespacedScaleClient is an ScaleInterface for fetching @@ -105,6 +109,27 @@ type namespacedScaleClient struct { namespace string } +// convertToScale converts the response body to autoscaling/v1.Scale +func convertToScale(result *restclient.Result) (*autoscaling.Scale, error) { + scaleBytes, err := result.Raw() + if err != nil { + return nil, err + } + decoder := scaleConverter.codecs.UniversalDecoder(scaleConverter.ScaleVersions()...) + rawScaleObj, err := runtime.Decode(decoder, scaleBytes) + if err != nil { + return nil, err + } + + // convert whatever this is to autoscaling/v1.Scale + scaleObj, err := scaleConverter.ConvertToVersion(rawScaleObj, autoscaling.SchemeGroupVersion) + if err != nil { + return nil, fmt.Errorf("received an object from a /scale endpoint which was not convertible to autoscaling Scale: %v", err) + } + + return scaleObj.(*autoscaling.Scale), nil +} + func (c *scaleClient) Scales(namespace string) ScaleInterface { return &namespacedScaleClient{ client: c, @@ -134,23 +159,7 @@ func (c *namespacedScaleClient) Get(resource schema.GroupResource, name string) return nil, err } - scaleBytes, err := result.Raw() - if err != nil { - return nil, err - } - decoder := scaleConverter.codecs.UniversalDecoder(scaleConverter.ScaleVersions()...) - rawScaleObj, err := runtime.Decode(decoder, scaleBytes) - if err != nil { - return nil, err - } - - // convert whatever this is to autoscaling/v1.Scale - scaleObj, err := scaleConverter.ConvertToVersion(rawScaleObj, autoscaling.SchemeGroupVersion) - if err != nil { - return nil, fmt.Errorf("received an object from a /scale endpoint which was not convertible to autoscaling Scale: %v", err) - } - - return scaleObj.(*autoscaling.Scale), nil + return convertToScale(&result) } func (c *namespacedScaleClient) Update(resource schema.GroupResource, scale *autoscaling.Scale) (*autoscaling.Scale, error) { @@ -195,21 +204,22 @@ func (c *namespacedScaleClient) Update(resource schema.GroupResource, scale *aut return nil, err } - scaleBytes, err := result.Raw() - if err != nil { - return nil, err - } - decoder := scaleConverter.codecs.UniversalDecoder(scaleConverter.ScaleVersions()...) - rawScaleObj, err := runtime.Decode(decoder, scaleBytes) - if err != nil { - return nil, err - } - - // convert whatever this is back to autoscaling/v1.Scale - scaleObj, err := scaleConverter.ConvertToVersion(rawScaleObj, autoscaling.SchemeGroupVersion) - if err != nil { - return nil, fmt.Errorf("received an object from a /scale endpoint which was not convertible to autoscaling Scale: %v", err) - } - - return scaleObj.(*autoscaling.Scale), err + return convertToScale(&result) +} + +func (c *namespacedScaleClient) Patch(gvr schema.GroupVersionResource, name string, pt types.PatchType, data []byte) (*autoscaling.Scale, error) { + groupVersion := gvr.GroupVersion() + result := c.client.clientBase.Patch(pt). + AbsPath(c.client.apiPathFor(groupVersion)). + Namespace(c.namespace). + Resource(gvr.Resource). + Name(name). + SubResource("scale"). + Body(data). + Do() + if err := result.Error(); err != nil { + return nil, err + } + + return convertToScale(&result) } diff --git a/scale/client_test.go b/scale/client_test.go index 7a7b058d..0328ed63 100644 --- a/scale/client_test.go +++ b/scale/client_test.go @@ -25,9 +25,11 @@ import ( "net/http" "testing" + jsonpatch "github.com/evanphx/json-patch" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" fakedisco "k8s.io/client-go/discovery/fake" "k8s.io/client-go/dynamic" fakerest "k8s.io/client-go/rest/fake" @@ -197,6 +199,37 @@ func fakeScaleClient(t *testing.T) (ScalesGetter, []schema.GroupResource) { return nil, err } return &http.Response{StatusCode: 200, Header: defaultHeaders(), Body: bytesBody(res)}, nil + case "PATCH": + body, err := ioutil.ReadAll(req.Body) + if err != nil { + return nil, err + } + originScale, err := json.Marshal(scale) + if err != nil { + return nil, err + } + var res []byte + contentType := req.Header.Get("Content-Type") + pt := types.PatchType(contentType) + switch pt { + case types.MergePatchType: + res, err = jsonpatch.MergePatch(originScale, body) + if err != nil { + return nil, err + } + case types.JSONPatchType: + patch, err := jsonpatch.DecodePatch(body) + if err != nil { + return nil, err + } + res, err = patch.Apply(originScale) + if err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("invalid patch type") + } + return &http.Response{StatusCode: 200, Header: defaultHeaders(), Body: bytesBody(res)}, nil default: return nil, fmt.Errorf("unexpected request for URL %q with method %q", req.URL.String(), req.Method) } @@ -213,10 +246,10 @@ func fakeScaleClient(t *testing.T) (ScalesGetter, []schema.GroupResource) { client := New(fakeClient, restMapper, dynamic.LegacyAPIPathResolverFunc, resolver) groupResources := []schema.GroupResource{ - {Group: corev1.GroupName, Resource: "replicationcontroller"}, - {Group: extv1beta1.GroupName, Resource: "replicaset"}, - {Group: appsv1beta2.GroupName, Resource: "deployment"}, - {Group: "cheese.testing.k8s.io", Resource: "cheddar"}, + {Group: corev1.GroupName, Resource: "replicationcontrollers"}, + {Group: extv1beta1.GroupName, Resource: "replicasets"}, + {Group: appsv1beta2.GroupName, Resource: "deployments"}, + {Group: "cheese.testing.k8s.io", Resource: "cheddars"}, } return client, groupResources @@ -277,3 +310,55 @@ func TestUpdateScale(t *testing.T) { assert.Equal(t, expectedScale, scale, "should have returned the expected scale for %s", groupResource.String()) } } + +func TestPatchScale(t *testing.T) { + scaleClient, groupResources := fakeScaleClient(t) + expectedScale := &autoscalingv1.Scale{ + TypeMeta: metav1.TypeMeta{ + Kind: "Scale", + APIVersion: autoscalingv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: autoscalingv1.ScaleSpec{Replicas: 5}, + Status: autoscalingv1.ScaleStatus{ + Replicas: 10, + Selector: "foo=bar", + }, + } + gvrs := make([]schema.GroupVersionResource, 0, len(groupResources)) + for _, gr := range groupResources { + switch gr.Group { + case corev1.GroupName: + gvrs = append(gvrs, gr.WithVersion(corev1.SchemeGroupVersion.Version)) + case extv1beta1.GroupName: + gvrs = append(gvrs, gr.WithVersion(extv1beta1.SchemeGroupVersion.Version)) + case appsv1beta2.GroupName: + gvrs = append(gvrs, gr.WithVersion(appsv1beta2.SchemeGroupVersion.Version)) + default: + // Group cheese.testing.k8s.io + gvrs = append(gvrs, gr.WithVersion("v27alpha15")) + } + } + + patch := []byte(`{"spec":{"replicas":5}}`) + for _, gvr := range gvrs { + scale, err := scaleClient.Scales("default").Patch(gvr, "foo", types.MergePatchType, patch) + if !assert.NoError(t, err, "should have been able to fetch a scale for %s", gvr.String()) { + continue + } + assert.NotNil(t, scale, "should have returned a non-nil scale for %s", gvr.String()) + assert.Equal(t, expectedScale, scale, "should have returned the expected scale for %s", gvr.String()) + } + + patch = []byte(`[{"op":"replace","path":"/spec/replicas","value":5}]`) + for _, gvr := range gvrs { + scale, err := scaleClient.Scales("default").Patch(gvr, "foo", types.JSONPatchType, patch) + if !assert.NoError(t, err, "should have been able to fetch a scale for %s", gvr.String()) { + continue + } + assert.NotNil(t, scale, "should have returned a non-nil scale for %s", gvr.String()) + assert.Equal(t, expectedScale, scale, "should have returned the expected scale for %s", gvr.String()) + } +} diff --git a/scale/fake/client.go b/scale/fake/client.go index 1736680f..e3bc57ff 100644 --- a/scale/fake/client.go +++ b/scale/fake/client.go @@ -22,6 +22,7 @@ package fake import ( autoscalingapi "k8s.io/api/autoscaling/v1" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/scale" "k8s.io/client-go/testing" ) @@ -63,5 +64,15 @@ func (f *fakeNamespacedScaleClient) Update(resource schema.GroupResource, scale } return obj.(*autoscalingapi.Scale), err - +} + +func (f *fakeNamespacedScaleClient) Patch(gvr schema.GroupVersionResource, name string, pt types.PatchType, patch []byte) (*autoscalingapi.Scale, error) { + obj, err := f.fake. + Invokes(testing.NewPatchSubresourceAction(gvr, f.namespace, name, pt, patch, "scale"), &autoscalingapi.Scale{}) + + if err != nil { + return nil, err + } + + return obj.(*autoscalingapi.Scale), err } diff --git a/scale/interfaces.go b/scale/interfaces.go index 13f2cfb8..48d0da6f 100644 --- a/scale/interfaces.go +++ b/scale/interfaces.go @@ -19,6 +19,7 @@ package scale import ( autoscalingapi "k8s.io/api/autoscaling/v1" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" ) // ScalesGetter can produce a ScaleInterface @@ -36,4 +37,7 @@ type ScaleInterface interface { // Update updates the scale of the given scalable resource. Update(resource schema.GroupResource, scale *autoscalingapi.Scale) (*autoscalingapi.Scale, error) + + // Patch patches the scale of the given scalable resource. + Patch(gvr schema.GroupVersionResource, name string, pt types.PatchType, data []byte) (*autoscalingapi.Scale, error) }