package proxy import ( "context" "fmt" "net/http" "reflect" "sync" "testing" "time" "github.com/google/go-cmp/cmp" corev1 "k8s.io/api/core/v1" metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/diff" "k8s.io/apimachinery/pkg/watch" "k8s.io/apiserver/pkg/endpoints/request" "github.com/karmada-io/karmada/pkg/search/proxy/store" "github.com/karmada-io/karmada/pkg/util/lifted" ) var ( podGVR = corev1.SchemeGroupVersion.WithResource("pods") nodeGVR = corev1.SchemeGroupVersion.WithResource("nodes") ) func TestCacheProxy_connect(t *testing.T) { type args struct { url string } type want struct { namespace string name string gvr schema.GroupVersionResource getOptions *metav1.GetOptions listOptions *metainternalversion.ListOptions } var actual want p := &cacheProxy{ store: &restReaderFuncs{ GetFunc: func(ctx context.Context, gvr schema.GroupVersionResource, name string, options *metav1.GetOptions) (runtime.Object, error) { actual = want{} actual.namespace = request.NamespaceValue(ctx) actual.name = name actual.gvr = gvr actual.getOptions = options return nil, nil }, ListFunc: func(ctx context.Context, gvr schema.GroupVersionResource, options *metainternalversion.ListOptions) (runtime.Object, error) { actual = want{} actual.namespace = request.NamespaceValue(ctx) actual.gvr = gvr actual.listOptions = options return nil, nil }, WatchFunc: func(ctx context.Context, gvr schema.GroupVersionResource, options *metainternalversion.ListOptions) (watch.Interface, error) { actual = want{} actual.namespace = request.NamespaceValue(ctx) actual.gvr = gvr actual.listOptions = options w := newEmptyWatch() // avoid block in ServeHTTP time.AfterFunc(time.Millisecond*10, func() { w.Stop() }) return w, nil }, }, restMapper: restMapper, } tests := []struct { name string args args want want }{ { name: "get node", args: args{ url: "/api/v1/nodes/foo", }, want: want{ name: "foo", gvr: nodeGVR, getOptions: &metav1.GetOptions{}, }, }, { name: "get pod", args: args{ url: "/api/v1/namespaces/default/pods/foo", }, want: want{ namespace: "default", name: "foo", gvr: podGVR, getOptions: &metav1.GetOptions{}, }, }, { name: "get pod with options", args: args{ url: "/api/v1/namespaces/default/pods/foo?resourceVersion=1000", }, want: want{ namespace: "default", name: "foo", gvr: podGVR, getOptions: &metav1.GetOptions{ResourceVersion: "1000"}, }, }, { name: "list nodes", args: args{ url: "/api/v1/nodes", }, want: want{ gvr: nodeGVR, listOptions: &metainternalversion.ListOptions{}, }, }, { name: "list pod", args: args{ url: "/api/v1/namespaces/default/pods", }, want: want{ namespace: "default", gvr: podGVR, listOptions: &metainternalversion.ListOptions{}, }, }, { name: "list pod with options", args: args{ url: "/api/v1/namespaces/default/pods?fieldSelector=metadata.name%3Dbar&labelSelector=app%3Dfoo&limit=500&resourceVersion=1000&container=bar", }, want: want{ namespace: "default", gvr: podGVR, listOptions: &metainternalversion.ListOptions{ LabelSelector: asLabelSelector("app=foo"), FieldSelector: fields.OneTermEqualSelector("metadata.name", "bar"), ResourceVersion: "1000", Limit: 500, }, }, }, { name: "watch node", args: args{ url: "/api/v1/nodes?watch=true", }, want: want{ gvr: nodeGVR, listOptions: &metainternalversion.ListOptions{ LabelSelector: labels.NewSelector(), FieldSelector: fields.Everything(), Watch: true, }, }, }, { name: "watch pod", args: args{ url: "/api/v1/namespaces/default/pods?watch=true", }, want: want{ namespace: "default", gvr: podGVR, listOptions: &metainternalversion.ListOptions{ LabelSelector: labels.NewSelector(), FieldSelector: fields.Everything(), Watch: true, }, }, }, { name: "watch pod with options", args: args{ url: "/api/v1/namespaces/default/pods?watch=true&fieldSelector=metadata.name%3Dbar&labelSelector=app%3Dfoo&limit=500&resourceVersion=1000&container=bar", }, want: want{ namespace: "default", gvr: podGVR, listOptions: &metainternalversion.ListOptions{ LabelSelector: asLabelSelector("app=foo"), FieldSelector: fields.OneTermEqualSelector("metadata.name", "bar"), ResourceVersion: "1000", Limit: 500, Watch: true, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // reset before each test actual = want{} req, err := http.NewRequest("GET", tt.args.url, nil) if err != nil { t.Error(err) return } requestInfo := lifted.NewRequestInfo(req) req = req.WithContext(request.WithRequestInfo(req.Context(), requestInfo)) if requestInfo.Namespace != "" { req = req.WithContext(request.WithNamespace(req.Context(), requestInfo.Namespace)) } h, err := p.connect(req.Context()) if err != nil { t.Error(err) return } h.ServeHTTP(&emptyResponseWriter{}, req) if tt.want.namespace != actual.namespace { t.Errorf("want namespace %v, get %v", tt.want.namespace, actual.namespace) } if tt.want.name != actual.name { t.Errorf("want name %v, get %v", tt.want.name, actual.name) } if tt.want.gvr != actual.gvr { t.Errorf("want gvr %v, get %v", tt.want.gvr, actual.gvr) } if !reflect.DeepEqual(tt.want.getOptions, actual.getOptions) { t.Errorf("getOptions diff: %v", cmp.Diff(tt.want.getOptions, actual.getOptions)) } if !reflect.DeepEqual(tt.want.listOptions, actual.listOptions) { t.Errorf("listOptions diff: %v", diff.ObjectGoPrintSideBySide(tt.want.listOptions, actual.listOptions)) } }) } } type restReaderFuncs struct { GetFunc func(ctx context.Context, gvr schema.GroupVersionResource, name string, options *metav1.GetOptions) (runtime.Object, error) ListFunc func(ctx context.Context, gvr schema.GroupVersionResource, options *metainternalversion.ListOptions) (runtime.Object, error) WatchFunc func(ctx context.Context, gvr schema.GroupVersionResource, options *metainternalversion.ListOptions) (watch.Interface, error) } var _ store.RESTReader = &restReaderFuncs{} func (r *restReaderFuncs) Get(ctx context.Context, gvr schema.GroupVersionResource, name string, options *metav1.GetOptions) (runtime.Object, error) { if r.GetFunc == nil { panic("implement me") } return r.GetFunc(ctx, gvr, name, options) } func (r *restReaderFuncs) List(ctx context.Context, gvr schema.GroupVersionResource, options *metainternalversion.ListOptions) (runtime.Object, error) { if r.GetFunc == nil { panic("implement me") } return r.ListFunc(ctx, gvr, options) } func (r *restReaderFuncs) Watch(ctx context.Context, gvr schema.GroupVersionResource, options *metainternalversion.ListOptions) (watch.Interface, error) { if r.GetFunc == nil { panic("implement me") } return r.WatchFunc(ctx, gvr, options) } type emptyResponseWriter struct{} var _ http.ResponseWriter = &emptyResponseWriter{} var _ http.Flusher = &emptyResponseWriter{} func (n *emptyResponseWriter) Header() http.Header { return make(http.Header) } func (n *emptyResponseWriter) Write([]byte) (int, error) { return 0, nil } func (n *emptyResponseWriter) WriteHeader(int) { } func (n *emptyResponseWriter) Flush() { } type emptyWatch struct { ch chan watch.Event isClosed bool lock sync.Mutex } func newEmptyWatch() watch.Interface { w := &emptyWatch{ ch: make(chan watch.Event), } return w } func (e *emptyWatch) Stop() { e.lock.Lock() defer e.lock.Unlock() if e.isClosed { return } e.isClosed = true close(e.ch) } func (e *emptyWatch) ResultChan() <-chan watch.Event { return e.ch } func asLabelSelector(s string) labels.Selector { selector, err := labels.Parse(s) if err != nil { panic(fmt.Sprintf("Fail to parse %s to labels: %v", s, err)) } return selector }