client-go/dynamic/client_test.go
Lukasz Szaszkiewicz d274c7ba36 client-go/rest/request: decodes initialEventsListBlueprint for watchlist requests
Kubernetes-commit: 7be192ae0bd5d9628c5ee37c2e8b843d602c0fa3
2024-10-02 15:03:39 +02:00

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)
}
}