/* 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 apiserver import ( "io" "net/http" "net/http/httptest" "net/http/httputil" "testing" "github.com/google/go-cmp/cmp" apiequality "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/tools/cache" apiregistration "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" aggregatorscheme "k8s.io/kube-aggregator/pkg/apiserver/scheme" listers "k8s.io/kube-aggregator/pkg/client/listers/apiregistration/v1" ) func TestAPIs(t *testing.T) { tests := []struct { name string enabled sets.String apiservices []*apiregistration.APIService expected *metav1.APIGroupList }{ { name: "empty", enabled: sets.NewString("v1", "v1beta1"), apiservices: []*apiregistration.APIService{}, expected: &metav1.APIGroupList{ TypeMeta: metav1.TypeMeta{Kind: "APIGroupList", APIVersion: "v1"}, Groups: []metav1.APIGroup{ discoveryGroup(sets.NewString("v1", "v1beta1")), }, }, }, { name: "v1 only", enabled: sets.NewString("v1"), apiservices: []*apiregistration.APIService{}, expected: &metav1.APIGroupList{ TypeMeta: metav1.TypeMeta{Kind: "APIGroupList", APIVersion: "v1"}, Groups: []metav1.APIGroup{ discoveryGroup(sets.NewString("v1")), }, }, }, { name: "simple add", enabled: sets.NewString("v1", "v1beta1"), apiservices: []*apiregistration.APIService{ { ObjectMeta: metav1.ObjectMeta{Name: "v1.foo"}, Spec: apiregistration.APIServiceSpec{ Service: &apiregistration.ServiceReference{ Namespace: "ns", Name: "api", }, Group: "foo", Version: "v1", GroupPriorityMinimum: 11, }, Status: apiregistration.APIServiceStatus{ Conditions: []apiregistration.APIServiceCondition{ {Type: apiregistration.Available, Status: apiregistration.ConditionTrue}, }, }, }, { ObjectMeta: metav1.ObjectMeta{Name: "v1.bar"}, Spec: apiregistration.APIServiceSpec{ Service: &apiregistration.ServiceReference{ Namespace: "ns", Name: "api", }, Group: "bar", Version: "v1", GroupPriorityMinimum: 10, }, Status: apiregistration.APIServiceStatus{ Conditions: []apiregistration.APIServiceCondition{ {Type: apiregistration.Available, Status: apiregistration.ConditionTrue}, }, }, }, }, expected: &metav1.APIGroupList{ TypeMeta: metav1.TypeMeta{Kind: "APIGroupList", APIVersion: "v1"}, Groups: []metav1.APIGroup{ discoveryGroup(sets.NewString("v1", "v1beta1")), { Name: "foo", Versions: []metav1.GroupVersionForDiscovery{ { GroupVersion: "foo/v1", Version: "v1", }, }, PreferredVersion: metav1.GroupVersionForDiscovery{ GroupVersion: "foo/v1", Version: "v1", }, }, { Name: "bar", Versions: []metav1.GroupVersionForDiscovery{ { GroupVersion: "bar/v1", Version: "v1", }, }, PreferredVersion: metav1.GroupVersionForDiscovery{ GroupVersion: "bar/v1", Version: "v1", }, }, }, }, }, { name: "sorting", enabled: sets.NewString("v1", "v1beta1"), apiservices: []*apiregistration.APIService{ { ObjectMeta: metav1.ObjectMeta{Name: "v1.foo"}, Spec: apiregistration.APIServiceSpec{ Service: &apiregistration.ServiceReference{ Namespace: "ns", Name: "api", }, Group: "foo", Version: "v1", GroupPriorityMinimum: 20, VersionPriority: 10, }, Status: apiregistration.APIServiceStatus{ Conditions: []apiregistration.APIServiceCondition{ {Type: apiregistration.Available, Status: apiregistration.ConditionTrue}, }, }, }, { ObjectMeta: metav1.ObjectMeta{Name: "v2.bar"}, Spec: apiregistration.APIServiceSpec{ Service: &apiregistration.ServiceReference{ Namespace: "ns", Name: "api", }, Group: "bar", Version: "v2", GroupPriorityMinimum: 11, }, Status: apiregistration.APIServiceStatus{ Conditions: []apiregistration.APIServiceCondition{ {Type: apiregistration.Available, Status: apiregistration.ConditionTrue}, }, }, }, { ObjectMeta: metav1.ObjectMeta{Name: "v2.foo"}, Spec: apiregistration.APIServiceSpec{ Service: &apiregistration.ServiceReference{ Namespace: "ns", Name: "api", }, Group: "foo", Version: "v2", GroupPriorityMinimum: 1, VersionPriority: 15, }, Status: apiregistration.APIServiceStatus{ Conditions: []apiregistration.APIServiceCondition{ {Type: apiregistration.Available, Status: apiregistration.ConditionTrue}, }, }, }, { ObjectMeta: metav1.ObjectMeta{Name: "v1.bar"}, Spec: apiregistration.APIServiceSpec{ Service: &apiregistration.ServiceReference{ Namespace: "ns", Name: "api", }, Group: "bar", Version: "v1", GroupPriorityMinimum: 11, }, Status: apiregistration.APIServiceStatus{ Conditions: []apiregistration.APIServiceCondition{ {Type: apiregistration.Available, Status: apiregistration.ConditionTrue}, }, }, }, }, expected: &metav1.APIGroupList{ TypeMeta: metav1.TypeMeta{Kind: "APIGroupList", APIVersion: "v1"}, Groups: []metav1.APIGroup{ discoveryGroup(sets.NewString("v1", "v1beta1")), { Name: "foo", Versions: []metav1.GroupVersionForDiscovery{ { GroupVersion: "foo/v2", Version: "v2", }, { GroupVersion: "foo/v1", Version: "v1", }, }, PreferredVersion: metav1.GroupVersionForDiscovery{ GroupVersion: "foo/v2", Version: "v2", }, }, { Name: "bar", Versions: []metav1.GroupVersionForDiscovery{ { GroupVersion: "bar/v2", Version: "v2", }, { GroupVersion: "bar/v1", Version: "v1", }, }, PreferredVersion: metav1.GroupVersionForDiscovery{ GroupVersion: "bar/v2", Version: "v2", }, }, }, }, }, { name: "unavailable service", enabled: sets.NewString("v1", "v1beta1"), apiservices: []*apiregistration.APIService{ { ObjectMeta: metav1.ObjectMeta{Name: "v1.foo"}, Spec: apiregistration.APIServiceSpec{ Service: &apiregistration.ServiceReference{ Namespace: "ns", Name: "api", }, Group: "foo", Version: "v1", GroupPriorityMinimum: 11, }, Status: apiregistration.APIServiceStatus{ Conditions: []apiregistration.APIServiceCondition{ {Type: apiregistration.Available, Status: apiregistration.ConditionFalse}, }, }, }, }, expected: &metav1.APIGroupList{ TypeMeta: metav1.TypeMeta{Kind: "APIGroupList", APIVersion: "v1"}, Groups: []metav1.APIGroup{ discoveryGroup(sets.NewString("v1", "v1beta1")), { Name: "foo", Versions: []metav1.GroupVersionForDiscovery{ { GroupVersion: "foo/v1", Version: "v1", }, }, PreferredVersion: metav1.GroupVersionForDiscovery{ GroupVersion: "foo/v1", Version: "v1", }, }, }, }, }, } for _, tc := range tests { indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}) handler := &apisHandler{ codecs: aggregatorscheme.Codecs, lister: listers.NewAPIServiceLister(indexer), discoveryGroup: discoveryGroup(tc.enabled), } for _, o := range tc.apiservices { indexer.Add(o) } server := httptest.NewServer(handler) defer server.Close() resp, err := http.Get(server.URL + "/apis") if err != nil { t.Errorf("%s: %v", tc.name, err) continue } bytes, err := io.ReadAll(resp.Body) if err != nil { t.Errorf("%s: %v", tc.name, err) continue } actual := &metav1.APIGroupList{} if err := runtime.DecodeInto(aggregatorscheme.Codecs.UniversalDecoder(), bytes, actual); err != nil { t.Errorf("%s: %v", tc.name, err) continue } if !apiequality.Semantic.DeepEqual(tc.expected, actual) { t.Errorf("%s: %v", tc.name, cmp.Diff(tc.expected, actual)) continue } } } func TestAPIGroupMissing(t *testing.T) { indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}) handler := &apiGroupHandler{ codecs: aggregatorscheme.Codecs, lister: listers.NewAPIServiceLister(indexer), groupName: "groupName", delegate: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusForbidden) }), } server := httptest.NewServer(handler) defer server.Close() // this call should delegate resp, err := http.Get(server.URL + "/apis/groupName/foo") if err != nil { t.Fatal(err) } if resp.StatusCode != http.StatusForbidden { t.Fatalf("expected %v, got %v", http.StatusForbidden, resp.StatusCode) } // groupName still has no api services for it (like it was deleted), it should delegate resp, err = http.Get(server.URL + "/apis/groupName/") if err != nil { t.Fatal(err) } if resp.StatusCode != http.StatusForbidden { t.Fatalf("expected %v, got %v", http.StatusForbidden, resp.StatusCode) } // missing group should delegate still has no api services for it (like it was deleted) resp, err = http.Get(server.URL + "/apis/missing") if err != nil { t.Fatal(err) } if resp.StatusCode != http.StatusForbidden { t.Fatalf("expected %v, got %v", http.StatusForbidden, resp.StatusCode) } } func TestAPIGroup(t *testing.T) { tests := []struct { name string group string apiservices []*apiregistration.APIService expected *metav1.APIGroup }{ { name: "sorting", group: "foo", apiservices: []*apiregistration.APIService{ { ObjectMeta: metav1.ObjectMeta{Name: "v1.foo"}, Spec: apiregistration.APIServiceSpec{ Service: &apiregistration.ServiceReference{ Namespace: "ns", Name: "api", }, Group: "foo", Version: "v1", GroupPriorityMinimum: 20, VersionPriority: 10, }, Status: apiregistration.APIServiceStatus{ Conditions: []apiregistration.APIServiceCondition{ {Type: apiregistration.Available, Status: apiregistration.ConditionTrue}, }, }, }, { ObjectMeta: metav1.ObjectMeta{Name: "v2.bar"}, Spec: apiregistration.APIServiceSpec{ Service: &apiregistration.ServiceReference{ Namespace: "ns", Name: "api", }, Group: "bar", Version: "v2", GroupPriorityMinimum: 11, }, Status: apiregistration.APIServiceStatus{ Conditions: []apiregistration.APIServiceCondition{ {Type: apiregistration.Available, Status: apiregistration.ConditionTrue}, }, }, }, { ObjectMeta: metav1.ObjectMeta{Name: "v2.foo"}, Spec: apiregistration.APIServiceSpec{ Service: &apiregistration.ServiceReference{ Namespace: "ns", Name: "api", }, Group: "foo", Version: "v2", GroupPriorityMinimum: 1, VersionPriority: 15, }, Status: apiregistration.APIServiceStatus{ Conditions: []apiregistration.APIServiceCondition{ {Type: apiregistration.Available, Status: apiregistration.ConditionTrue}, }, }, }, { ObjectMeta: metav1.ObjectMeta{Name: "v1.bar"}, Spec: apiregistration.APIServiceSpec{ Service: &apiregistration.ServiceReference{ Namespace: "ns", Name: "api", }, Group: "bar", Version: "v1", GroupPriorityMinimum: 11, }, Status: apiregistration.APIServiceStatus{ Conditions: []apiregistration.APIServiceCondition{ {Type: apiregistration.Available, Status: apiregistration.ConditionTrue}, }, }, }, }, expected: &metav1.APIGroup{ TypeMeta: metav1.TypeMeta{Kind: "APIGroup", APIVersion: "v1"}, Name: "foo", Versions: []metav1.GroupVersionForDiscovery{ { GroupVersion: "foo/v2", Version: "v2", }, { GroupVersion: "foo/v1", Version: "v1", }, }, PreferredVersion: metav1.GroupVersionForDiscovery{ GroupVersion: "foo/v2", Version: "v2", }, }, }, } for _, tc := range tests { indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}) handler := &apiGroupHandler{ codecs: aggregatorscheme.Codecs, lister: listers.NewAPIServiceLister(indexer), groupName: "foo", } for _, o := range tc.apiservices { indexer.Add(o) } server := httptest.NewServer(handler) defer server.Close() resp, err := http.Get(server.URL + "/apis/" + tc.group) if err != nil { t.Errorf("%s: %v", tc.name, err) continue } if resp.StatusCode != http.StatusOK { response, _ := httputil.DumpResponse(resp, true) t.Errorf("%s: %v", tc.name, string(response)) continue } bytes, err := io.ReadAll(resp.Body) if err != nil { t.Errorf("%s: %v", tc.name, err) continue } actual := &metav1.APIGroup{} if err := runtime.DecodeInto(aggregatorscheme.Codecs.UniversalDecoder(), bytes, actual); err != nil { t.Errorf("%s: %v", tc.name, err) continue } if !apiequality.Semantic.DeepEqual(tc.expected, actual) { t.Errorf("%s: %v", tc.name, cmp.Diff(tc.expected, actual)) continue } } }