From 0c4682e95603e42d55151481bab9801dd09b2bcf Mon Sep 17 00:00:00 2001 From: scott Date: Wed, 25 Aug 2021 17:55:25 +0800 Subject: [PATCH] explain the reason why metaclient special processing metav1.DeleteOptions encoding metaclient explicitly specifies the Content-Type when executing Delete and DeleteCollection, and add test for that Kubernetes-commit: 8976f6f6d9af22ad40df891565c19e4dfd67f591 --- metadata/metadata.go | 9 ++++++ metadata/metadata_test.go | 60 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/metadata/metadata.go b/metadata/metadata.go index 72b55799..8d4aa262 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -124,6 +124,12 @@ func (c *client) Delete(ctx context.Context, name string, opts metav1.DeleteOpti if len(name) == 0 { return fmt.Errorf("name is required") } + // if DeleteOptions are delivered to Negotiator for serialization, + // HTTP-Request header will bring "Content-Type: application/vnd.kubernetes.protobuf" + // apiextensions-apiserver uses unstructuredNegotiatedSerializer to decode the input, + // server-side will reply with 406 errors. + // The special treatment here is to be compatible with CRD Handler + // see: https://github.com/kubernetes/kubernetes/blob/1a845ccd076bbf1b03420fe694c85a5cd3bd6bed/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go#L843 deleteOptionsByte, err := runtime.Encode(deleteOptionsCodec.LegacyCodec(schema.GroupVersion{Version: "v1"}), &opts) if err != nil { return err @@ -132,6 +138,7 @@ func (c *client) Delete(ctx context.Context, name string, opts metav1.DeleteOpti result := c.client.client. Delete(). AbsPath(append(c.makeURLSegments(name), subresources...)...). + SetHeader("Content-Type", runtime.ContentTypeJSON). Body(deleteOptionsByte). Do(ctx) return result.Error() @@ -139,6 +146,7 @@ func (c *client) Delete(ctx context.Context, name string, opts metav1.DeleteOpti // DeleteCollection triggers deletion of all resources in the specified scope (namespace or cluster). func (c *client) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOptions metav1.ListOptions) error { + // See comment on Delete deleteOptionsByte, err := runtime.Encode(deleteOptionsCodec.LegacyCodec(schema.GroupVersion{Version: "v1"}), &opts) if err != nil { return err @@ -147,6 +155,7 @@ func (c *client) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions result := c.client.client. Delete(). AbsPath(c.makeURLSegments("")...). + SetHeader("Content-Type", runtime.ContentTypeJSON). Body(deleteOptionsByte). SpecificallyVersionedParams(&listOptions, dynamicParameterCodec, versionV1). Do(ctx) diff --git a/metadata/metadata_test.go b/metadata/metadata_test.go index 85acd1d2..d82a1758 100644 --- a/metadata/metadata_test.go +++ b/metadata/metadata_test.go @@ -19,6 +19,7 @@ package metadata import ( "context" "encoding/json" + "io/ioutil" "net/http" "net/http/httptest" "reflect" @@ -35,6 +36,10 @@ import ( func TestClient(t *testing.T) { gvr := schema.GroupVersionResource{Group: "group", Version: "v1", Resource: "resource"} + statusOK := &metav1.Status{ + Status: metav1.StatusSuccess, + Code: http.StatusOK, + } writeJSON := func(t *testing.T, w http.ResponseWriter, obj runtime.Object) { data, err := json.Marshal(obj) @@ -229,6 +234,61 @@ func TestClient(t *testing.T) { } }, }, + + { + name: "Delete fails if DeleteOptions cannot be serialized to JSON", + handler: func(t *testing.T, w http.ResponseWriter, req *http.Request) { + if req.Header.Get("Content-Type") != runtime.ContentTypeJSON { + t.Fatal(req.Header.Get("Content-Type")) + } + if req.Method != "DELETE" && req.URL.String() != "/apis/group/v1/namespaces/ns/resource/name" { + t.Fatal(req.URL.String()) + } + defer req.Body.Close() + buf, err := ioutil.ReadAll(req.Body) + if err != nil { + t.Fatal(err) + } + if !json.Valid(buf) { + t.Fatalf("request body is not a valid JSON: %s", buf) + } + writeJSON(t, w, statusOK) + }, + want: func(t *testing.T, client *Client) { + err := client.Resource(gvr).Namespace("ns").Delete(context.TODO(), "name", metav1.DeleteOptions{}) + if err != nil { + t.Fatal(err) + } + }, + }, + + { + name: "DeleteCollection fails if DeleteOptions cannot be serialized to JSON", + handler: func(t *testing.T, w http.ResponseWriter, req *http.Request) { + if req.Header.Get("Content-Type") != runtime.ContentTypeJSON { + t.Fatal(req.Header.Get("Content-Type")) + } + if req.Method != "DELETE" && req.URL.String() != "/apis/group/v1/namespaces/ns/resource/name" { + t.Fatal(req.URL.String()) + } + defer req.Body.Close() + buf, err := ioutil.ReadAll(req.Body) + if err != nil { + t.Fatal(err) + } + if !json.Valid(buf) { + t.Fatalf("request body is not a valid JSON: %s", buf) + } + + writeJSON(t, w, statusOK) + }, + want: func(t *testing.T, client *Client) { + err := client.Resource(gvr).Namespace("ns").DeleteCollection(context.TODO(), metav1.DeleteOptions{}, metav1.ListOptions{}) + if err != nil { + t.Fatal(err) + } + }, + }, } for _, tt := range testCases {