diff --git a/pkg/apiserver/cmapis.go b/pkg/apiserver/cmapis.go index 652b3e8a7..1abc43f9d 100644 --- a/pkg/apiserver/cmapis.go +++ b/pkg/apiserver/cmapis.go @@ -32,7 +32,7 @@ import ( ) func (s *CustomMetricsAdapterServer) InstallCustomMetricsAPI() error { - groupInfo := genericapiserver.NewDefaultAPIGroupInfo(custom_metrics.GroupName, Scheme, metav1.ParameterCodec, Codecs) + groupInfo := genericapiserver.NewDefaultAPIGroupInfo(custom_metrics.GroupName, Scheme, runtime.NewParameterCodec(Scheme), Codecs) mainGroupVer := groupInfo.PrioritizedVersions[0] preferredVersionForDiscovery := metav1.GroupVersionForDiscovery{ diff --git a/pkg/apiserver/installer/apiserver_test.go b/pkg/apiserver/installer/apiserver_test.go index 655f4408f..357e8c03d 100644 --- a/pkg/apiserver/installer/apiserver_test.go +++ b/pkg/apiserver/installer/apiserver_test.go @@ -85,9 +85,9 @@ func init() { &metav1.APIResourceList{}, ) - customMetricsGroupInfo = genericapiserver.NewDefaultAPIGroupInfo(custom_metrics.GroupName, Scheme, metav1.ParameterCodec, Codecs) + customMetricsGroupInfo = genericapiserver.NewDefaultAPIGroupInfo(custom_metrics.GroupName, Scheme, runtime.NewParameterCodec(Scheme), Codecs) customMetricsGroupVersion = customMetricsGroupInfo.PrioritizedVersions[0] - externalMetricsGroupInfo = genericapiserver.NewDefaultAPIGroupInfo(external_metrics.GroupName, Scheme, metav1.ParameterCodec, Codecs) + externalMetricsGroupInfo = genericapiserver.NewDefaultAPIGroupInfo(external_metrics.GroupName, Scheme, runtime.NewParameterCodec(Scheme), Codecs) externalMetricsGroupVersion = externalMetricsGroupInfo.PrioritizedVersions[0] } @@ -165,7 +165,7 @@ func handleExternalMetrics(prov provider.ExternalMetricsProvider) http.Handler { Linker: runtime.SelfLinker(meta.NewAccessor()), }, ResourceLister: provider.NewExternalMetricResourceLister(prov), - Handlers: &CMHandlers{}, + Handlers: &EMHandlers{}, } if err := group.InstallREST(container); err != nil { @@ -186,7 +186,7 @@ type fakeCMProvider struct { metrics []provider.CustomMetricInfo } -func (p *fakeCMProvider) valuesFor(name types.NamespacedName, info provider.CustomMetricInfo) (string, []custom_metrics.MetricValue, bool) { +func (p *fakeCMProvider) valuesFor(name types.NamespacedName, info provider.CustomMetricInfo, metricSelector labels.Selector) (string, []custom_metrics.MetricValue, bool) { if info.Namespaced { metricId := name.Namespace + "/" + info.GroupResource.String() + "/" + name.Name + "/" + info.Metric values, ok := p.namespacedValues[metricId] @@ -198,8 +198,8 @@ func (p *fakeCMProvider) valuesFor(name types.NamespacedName, info provider.Cust } } -func (p *fakeCMProvider) GetMetricByName(name types.NamespacedName, info provider.CustomMetricInfo) (*custom_metrics.MetricValue, error) { - metricId, values, ok := p.valuesFor(name, info) +func (p *fakeCMProvider) GetMetricByName(name types.NamespacedName, info provider.CustomMetricInfo, metricSelector labels.Selector) (*custom_metrics.MetricValue, error) { + metricId, values, ok := p.valuesFor(name, info, metricSelector) if !ok { return nil, fmt.Errorf("non-existent metric requested (id: %s)", metricId) } @@ -207,8 +207,8 @@ func (p *fakeCMProvider) GetMetricByName(name types.NamespacedName, info provide return &values[0], nil } -func (p *fakeCMProvider) GetMetricBySelector(namespace string, selector labels.Selector, info provider.CustomMetricInfo) (*custom_metrics.MetricValueList, error) { - metricId, values, ok := p.valuesFor(types.NamespacedName{Namespace: namespace, Name: "*"}, info) +func (p *fakeCMProvider) GetMetricBySelector(namespace string, selector labels.Selector, info provider.CustomMetricInfo, metricSelector labels.Selector) (*custom_metrics.MetricValueList, error) { + metricId, values, ok := p.valuesFor(types.NamespacedName{Namespace: namespace, Name: "*"}, info, metricSelector) if !ok { return nil, fmt.Errorf("non-existent metric requested (id: %s)", metricId) } diff --git a/pkg/apiserver/installer/cmhandlers.go b/pkg/apiserver/installer/cmhandlers.go index 7c5ac3ed1..85edb1bc6 100644 --- a/pkg/apiserver/installer/cmhandlers.go +++ b/pkg/apiserver/installer/cmhandlers.go @@ -47,7 +47,7 @@ func (ch *CMHandlers) registerResourceHandlers(a *MetricsAPIInstaller, ws *restf kind := fqKindToRegister.Kind - lister := a.group.DynamicStorage.(rest.Lister) + lister := a.group.DynamicStorage.(rest.ListerWithOptions) list := lister.NewList() listGVKs, _, err := a.group.Typer.ObjectKinds(list) if err != nil { @@ -64,6 +64,20 @@ func (ch *CMHandlers) registerResourceHandlers(a *MetricsAPIInstaller, ws *restf return err } + listOptions, _, _ := lister.NewListOptions() + listOptionsInternalKinds, _, err := a.group.Typer.ObjectKinds(listOptions) + if err != nil { + return err + } + listOptionsInternalKind := listOptionsInternalKinds[0] + versionedListExtraOptions, err := a.group.Creater.New(a.group.GroupVersion.WithKind(listOptionsInternalKind.Kind)) + if err != nil { + versionedListExtraOptions, err = a.group.Creater.New(optionsExternalVersion.WithKind(listOptionsInternalKind.Kind)) + if err != nil { + return err + } + } + nameParam := ws.PathParameter("name", "name of the described resource").DataType("string") resourceParam := ws.PathParameter("resource", "the name of the resource").DataType("string") subresourceParam := ws.PathParameter("subresource", "the name of the subresource").DataType("string") @@ -135,7 +149,7 @@ func (ch *CMHandlers) registerResourceHandlers(a *MetricsAPIInstaller, ws *restf reqScope.Subresource, "cluster", "custom-metrics", - restfulListResource(lister, nil, reqScope, false, a.minRequestTimeout), + restfulListResourceWithOptions(lister, nil, reqScope, false, a.minRequestTimeout), ) // install the root-scoped route @@ -149,6 +163,9 @@ func (ch *CMHandlers) registerResourceHandlers(a *MetricsAPIInstaller, ws *restf if err := addObjectParams(ws, rootScopedRoute, versionedListOptions); err != nil { return err } + if err := addObjectParams(ws, rootScopedRoute, versionedListExtraOptions); err != nil { + return err + } addParams(rootScopedRoute, rootScopedParams) ws.Route(rootScopedRoute) @@ -168,7 +185,7 @@ func (ch *CMHandlers) registerResourceHandlers(a *MetricsAPIInstaller, ws *restf reqScope.Subresource, "resource", "custom-metrics", - restfulListResource(lister, nil, reqScope, false, a.minRequestTimeout), + restfulListResourceWithOptions(lister, nil, reqScope, false, a.minRequestTimeout), ) namespacedRoute := ws.GET(namespacedPath).To(namespacedHandler). @@ -181,6 +198,9 @@ func (ch *CMHandlers) registerResourceHandlers(a *MetricsAPIInstaller, ws *restf if err := addObjectParams(ws, namespacedRoute, versionedListOptions); err != nil { return err } + if err := addObjectParams(ws, namespacedRoute, versionedListExtraOptions); err != nil { + return err + } addParams(namespacedRoute, namespacedParams) ws.Route(namespacedRoute) @@ -201,7 +221,7 @@ func (ch *CMHandlers) registerResourceHandlers(a *MetricsAPIInstaller, ws *restf reqScope.Subresource, "resource", "custom-metrics", - restfulListResource(lister, nil, reqScope, false, a.minRequestTimeout), + restfulListResourceWithOptions(lister, nil, reqScope, false, a.minRequestTimeout), ) namespaceSpecificRoute := ws.GET(namespaceSpecificPath).To(namespaceSpecificHandler). @@ -214,6 +234,9 @@ func (ch *CMHandlers) registerResourceHandlers(a *MetricsAPIInstaller, ws *restf if err := addObjectParams(ws, namespaceSpecificRoute, versionedListOptions); err != nil { return err } + if err := addObjectParams(ws, namespaceSpecificRoute, versionedListExtraOptions); err != nil { + return err + } addParams(namespaceSpecificRoute, namespaceSpecificParams) ws.Route(namespaceSpecificRoute) diff --git a/pkg/apiserver/installer/installer.go b/pkg/apiserver/installer/installer.go index 5256990d4..dbe164df1 100644 --- a/pkg/apiserver/installer/installer.go +++ b/pkg/apiserver/installer/installer.go @@ -286,3 +286,9 @@ func restfulListResource(r rest.Lister, rw rest.Watcher, scope handlers.RequestS handlers.ListResource(r, rw, scope, forceWatch, minRequestTimeout)(res.ResponseWriter, req.Request) } } + +func restfulListResourceWithOptions(r rest.ListerWithOptions, rw rest.Watcher, scope handlers.RequestScope, forceWatch bool, minRequestTimeout time.Duration) restful.RouteFunction { + return func(req *restful.Request, res *restful.Response) { + handlers.ListResourceWithOptions(r, rw, scope, forceWatch, minRequestTimeout)(res.ResponseWriter, req.Request) + } +} diff --git a/pkg/provider/errors.go b/pkg/provider/errors.go index d14b7e614..821f2addb 100644 --- a/pkg/provider/errors.go +++ b/pkg/provider/errors.go @@ -23,6 +23,7 @@ import ( apierr "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -47,3 +48,14 @@ func NewMetricNotFoundForError(resource schema.GroupResource, metricName string, Message: fmt.Sprintf("the server could not find the metric %s for %s %s", metricName, resource.String(), resourceName), }} } + +// NewMetricNotFoundForError returns a StatusError indicating the given metric could not be found for +// the given named object. It is similar to NewNotFound, but more specialized +func NewMetricNotFoundForSelectorError(resource schema.GroupResource, metricName string, resourceName string, selector labels.Selector) *apierr.StatusError { + return &apierr.StatusError{metav1.Status{ + Status: metav1.StatusFailure, + Code: int32(http.StatusNotFound), + Reason: metav1.StatusReasonNotFound, + Message: fmt.Sprintf("the server could not find the metric %s for %s %s with selector %s", metricName, resource.String(), resourceName, selector.String()), + }} +} diff --git a/pkg/provider/interfaces.go b/pkg/provider/interfaces.go index 4c7cc4db5..cde7e5cce 100644 --- a/pkg/provider/interfaces.go +++ b/pkg/provider/interfaces.go @@ -84,11 +84,11 @@ func (i CustomMetricInfo) Normalized(mapper apimeta.RESTMapper) (normalizedInfo type CustomMetricsProvider interface { // GetMetricByName fetches a particular metric for a particular object. // The namespace will be empty if the metric is root-scoped. - GetMetricByName(name types.NamespacedName, info CustomMetricInfo) (*custom_metrics.MetricValue, error) + GetMetricByName(name types.NamespacedName, info CustomMetricInfo, metricSelector labels.Selector) (*custom_metrics.MetricValue, error) // GetMetricBySelector fetches a particular metric for a set of objects matching // the given label selector. The namespace will be empty if the metric is root-scoped. - GetMetricBySelector(namespace string, selector labels.Selector, info CustomMetricInfo) (*custom_metrics.MetricValueList, error) + GetMetricBySelector(namespace string, selector labels.Selector, info CustomMetricInfo, metricSelector labels.Selector) (*custom_metrics.MetricValueList, error) // ListAllMetrics provides a list of all available metrics at // the current time. Note that this is not allowed to return diff --git a/pkg/registry/custom_metrics/reststorage.go b/pkg/registry/custom_metrics/reststorage.go index 8833cec71..61fb0563d 100644 --- a/pkg/registry/custom_metrics/reststorage.go +++ b/pkg/registry/custom_metrics/reststorage.go @@ -37,7 +37,7 @@ type REST struct { } var _ rest.Storage = &REST{} -var _ rest.Lister = &REST{} +var _ rest.ListerWithOptions = &REST{} func NewREST(cmProvider provider.CustomMetricsProvider) *REST { return &REST{ @@ -51,19 +51,37 @@ func (r *REST) New() runtime.Object { return &custom_metrics.MetricValue{} } -// Implement Lister +// Implement ListerWithOptions func (r *REST) NewList() runtime.Object { return &custom_metrics.MetricValueList{} } -func (r *REST) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) { +func (r *REST) NewListOptions() (runtime.Object, bool, string) { + return &custom_metrics.MetricListOptions{}, true, "metricName" +} + +func (r *REST) List(ctx context.Context, options *metainternalversion.ListOptions, metricOpts runtime.Object) (runtime.Object, error) { + metricOptions, ok := metricOpts.(*custom_metrics.MetricListOptions) + if !ok { + return nil, fmt.Errorf("invalid options object: %#v", options) + } + // populate the label selector, defaulting to all selector := labels.Everything() if options != nil && options.LabelSelector != nil { selector = options.LabelSelector } + metricLabelSelector := labels.Everything() + if metricOptions != nil && len(metricOptions.MetricLabelSelector) > 0 { + sel, err := labels.Parse(metricOptions.MetricLabelSelector) + if err != nil { + return nil, err + } + metricLabelSelector = sel + } + // grab the name, if present, from the field selector list options // (this is how the list handler logic injects it) // (otherwise we'd have to write a custom list handler) @@ -97,18 +115,18 @@ func (r *REST) List(ctx context.Context, options *metainternalversion.ListOption // handle namespaced and root metrics if name == "*" { - return r.handleWildcardOp(namespace, groupResource, selector, metricName) + return r.handleWildcardOp(namespace, groupResource, selector, metricName, metricLabelSelector) } else { - return r.handleIndividualOp(namespace, groupResource, name, metricName) + return r.handleIndividualOp(namespace, groupResource, name, metricName, metricLabelSelector) } } -func (r *REST) handleIndividualOp(namespace string, groupResource schema.GroupResource, name string, metricName string) (*custom_metrics.MetricValueList, error) { +func (r *REST) handleIndividualOp(namespace string, groupResource schema.GroupResource, name string, metricName string, metricLabelSelector labels.Selector) (*custom_metrics.MetricValueList, error) { singleRes, err := r.cmProvider.GetMetricByName(types.NamespacedName{Namespace: namespace, Name: name}, provider.CustomMetricInfo{ GroupResource: groupResource, Metric: metricName, Namespaced: namespace != "", - }) + }, metricLabelSelector) if err != nil { return nil, err } @@ -118,10 +136,10 @@ func (r *REST) handleIndividualOp(namespace string, groupResource schema.GroupRe }, nil } -func (r *REST) handleWildcardOp(namespace string, groupResource schema.GroupResource, selector labels.Selector, metricName string) (*custom_metrics.MetricValueList, error) { +func (r *REST) handleWildcardOp(namespace string, groupResource schema.GroupResource, selector labels.Selector, metricName string, metricLabelSelector labels.Selector) (*custom_metrics.MetricValueList, error) { return r.cmProvider.GetMetricBySelector(namespace, selector, provider.CustomMetricInfo{ GroupResource: groupResource, Metric: metricName, Namespaced: namespace != "", - }) + }, metricLabelSelector) } diff --git a/test-adapter/provider/provider.go b/test-adapter/provider/provider.go index 26c6f8479..0a611fda7 100644 --- a/test-adapter/provider/provider.go +++ b/test-adapter/provider/provider.go @@ -96,13 +96,18 @@ var ( } ) +type metricValue struct { + labels labels.Set + value resource.Quantity +} + // testingProvider is a sample implementation of provider.MetricsProvider which stores a map of fake metrics type testingProvider struct { client dynamic.Interface mapper apimeta.RESTMapper valuesLock sync.RWMutex - values map[CustomMetricResource]resource.Quantity + values map[CustomMetricResource]metricValue externalMetrics []externalMetric } @@ -111,7 +116,7 @@ func NewFakeProvider(client dynamic.Interface, mapper apimeta.RESTMapper) (provi provider := &testingProvider{ client: client, mapper: mapper, - values: make(map[CustomMetricResource]resource.Quantity), + values: make(map[CustomMetricResource]metricValue), externalMetrics: testingExternalMetrics, } return provider, provider.webService() @@ -163,6 +168,16 @@ func (p *testingProvider) updateMetric(request *restful.Request, response *restf groupResource := schema.ParseGroupResource(resourceType) + metricLabels := labels.Set{} + sel := request.QueryParameter("labels") + if len(sel) > 0 { + metricLabels, err = labels.ConvertSelectorToLabelsMap(sel) + if err != nil { + response.WriteErrorString(http.StatusBadRequest, err.Error()) + return + } + } + info := provider.CustomMetricInfo{ GroupResource: groupResource, Metric: metricName, @@ -182,11 +197,14 @@ func (p *testingProvider) updateMetric(request *restful.Request, response *restf CustomMetricInfo: info, NamespacedName: namespacedName, } - p.values[metricInfo] = *value + p.values[metricInfo] = metricValue{ + labels: metricLabels, + value: *value, + } } // valueFor is a helper function to get just the value of a specific metric -func (p *testingProvider) valueFor(info provider.CustomMetricInfo, name types.NamespacedName) (resource.Quantity, error) { +func (p *testingProvider) valueFor(info provider.CustomMetricInfo, name types.NamespacedName, metricSelector labels.Selector) (resource.Quantity, error) { info, _, err := info.Normalized(p.mapper) if err != nil { return resource.Quantity{}, err @@ -201,11 +219,15 @@ func (p *testingProvider) valueFor(info provider.CustomMetricInfo, name types.Na return resource.Quantity{}, provider.NewMetricNotFoundForError(info.GroupResource, info.Metric, name.Name) } - return value, nil + if !metricSelector.Matches(value.labels) { + return resource.Quantity{}, provider.NewMetricNotFoundForSelectorError(info.GroupResource, info.Metric, name.Name, metricSelector) + } + + return value.value, nil } // metricFor is a helper function which formats a value, metric, and object info into a MetricValue which can be returned by the metrics API -func (p *testingProvider) metricFor(value resource.Quantity, name types.NamespacedName, selector labels.Selector, info provider.CustomMetricInfo) (*custom_metrics.MetricValue, error) { +func (p *testingProvider) metricFor(value resource.Quantity, name types.NamespacedName, selector labels.Selector, info provider.CustomMetricInfo, metricSelector labels.Selector) (*custom_metrics.MetricValue, error) { objRef, err := helpers.ReferenceFor(p.mapper, name, info) if err != nil { return nil, err @@ -220,19 +242,19 @@ func (p *testingProvider) metricFor(value resource.Quantity, name types.Namespac Value: value, } - if len(selector.String()) > 0 { - labelSelector, err := metav1.ParseToLabelSelector(selector.String()) + if len(metricSelector.String()) > 0 { + sel, err := metav1.ParseToLabelSelector(metricSelector.String()) if err != nil { return nil, err } - metric.Metric.Selector = labelSelector + metric.Metric.Selector = sel } return metric, nil } // metricsFor is a wrapper used by GetMetricBySelector to format several metrics which match a resource selector -func (p *testingProvider) metricsFor(namespace string, selector labels.Selector, info provider.CustomMetricInfo) (*custom_metrics.MetricValueList, error) { +func (p *testingProvider) metricsFor(namespace string, selector labels.Selector, info provider.CustomMetricInfo, metricSelector labels.Selector) (*custom_metrics.MetricValueList, error) { names, err := helpers.ListObjectNames(p.mapper, p.client, namespace, selector, info) if err != nil { return nil, err @@ -241,7 +263,7 @@ func (p *testingProvider) metricsFor(namespace string, selector labels.Selector, res := make([]custom_metrics.MetricValue, 0, len(names)) for _, name := range names { namespacedName := types.NamespacedName{Name: name, Namespace: namespace} - value, err := p.valueFor(info, namespacedName) + value, err := p.valueFor(info, namespacedName, metricSelector) if err != nil { if apierr.IsNotFound(err) { continue @@ -249,7 +271,7 @@ func (p *testingProvider) metricsFor(namespace string, selector labels.Selector, return nil, err } - metric, err := p.metricFor(value, namespacedName, selector, info) + metric, err := p.metricFor(value, namespacedName, selector, info, metricSelector) if err != nil { return nil, err } @@ -261,22 +283,22 @@ func (p *testingProvider) metricsFor(namespace string, selector labels.Selector, }, nil } -func (p *testingProvider) GetMetricByName(name types.NamespacedName, info provider.CustomMetricInfo) (*custom_metrics.MetricValue, error) { +func (p *testingProvider) GetMetricByName(name types.NamespacedName, info provider.CustomMetricInfo, metricSelector labels.Selector) (*custom_metrics.MetricValue, error) { p.valuesLock.RLock() defer p.valuesLock.RUnlock() - value, err := p.valueFor(info, name) + value, err := p.valueFor(info, name, metricSelector) if err != nil { return nil, err } - return p.metricFor(value, name, labels.Everything(), info) + return p.metricFor(value, name, labels.Everything(), info, metricSelector) } -func (p *testingProvider) GetMetricBySelector(namespace string, selector labels.Selector, info provider.CustomMetricInfo) (*custom_metrics.MetricValueList, error) { +func (p *testingProvider) GetMetricBySelector(namespace string, selector labels.Selector, info provider.CustomMetricInfo, metricSelector labels.Selector) (*custom_metrics.MetricValueList, error) { p.valuesLock.RLock() defer p.valuesLock.RUnlock() - return p.metricsFor(namespace, selector, info) + return p.metricsFor(namespace, selector, info, metricSelector) } func (p *testingProvider) ListAllMetrics() []provider.CustomMetricInfo {