Skip to content

Commit e6c0e3e

Browse files
committed
Add unit test for External Metrics apiserver
1 parent 062312b commit e6c0e3e

File tree

2 files changed

+169
-72
lines changed

2 files changed

+169
-72
lines changed

pkg/apiserver/installer/apiserver_test.go

+166-69
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,16 @@ import (
3838
"k8s.io/apiserver/pkg/endpoints/request"
3939
genericapiserver "k8s.io/apiserver/pkg/server"
4040
"k8s.io/metrics/pkg/apis/custom_metrics"
41-
"k8s.io/metrics/pkg/apis/custom_metrics/install"
41+
installcm "k8s.io/metrics/pkg/apis/custom_metrics/install"
4242
cmv1beta1 "k8s.io/metrics/pkg/apis/custom_metrics/v1beta1"
43+
installem "k8s.io/metrics/pkg/apis/external_metrics/install"
44+
emv1beta1 "k8s.io/metrics/pkg/apis/external_metrics/v1beta1"
4345

4446
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/provider"
45-
metricstorage "github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/registry/custom_metrics"
47+
custommetricstorage "github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/registry/custom_metrics"
48+
externalmetricstorage "github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/registry/external_metrics"
49+
sampleprovider "github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/sample-cmd/provider"
50+
"k8s.io/metrics/pkg/apis/external_metrics"
4651
)
4752

4853
// defaultAPIServer exposes nested objects for testability.
@@ -52,20 +57,23 @@ type defaultAPIServer struct {
5257
}
5358

5459
var (
55-
groupFactoryRegistry = make(announced.APIGroupFactoryRegistry)
56-
registry = registered.NewOrDie("")
57-
Scheme = runtime.NewScheme()
58-
Codecs = serializer.NewCodecFactory(Scheme)
59-
prefix = genericapiserver.APIGroupPrefix
60-
groupVersion schema.GroupVersion
61-
groupMeta *apimachinery.GroupMeta
62-
codec = Codecs.LegacyCodec()
63-
emptySet = labels.Set{}
64-
matchingSet = labels.Set{"foo": "bar"}
60+
groupFactoryRegistry = make(announced.APIGroupFactoryRegistry)
61+
registry = registered.NewOrDie("")
62+
Scheme = runtime.NewScheme()
63+
Codecs = serializer.NewCodecFactory(Scheme)
64+
prefix = genericapiserver.APIGroupPrefix
65+
customMetricsGroupVersion schema.GroupVersion
66+
customMetricsGroupMeta *apimachinery.GroupMeta
67+
externalMetricsGroupVersion schema.GroupVersion
68+
externalMetricsGroupMeta *apimachinery.GroupMeta
69+
codec = Codecs.LegacyCodec()
70+
emptySet = labels.Set{}
71+
matchingSet = labels.Set{"foo": "bar"}
6572
)
6673

6774
func init() {
68-
install.Install(groupFactoryRegistry, registry, Scheme)
75+
installcm.Install(groupFactoryRegistry, registry, Scheme)
76+
installem.Install(groupFactoryRegistry, registry, Scheme)
6977

7078
// we need to add the options to empty v1
7179
// TODO fix the server code to avoid this
@@ -81,8 +89,10 @@ func init() {
8189
&metav1.APIResourceList{},
8290
)
8391

84-
groupMeta = registry.GroupOrDie(custom_metrics.GroupName)
85-
groupVersion = groupMeta.GroupVersion
92+
customMetricsGroupMeta = registry.GroupOrDie(custom_metrics.GroupName)
93+
customMetricsGroupVersion = customMetricsGroupMeta.GroupVersion
94+
externalMetricsGroupMeta = registry.GroupOrDie(external_metrics.GroupName)
95+
externalMetricsGroupVersion = externalMetricsGroupMeta.GroupVersion
8696
}
8797

8898
func extractBody(response *http.Response, object runtime.Object) error {
@@ -103,17 +113,17 @@ func extractBodyString(response *http.Response) (string, error) {
103113
return string(body), err
104114
}
105115

106-
func handle(prov provider.CustomMetricsProvider) http.Handler {
116+
func handleCustomMetrics(prov provider.CustomMetricsProvider) http.Handler {
107117
container := restful.NewContainer()
108118
container.Router(restful.CurlyRouter{})
109119
mux := container.ServeMux
110-
resourceStorage := metricstorage.NewREST(prov)
120+
resourceStorage := custommetricstorage.NewREST(prov)
111121
reqContextMapper := request.NewRequestContextMapper()
112122
group := &MetricsAPIGroupVersion{
113123
DynamicStorage: resourceStorage,
114124
APIGroupVersion: &genericapi.APIGroupVersion{
115125
Root: prefix,
116-
GroupVersion: groupVersion,
126+
GroupVersion: customMetricsGroupVersion,
117127

118128
ParameterCodec: metav1.ParameterCodec,
119129
Serializer: Codecs,
@@ -122,13 +132,14 @@ func handle(prov provider.CustomMetricsProvider) http.Handler {
122132
UnsafeConvertor: runtime.UnsafeObjectConvertor(Scheme),
123133
Copier: Scheme,
124134
Typer: Scheme,
125-
Linker: groupMeta.SelfLinker,
126-
Mapper: groupMeta.RESTMapper,
135+
Linker: customMetricsGroupMeta.SelfLinker,
136+
Mapper: customMetricsGroupMeta.RESTMapper,
127137

128138
Context: reqContextMapper,
129139
OptionsExternalVersion: &schema.GroupVersion{Version: "v1"},
130140
},
131-
ResourceLister: provider.NewResourceLister(prov),
141+
ResourceLister: provider.NewCustomMetricResourceLister(prov),
142+
Handlers: &CMHandlers{},
132143
}
133144

134145
if err := group.InstallREST(container); err != nil {
@@ -142,29 +153,69 @@ func handle(prov provider.CustomMetricsProvider) http.Handler {
142153
return handler
143154
}
144155

145-
type fakeProvider struct {
156+
func handleExternalMetrics(prov provider.ExternalMetricsProvider) http.Handler {
157+
container := restful.NewContainer()
158+
container.Router(restful.CurlyRouter{})
159+
mux := container.ServeMux
160+
resourceStorage := externalmetricstorage.NewREST(prov)
161+
reqContextMapper := request.NewRequestContextMapper()
162+
group := &MetricsAPIGroupVersion{
163+
DynamicStorage: resourceStorage,
164+
APIGroupVersion: &genericapi.APIGroupVersion{
165+
Root: prefix,
166+
GroupVersion: externalMetricsGroupVersion,
167+
168+
ParameterCodec: metav1.ParameterCodec,
169+
Serializer: Codecs,
170+
Creater: Scheme,
171+
Convertor: Scheme,
172+
UnsafeConvertor: runtime.UnsafeObjectConvertor(Scheme),
173+
Copier: Scheme,
174+
Typer: Scheme,
175+
Linker: externalMetricsGroupMeta.SelfLinker,
176+
Mapper: externalMetricsGroupMeta.RESTMapper,
177+
178+
Context: reqContextMapper,
179+
OptionsExternalVersion: &schema.GroupVersion{Version: "v1"},
180+
},
181+
ResourceLister: provider.NewExternalMetricResourceLister(prov),
182+
Handlers: &EMHandlers{},
183+
}
184+
185+
if err := group.InstallREST(container); err != nil {
186+
panic(fmt.Sprintf("unable to install container %s: %v", group.GroupVersion, err))
187+
}
188+
189+
var handler http.Handler = &defaultAPIServer{mux, container}
190+
reqInfoResolver := genericapiserver.NewRequestInfoResolver(&genericapiserver.Config{})
191+
handler = genericapifilters.WithRequestInfo(handler, reqInfoResolver, reqContextMapper)
192+
handler = request.WithRequestContext(handler, reqContextMapper)
193+
return handler
194+
}
195+
196+
type fakeCMProvider struct {
146197
rootValues map[string][]custom_metrics.MetricValue
147198
namespacedValues map[string][]custom_metrics.MetricValue
148199
rootSubsetCounts map[string]int
149200
namespacedSubsetCounts map[string]int
150-
metrics []provider.MetricInfo
201+
metrics []provider.CustomMetricInfo
151202
}
152203

153-
func (p *fakeProvider) GetRootScopedMetricByName(groupResource schema.GroupResource, name string, metricName string) (*custom_metrics.MetricValue, error) {
204+
func (p *fakeCMProvider) GetRootScopedMetricByName(groupResource schema.GroupResource, name string, metricName string) (*custom_metrics.MetricValue, error) {
154205
metricId := groupResource.String() + "/" + name + "/" + metricName
155206
values, ok := p.rootValues[metricId]
156207
if !ok {
157-
return nil, fmt.Errorf("non-existant metric requested (id: %s)", metricId)
208+
return nil, fmt.Errorf("non-existent metric requested (id: %s)", metricId)
158209
}
159210

160211
return &values[0], nil
161212
}
162213

163-
func (p *fakeProvider) GetRootScopedMetricBySelector(groupResource schema.GroupResource, selector labels.Selector, metricName string) (*custom_metrics.MetricValueList, error) {
214+
func (p *fakeCMProvider) GetRootScopedMetricBySelector(groupResource schema.GroupResource, selector labels.Selector, metricName string) (*custom_metrics.MetricValueList, error) {
164215
metricId := groupResource.String() + "/*/" + metricName
165216
values, ok := p.rootValues[metricId]
166217
if !ok {
167-
return nil, fmt.Errorf("non-existant metric requested (id: %s)", metricId)
218+
return nil, fmt.Errorf("non-existent metric requested (id: %s)", metricId)
168219
}
169220

170221
var trimmedValues custom_metrics.MetricValueList
@@ -193,21 +244,21 @@ func (p *fakeProvider) GetRootScopedMetricBySelector(groupResource schema.GroupR
193244
return &trimmedValues, nil
194245
}
195246

196-
func (p *fakeProvider) GetNamespacedMetricByName(groupResource schema.GroupResource, namespace string, name string, metricName string) (*custom_metrics.MetricValue, error) {
247+
func (p *fakeCMProvider) GetNamespacedMetricByName(groupResource schema.GroupResource, namespace string, name string, metricName string) (*custom_metrics.MetricValue, error) {
197248
metricId := namespace + "/" + groupResource.String() + "/" + name + "/" + metricName
198249
values, ok := p.namespacedValues[metricId]
199250
if !ok {
200-
return nil, fmt.Errorf("non-existant metric requested (id: %s)", metricId)
251+
return nil, fmt.Errorf("non-existent metric requested (id: %s)", metricId)
201252
}
202253

203254
return &values[0], nil
204255
}
205256

206-
func (p *fakeProvider) GetNamespacedMetricBySelector(groupResource schema.GroupResource, namespace string, selector labels.Selector, metricName string) (*custom_metrics.MetricValueList, error) {
257+
func (p *fakeCMProvider) GetNamespacedMetricBySelector(groupResource schema.GroupResource, namespace string, selector labels.Selector, metricName string) (*custom_metrics.MetricValueList, error) {
207258
metricId := namespace + "/" + groupResource.String() + "/*/" + metricName
208259
values, ok := p.namespacedValues[metricId]
209260
if !ok {
210-
return nil, fmt.Errorf("non-existant metric requested (id: %s)", metricId)
261+
return nil, fmt.Errorf("non-existent metric requested (id: %s)", metricId)
211262
}
212263

213264
var trimmedValues custom_metrics.MetricValueList
@@ -236,45 +287,43 @@ func (p *fakeProvider) GetNamespacedMetricBySelector(groupResource schema.GroupR
236287
return &trimmedValues, nil
237288
}
238289

239-
func (p *fakeProvider) ListAllMetrics() []provider.MetricInfo {
290+
func (p *fakeCMProvider) ListAllMetrics() []provider.CustomMetricInfo {
240291
return p.metrics
241292
}
242293

294+
type T struct {
295+
Method string
296+
Path string
297+
Status int
298+
ExpectedCount int
299+
}
300+
243301
func TestCustomMetricsAPI(t *testing.T) {
244302
totalNodesCount := 4
245303
totalPodsCount := 16
246304
matchingNodesCount := 2
247305
matchingPodsCount := 8
248306

249-
type T struct {
250-
Method string
251-
Path string
252-
Status int
253-
ExpectedCount int
254-
}
255307
cases := map[string]T{
256308
// checks which should fail
257309
"GET long prefix": {"GET", "/" + prefix + "/", http.StatusNotFound, 0},
258310

259-
"root GET missing storage": {"GET", "/" + prefix + "/" + groupVersion.Group + "/" + groupVersion.Version + "/blah", http.StatusNotFound, 0},
260-
261-
"namespaced GET long prefix": {"GET", "/" + prefix + "/", http.StatusNotFound, 0},
262-
"namespaced GET missing storage": {"GET", "/" + prefix + "/" + groupVersion.Group + "/" + groupVersion.Version + "/blah", http.StatusNotFound, 0},
311+
"root GET missing storage": {"GET", "/" + prefix + "/" + customMetricsGroupVersion.Group + "/" + customMetricsGroupVersion.Version + "/blah", http.StatusNotFound, 0},
263312

264-
"GET at root resource leaf": {"GET", "/" + prefix + "/" + groupVersion.Group + "/" + groupVersion.Version + "/nodes/foo", http.StatusNotFound, 0},
265-
"GET at namespaced resource leaft": {"GET", "/" + prefix + "/" + groupVersion.Group + "/" + groupVersion.Version + "/namespaces/ns/pods/bar", http.StatusNotFound, 0},
313+
"GET at root resource leaf": {"GET", "/" + prefix + "/" + customMetricsGroupVersion.Group + "/" + customMetricsGroupVersion.Version + "/nodes/foo", http.StatusNotFound, 0},
314+
"GET at namespaced resource leaft": {"GET", "/" + prefix + "/" + customMetricsGroupVersion.Group + "/" + customMetricsGroupVersion.Version + "/namespaces/ns/pods/bar", http.StatusNotFound, 0},
266315

267316
// Positive checks to make sure everything is wired correctly
268-
"GET for all nodes (root)": {"GET", "/" + prefix + "/" + groupVersion.Group + "/" + groupVersion.Version + "/nodes/*/some-metric", http.StatusOK, totalNodesCount},
269-
"GET for all pods (namespaced)": {"GET", "/" + prefix + "/" + groupVersion.Group + "/" + groupVersion.Version + "/namespaces/ns/pods/*/some-metric", http.StatusOK, totalPodsCount},
270-
"GET for namespace": {"GET", "/" + prefix + "/" + groupVersion.Group + "/" + groupVersion.Version + "/namespaces/ns/metrics/some-metric", http.StatusOK, 1},
271-
"GET for label selected nodes (root)": {"GET", "/" + prefix + "/" + groupVersion.Group + "/" + groupVersion.Version + "/nodes/*/some-metric?labelSelector=foo%3Dbar", http.StatusOK, matchingNodesCount},
272-
"GET for label selected pods (namespaced)": {"GET", "/" + prefix + "/" + groupVersion.Group + "/" + groupVersion.Version + "/namespaces/ns/pods/*/some-metric?labelSelector=foo%3Dbar", http.StatusOK, matchingPodsCount},
273-
"GET for single node (root)": {"GET", "/" + prefix + "/" + groupVersion.Group + "/" + groupVersion.Version + "/nodes/foo/some-metric", http.StatusOK, 1},
274-
"GET for single pod (namespaced)": {"GET", "/" + prefix + "/" + groupVersion.Group + "/" + groupVersion.Version + "/namespaces/ns/pods/foo/some-metric", http.StatusOK, 1},
317+
"GET for all nodes (root)": {"GET", "/" + prefix + "/" + customMetricsGroupVersion.Group + "/" + customMetricsGroupVersion.Version + "/nodes/*/some-metric", http.StatusOK, totalNodesCount},
318+
"GET for all pods (namespaced)": {"GET", "/" + prefix + "/" + customMetricsGroupVersion.Group + "/" + customMetricsGroupVersion.Version + "/namespaces/ns/pods/*/some-metric", http.StatusOK, totalPodsCount},
319+
"GET for namespace": {"GET", "/" + prefix + "/" + customMetricsGroupVersion.Group + "/" + customMetricsGroupVersion.Version + "/namespaces/ns/metrics/some-metric", http.StatusOK, 1},
320+
"GET for label selected nodes (root)": {"GET", "/" + prefix + "/" + customMetricsGroupVersion.Group + "/" + customMetricsGroupVersion.Version + "/nodes/*/some-metric?labelSelector=foo%3Dbar", http.StatusOK, matchingNodesCount},
321+
"GET for label selected pods (namespaced)": {"GET", "/" + prefix + "/" + customMetricsGroupVersion.Group + "/" + customMetricsGroupVersion.Version + "/namespaces/ns/pods/*/some-metric?labelSelector=foo%3Dbar", http.StatusOK, matchingPodsCount},
322+
"GET for single node (root)": {"GET", "/" + prefix + "/" + customMetricsGroupVersion.Group + "/" + customMetricsGroupVersion.Version + "/nodes/foo/some-metric", http.StatusOK, 1},
323+
"GET for single pod (namespaced)": {"GET", "/" + prefix + "/" + customMetricsGroupVersion.Group + "/" + customMetricsGroupVersion.Version + "/namespaces/ns/pods/foo/some-metric", http.StatusOK, 1},
275324
}
276325

277-
prov := &fakeProvider{
326+
prov := &fakeCMProvider{
278327
rootValues: map[string][]custom_metrics.MetricValue{
279328
"nodes/*/some-metric": make([]custom_metrics.MetricValue, totalNodesCount),
280329
"nodes/foo/some-metric": make([]custom_metrics.MetricValue, 1),
@@ -293,33 +342,59 @@ func TestCustomMetricsAPI(t *testing.T) {
293342
},
294343
}
295344

296-
server := httptest.NewServer(handle(prov))
345+
server := httptest.NewServer(handleCustomMetrics(prov))
297346
defer server.Close()
298347
client := http.Client{}
299348
for k, v := range cases {
300-
request, err := http.NewRequest(v.Method, server.URL+v.Path, nil)
301-
if err != nil {
302-
t.Fatalf("unexpected error (%s): %v", k, err)
303-
}
304-
305-
response, err := client.Do(request)
349+
response, err := executeRequest(t, k, v, server, &client)
306350
if err != nil {
307-
t.Errorf("unexpected error (%s): %v", k, err)
351+
t.Errorf(err.Error())
308352
continue
309353
}
310-
311-
if response.StatusCode != v.Status {
312-
body, err := extractBodyString(response)
313-
bodyPart := body
314-
if err != nil {
315-
bodyPart = fmt.Sprintf("[error extracting body: %v]", err)
354+
if v.ExpectedCount > 0 {
355+
lst := &cmv1beta1.MetricValueList{}
356+
if err := extractBody(response, lst); err != nil {
357+
t.Errorf("unexpected error (%s): %v", k, err)
358+
continue
359+
}
360+
if len(lst.Items) != v.ExpectedCount {
361+
t.Errorf("Expected %d items, got %d (%s): %#v", v.ExpectedCount, len(lst.Items), k, lst.Items)
362+
continue
316363
}
317-
t.Errorf("Expected %d for %s (%s), Got %#v -- %s", v.Status, v.Method, k, response, bodyPart)
318-
continue
319364
}
365+
}
366+
}
367+
368+
func TestExternalMetricsAPI(t *testing.T) {
369+
cases := map[string]T{
370+
// checks which should fail
371+
372+
"GET long prefix": {"GET", "/" + prefix + "/", http.StatusNotFound, 0},
373+
"GET at root scope": {"GET", "/" + prefix + "/" + externalMetricsGroupVersion.Group + "/" + externalMetricsGroupVersion.Version + "/nonexistent-metric", http.StatusNotFound, 0},
374+
"GET without metric name": {"GET", "/" + prefix + "/" + externalMetricsGroupVersion.Group + "/" + externalMetricsGroupVersion.Version + "/namespaces/foo", http.StatusNotFound, 0},
375+
"GET for metric with slashes": {"GET", "/" + prefix + "/" + externalMetricsGroupVersion.Group + "/" + externalMetricsGroupVersion.Version + "/namespaces/foo/group/metric", http.StatusNotFound, 0},
376+
377+
// Positive checks to make sure everything is wired correctly
378+
"GET for external metric": {"GET", "/" + prefix + "/" + externalMetricsGroupVersion.Group + "/" + externalMetricsGroupVersion.Version + "/namespaces/default/my-external-metric", http.StatusOK, 2},
379+
"GET for external metric with selector": {"GET", "/" + prefix + "/" + externalMetricsGroupVersion.Group + "/" + externalMetricsGroupVersion.Version + "/namespaces/default/my-external-metric?labelSelector=foo%3Dbar", http.StatusOK, 1},
380+
"GET for nonexistent metric": {"GET", "/" + prefix + "/" + externalMetricsGroupVersion.Group + "/" + externalMetricsGroupVersion.Version + "/namespaces/foo/nonexistent-metric", http.StatusOK, 0},
381+
}
320382

383+
// "real" fake provider implementation can be used in test, because it doesn't have any dependencies.
384+
// Note: this provider has a hardcoded list of external metrics.
385+
prov := sampleprovider.NewFakeProvider(nil, nil)
386+
387+
server := httptest.NewServer(handleExternalMetrics(prov))
388+
defer server.Close()
389+
client := http.Client{}
390+
for k, v := range cases {
391+
response, err := executeRequest(t, k, v, server, &client)
392+
if err != nil {
393+
t.Errorf(err.Error())
394+
continue
395+
}
321396
if v.ExpectedCount > 0 {
322-
lst := &cmv1beta1.MetricValueList{}
397+
lst := &emv1beta1.ExternalMetricValueList{}
323398
if err := extractBody(response, lst); err != nil {
324399
t.Errorf("unexpected error (%s): %v", k, err)
325400
continue
@@ -331,3 +406,25 @@ func TestCustomMetricsAPI(t *testing.T) {
331406
}
332407
}
333408
}
409+
410+
func executeRequest(t *testing.T, k string, v T, server *httptest.Server, client *http.Client) (*http.Response, error) {
411+
request, err := http.NewRequest(v.Method, server.URL+v.Path, nil)
412+
if err != nil {
413+
t.Fatalf("unexpected error (%s): %v", k, err)
414+
}
415+
416+
response, err := client.Do(request)
417+
if err != nil {
418+
return nil, fmt.Errorf("unexpected error (%s): %v", k, err)
419+
}
420+
421+
if response.StatusCode != v.Status {
422+
body, err := extractBodyString(response)
423+
bodyPart := body
424+
if err != nil {
425+
bodyPart = fmt.Sprintf("[error extracting body: %v]", err)
426+
}
427+
return nil, fmt.Errorf("Expected %d for %s (%s), Got %#v -- %s", v.Status, v.Method, k, response, bodyPart)
428+
}
429+
return response, nil
430+
}

0 commit comments

Comments
 (0)