karmada/pkg/search/proxy/framework/plugins/cluster/cluster_test.go

445 lines
12 KiB
Go

package cluster
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
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/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes/fake"
clusterapis "github.com/karmada-io/karmada/pkg/apis/cluster"
clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1"
karmadafake "github.com/karmada-io/karmada/pkg/generated/clientset/versioned/fake"
karmadainformers "github.com/karmada-io/karmada/pkg/generated/informers/externalversions"
"github.com/karmada-io/karmada/pkg/search/proxy/framework"
"github.com/karmada-io/karmada/pkg/search/proxy/store"
proxytest "github.com/karmada-io/karmada/pkg/search/proxy/testing"
utiltest "github.com/karmada-io/karmada/pkg/util/testing"
)
func TestModifyRequest(t *testing.T) {
newObjectFunc := func(annotations map[string]string, resourceVersion string) *unstructured.Unstructured {
obj := &unstructured.Unstructured{}
obj.SetAPIVersion("v1")
obj.SetKind("Pod")
obj.SetAnnotations(annotations)
obj.SetResourceVersion(resourceVersion)
return obj
}
type args struct {
body interface{}
cluster string
}
type want struct {
body interface{}
}
tests := []struct {
name string
args args
want want
}{
{
name: "Empty body",
args: args{
body: nil,
},
want: want{
body: nil,
},
},
{
name: "Body with nil annotations",
args: args{
body: newObjectFunc(nil, ""),
},
want: want{
body: newObjectFunc(nil, ""),
},
},
{
name: "Body with empty annotations",
args: args{
body: newObjectFunc(map[string]string{}, ""),
},
want: want{
body: newObjectFunc(map[string]string{}, ""),
},
},
{
name: "Body with cache source annotation",
args: args{
body: newObjectFunc(map[string]string{clusterv1alpha1.CacheSourceAnnotationKey: "bar"}, ""),
},
want: want{
body: newObjectFunc(map[string]string{}, ""),
},
},
{
name: "Body with single cluster resource version",
args: args{
body: newObjectFunc(nil, "1234"),
cluster: "cluster1",
},
want: want{
body: newObjectFunc(nil, "1234"),
},
},
{
name: "Body with multi cluster resource version",
args: args{
body: newObjectFunc(nil, store.BuildMultiClusterResourceVersion(map[string]string{"cluster1": "1234", "cluster2": "5678"})),
cluster: "cluster1",
},
want: want{
body: newObjectFunc(nil, "1234"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var body io.Reader
if tt.args.body != nil {
buf := bytes.NewBuffer(nil)
err := json.NewEncoder(buf).Encode(tt.args.body)
if err != nil {
t.Error(err)
return
}
body = buf
}
req, _ := http.NewRequest("PUT", "/api/v1/namespaces/default/pods/foo", body)
err := modifyRequest(req, tt.args.cluster)
if err != nil {
t.Error(err)
return
}
var get runtime.Object
if req.ContentLength != 0 {
data, err := ioutil.ReadAll(req.Body)
if err != nil {
t.Error(err)
return
}
if int64(len(data)) != req.ContentLength {
t.Errorf("expect contentLength %v, but got %v", len(data), req.ContentLength)
return
}
get, _, err = unstructured.UnstructuredJSONScheme.Decode(data, nil, nil)
if err != nil {
t.Error(err)
return
}
}
if !reflect.DeepEqual(tt.want.body, get) {
t.Errorf("get body diff: %v", cmp.Diff(tt.want.body, get))
}
})
}
}
func Test_clusterProxy_connect(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
fmt.Fprint(rw, "ok")
}))
reqCtx := request.WithUser(context.TODO(), &user.DefaultInfo{})
type fields struct {
store store.Store
secrets []runtime.Object
clusters []runtime.Object
}
type args struct {
requestInfo *request.RequestInfo
request *http.Request
}
type want struct {
err error
body string
}
tests := []struct {
name string
fields fields
args args
want want
}{
{
name: "create not supported",
fields: fields{},
args: args{
requestInfo: &request.RequestInfo{Verb: "create"},
},
want: want{
err: apierrors.NewMethodNotSupported(proxytest.PodGVR.GroupResource(), "create"),
},
},
{
name: "get cache error",
fields: fields{
store: &proxytest.MockStore{
GetResourceFromCacheFunc: func(ctx context.Context, gvr schema.GroupVersionResource, namespace, name string) (runtime.Object, string, error) {
return nil, "", errors.New("test error")
},
},
},
args: args{
requestInfo: &request.RequestInfo{Verb: "get"},
},
want: want{
err: errors.New("test error"),
},
},
{
name: "cluster not found",
fields: fields{
store: &proxytest.MockStore{
GetResourceFromCacheFunc: func(ctx context.Context, gvr schema.GroupVersionResource, namespace, name string) (runtime.Object, string, error) {
return nil, "cluster1", nil
},
},
},
args: args{
requestInfo: &request.RequestInfo{Verb: "get"},
},
want: want{
err: apierrors.NewNotFound(proxytest.ClusterGVR.GroupResource(), "cluster1"),
},
},
{
name: "API endpoint of cluster cluster1 should not be empty",
fields: fields{
store: &proxytest.MockStore{
GetResourceFromCacheFunc: func(ctx context.Context, gvr schema.GroupVersionResource, namespace, name string) (runtime.Object, string, error) {
return nil, "cluster1", nil
},
},
clusters: []runtime.Object{&clusterv1alpha1.Cluster{
ObjectMeta: metav1.ObjectMeta{Name: "cluster1"},
Spec: clusterv1alpha1.ClusterSpec{},
}},
},
args: args{
requestInfo: &request.RequestInfo{Verb: "get"},
},
want: want{
err: errors.New("API endpoint of cluster cluster1 should not be empty"),
},
},
{
name: "impersonatorSecretRef is nil",
fields: fields{
store: &proxytest.MockStore{
GetResourceFromCacheFunc: func(ctx context.Context, gvr schema.GroupVersionResource, namespace, name string) (runtime.Object, string, error) {
return nil, "cluster1", nil
},
},
clusters: []runtime.Object{&clusterv1alpha1.Cluster{
ObjectMeta: metav1.ObjectMeta{Name: "cluster1"},
Spec: clusterv1alpha1.ClusterSpec{
APIEndpoint: s.URL,
},
}},
},
args: args{
requestInfo: &request.RequestInfo{Verb: "get"},
},
want: want{
err: errors.New("the impersonatorSecretRef of cluster cluster1 is nil"),
},
},
{
name: "secret not found",
fields: fields{
store: &proxytest.MockStore{
GetResourceFromCacheFunc: func(ctx context.Context, gvr schema.GroupVersionResource, namespace, name string) (runtime.Object, string, error) {
return nil, "cluster1", nil
},
},
clusters: []runtime.Object{&clusterv1alpha1.Cluster{
ObjectMeta: metav1.ObjectMeta{Name: "cluster1"},
Spec: clusterv1alpha1.ClusterSpec{
APIEndpoint: s.URL,
ImpersonatorSecretRef: &clusterv1alpha1.LocalSecretReference{
Namespace: "default",
Name: "secret",
},
},
}},
},
args: args{
requestInfo: &request.RequestInfo{Verb: "get"},
},
want: want{
err: apierrors.NewNotFound(proxytest.SecretGVR.GroupResource(), "secret"),
},
},
{
name: "response ok",
fields: fields{
store: &proxytest.MockStore{
GetResourceFromCacheFunc: func(ctx context.Context, gvr schema.GroupVersionResource, namespace, name string) (runtime.Object, string, error) {
return nil, "cluster1", nil
},
},
clusters: []runtime.Object{&clusterv1alpha1.Cluster{
ObjectMeta: metav1.ObjectMeta{Name: "cluster1"},
Spec: clusterv1alpha1.ClusterSpec{
APIEndpoint: s.URL,
ImpersonatorSecretRef: &clusterv1alpha1.LocalSecretReference{
Namespace: "default",
Name: "secret",
},
},
}},
secrets: []runtime.Object{&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "secret",
},
Data: map[string][]byte{
clusterapis.SecretTokenKey: []byte("token"),
},
}},
},
args: args{
requestInfo: &request.RequestInfo{Verb: "get"},
request: makeRequest(reqCtx, "GET", "/test", nil),
},
want: want{
err: nil,
body: "ok",
},
},
{
name: "update error",
fields: fields{
store: &proxytest.MockStore{
GetResourceFromCacheFunc: func(ctx context.Context, gvr schema.GroupVersionResource, namespace, name string) (runtime.Object, string, error) {
return nil, "cluster1", nil
},
},
clusters: []runtime.Object{&clusterv1alpha1.Cluster{
ObjectMeta: metav1.ObjectMeta{Name: "cluster1"},
Spec: clusterv1alpha1.ClusterSpec{
APIEndpoint: s.URL,
ImpersonatorSecretRef: &clusterv1alpha1.LocalSecretReference{
Namespace: "default",
Name: "secret",
},
},
}},
secrets: []runtime.Object{&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "secret",
},
Data: map[string][]byte{
clusterapis.SecretTokenKey: []byte("token"),
},
}},
},
args: args{
requestInfo: &request.RequestInfo{Verb: "update"},
request: (&http.Request{
Method: "PUT",
URL: &url.URL{Scheme: "https", Host: "localhost", Path: "/test"},
Body: ioutil.NopCloser(&alwaysErrorReader{}),
ContentLength: 10,
Header: make(http.Header),
}).WithContext(reqCtx),
},
want: want{
err: nil,
body: io.ErrUnexpectedEOF.Error(),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stopCh := make(chan struct{})
defer close(stopCh)
kubeFactory := informers.NewSharedInformerFactory(fake.NewSimpleClientset(tt.fields.secrets...), 0)
karmadaFactory := karmadainformers.NewSharedInformerFactory(karmadafake.NewSimpleClientset(tt.fields.clusters...), 0)
c := &Cluster{
store: tt.fields.store,
clusterLister: karmadaFactory.Cluster().V1alpha1().Clusters().Lister(),
secretLister: kubeFactory.Core().V1().Secrets().Lister(),
}
kubeFactory.Start(stopCh)
karmadaFactory.Start(stopCh)
kubeFactory.WaitForCacheSync(stopCh)
karmadaFactory.WaitForCacheSync(stopCh)
response := httptest.NewRecorder()
h, err := c.Connect(context.TODO(), framework.ProxyRequest{
RequestInfo: tt.args.requestInfo,
GroupVersionResource: proxytest.PodGVR,
ProxyPath: "/proxy",
Responder: utiltest.NewResponder(response),
HTTPReq: tt.args.request,
})
if !proxytest.ErrorMessageEquals(err, tt.want.err) {
t.Errorf("Connect() error = %v, want %v", err, tt.want.err)
return
}
if err != nil {
return
}
if h == nil {
t.Error("got handler nil")
}
h.ServeHTTP(response, tt.args.request)
body := response.Body.String()
if body != tt.want.body {
t.Errorf("got body = %v, want %v", body, tt.want.body)
}
})
}
}
func makeRequest(ctx context.Context, method, url string, body io.Reader) *http.Request {
if ctx == nil {
ctx = context.TODO()
}
r, err := http.NewRequestWithContext(ctx, method, url, body)
if err != nil {
panic(err)
}
return r
}
type alwaysErrorReader struct{}
func (alwaysErrorReader) Read([]byte) (int, error) {
return 0, io.ErrUnexpectedEOF
}