mirror of
https://github.com/kubernetes/client-go.git
synced 2025-06-07 22:24:42 +00:00
973 lines
32 KiB
Go
973 lines
32 KiB
Go
/*
|
|
Copyright 2016 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package dynamic
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/apimachinery/pkg/runtime/serializer/streaming"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/apimachinery/pkg/watch"
|
|
clientfeatures "k8s.io/client-go/features"
|
|
clientfeaturestesting "k8s.io/client-go/features/testing"
|
|
restclient "k8s.io/client-go/rest"
|
|
restclientwatch "k8s.io/client-go/rest/watch"
|
|
)
|
|
|
|
func getJSON(version, kind, name string) []byte {
|
|
return []byte(fmt.Sprintf(`{"apiVersion": %q, "kind": %q, "metadata": {"name": %q}}`, version, kind, name))
|
|
}
|
|
|
|
func getListJSON(version, kind string, items ...[]byte) []byte {
|
|
json := fmt.Sprintf(`{"apiVersion": %q, "kind": %q, "items": [%s]}`,
|
|
version, kind, bytes.Join(items, []byte(",")))
|
|
return []byte(json)
|
|
}
|
|
|
|
func getObject(version, kind, name string) *unstructured.Unstructured {
|
|
return &unstructured.Unstructured{
|
|
Object: map[string]interface{}{
|
|
"apiVersion": version,
|
|
"kind": kind,
|
|
"metadata": map[string]interface{}{
|
|
"name": name,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func getObjectFromJSON(b []byte) *unstructured.Unstructured {
|
|
obj := &unstructured.Unstructured{}
|
|
_ = obj.UnmarshalJSON(b) // can ignore parse error because the comparison will fail
|
|
return obj
|
|
}
|
|
|
|
func getClientServer(h func(http.ResponseWriter, *http.Request)) (Interface, *httptest.Server, error) {
|
|
srv := httptest.NewServer(http.HandlerFunc(h))
|
|
cl, err := NewForConfig(&restclient.Config{
|
|
Host: srv.URL,
|
|
})
|
|
if err != nil {
|
|
srv.Close()
|
|
return nil, nil, err
|
|
}
|
|
return cl, srv, nil
|
|
}
|
|
|
|
func TestList(t *testing.T) {
|
|
tcs := []struct {
|
|
name string
|
|
namespace string
|
|
path string
|
|
resp []byte
|
|
want *unstructured.UnstructuredList
|
|
}{
|
|
{
|
|
name: "normal_list",
|
|
path: "/apis/gtest/vtest/rtest",
|
|
resp: getListJSON("vTest", "rTestList",
|
|
getJSON("vTest", "rTest", "item1"),
|
|
getJSON("vTest", "rTest", "item2")),
|
|
want: &unstructured.UnstructuredList{
|
|
Object: map[string]interface{}{
|
|
"apiVersion": "vTest",
|
|
"kind": "rTestList",
|
|
},
|
|
Items: []unstructured.Unstructured{
|
|
*getObject("vTest", "rTest", "item1"),
|
|
*getObject("vTest", "rTest", "item2"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "namespaced_list",
|
|
namespace: "nstest",
|
|
path: "/apis/gtest/vtest/namespaces/nstest/rtest",
|
|
resp: getListJSON("vTest", "rTestList",
|
|
getJSON("vTest", "rTest", "item1"),
|
|
getJSON("vTest", "rTest", "item2")),
|
|
want: &unstructured.UnstructuredList{
|
|
Object: map[string]interface{}{
|
|
"apiVersion": "vTest",
|
|
"kind": "rTestList",
|
|
},
|
|
Items: []unstructured.Unstructured{
|
|
*getObject("vTest", "rTest", "item1"),
|
|
*getObject("vTest", "rTest", "item2"),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, tc := range tcs {
|
|
resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: "rtest"}
|
|
cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "GET" {
|
|
t.Errorf("List(%q) got HTTP method %s. wanted GET", tc.name, r.Method)
|
|
}
|
|
|
|
if r.URL.Path != tc.path {
|
|
t.Errorf("List(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", runtime.ContentTypeJSON)
|
|
w.Write(tc.resp)
|
|
})
|
|
if err != nil {
|
|
t.Errorf("unexpected error when creating client: %v", err)
|
|
continue
|
|
}
|
|
defer srv.Close()
|
|
|
|
got, err := cl.Resource(resource).Namespace(tc.namespace).List(context.TODO(), metav1.ListOptions{})
|
|
if err != nil {
|
|
t.Errorf("unexpected error when listing %q: %v", tc.name, err)
|
|
continue
|
|
}
|
|
|
|
if !reflect.DeepEqual(got, tc.want) {
|
|
t.Errorf("List(%q) want: %v\ngot: %v", tc.name, tc.want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWatchList(t *testing.T) {
|
|
clientfeaturestesting.SetFeatureDuringTest(t, clientfeatures.WatchListClient, true)
|
|
|
|
type requestParam struct {
|
|
Path string
|
|
Query string
|
|
}
|
|
|
|
scenarios := []struct {
|
|
name string
|
|
namespace string
|
|
watchResponse []watch.Event
|
|
listResponse []byte
|
|
|
|
expectedRequestParams []requestParam
|
|
expectedList *unstructured.UnstructuredList
|
|
}{
|
|
{
|
|
name: "watch-list request for cluster wide resource",
|
|
watchResponse: []watch.Event{
|
|
{Type: watch.Added, Object: getObject("gtest/vTest", "rTest", "item1")},
|
|
{Type: watch.Added, Object: getObject("gtest/vTest", "rTest", "item2")},
|
|
{Type: watch.Bookmark, Object: func() runtime.Object {
|
|
obj := getObject("gtest/vTest", "rTest", "item2")
|
|
obj.SetResourceVersion("10")
|
|
obj.SetAnnotations(map[string]string{
|
|
metav1.InitialEventsAnnotationKey: "true",
|
|
metav1.InitialEventsListBlueprintAnnotationKey: base64.StdEncoding.EncodeToString(getJSON("vTest", "rTests", "")),
|
|
})
|
|
return obj
|
|
}()},
|
|
},
|
|
expectedRequestParams: []requestParam{
|
|
{
|
|
Path: "/apis/gtest/vtest/rtest",
|
|
Query: "allowWatchBookmarks=true&resourceVersionMatch=NotOlderThan&sendInitialEvents=true&watch=true",
|
|
},
|
|
},
|
|
expectedList: &unstructured.UnstructuredList{
|
|
Object: map[string]interface{}{
|
|
"apiVersion": "vTest",
|
|
"kind": "rTests",
|
|
"metadata": map[string]interface{}{
|
|
"name": "",
|
|
"resourceVersion": "10",
|
|
},
|
|
},
|
|
Items: []unstructured.Unstructured{
|
|
*getObject("gtest/vTest", "rTest", "item1"),
|
|
*getObject("gtest/vTest", "rTest", "item2"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "watch-list request for namespaced watch resource",
|
|
namespace: "nstest",
|
|
watchResponse: []watch.Event{
|
|
{Type: watch.Added, Object: getObject("gtest/vTest", "rTest", "item1")},
|
|
{Type: watch.Bookmark, Object: func() runtime.Object {
|
|
obj := getObject("gtest/vTest", "rTest", "item2")
|
|
obj.SetResourceVersion("39")
|
|
obj.SetAnnotations(map[string]string{
|
|
metav1.InitialEventsAnnotationKey: "true",
|
|
metav1.InitialEventsListBlueprintAnnotationKey: base64.StdEncoding.EncodeToString(getJSON("vTest", "rTests", "")),
|
|
})
|
|
return obj
|
|
}()},
|
|
},
|
|
expectedRequestParams: []requestParam{
|
|
{
|
|
Path: "/apis/gtest/vtest/namespaces/nstest/rtest",
|
|
Query: "allowWatchBookmarks=true&resourceVersionMatch=NotOlderThan&sendInitialEvents=true&watch=true",
|
|
},
|
|
},
|
|
expectedList: &unstructured.UnstructuredList{
|
|
Object: map[string]interface{}{
|
|
"apiVersion": "vTest",
|
|
"kind": "rTests",
|
|
"metadata": map[string]interface{}{
|
|
"name": "",
|
|
"resourceVersion": "39",
|
|
},
|
|
},
|
|
Items: []unstructured.Unstructured{
|
|
*getObject("gtest/vTest", "rTest", "item1"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "watch-list request falls back to standard list on any error",
|
|
namespace: "nstest",
|
|
// watchList method in client-go expect only watch.Add and watch.Bookmark events
|
|
// receiving watch.Error will cause this method to report an error which will
|
|
// trigger the fallback logic
|
|
watchResponse: []watch.Event{
|
|
{Type: watch.Error, Object: getObject("gtest/vTest", "rTest", "item1")},
|
|
},
|
|
listResponse: getListJSON("vTest", "UnstructuredList",
|
|
getJSON("gtest/vTest", "rTest", "item1"),
|
|
getJSON("gtest/vTest", "rTest", "item2")),
|
|
expectedRequestParams: []requestParam{
|
|
// a watch-list request first
|
|
{
|
|
Path: "/apis/gtest/vtest/namespaces/nstest/rtest",
|
|
Query: "allowWatchBookmarks=true&resourceVersionMatch=NotOlderThan&sendInitialEvents=true&watch=true",
|
|
},
|
|
// a standard list request second
|
|
{
|
|
Path: "/apis/gtest/vtest/namespaces/nstest/rtest",
|
|
},
|
|
},
|
|
expectedList: &unstructured.UnstructuredList{
|
|
Object: map[string]interface{}{
|
|
"apiVersion": "vTest",
|
|
"kind": "UnstructuredList",
|
|
},
|
|
Items: []unstructured.Unstructured{
|
|
*getObject("gtest/vTest", "rTest", "item1"),
|
|
*getObject("gtest/vTest", "rTest", "item2"),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, scenario := range scenarios {
|
|
t.Run(scenario.name, func(t *testing.T) {
|
|
var actualRequestParams []requestParam
|
|
resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: "rtest"}
|
|
cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "GET" {
|
|
t.Errorf("unexpected HTTP method %s. expected GET", r.Method)
|
|
}
|
|
actualRequestParams = append(actualRequestParams, requestParam{
|
|
Path: r.URL.Path,
|
|
Query: r.URL.RawQuery,
|
|
})
|
|
|
|
w.Header().Set("Content-Type", runtime.ContentTypeJSON)
|
|
// handle LIST response
|
|
if len(scenario.listResponse) > 0 {
|
|
if _, err := w.Write(scenario.listResponse); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
// handle WATCH response
|
|
enc := restclientwatch.NewEncoder(streaming.NewEncoder(w, unstructured.UnstructuredJSONScheme), unstructured.UnstructuredJSONScheme)
|
|
for _, e := range scenario.watchResponse {
|
|
if err := enc.Encode(&e); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error when creating test client and server: %v", err)
|
|
}
|
|
defer srv.Close()
|
|
|
|
actualList, err := cl.Resource(resource).Namespace(scenario.namespace).List(context.TODO(), metav1.ListOptions{})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if !cmp.Equal(scenario.expectedRequestParams, actualRequestParams) {
|
|
t.Fatalf("unexpected request params: %v", cmp.Diff(scenario.expectedRequestParams, actualRequestParams))
|
|
}
|
|
if !cmp.Equal(scenario.expectedList, actualList) {
|
|
t.Errorf("received expected list, diff: %s", cmp.Diff(scenario.expectedList, actualList))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGet(t *testing.T) {
|
|
tcs := []struct {
|
|
resource string
|
|
subresource []string
|
|
namespace string
|
|
name string
|
|
path string
|
|
resp []byte
|
|
want *unstructured.Unstructured
|
|
}{
|
|
{
|
|
resource: "rtest",
|
|
name: "normal_get",
|
|
path: "/apis/gtest/vtest/rtest/normal_get",
|
|
resp: getJSON("vTest", "rTest", "normal_get"),
|
|
want: getObject("vTest", "rTest", "normal_get"),
|
|
},
|
|
{
|
|
resource: "rtest",
|
|
namespace: "nstest",
|
|
name: "namespaced_get",
|
|
path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_get",
|
|
resp: getJSON("vTest", "rTest", "namespaced_get"),
|
|
want: getObject("vTest", "rTest", "namespaced_get"),
|
|
},
|
|
{
|
|
resource: "rtest",
|
|
subresource: []string{"srtest"},
|
|
name: "normal_subresource_get",
|
|
path: "/apis/gtest/vtest/rtest/normal_subresource_get/srtest",
|
|
resp: getJSON("vTest", "srTest", "normal_subresource_get"),
|
|
want: getObject("vTest", "srTest", "normal_subresource_get"),
|
|
},
|
|
{
|
|
resource: "rtest",
|
|
subresource: []string{"srtest"},
|
|
namespace: "nstest",
|
|
name: "namespaced_subresource_get",
|
|
path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_subresource_get/srtest",
|
|
resp: getJSON("vTest", "srTest", "namespaced_subresource_get"),
|
|
want: getObject("vTest", "srTest", "namespaced_subresource_get"),
|
|
},
|
|
{
|
|
resource: "rtest",
|
|
subresource: []string{"srtest"},
|
|
namespace: "nstest",
|
|
name: "namespaced_subresource_get_list",
|
|
path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_subresource_get_list/srtest",
|
|
resp: getListJSON("vTest", "srTest", getJSON("vTest", "srTest", "a1")),
|
|
want: getObjectFromJSON(getListJSON("vTest", "srTest", getJSON("vTest", "srTest", "a1"))),
|
|
},
|
|
}
|
|
for _, tc := range tcs {
|
|
resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: tc.resource}
|
|
cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "GET" {
|
|
t.Errorf("Get(%q) got HTTP method %s. wanted GET", tc.name, r.Method)
|
|
}
|
|
|
|
if r.URL.Path != tc.path {
|
|
t.Errorf("Get(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", runtime.ContentTypeJSON)
|
|
w.Write(tc.resp)
|
|
})
|
|
if err != nil {
|
|
t.Errorf("unexpected error when creating client: %v", err)
|
|
continue
|
|
}
|
|
defer srv.Close()
|
|
|
|
got, err := cl.Resource(resource).Namespace(tc.namespace).Get(context.TODO(), tc.name, metav1.GetOptions{}, tc.subresource...)
|
|
if err != nil {
|
|
t.Errorf("unexpected error when getting %q: %v", tc.name, err)
|
|
continue
|
|
}
|
|
|
|
if !reflect.DeepEqual(got, tc.want) {
|
|
t.Errorf("Get(%q) want: %v\ngot: %v", tc.name, tc.want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDelete(t *testing.T) {
|
|
background := metav1.DeletePropagationBackground
|
|
uid := types.UID("uid")
|
|
|
|
statusOK := &metav1.Status{
|
|
TypeMeta: metav1.TypeMeta{Kind: "Status"},
|
|
Status: metav1.StatusSuccess,
|
|
}
|
|
tcs := []struct {
|
|
subresource []string
|
|
namespace string
|
|
name string
|
|
path string
|
|
deleteOptions metav1.DeleteOptions
|
|
}{
|
|
{
|
|
name: "normal_delete",
|
|
path: "/apis/gtest/vtest/rtest/normal_delete",
|
|
},
|
|
{
|
|
namespace: "nstest",
|
|
name: "namespaced_delete",
|
|
path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_delete",
|
|
},
|
|
{
|
|
subresource: []string{"srtest"},
|
|
name: "normal_delete",
|
|
path: "/apis/gtest/vtest/rtest/normal_delete/srtest",
|
|
},
|
|
{
|
|
subresource: []string{"srtest"},
|
|
namespace: "nstest",
|
|
name: "namespaced_delete",
|
|
path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_delete/srtest",
|
|
},
|
|
{
|
|
namespace: "nstest",
|
|
name: "namespaced_delete_with_options",
|
|
path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_delete_with_options",
|
|
deleteOptions: metav1.DeleteOptions{Preconditions: &metav1.Preconditions{UID: &uid}, PropagationPolicy: &background},
|
|
},
|
|
}
|
|
for _, tc := range tcs {
|
|
resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: "rtest"}
|
|
cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "DELETE" {
|
|
t.Errorf("Delete(%q) got HTTP method %s. wanted DELETE", tc.name, r.Method)
|
|
}
|
|
|
|
if r.URL.Path != tc.path {
|
|
t.Errorf("Delete(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path)
|
|
}
|
|
|
|
content := r.Header.Get("Content-Type")
|
|
if content != runtime.ContentTypeJSON {
|
|
t.Errorf("Delete(%q) got Content-Type %s. wanted %s", tc.name, content, runtime.ContentTypeJSON)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", runtime.ContentTypeJSON)
|
|
unstructured.UnstructuredJSONScheme.Encode(statusOK, w)
|
|
})
|
|
if err != nil {
|
|
t.Errorf("unexpected error when creating client: %v", err)
|
|
continue
|
|
}
|
|
defer srv.Close()
|
|
|
|
err = cl.Resource(resource).Namespace(tc.namespace).Delete(context.TODO(), tc.name, tc.deleteOptions, tc.subresource...)
|
|
if err != nil {
|
|
t.Errorf("unexpected error when deleting %q: %v", tc.name, err)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDeleteCollection(t *testing.T) {
|
|
statusOK := &metav1.Status{
|
|
TypeMeta: metav1.TypeMeta{Kind: "Status"},
|
|
Status: metav1.StatusSuccess,
|
|
}
|
|
tcs := []struct {
|
|
namespace string
|
|
name string
|
|
path string
|
|
}{
|
|
{
|
|
name: "normal_delete_collection",
|
|
path: "/apis/gtest/vtest/rtest",
|
|
},
|
|
{
|
|
namespace: "nstest",
|
|
name: "namespaced_delete_collection",
|
|
path: "/apis/gtest/vtest/namespaces/nstest/rtest",
|
|
},
|
|
}
|
|
for _, tc := range tcs {
|
|
resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: "rtest"}
|
|
cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "DELETE" {
|
|
t.Errorf("DeleteCollection(%q) got HTTP method %s. wanted DELETE", tc.name, r.Method)
|
|
}
|
|
|
|
if r.URL.Path != tc.path {
|
|
t.Errorf("DeleteCollection(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path)
|
|
}
|
|
|
|
content := r.Header.Get("Content-Type")
|
|
if content != runtime.ContentTypeJSON {
|
|
t.Errorf("DeleteCollection(%q) got Content-Type %s. wanted %s", tc.name, content, runtime.ContentTypeJSON)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", runtime.ContentTypeJSON)
|
|
unstructured.UnstructuredJSONScheme.Encode(statusOK, w)
|
|
})
|
|
if err != nil {
|
|
t.Errorf("unexpected error when creating client: %v", err)
|
|
continue
|
|
}
|
|
defer srv.Close()
|
|
|
|
err = cl.Resource(resource).Namespace(tc.namespace).DeleteCollection(context.TODO(), metav1.DeleteOptions{}, metav1.ListOptions{})
|
|
if err != nil {
|
|
t.Errorf("unexpected error when deleting collection %q: %v", tc.name, err)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCreate(t *testing.T) {
|
|
tcs := []struct {
|
|
resource string
|
|
subresource []string
|
|
name string
|
|
namespace string
|
|
obj *unstructured.Unstructured
|
|
path string
|
|
}{
|
|
{
|
|
resource: "rtest",
|
|
name: "normal_create",
|
|
path: "/apis/gtest/vtest/rtest",
|
|
obj: getObject("gtest/vTest", "rTest", "normal_create"),
|
|
},
|
|
{
|
|
resource: "rtest",
|
|
name: "namespaced_create",
|
|
namespace: "nstest",
|
|
path: "/apis/gtest/vtest/namespaces/nstest/rtest",
|
|
obj: getObject("gtest/vTest", "rTest", "namespaced_create"),
|
|
},
|
|
{
|
|
resource: "rtest",
|
|
subresource: []string{"srtest"},
|
|
name: "normal_subresource_create",
|
|
path: "/apis/gtest/vtest/rtest/normal_subresource_create/srtest",
|
|
obj: getObject("vTest", "srTest", "normal_subresource_create"),
|
|
},
|
|
{
|
|
resource: "rtest/",
|
|
subresource: []string{"srtest"},
|
|
name: "namespaced_subresource_create",
|
|
namespace: "nstest",
|
|
path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_subresource_create/srtest",
|
|
obj: getObject("vTest", "srTest", "namespaced_subresource_create"),
|
|
},
|
|
}
|
|
for _, tc := range tcs {
|
|
resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: tc.resource}
|
|
cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
t.Errorf("Create(%q) got HTTP method %s. wanted POST", tc.name, r.Method)
|
|
}
|
|
|
|
if r.URL.Path != tc.path {
|
|
t.Errorf("Create(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path)
|
|
}
|
|
|
|
content := r.Header.Get("Content-Type")
|
|
if content != runtime.ContentTypeJSON {
|
|
t.Errorf("Create(%q) got Content-Type %s. wanted %s", tc.name, content, runtime.ContentTypeJSON)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", runtime.ContentTypeJSON)
|
|
data, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
t.Errorf("Create(%q) unexpected error reading body: %v", tc.name, err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Write(data)
|
|
})
|
|
if err != nil {
|
|
t.Errorf("unexpected error when creating client: %v", err)
|
|
continue
|
|
}
|
|
defer srv.Close()
|
|
|
|
got, err := cl.Resource(resource).Namespace(tc.namespace).Create(context.TODO(), tc.obj, metav1.CreateOptions{}, tc.subresource...)
|
|
if err != nil {
|
|
t.Errorf("unexpected error when creating %q: %v", tc.name, err)
|
|
continue
|
|
}
|
|
|
|
if !reflect.DeepEqual(got, tc.obj) {
|
|
t.Errorf("Create(%q) want: %v\ngot: %v", tc.name, tc.obj, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUpdate(t *testing.T) {
|
|
tcs := []struct {
|
|
resource string
|
|
subresource []string
|
|
name string
|
|
namespace string
|
|
obj *unstructured.Unstructured
|
|
path string
|
|
}{
|
|
{
|
|
resource: "rtest",
|
|
name: "normal_update",
|
|
path: "/apis/gtest/vtest/rtest/normal_update",
|
|
obj: getObject("gtest/vTest", "rTest", "normal_update"),
|
|
},
|
|
{
|
|
resource: "rtest",
|
|
name: "namespaced_update",
|
|
namespace: "nstest",
|
|
path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_update",
|
|
obj: getObject("gtest/vTest", "rTest", "namespaced_update"),
|
|
},
|
|
{
|
|
resource: "rtest",
|
|
subresource: []string{"srtest"},
|
|
name: "normal_subresource_update",
|
|
path: "/apis/gtest/vtest/rtest/normal_update/srtest",
|
|
obj: getObject("gtest/vTest", "srTest", "normal_update"),
|
|
},
|
|
{
|
|
resource: "rtest",
|
|
subresource: []string{"srtest"},
|
|
name: "namespaced_subresource_update",
|
|
namespace: "nstest",
|
|
path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_update/srtest",
|
|
obj: getObject("gtest/vTest", "srTest", "namespaced_update"),
|
|
},
|
|
}
|
|
for _, tc := range tcs {
|
|
resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: tc.resource}
|
|
cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "PUT" {
|
|
t.Errorf("Update(%q) got HTTP method %s. wanted PUT", tc.name, r.Method)
|
|
}
|
|
|
|
if r.URL.Path != tc.path {
|
|
t.Errorf("Update(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path)
|
|
}
|
|
|
|
content := r.Header.Get("Content-Type")
|
|
if content != runtime.ContentTypeJSON {
|
|
t.Errorf("Uppdate(%q) got Content-Type %s. wanted %s", tc.name, content, runtime.ContentTypeJSON)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", runtime.ContentTypeJSON)
|
|
data, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
t.Errorf("Update(%q) unexpected error reading body: %v", tc.name, err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Write(data)
|
|
})
|
|
if err != nil {
|
|
t.Errorf("unexpected error when creating client: %v", err)
|
|
continue
|
|
}
|
|
defer srv.Close()
|
|
|
|
got, err := cl.Resource(resource).Namespace(tc.namespace).Update(context.TODO(), tc.obj, metav1.UpdateOptions{}, tc.subresource...)
|
|
if err != nil {
|
|
t.Errorf("unexpected error when updating %q: %v", tc.name, err)
|
|
continue
|
|
}
|
|
|
|
if !reflect.DeepEqual(got, tc.obj) {
|
|
t.Errorf("Update(%q) want: %v\ngot: %v", tc.name, tc.obj, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWatch(t *testing.T) {
|
|
tcs := []struct {
|
|
name string
|
|
namespace string
|
|
events []watch.Event
|
|
path string
|
|
query string
|
|
}{
|
|
{
|
|
name: "normal_watch",
|
|
path: "/apis/gtest/vtest/rtest",
|
|
query: "watch=true",
|
|
events: []watch.Event{
|
|
{Type: watch.Added, Object: getObject("gtest/vTest", "rTest", "normal_watch")},
|
|
{Type: watch.Modified, Object: getObject("gtest/vTest", "rTest", "normal_watch")},
|
|
{Type: watch.Deleted, Object: getObject("gtest/vTest", "rTest", "normal_watch")},
|
|
},
|
|
},
|
|
{
|
|
name: "namespaced_watch",
|
|
namespace: "nstest",
|
|
path: "/apis/gtest/vtest/namespaces/nstest/rtest",
|
|
query: "watch=true",
|
|
events: []watch.Event{
|
|
{Type: watch.Added, Object: getObject("gtest/vTest", "rTest", "namespaced_watch")},
|
|
{Type: watch.Modified, Object: getObject("gtest/vTest", "rTest", "namespaced_watch")},
|
|
{Type: watch.Deleted, Object: getObject("gtest/vTest", "rTest", "namespaced_watch")},
|
|
},
|
|
},
|
|
}
|
|
for _, tc := range tcs {
|
|
resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: "rtest"}
|
|
cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "GET" {
|
|
t.Errorf("Watch(%q) got HTTP method %s. wanted GET", tc.name, r.Method)
|
|
}
|
|
|
|
if r.URL.Path != tc.path {
|
|
t.Errorf("Watch(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path)
|
|
}
|
|
if r.URL.RawQuery != tc.query {
|
|
t.Errorf("Watch(%q) got query %s. wanted %s", tc.name, r.URL.RawQuery, tc.query)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
enc := restclientwatch.NewEncoder(streaming.NewEncoder(w, unstructured.UnstructuredJSONScheme), unstructured.UnstructuredJSONScheme)
|
|
for _, e := range tc.events {
|
|
enc.Encode(&e)
|
|
}
|
|
})
|
|
if err != nil {
|
|
t.Errorf("unexpected error when creating client: %v", err)
|
|
continue
|
|
}
|
|
defer srv.Close()
|
|
|
|
watcher, err := cl.Resource(resource).Namespace(tc.namespace).Watch(context.TODO(), metav1.ListOptions{})
|
|
if err != nil {
|
|
t.Errorf("unexpected error when watching %q: %v", tc.name, err)
|
|
continue
|
|
}
|
|
|
|
for _, want := range tc.events {
|
|
got := <-watcher.ResultChan()
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("Watch(%q) want: %v\ngot: %v", tc.name, want, got)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPatch(t *testing.T) {
|
|
tcs := []struct {
|
|
resource string
|
|
subresource []string
|
|
name string
|
|
namespace string
|
|
patch []byte
|
|
want *unstructured.Unstructured
|
|
path string
|
|
}{
|
|
{
|
|
resource: "rtest",
|
|
name: "normal_patch",
|
|
path: "/apis/gtest/vtest/rtest/normal_patch",
|
|
patch: getJSON("gtest/vTest", "rTest", "normal_patch"),
|
|
want: getObject("gtest/vTest", "rTest", "normal_patch"),
|
|
},
|
|
{
|
|
resource: "rtest",
|
|
name: "namespaced_patch",
|
|
namespace: "nstest",
|
|
path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_patch",
|
|
patch: getJSON("gtest/vTest", "rTest", "namespaced_patch"),
|
|
want: getObject("gtest/vTest", "rTest", "namespaced_patch"),
|
|
},
|
|
{
|
|
resource: "rtest",
|
|
subresource: []string{"srtest"},
|
|
name: "normal_subresource_patch",
|
|
path: "/apis/gtest/vtest/rtest/normal_subresource_patch/srtest",
|
|
patch: getJSON("gtest/vTest", "srTest", "normal_subresource_patch"),
|
|
want: getObject("gtest/vTest", "srTest", "normal_subresource_patch"),
|
|
},
|
|
{
|
|
resource: "rtest",
|
|
subresource: []string{"srtest"},
|
|
name: "namespaced_subresource_patch",
|
|
namespace: "nstest",
|
|
path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_subresource_patch/srtest",
|
|
patch: getJSON("gtest/vTest", "srTest", "namespaced_subresource_patch"),
|
|
want: getObject("gtest/vTest", "srTest", "namespaced_subresource_patch"),
|
|
},
|
|
}
|
|
for _, tc := range tcs {
|
|
resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: tc.resource}
|
|
cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "PATCH" {
|
|
t.Errorf("Patch(%q) got HTTP method %s. wanted PATCH", tc.name, r.Method)
|
|
}
|
|
|
|
if r.URL.Path != tc.path {
|
|
t.Errorf("Patch(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path)
|
|
}
|
|
|
|
content := r.Header.Get("Content-Type")
|
|
if content != string(types.StrategicMergePatchType) {
|
|
t.Errorf("Patch(%q) got Content-Type %s. wanted %s", tc.name, content, types.StrategicMergePatchType)
|
|
}
|
|
|
|
data, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
t.Errorf("Patch(%q) unexpected error reading body: %v", tc.name, err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write(data)
|
|
})
|
|
if err != nil {
|
|
t.Errorf("unexpected error when creating client: %v", err)
|
|
continue
|
|
}
|
|
defer srv.Close()
|
|
|
|
got, err := cl.Resource(resource).Namespace(tc.namespace).Patch(context.TODO(), tc.name, types.StrategicMergePatchType, tc.patch, metav1.PatchOptions{}, tc.subresource...)
|
|
if err != nil {
|
|
t.Errorf("unexpected error when patching %q: %v", tc.name, err)
|
|
continue
|
|
}
|
|
|
|
if !reflect.DeepEqual(got, tc.want) {
|
|
t.Errorf("Patch(%q) want: %v\ngot: %v", tc.name, tc.want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInvalidSegments(t *testing.T) {
|
|
name := "bad/name"
|
|
namespace := "bad/namespace"
|
|
resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: "rtest"}
|
|
obj := &unstructured.Unstructured{
|
|
Object: map[string]interface{}{
|
|
"apiVersion": "vtest",
|
|
"kind": "vkind",
|
|
"metadata": map[string]interface{}{
|
|
"name": name,
|
|
},
|
|
},
|
|
}
|
|
cl, err := NewForConfig(&restclient.Config{
|
|
Host: "127.0.0.1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to create config: %v", err)
|
|
}
|
|
|
|
_, err = cl.Resource(resource).Namespace(namespace).Create(context.TODO(), obj, metav1.CreateOptions{})
|
|
if err == nil || !strings.Contains(err.Error(), "invalid namespace") {
|
|
t.Fatalf("Expected `invalid namespace` error, got: %v", err)
|
|
}
|
|
|
|
_, err = cl.Resource(resource).Update(context.TODO(), obj, metav1.UpdateOptions{})
|
|
if err == nil || !strings.Contains(err.Error(), "invalid resource name") {
|
|
t.Fatalf("Expected `invalid resource name` error, got: %v", err)
|
|
}
|
|
_, err = cl.Resource(resource).Namespace(namespace).Update(context.TODO(), obj, metav1.UpdateOptions{})
|
|
if err == nil || !strings.Contains(err.Error(), "invalid namespace") {
|
|
t.Fatalf("Expected `invalid namespace` error, got: %v", err)
|
|
}
|
|
|
|
_, err = cl.Resource(resource).UpdateStatus(context.TODO(), obj, metav1.UpdateOptions{})
|
|
if err == nil || !strings.Contains(err.Error(), "invalid resource name") {
|
|
t.Fatalf("Expected `invalid resource name` error, got: %v", err)
|
|
}
|
|
_, err = cl.Resource(resource).Namespace(namespace).UpdateStatus(context.TODO(), obj, metav1.UpdateOptions{})
|
|
if err == nil || !strings.Contains(err.Error(), "invalid namespace") {
|
|
t.Fatalf("Expected `invalid namespace` error, got: %v", err)
|
|
}
|
|
|
|
err = cl.Resource(resource).Delete(context.TODO(), name, metav1.DeleteOptions{})
|
|
if err == nil || !strings.Contains(err.Error(), "invalid resource name") {
|
|
t.Fatalf("Expected `invalid resource name` error, got: %v", err)
|
|
}
|
|
err = cl.Resource(resource).Namespace(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{})
|
|
if err == nil || !strings.Contains(err.Error(), "invalid namespace") {
|
|
t.Fatalf("Expected `invalid namespace` error, got: %v", err)
|
|
}
|
|
|
|
err = cl.Resource(resource).Namespace(namespace).DeleteCollection(context.TODO(), metav1.DeleteOptions{}, metav1.ListOptions{})
|
|
if err == nil || !strings.Contains(err.Error(), "invalid namespace") {
|
|
t.Fatalf("Expected `invalid namespace` error, got: %v", err)
|
|
}
|
|
|
|
_, err = cl.Resource(resource).Get(context.TODO(), name, metav1.GetOptions{})
|
|
if err == nil || !strings.Contains(err.Error(), "invalid resource name") {
|
|
t.Fatalf("Expected `invalid resource name` error, got: %v", err)
|
|
}
|
|
_, err = cl.Resource(resource).Namespace(namespace).Get(context.TODO(), name, metav1.GetOptions{})
|
|
if err == nil || !strings.Contains(err.Error(), "invalid namespace") {
|
|
t.Fatalf("Expected `invalid namespace` error, got: %v", err)
|
|
}
|
|
|
|
_, err = cl.Resource(resource).Namespace(namespace).List(context.TODO(), metav1.ListOptions{})
|
|
if err == nil || !strings.Contains(err.Error(), "invalid namespace") {
|
|
t.Fatalf("Expected `invalid namespace` error, got: %v", err)
|
|
}
|
|
|
|
_, err = cl.Resource(resource).Namespace(namespace).Watch(context.TODO(), metav1.ListOptions{})
|
|
if err == nil || !strings.Contains(err.Error(), "invalid namespace") {
|
|
t.Fatalf("Expected `invalid namespace` error, got: %v", err)
|
|
}
|
|
|
|
_, err = cl.Resource(resource).Patch(context.TODO(), name, types.StrategicMergePatchType, []byte("{}"), metav1.PatchOptions{})
|
|
if err == nil || !strings.Contains(err.Error(), "invalid resource name") {
|
|
t.Fatalf("Expected `invalid resource name` error, got: %v", err)
|
|
}
|
|
_, err = cl.Resource(resource).Namespace(namespace).Patch(context.TODO(), name, types.StrategicMergePatchType, []byte("{}"), metav1.PatchOptions{})
|
|
if err == nil || !strings.Contains(err.Error(), "invalid namespace") {
|
|
t.Fatalf("Expected `invalid namespace` error, got: %v", err)
|
|
}
|
|
|
|
_, err = cl.Resource(resource).Apply(context.TODO(), name, obj, metav1.ApplyOptions{})
|
|
if err == nil || !strings.Contains(err.Error(), "invalid resource name") {
|
|
t.Fatalf("Expected `invalid resource name` error, got: %v", err)
|
|
}
|
|
_, err = cl.Resource(resource).Namespace(namespace).Apply(context.TODO(), name, obj, metav1.ApplyOptions{})
|
|
if err == nil || !strings.Contains(err.Error(), "invalid namespace") {
|
|
t.Fatalf("Expected `invalid namespace` error, got: %v", err)
|
|
}
|
|
|
|
_, err = cl.Resource(resource).ApplyStatus(context.TODO(), name, obj, metav1.ApplyOptions{})
|
|
if err == nil || !strings.Contains(err.Error(), "invalid resource name") {
|
|
t.Fatalf("Expected `invalid resource name` error, got: %v", err)
|
|
}
|
|
_, err = cl.Resource(resource).Namespace(namespace).ApplyStatus(context.TODO(), name, obj, metav1.ApplyOptions{})
|
|
if err == nil || !strings.Contains(err.Error(), "invalid namespace") {
|
|
t.Fatalf("Expected `invalid namespace` error, got: %v", err)
|
|
}
|
|
}
|