diff --git a/glide.lock b/glide.lock new file mode 100644 index 000000000..2b782546d --- /dev/null +++ b/glide.lock @@ -0,0 +1,413 @@ +hash: 807f261d8170be02e0c3802e4d005c334a04419a8b2e5f75d68498385202f5ff +updated: 2017-04-05T18:21:53.389585483-04:00 +imports: +- name: bitbucket.org/ww/goautoneg + version: 75cd24fc2f2c2a2088577d12123ddee5f54e0675 +- name: github.com/beorn7/perks + version: 3ac7bf7a47d159a033b107610db8a1b6575507a4 + subpackages: + - quantile +- name: github.com/coreos/etcd + version: cc198e22d3b8fd7ec98304c95e68ee375be54589 + subpackages: + - alarm + - auth + - auth/authpb + - client + - clientv3 + - compactor + - discovery + - error + - etcdserver + - etcdserver/api + - etcdserver/api/v2http + - etcdserver/api/v2http/httptypes + - etcdserver/api/v3rpc + - etcdserver/api/v3rpc/rpctypes + - etcdserver/auth + - etcdserver/etcdserverpb + - etcdserver/membership + - etcdserver/stats + - integration + - lease + - lease/leasehttp + - lease/leasepb + - mvcc + - mvcc/backend + - mvcc/mvccpb + - pkg/adt + - pkg/contention + - pkg/crc + - pkg/fileutil + - pkg/httputil + - pkg/idutil + - pkg/ioutil + - pkg/logutil + - pkg/netutil + - pkg/pathutil + - pkg/pbutil + - pkg/runtime + - pkg/schedule + - pkg/testutil + - pkg/tlsutil + - pkg/transport + - pkg/types + - pkg/wait + - raft + - raft/raftpb + - rafthttp + - snap + - snap/snappb + - store + - version + - wal + - wal/walpb +- name: github.com/coreos/go-systemd + version: 48702e0da86bd25e76cfef347e2adeb434a0d0a6 + subpackages: + - daemon + - journal +- name: github.com/coreos/pkg + version: fa29b1d70f0beaddd4c7021607cc3c3be8ce94b8 + subpackages: + - capnslog + - health + - httputil + - timeutil +- name: github.com/davecgh/go-spew + version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d + subpackages: + - spew +- name: github.com/docker/distribution + version: cd27f179f2c10c5d300e6d09025b538c475b0d51 + subpackages: + - digest + - reference +- name: github.com/elazarl/go-bindata-assetfs + version: 3dcc96556217539f50599357fb481ac0dc7439b9 +- name: github.com/emicklei/go-restful + version: 09691a3b6378b740595c1002f40c34dd5f218a22 + subpackages: + - log + - swagger +- name: github.com/evanphx/json-patch + version: ba18e35c5c1b36ef6334cad706eb681153d2d379 +- name: github.com/ghodss/yaml + version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee +- name: github.com/go-openapi/jsonpointer + version: 46af16f9f7b149af66e5d1bd010e3574dc06de98 +- name: github.com/go-openapi/jsonreference + version: 13c6e3589ad90f49bd3e3bbe2c2cb3d7a4142272 +- name: github.com/go-openapi/spec + version: 6aced65f8501fe1217321abf0749d354824ba2ff +- name: github.com/go-openapi/swag + version: 1d0bd113de87027671077d3c71eb3ac5d7dbba72 +- name: github.com/gogo/protobuf + version: e18d7aa8f8c624c915db340349aad4c49b10d173 + subpackages: + - proto + - sortkeys +- name: github.com/golang/glog + version: 44145f04b68cf362d9c4df2182967c2275eaefed +- name: github.com/golang/groupcache + version: 02826c3e79038b59d737d3b1c0a1d937f71a4433 + subpackages: + - lru +- name: github.com/golang/protobuf + version: 8616e8ee5e20a1704615e6c8d7afcdac06087a67 + subpackages: + - jsonpb + - proto +- name: github.com/google/gofuzz + version: 44d81051d367757e1c7c6a5a86423ece9afcf63c +- name: github.com/grpc-ecosystem/grpc-gateway + version: f52d055dc48aec25854ed7d31862f78913cf17d1 + subpackages: + - runtime + - runtime/internal + - utilities +- name: github.com/howeyc/gopass + version: 3ca23474a7c7203e0a0a070fd33508f6efdb9b3d +- name: github.com/imdario/mergo + version: 6633656539c1639d9d78127b7d47c622b5d7b6dc +- name: github.com/inconshreveable/mousetrap + version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 +- name: github.com/juju/ratelimit + version: 77ed1c8a01217656d2080ad51981f6e99adaa177 +- name: github.com/mailru/easyjson + version: d5b7844b561a7bc640052f1b935f7b800330d7e0 + subpackages: + - buffer + - jlexer + - jwriter +- name: github.com/matttproud/golang_protobuf_extensions + version: fc2b8d3a73c4867e51861bbdd5ae3c1f0869dd6a + subpackages: + - pbutil +- name: github.com/pborman/uuid + version: ca53cad383cad2479bbba7f7a1a05797ec1386e4 +- name: github.com/pkg/errors + version: a22138067af1c4942683050411a841ade67fe1eb +- name: github.com/prometheus/client_golang + version: e51041b3fa41cece0dca035740ba6411905be473 + subpackages: + - prometheus +- name: github.com/prometheus/client_model + version: fa8ad6fec33561be4280a8f0514318c79d7f6cb6 + subpackages: + - go +- name: github.com/prometheus/common + version: ffe929a3f4c4faeaa10f2b9535c2b1be3ad15650 + subpackages: + - expfmt + - model +- name: github.com/prometheus/procfs + version: 454a56f35412459b5e684fd5ec0f9211b94f002a +- name: github.com/PuerkitoBio/purell + version: 8a290539e2e8629dbc4e6bad948158f790ec31f4 +- name: github.com/PuerkitoBio/urlesc + version: 5bd2802263f21d8788851d5305584c82a5c75d7e +- name: github.com/spf13/cobra + version: f62e98d28ab7ad31d707ba837a966378465c7b57 +- name: github.com/spf13/pflag + version: 9ff6c6923cfffbcd502984b8e0c80539a94968b7 +- name: github.com/ugorji/go + version: ded73eae5db7e7a0ef6f55aace87a2873c5d2b74 + subpackages: + - codec +- name: golang.org/x/crypto + version: d172538b2cfce0c13cee31e647d0367aa8cd2486 + subpackages: + - bcrypt + - blowfish + - ssh/terminal +- name: golang.org/x/net + version: e90d6d0afc4c315a0d87a568ae68577cc15149a0 + subpackages: + - context + - html + - html/atom + - http2 + - http2/hpack + - idna + - internal/timeseries + - lex/httplex + - trace + - websocket +- name: golang.org/x/sys + version: 8f0908ab3b2457e2e15403d3697c9ef5cb4b57a9 + subpackages: + - unix +- name: golang.org/x/text + version: 2910a502d2bf9e43193af9d68ca516529614eed3 + subpackages: + - cases + - internal/tag + - language + - runes + - secure/bidirule + - secure/precis + - transform + - unicode/bidi + - unicode/norm + - width +- name: google.golang.org/grpc + version: 231b4cfea0e79843053a33f5fe90bd4d84b23cd3 + subpackages: + - codes + - credentials + - grpclog + - internal + - metadata + - naming + - peer + - transport +- name: gopkg.in/inf.v0 + version: 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4 +- name: gopkg.in/natefinch/lumberjack.v2 + version: 20b71e5b60d756d3d2f80def009790325acc2b23 +- name: gopkg.in/yaml.v2 + version: 53feefa2559fb8dfa8d81baad31be332c97d6c77 +- name: k8s.io/apimachinery + version: 20e10d54608f05c3059443a6c0afb9979641e88d + subpackages: + - pkg/api/equality + - pkg/api/errors + - pkg/api/meta + - pkg/api/resource + - pkg/api/validation + - pkg/api/validation/path + - pkg/apimachinery + - pkg/apimachinery/announced + - pkg/apimachinery/registered + - pkg/apis/meta/internalversion + - pkg/apis/meta/v1 + - pkg/apis/meta/v1/unstructured + - pkg/apis/meta/v1/validation + - pkg/conversion + - pkg/conversion/queryparams + - pkg/conversion/unstructured + - pkg/fields + - pkg/labels + - pkg/openapi + - pkg/runtime + - pkg/runtime/schema + - pkg/runtime/serializer + - pkg/runtime/serializer/json + - pkg/runtime/serializer/protobuf + - pkg/runtime/serializer/recognizer + - pkg/runtime/serializer/streaming + - pkg/runtime/serializer/versioning + - pkg/selection + - pkg/types + - pkg/util/diff + - pkg/util/errors + - pkg/util/framer + - pkg/util/httpstream + - pkg/util/intstr + - pkg/util/json + - pkg/util/mergepatch + - pkg/util/net + - pkg/util/rand + - pkg/util/runtime + - pkg/util/sets + - pkg/util/strategicpatch + - pkg/util/uuid + - pkg/util/validation + - pkg/util/validation/field + - pkg/util/wait + - pkg/util/yaml + - pkg/version + - pkg/watch + - third_party/forked/golang/json + - third_party/forked/golang/netutil + - third_party/forked/golang/reflect +- name: k8s.io/apiserver + version: dcf548fbe26dacc3a78d18e1135adf17006552e9 + subpackages: + - pkg/admission + - pkg/apis/apiserver + - pkg/apis/apiserver/install + - pkg/apis/apiserver/v1alpha1 + - pkg/authentication/authenticator + - pkg/authentication/authenticatorfactory + - pkg/authentication/group + - pkg/authentication/request/anonymous + - pkg/authentication/request/bearertoken + - pkg/authentication/request/headerrequest + - pkg/authentication/request/union + - pkg/authentication/request/x509 + - pkg/authentication/serviceaccount + - pkg/authentication/token/tokenfile + - pkg/authentication/user + - pkg/authorization/authorizer + - pkg/authorization/authorizerfactory + - pkg/authorization/union + - pkg/endpoints + - pkg/endpoints/filters + - pkg/endpoints/handlers + - pkg/endpoints/handlers/negotiation + - pkg/endpoints/handlers/responsewriters + - pkg/endpoints/metrics + - pkg/endpoints/openapi + - pkg/endpoints/request + - pkg/features + - pkg/registry/generic + - pkg/registry/generic/registry + - pkg/registry/rest + - pkg/server + - pkg/server/filters + - pkg/server/healthz + - pkg/server/httplog + - pkg/server/mux + - pkg/server/openapi + - pkg/server/options + - pkg/server/routes + - pkg/server/routes/data/swagger + - pkg/server/storage + - pkg/storage + - pkg/storage/errors + - pkg/storage/etcd + - pkg/storage/etcd/metrics + - pkg/storage/etcd/util + - pkg/storage/etcd3 + - pkg/storage/names + - pkg/storage/storagebackend + - pkg/storage/storagebackend/factory + - pkg/util/cache + - pkg/util/feature + - pkg/util/flag + - pkg/util/flushwriter + - pkg/util/logs + - pkg/util/proxy + - pkg/util/trace + - pkg/util/trie + - pkg/util/webhook + - pkg/util/wsstream + - plugin/pkg/authenticator/token/webhook + - plugin/pkg/authorizer/webhook +- name: k8s.io/client-go + version: dabf37f5df16a224729883d9f616ce4a2c282e95 + subpackages: + - kubernetes/scheme + - kubernetes/typed/authentication/v1beta1 + - kubernetes/typed/authorization/v1beta1 + - kubernetes/typed/core/v1 + - pkg/api + - pkg/api/install + - pkg/api/v1 + - pkg/apis/apps + - pkg/apis/apps/v1beta1 + - pkg/apis/authentication + - pkg/apis/authentication/install + - pkg/apis/authentication/v1 + - pkg/apis/authentication/v1beta1 + - pkg/apis/authorization + - pkg/apis/authorization/install + - pkg/apis/authorization/v1 + - pkg/apis/authorization/v1beta1 + - pkg/apis/autoscaling + - pkg/apis/autoscaling/v1 + - pkg/apis/autoscaling/v2alpha1 + - pkg/apis/batch + - pkg/apis/batch/v1 + - pkg/apis/batch/v2alpha1 + - pkg/apis/certificates + - pkg/apis/certificates/v1beta1 + - pkg/apis/extensions + - pkg/apis/extensions/v1beta1 + - pkg/apis/policy + - pkg/apis/policy/v1beta1 + - pkg/apis/rbac + - pkg/apis/rbac/v1alpha1 + - pkg/apis/rbac/v1beta1 + - pkg/apis/settings + - pkg/apis/settings/v1alpha1 + - pkg/apis/storage + - pkg/apis/storage/v1 + - pkg/apis/storage/v1beta1 + - pkg/util + - pkg/util/parsers + - pkg/version + - rest + - rest/watch + - tools/auth + - tools/cache + - tools/clientcmd + - tools/clientcmd/api + - tools/clientcmd/api/latest + - tools/clientcmd/api/v1 + - tools/metrics + - transport + - util/cert + - util/clock + - util/flowcontrol + - util/homedir + - util/integer +- name: k8s.io/metrics + version: fd2415bb9381a6731027b48a8c6b78f28e13f876 + subpackages: + - pkg/apis/custom_metrics + - pkg/apis/custom_metrics/install + - pkg/apis/custom_metrics/v1alpha1 +testImports: [] diff --git a/glide.yaml b/glide.yaml new file mode 100644 index 000000000..ef3278dc4 --- /dev/null +++ b/glide.yaml @@ -0,0 +1,45 @@ +package: k8s.io/custom-metrics-boilerplate +import: +- package: github.com/emicklei/go-restful +- package: github.com/spf13/cobra +- package: k8s.io/apimachinery + subpackages: + - pkg/api/errors + - pkg/api/meta + - pkg/api/resource + - pkg/apimachinery + - pkg/apimachinery/announced + - pkg/apimachinery/registered + - pkg/apis/meta/internalversion + - pkg/apis/meta/v1 + - pkg/conversion + - pkg/labels + - pkg/runtime + - pkg/runtime/schema + - pkg/runtime/serializer + - pkg/util/errors + - pkg/util/wait + - pkg/version +- package: k8s.io/apiserver + subpackages: + - pkg/endpoints + - pkg/endpoints/handlers + - pkg/endpoints/handlers/negotiation + - pkg/endpoints/metrics + - pkg/endpoints/request + - pkg/registry/rest + - pkg/server + - pkg/server/options + - pkg/util/logs +- package: k8s.io/client-go + subpackages: + - kubernetes/scheme + - kubernetes/typed/core/v1 + - pkg/api + - pkg/api/install + - rest + - tools/clientcmd +- package: k8s.io/metrics + subpackages: + - pkg/apis/custom_metrics + - pkg/apis/custom_metrics/install diff --git a/pkg/apiserver/cmapis.go b/pkg/apiserver/cmapis.go index e72ae302a..2c234bc2c 100644 --- a/pkg/apiserver/cmapis.go +++ b/pkg/apiserver/cmapis.go @@ -17,17 +17,17 @@ limitations under the License. package apiserver import ( + "k8s.io/apimachinery/pkg/apimachinery" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/apimachinery" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apiserver/pkg/registry/rest" - genericapiserver "k8s.io/apiserver/pkg/server" genericapi "k8s.io/apiserver/pkg/endpoints" + genericapiserver "k8s.io/apiserver/pkg/server" - "k8s.io/metrics/pkg/apis/custom_metrics" - metricstorage "k8s.io/custom-metrics-boilerplate/pkg/registry/custom_metrics" + specificapi "k8s.io/custom-metrics-boilerplate/pkg/apiserver/installer" "k8s.io/custom-metrics-boilerplate/pkg/provider" + metricstorage "k8s.io/custom-metrics-boilerplate/pkg/registry/custom_metrics" + "k8s.io/metrics/pkg/apis/custom_metrics" ) func (s *CustomMetricsAdapterServer) InstallCustomMetricsAPI() error { @@ -38,14 +38,14 @@ func (s *CustomMetricsAdapterServer) InstallCustomMetricsAPI() error { GroupVersion: groupMeta.GroupVersion.String(), Version: groupMeta.GroupVersion.Version, } - groupVersion := metav1.GroupVersionForDiscovery{ - GroupVersion: groupMeta.GroupVersion.String(), - Version: groupMeta.GroupVersion.Version, - } + groupVersion := metav1.GroupVersionForDiscovery{ + GroupVersion: groupMeta.GroupVersion.String(), + Version: groupMeta.GroupVersion.Version, + } apiGroup := metav1.APIGroup{ - Name: groupMeta.GroupVersion.String(), - Versions: []metav1.GroupVersionForDiscovery{groupVersion}, - PreferredVersion: preferredVersionForDiscovery, + Name: groupMeta.GroupVersion.String(), + Versions: []metav1.GroupVersionForDiscovery{groupVersion}, + PreferredVersion: preferredVersionForDiscovery, } cmAPI := s.cmAPI(groupMeta, &groupMeta.GroupVersion) @@ -54,41 +54,35 @@ func (s *CustomMetricsAdapterServer) InstallCustomMetricsAPI() error { return err } - path := genericapiserver.APIGroupPrefix+"/"+groupMeta.GroupVersion.Group + path := genericapiserver.APIGroupPrefix + "/" + groupMeta.GroupVersion.Group s.GenericAPIServer.HandlerContainer.Add(genericapi.NewGroupWebService(s.GenericAPIServer.Serializer, path, apiGroup)) return nil } -func (s *CustomMetricsAdapterServer) cmAPI(groupMeta *apimachinery.GroupMeta, groupVersion *schema.GroupVersion) *genericapi.APIGroupVersion { +func (s *CustomMetricsAdapterServer) cmAPI(groupMeta *apimachinery.GroupMeta, groupVersion *schema.GroupVersion) *specificapi.MetricsAPIGroupVersion { resourceStorage := metricstorage.NewREST(s.Provider) - storage := map[string]rest.Storage{ - // TODO: make this non-returning storage - "*": resourceStorage, - "*/*": resourceStorage, - } - - return &genericapi.APIGroupVersion{ - Root: genericapiserver.APIGroupPrefix, - GroupVersion: *groupVersion, - - ParameterCodec: metav1.ParameterCodec, - Serializer: Codecs, - Creater: Scheme, - Convertor: Scheme, - UnsafeConvertor: runtime.UnsafeObjectConvertor(Scheme), - Copier: Scheme, - Typer: Scheme, - SubresourceGroupVersionKind: nil, // TODO: do we need this? - Linker: groupMeta.SelfLinker, - Mapper: groupMeta.RESTMapper, - Storage: storage, - - // TODO: Admit? - Context: s.GenericAPIServer.RequestContextMapper(), - MinRequestTimeout: s.GenericAPIServer.MinRequestTimeout(), - OptionsExternalVersion: &schema.GroupVersion{Version: "v1"}, - - ResourceLister: provider.NewResourceLister(s.Provider), + return &specificapi.MetricsAPIGroupVersion{ + DynamicStorage: resourceStorage, + APIGroupVersion: &genericapi.APIGroupVersion{ + Root: genericapiserver.APIGroupPrefix, + GroupVersion: *groupVersion, + + ParameterCodec: metav1.ParameterCodec, + Serializer: Codecs, + Creater: Scheme, + Convertor: Scheme, + UnsafeConvertor: runtime.UnsafeObjectConvertor(Scheme), + Copier: Scheme, + Typer: Scheme, + Linker: groupMeta.SelfLinker, + Mapper: groupMeta.RESTMapper, + + Context: s.GenericAPIServer.RequestContextMapper(), + MinRequestTimeout: s.GenericAPIServer.MinRequestTimeout(), + OptionsExternalVersion: &schema.GroupVersion{Version: "v1"}, + + ResourceLister: provider.NewResourceLister(s.Provider), + }, } } diff --git a/pkg/apiserver/installer/apiserver_test.go b/pkg/apiserver/installer/apiserver_test.go new file mode 100644 index 000000000..6aee9c01c --- /dev/null +++ b/pkg/apiserver/installer/apiserver_test.go @@ -0,0 +1,329 @@ +/* +Copyright 2017 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 installer + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/emicklei/go-restful" + + "k8s.io/apimachinery/pkg/apimachinery/announced" + "k8s.io/apimachinery/pkg/apimachinery/registered" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + genericapiserver "k8s.io/apiserver/pkg/server" + genericapi "k8s.io/apiserver/pkg/endpoints" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apimachinery/pkg/apimachinery" + "k8s.io/metrics/pkg/apis/custom_metrics/install" + "k8s.io/metrics/pkg/apis/custom_metrics" + cmv1alpha1 "k8s.io/metrics/pkg/apis/custom_metrics/v1alpha1" + + "k8s.io/custom-metrics-boilerplate/pkg/provider" + metricstorage "k8s.io/custom-metrics-boilerplate/pkg/registry/custom_metrics" + +) + +// defaultAPIServer exposes nested objects for testability. +type defaultAPIServer struct { + http.Handler + container *restful.Container +} + +var ( + groupFactoryRegistry = make(announced.APIGroupFactoryRegistry) + registry = registered.NewOrDie("") + Scheme = runtime.NewScheme() + Codecs = serializer.NewCodecFactory(Scheme) + prefix = genericapiserver.APIGroupPrefix + groupVersion schema.GroupVersion + groupMeta *apimachinery.GroupMeta + codec = Codecs.LegacyCodec() + emptySet = labels.Set{} + matchingSet = labels.Set{"foo": "bar"} +) + +func init() { + install.Install(groupFactoryRegistry, registry, Scheme) + + // we need to add the options to empty v1 + // TODO fix the server code to avoid this + metav1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) + + // TODO: keep the generic API server from wanting this + unversioned := schema.GroupVersion{Group: "", Version: "v1"} + Scheme.AddUnversionedTypes(unversioned, + &metav1.Status{}, + &metav1.APIVersions{}, + &metav1.APIGroupList{}, + &metav1.APIGroup{}, + &metav1.APIResourceList{}, + ) + + groupMeta = registry.GroupOrDie(custom_metrics.GroupName) + groupVersion = groupMeta.GroupVersion +} + +func extractBody(response *http.Response, object runtime.Object) error { + defer response.Body.Close() + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return err + } + return runtime.DecodeInto(codec, body, object) +} + +func extractBodyString(response *http.Response) (string, error) { + defer response.Body.Close() + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return "", err + } + return string(body), err +} + +func handle(prov provider.CustomMetricsProvider) http.Handler { + container := restful.NewContainer() + container.Router(restful.CurlyRouter{}) + mux := container.ServeMux + resourceStorage := metricstorage.NewREST(prov) + group := &MetricsAPIGroupVersion{ + DynamicStorage: resourceStorage, + APIGroupVersion: &genericapi.APIGroupVersion{ + Root: prefix, + GroupVersion: groupVersion, + + ParameterCodec: metav1.ParameterCodec, + Serializer: Codecs, + Creater: Scheme, + Convertor: Scheme, + UnsafeConvertor: runtime.UnsafeObjectConvertor(Scheme), + Copier: Scheme, + Typer: Scheme, + Linker: groupMeta.SelfLinker, + Mapper: groupMeta.RESTMapper, + + Context: request.NewRequestContextMapper(), + OptionsExternalVersion: &schema.GroupVersion{Version: "v1"}, + + ResourceLister: provider.NewResourceLister(prov), + }, + } + + if err := group.InstallREST(container); err != nil { + panic(fmt.Sprintf("unable to install container %s: %v", group.GroupVersion, err)) + } + + return &defaultAPIServer{mux, container} +} + +type fakeProvider struct { + rootValues map[string][]custom_metrics.MetricValue + namespacedValues map[string][]custom_metrics.MetricValue + rootSubsetCounts map[string]int + namespacedSubsetCounts map[string]int + metrics []provider.MetricInfo +} + +func (p *fakeProvider) GetRootScopedMetricByName(groupResource schema.GroupResource, name string, metricName string) (*custom_metrics.MetricValue, error) { + metricId := groupResource.String()+"/"+name+"/"+metricName + values, ok := p.rootValues[metricId] + if !ok { + return nil, fmt.Errorf("non-existant metric requested (id: %s)", metricId) + } + + return &values[0], nil +} + +func (p *fakeProvider) GetRootScopedMetricBySelector(groupResource schema.GroupResource, selector labels.Selector, metricName string) (*custom_metrics.MetricValueList, error) { + metricId := groupResource.String()+"/*/"+metricName + values, ok := p.rootValues[metricId] + if !ok { + return nil, fmt.Errorf("non-existant metric requested (id: %s)", metricId) + } + + var trimmedValues custom_metrics.MetricValueList + + if trimmedCount, ok := p.rootSubsetCounts[metricId]; ok { + trimmedValues = custom_metrics.MetricValueList{ + Items: make([]custom_metrics.MetricValue, 0, trimmedCount), + } + for i := range values { + var lbls labels.Labels + if i < trimmedCount { + lbls = matchingSet + } else { + lbls = emptySet + } + if selector.Matches(lbls) { + trimmedValues.Items = append(trimmedValues.Items, custom_metrics.MetricValue{}) + } + } + } else { + trimmedValues = custom_metrics.MetricValueList{ + Items: values, + } + } + + return &trimmedValues, nil +} + +func (p *fakeProvider) GetNamespacedMetricByName(groupResource schema.GroupResource, namespace string, name string, metricName string) (*custom_metrics.MetricValue, error) { + metricId := namespace+"/"+groupResource.String()+"/"+name+"/"+metricName + values, ok := p.namespacedValues[metricId] + if !ok { + return nil, fmt.Errorf("non-existant metric requested (id: %s)", metricId) + } + + return &values[0], nil +} + +func (p *fakeProvider) GetNamespacedMetricBySelector(groupResource schema.GroupResource, namespace string, selector labels.Selector, metricName string) (*custom_metrics.MetricValueList, error) { + metricId := namespace+"/"+groupResource.String()+"/*/"+metricName + values, ok := p.namespacedValues[metricId] + if !ok { + return nil, fmt.Errorf("non-existant metric requested (id: %s)", metricId) + } + + var trimmedValues custom_metrics.MetricValueList + + if trimmedCount, ok := p.namespacedSubsetCounts[metricId]; ok { + trimmedValues = custom_metrics.MetricValueList{ + Items: make([]custom_metrics.MetricValue, 0, trimmedCount), + } + for i := range values { + var lbls labels.Labels + if i < trimmedCount { + lbls = matchingSet + } else { + lbls = emptySet + } + if selector.Matches(lbls) { + trimmedValues.Items = append(trimmedValues.Items, custom_metrics.MetricValue{}) + } + } + } else { + trimmedValues = custom_metrics.MetricValueList{ + Items: values, + } + } + + return &trimmedValues, nil +} + +func (p *fakeProvider) ListAllMetrics() []provider.MetricInfo { + return p.metrics +} + +func TestCustomMetricsAPI(t *testing.T) { + totalNodesCount := 4 + totalPodsCount := 16 + matchingNodesCount := 2 + matchingPodsCount := 8 + + type T struct { + Method string + Path string + Status int + ExpectedCount int + } + cases := map[string]T{ + // checks which should fail + "GET long prefix": {"GET", "/" + prefix + "/", http.StatusNotFound, 0}, + + "root GET missing storage": {"GET", "/" + prefix + "/" + groupVersion.Group + "/" + groupVersion.Version + "/blah", http.StatusNotFound, 0}, + + "namespaced GET long prefix": {"GET", "/" + prefix + "/", http.StatusNotFound, 0}, + "namespaced GET missing storage": {"GET", "/" + prefix + "/" + groupVersion.Group + "/" + groupVersion.Version + "/blah", http.StatusNotFound, 0}, + + "GET at root resource leaf": {"GET", "/" + prefix + "/" + groupVersion.Group + "/" + groupVersion.Version + "/nodes/foo", http.StatusNotFound, 0}, + "GET at namespaced resource leaft": {"GET", "/" + prefix + "/" + groupVersion.Group + "/" + groupVersion.Version + "/namespaces/ns/pods/bar", http.StatusNotFound, 0}, + + // Positive checks to make sure everything is wired correctly + "GET for all nodes (root)": {"GET", "/" + prefix + "/" + groupVersion.Group + "/" + groupVersion.Version + "/nodes/*/some-metric", http.StatusOK, totalNodesCount}, + "GET for all pods (namespaced)": {"GET", "/" + prefix + "/" + groupVersion.Group + "/" + groupVersion.Version + "/namespaces/ns/pods/*/some-metric", http.StatusOK, totalPodsCount}, + "GET for namespace": {"GET", "/" + prefix + "/" + groupVersion.Group + "/" + groupVersion.Version + "/namespaces/ns/metrics/some-metric", http.StatusOK, 1}, + "GET for label selected nodes (root)": {"GET", "/" + prefix + "/" + groupVersion.Group + "/" + groupVersion.Version + "/nodes/*/some-metric?labelSelector=foo%3Dbar", http.StatusOK, matchingNodesCount}, + "GET for label selected pods (namespaced)": {"GET", "/" + prefix + "/" + groupVersion.Group + "/" + groupVersion.Version + "/namespaces/ns/pods/*/some-metric?labelSelector=foo%3Dbar", http.StatusOK, matchingPodsCount}, + "GET for single node (root)": {"GET", "/" + prefix + "/" + groupVersion.Group + "/" + groupVersion.Version + "/nodes/foo/some-metric", http.StatusOK, 1}, + "GET for single pod (namespaced)": {"GET", "/" + prefix + "/" + groupVersion.Group + "/" + groupVersion.Version + "/namespaces/ns/pods/foo/some-metric", http.StatusOK, 1}, + } + + prov := &fakeProvider{ + rootValues: map[string][]custom_metrics.MetricValue{ + "nodes/*/some-metric": make([]custom_metrics.MetricValue, totalNodesCount), + "nodes/foo/some-metric": make([]custom_metrics.MetricValue, 1), + "namespaces/ns/some-metric": make([]custom_metrics.MetricValue, 1), + }, + namespacedValues: map[string][]custom_metrics.MetricValue{ + "ns/pods/*/some-metric": make([]custom_metrics.MetricValue, totalPodsCount), + "ns/pods/foo/some-metric": make([]custom_metrics.MetricValue, 1), + }, + + rootSubsetCounts: map[string]int{ + "nodes/*/some-metric": matchingNodesCount, + }, + namespacedSubsetCounts: map[string]int{ + "ns/pods/*/some-metric": matchingPodsCount, + }, + } + + server := httptest.NewServer(handle(prov)) + defer server.Close() + client := http.Client{} + for k, v := range cases { + request, err := http.NewRequest(v.Method, server.URL+v.Path, nil) + if err != nil { + t.Fatalf("unexpected error (%s): %v", k, err) + } + + response, err := client.Do(request) + if err != nil { + t.Errorf("unexpected error (%s): %v", k, err) + continue + } + + if response.StatusCode != v.Status { + body, err := extractBodyString(response) + bodyPart := body + if err != nil { + bodyPart = fmt.Sprintf("[error extracting body: %v]", err) + } + t.Errorf("Expected %d for %s (%s), Got %#v -- %s", v.Status, v.Method, k, response, bodyPart) + continue + } + + if v.ExpectedCount > 0 { + lst := &cmv1alpha1.MetricValueList{} + if err := extractBody(response, lst); err != nil { + t.Errorf("unexpected error (%s): %v", k, err) + continue + } + if len(lst.Items) != v.ExpectedCount { + t.Errorf("Expected %d items, got %d (%s): %#v", v.ExpectedCount, len(lst.Items), k, lst.Items) + continue + } + } + } +} diff --git a/pkg/apiserver/installer/context/context.go b/pkg/apiserver/installer/context/context.go new file mode 100644 index 000000000..042995565 --- /dev/null +++ b/pkg/apiserver/installer/context/context.go @@ -0,0 +1,48 @@ +/* +Copyright 2017 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 context + +import ( + "k8s.io/apiserver/pkg/endpoints/request" +) + +// resourceInformation holds the resource and subresource for a request in the context. +type resourceInformation struct { + resource string + subresource string +} + +// contextKey is the type of the keys for the context in this file. +// It's private to avoid conflicts across packages. +type contextKey int + +const resourceKey contextKey = iota + +// WithResourceInformation returns a copy of parent in which the resource and subresource values are set +func WithResourceInformation(parent request.Context, resource, subresource string) request.Context { + return request.WithValue(parent, resourceKey, resourceInformation{resource, subresource}) +} + +// ResourceInformationFrom returns resource and subresource on the ctx +func ResourceInformationFrom(ctx request.Context) (resource string, subresource string, ok bool) { + resourceInfo, ok := ctx.Value(resourceKey).(resourceInformation) + if !ok { + return "", "", ok + } + + return resourceInfo.resource, resourceInfo.subresource, ok +} diff --git a/pkg/apiserver/installer/installer.go b/pkg/apiserver/installer/installer.go new file mode 100644 index 000000000..a56d93f53 --- /dev/null +++ b/pkg/apiserver/installer/installer.go @@ -0,0 +1,633 @@ +/* +Copyright 2017 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 installer + +import ( + "bytes" + "fmt" + "net/http" + "net/url" + gpath "path" + "reflect" + "strings" + "time" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/conversion" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apiserver/pkg/endpoints" + "k8s.io/apiserver/pkg/endpoints/handlers" + "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" + "k8s.io/apiserver/pkg/endpoints/metrics" + "k8s.io/apiserver/pkg/registry/rest" + "k8s.io/apiserver/pkg/endpoints/request" + + "github.com/emicklei/go-restful" + + specificcontext "k8s.io/custom-metrics-boilerplate/pkg/apiserver/installer/context" +) + +// NB: the contents of this file should mostly be a subset of the functionality +// in "k8s.io/apiserver/pkg/endpoints". It would be nice to eventual figure out +// a way to not have to recreate/copy a bunch of the structure from the normal API +// installer, so that this trivially tracks changes to the main installer. + +// MetricsAPIGroupVersion is similar to "k8s.io/apiserver/pkg/endpoints".APIGroupVersion, +// except that it installs the metrics REST handlers, which use wildcard resources +// and subresources. +// +// This basically only serves the limitted use case required by the metrics API server -- +// the only verb accepted is GET (and perhaps WATCH in the future). +type MetricsAPIGroupVersion struct { + DynamicStorage rest.Storage + + *endpoints.APIGroupVersion +} + +// InstallDynamicREST registers the dynamic REST handlers into a restful Container. +// It is expected that the provided path root prefix will serve all operations. Root MUST +// NOT end in a slash. It should mirror InstallREST in the plain APIGroupVersion. +func (g *MetricsAPIGroupVersion) InstallREST(container *restful.Container) error { + installer := g.newDynamicInstaller() + ws := installer.NewWebService() + + registrationErrors := installer.Install(ws) + lister := g.ResourceLister + if lister == nil { + return fmt.Errorf("must provide a dynamic lister for dynamic API groups") + } + endpoints.AddSupportedResourcesWebService(g.Serializer, ws, g.GroupVersion, lister) + container.Add(ws) + return utilerrors.NewAggregate(registrationErrors) +} + +// newDynamicInstaller is a helper to create the installer. It mirrors +// newInstaller in APIGroupVersion. +func (g *MetricsAPIGroupVersion) newDynamicInstaller() *MetricsAPIInstaller { + prefix := gpath.Join(g.Root, g.GroupVersion.Group, g.GroupVersion.Version) + installer := &MetricsAPIInstaller{ + group: g, + prefix: prefix, + minRequestTimeout: g.MinRequestTimeout, + } + + return installer +} + +// MetricsAPIInstaller is a specialized API installer for the metrics API. +// It is intended to be fully compliant with the Kubernetes API server conventions, +// but serves wildcard resource/subresource routes instead of hard-coded resources +// and subresources. +type MetricsAPIInstaller struct { + group *MetricsAPIGroupVersion + prefix string // Path prefix where API resources are to be registered. + minRequestTimeout time.Duration + + // TODO: do we want to embed a normal API installer here so we can serve normal + // endpoints side by side with dynamic ones (from the same API group)? +} + +// Install installs handlers for API resources. +func (a *MetricsAPIInstaller) Install(ws *restful.WebService) (errors []error) { + errors = make([]error, 0) + + err := a.registerResourceHandlers(a.group.DynamicStorage, ws) + if err != nil { + errors = append(errors, fmt.Errorf("error in registering custom metrics resource: %v", err)) + } + + return errors +} + +// NewWebService creates a new restful webservice with the api installer's prefix and version. +func (a *MetricsAPIInstaller) NewWebService() *restful.WebService { + ws := new(restful.WebService) + ws.Path(a.prefix) + // a.prefix contains "prefix/group/version" + ws.Doc("API at " + a.prefix) + // Backwards compatibility, we accepted objects with empty content-type at V1. + // If we stop using go-restful, we can default empty content-type to application/json on an + // endpoint by endpoint basis + ws.Consumes("*/*") + mediaTypes, streamMediaTypes := negotiation.MediaTypesForSerializer(a.group.Serializer) + ws.Produces(append(mediaTypes, streamMediaTypes...)...) + ws.ApiVersion(a.group.GroupVersion.String()) + + return ws +} + +// registerResourceHandlers registers the resource handlers for custom metrics. +// Compared to the normal installer, this plays fast and loose a bit, but should still +// follow the API conventions. +func (a *MetricsAPIInstaller) registerResourceHandlers(storage rest.Storage, ws *restful.WebService) error { + context := a.group.Context + + optionsExternalVersion := a.group.GroupVersion + if a.group.OptionsExternalVersion != nil { + optionsExternalVersion = *a.group.OptionsExternalVersion + } + + mapping, err := a.restMapping() + if err != nil { + return err + } + + fqKindToRegister, err := a.getResourceKind(storage) + if err != nil { + return err + } + + kind := fqKindToRegister.Kind + + lister := storage.(rest.Lister) + list := lister.NewList() + listGVKs, _, err := a.group.Typer.ObjectKinds(list) + if err != nil { + return err + } + versionedListPtr, err := a.group.Creater.New(a.group.GroupVersion.WithKind(listGVKs[0].Kind)) + if err != nil { + return err + } + versionedList := indirectArbitraryPointer(versionedListPtr) + + versionedListOptions, err := a.group.Creater.New(optionsExternalVersion.WithKind("ListOptions")) + if err != nil { + return err + } + + ctxFn := func(req *restful.Request) request.Context { + var ctx request.Context + if ctx != nil { + if existingCtx, ok := context.Get(req.Request); ok { + ctx = existingCtx + } + } + if ctx == nil { + ctx = request.NewContext() + } + + ctx = request.WithUserAgent(ctx, req.HeaderParameter("User-Agent")) + + // inject the resource, subresource, and name here so that + // we don't have to write custom handler logic + resource := req.PathParameter("resource") + subresource := req.PathParameter("subresource") + ctx = specificcontext.WithResourceInformation(ctx, resource, subresource) + + return ctx + } + + scope := mapping.Scope + 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") + + // metrics describing non-namespaced objects (e.g. nodes) + rootScopedParams := []*restful.Parameter{ + resourceParam, + nameParam, + subresourceParam, + } + rootScopedPath := "{resource}/{name}/{subresource}" + + // metrics describing namespaced objects (e.g. pods) + namespaceParam := ws.PathParameter(scope.ArgumentName(), scope.ParamDescription()).DataType("string") + namespacedParams := []*restful.Parameter{ + namespaceParam, + resourceParam, + nameParam, + subresourceParam, + } + namespacedPath := scope.ParamName() + "/{" + scope.ArgumentName() + "}/{resource}/{name}/{subresource}" + namespacedPathPrefix := gpath.Join(a.prefix, scope.ParamName()) + "/" + itemPathFn := func(name, namespace, resource, subresource string) bytes.Buffer { + var buf bytes.Buffer + buf.WriteString(namespacedPathPrefix) + buf.WriteString(url.QueryEscape(namespace)) + buf.WriteString("/") + buf.WriteString(url.QueryEscape(resource)) + buf.WriteString("/") + buf.WriteString(url.QueryEscape(name)) + buf.WriteString("/") + buf.WriteString(url.QueryEscape(subresource)) + return buf + } + + namespaceSpecificPath := scope.ParamName() + "/{" + scope.ArgumentName() + "}/metrics/{name}" + namespaceSpecificParams := []*restful.Parameter{ + namespaceParam, + nameParam, + } + namespaceSpecificItemPathFn := func(name, namespace, resource, subresource string) bytes.Buffer { + var buf bytes.Buffer + buf.WriteString(namespacedPathPrefix) + buf.WriteString(url.QueryEscape(namespace)) + buf.WriteString("/metrics/") + buf.WriteString(url.QueryEscape(name)) + return buf + } + namespaceSpecificCtxFn := func(req *restful.Request) request.Context { + var ctx request.Context + if ctx != nil { + if existingCtx, ok := context.Get(req.Request); ok { + ctx = existingCtx + } + } + if ctx == nil { + ctx = request.NewContext() + } + + ctx = request.WithUserAgent(ctx, req.HeaderParameter("User-Agent")) + + // inject the resource, subresource, and name here so that + // we don't have to write custom handler logic + ctx = specificcontext.WithResourceInformation(ctx, "metrics", "") + + return ctx + } + + mediaTypes, streamMediaTypes := negotiation.MediaTypesForSerializer(a.group.Serializer) + allMediaTypes := append(mediaTypes, streamMediaTypes...) + ws.Produces(allMediaTypes...) + + reqScope := handlers.RequestScope{ + ContextFunc: ctxFn, + Serializer: a.group.Serializer, + ParameterCodec: a.group.ParameterCodec, + Creater: a.group.Creater, + Convertor: a.group.Convertor, + Copier: a.group.Copier, + Typer: a.group.Typer, + UnsafeConvertor: a.group.UnsafeConvertor, + + // TODO: This seems wrong for cross-group subresources. It makes an assumption that a subresource and its parent are in the same group version. Revisit this. + Resource: a.group.GroupVersion.WithResource("*"), + Subresource: "*", + Kind: fqKindToRegister, + + MetaGroupVersion: metav1.SchemeGroupVersion, + } + if a.group.MetaGroupVersion != nil { + reqScope.MetaGroupVersion = *a.group.MetaGroupVersion + } + + // we need one path for namespaced resources, one for non-namespaced resources + doc := "list custom metrics describing an object or objects" + reqScope.Namer = rootScopeNaming{scope, a.group.Linker, gpath.Join(a.prefix, rootScopedPath, "/"), "/{subresource}"} + rootScopedHandler := metrics.InstrumentRouteFunc("LIST", "custom-metrics", handlers.ListResource(lister, nil, reqScope, false, a.minRequestTimeout)) + + // install the root-scoped route + rootScopedRoute := ws.GET(rootScopedPath).To(rootScopedHandler). + Doc(doc). + Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). + Operation("list"+kind). + Produces(allMediaTypes...). + Returns(http.StatusOK, "OK", versionedList). + Writes(versionedList) + if err := addObjectParams(ws, rootScopedRoute, versionedListOptions); err != nil { + return err + } + addParams(rootScopedRoute, rootScopedParams) + ws.Route(rootScopedRoute) + + // install the namespace-scoped route + reqScope.Namer = scopeNaming{scope, a.group.Linker, itemPathFn, false} + namespacedHandler := metrics.InstrumentRouteFunc("LIST", "custom-metrics-namespaced", handlers.ListResource(lister, nil, reqScope, false, a.minRequestTimeout)) + namespacedRoute := ws.GET(namespacedPath).To(namespacedHandler). + Doc(doc). + Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). + Operation("listNamespaced"+kind). + Produces(allMediaTypes...). + Returns(http.StatusOK, "OK", versionedList). + Writes(versionedList) + if err := addObjectParams(ws, namespacedRoute, versionedListOptions); err != nil { + return err + } + addParams(namespacedRoute, namespacedParams) + ws.Route(namespacedRoute) + + // install the special route for metrics describing namespaces (last b/c we modify the context func) + reqScope.ContextFunc = namespaceSpecificCtxFn + reqScope.Namer = scopeNaming{scope, a.group.Linker, namespaceSpecificItemPathFn, false} + namespaceSpecificHandler := metrics.InstrumentRouteFunc("LIST", "custom-metrics-for-namespace", handlers.ListResource(lister, nil, reqScope, false, a.minRequestTimeout)) + namespaceSpecificRoute := ws.GET(namespaceSpecificPath).To(namespaceSpecificHandler). + Doc(doc). + Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). + Operation("read"+kind+"ForNamespace"). + Produces(allMediaTypes...). + Returns(http.StatusOK, "OK", versionedList). + Writes(versionedList) + if err := addObjectParams(ws, namespaceSpecificRoute, versionedListOptions); err != nil { + return err + } + addParams(namespaceSpecificRoute, namespaceSpecificParams) + ws.Route(namespaceSpecificRoute) + + return nil +} + +// This magic incantation returns *ptrToObject for an arbitrary pointer +func indirectArbitraryPointer(ptrToObject interface{}) interface{} { + return reflect.Indirect(reflect.ValueOf(ptrToObject)).Interface() +} + +// getResourceKind returns the external group version kind registered for the given storage object. +func (a *MetricsAPIInstaller) getResourceKind(storage rest.Storage) (schema.GroupVersionKind, error) { + object := storage.New() + fqKinds, _, err := a.group.Typer.ObjectKinds(object) + if err != nil { + return schema.GroupVersionKind{}, err + } + + // a given go type can have multiple potential fully qualified kinds. Find the one that corresponds with the group + // we're trying to register here + fqKindToRegister := schema.GroupVersionKind{} + for _, fqKind := range fqKinds { + if fqKind.Group == a.group.GroupVersion.Group { + fqKindToRegister = a.group.GroupVersion.WithKind(fqKind.Kind) + break + } + + // TODO: keep rid of extensions api group dependency here + // This keeps it doing what it was doing before, but it doesn't feel right. + if fqKind.Group == "extensions" && fqKind.Kind == "ThirdPartyResourceData" { + fqKindToRegister = a.group.GroupVersion.WithKind(fqKind.Kind) + } + } + if fqKindToRegister.Empty() { + return schema.GroupVersionKind{}, fmt.Errorf("unable to locate fully qualified kind for %v: found %v when registering for %v", reflect.TypeOf(object), fqKinds, a.group.GroupVersion) + } + return fqKindToRegister, nil +} + +// restMapping returns rest mapper for the resource provided by DynamicStorage. +func (a *MetricsAPIInstaller) restMapping() (*meta.RESTMapping, error) { + // subresources must have parent resources, and follow the namespacing rules of their parent. + // So get the storage of the resource (which is the parent resource in case of subresources) + fqKindToRegister, err := a.getResourceKind(a.group.DynamicStorage) + if err != nil { + return nil, fmt.Errorf("unable to locate fully qualified kind for mapper resource for dynamic storage: %v", err) + } + return a.group.Mapper.RESTMapping(fqKindToRegister.GroupKind(), fqKindToRegister.Version) +} + +func addParams(route *restful.RouteBuilder, params []*restful.Parameter) { + for _, param := range params { + route.Param(param) + } +} + +// addObjectParams converts a runtime.Object into a set of go-restful Param() definitions on the route. +// The object must be a pointer to a struct; only fields at the top level of the struct that are not +// themselves interfaces or structs are used; only fields with a json tag that is non empty (the standard +// Go JSON behavior for omitting a field) become query parameters. The name of the query parameter is +// the JSON field name. If a description struct tag is set on the field, that description is used on the +// query parameter. In essence, it converts a standard JSON top level object into a query param schema. +func addObjectParams(ws *restful.WebService, route *restful.RouteBuilder, obj interface{}) error { + sv, err := conversion.EnforcePtr(obj) + if err != nil { + return err + } + st := sv.Type() + switch st.Kind() { + case reflect.Struct: + for i := 0; i < st.NumField(); i++ { + name := st.Field(i).Name + sf, ok := st.FieldByName(name) + if !ok { + continue + } + switch sf.Type.Kind() { + case reflect.Interface, reflect.Struct: + case reflect.Ptr: + // TODO: This is a hack to let metav1.Time through. This needs to be fixed in a more generic way eventually. bug #36191 + if (sf.Type.Elem().Kind() == reflect.Interface || sf.Type.Elem().Kind() == reflect.Struct) && strings.TrimPrefix(sf.Type.String(), "*") != "metav1.Time" { + continue + } + fallthrough + default: + jsonTag := sf.Tag.Get("json") + if len(jsonTag) == 0 { + continue + } + jsonName := strings.SplitN(jsonTag, ",", 2)[0] + if len(jsonName) == 0 { + continue + } + + var desc string + if docable, ok := obj.(documentable); ok { + desc = docable.SwaggerDoc()[jsonName] + } + route.Param(ws.QueryParameter(jsonName, desc).DataType(typeToJSON(sf.Type.String()))) + } + } + } + return nil +} + +// TODO: this is incomplete, expand as needed. +// Convert the name of a golang type to the name of a JSON type +func typeToJSON(typeName string) string { + switch typeName { + case "bool", "*bool": + return "boolean" + case "uint8", "*uint8", "int", "*int", "int32", "*int32", "int64", "*int64", "uint32", "*uint32", "uint64", "*uint64": + return "integer" + case "float64", "*float64", "float32", "*float32": + return "number" + case "metav1.Time", "*metav1.Time": + return "string" + case "byte", "*byte": + return "string" + case "v1.DeletionPropagation", "*v1.DeletionPropagation": + return "string" + + // TODO: Fix these when go-restful supports a way to specify an array query param: + // https://github.com/emicklei/go-restful/issues/225 + case "[]string", "[]*string": + return "string" + case "[]int32", "[]*int32": + return "integer" + + default: + return typeName + } +} + +// rootScopeNaming reads only names from a request and ignores namespaces. It implements ScopeNamer +// for root scoped resources. +type rootScopeNaming struct { + scope meta.RESTScope + runtime.SelfLinker + pathPrefix string + pathSuffix string +} + +// rootScopeNaming implements ScopeNamer +var _ handlers.ScopeNamer = rootScopeNaming{} + +// Namespace returns an empty string because root scoped objects have no namespace. +func (n rootScopeNaming) Namespace(req *restful.Request) (namespace string, err error) { + return "", nil +} + +// Name returns the name from the path and an empty string for namespace, or an error if the +// name is empty. +func (n rootScopeNaming) Name(req *restful.Request) (namespace, name string, err error) { + name = req.PathParameter("name") + if len(name) == 0 { + return "", "", errEmptyName + } + return "", name, nil +} + +// GenerateLink returns the appropriate path and query to locate an object by its canonical path. +func (n rootScopeNaming) GenerateLink(req *restful.Request, obj runtime.Object) (uri string, err error) { + _, name, err := n.ObjectName(obj) + if err != nil { + return "", err + } + if len(name) == 0 { + _, name, err = n.Name(req) + if err != nil { + return "", err + } + } + return n.pathPrefix + url.QueryEscape(name) + n.pathSuffix, nil +} + +// GenerateListLink returns the appropriate path and query to locate a list by its canonical path. +func (n rootScopeNaming) GenerateListLink(req *restful.Request) (uri string, err error) { + if len(req.Request.URL.RawPath) > 0 { + return req.Request.URL.RawPath, nil + } + return req.Request.URL.EscapedPath(), nil +} + +// ObjectName returns the name set on the object, or an error if the +// name cannot be returned. Namespace is empty +// TODO: distinguish between objects with name/namespace and without via a specific error. +func (n rootScopeNaming) ObjectName(obj runtime.Object) (namespace, name string, err error) { + name, err = n.SelfLinker.Name(obj) + if err != nil { + return "", "", err + } + if len(name) == 0 { + return "", "", errEmptyName + } + return "", name, nil +} + +// scopeNaming returns naming information from a request. It implements ScopeNamer for +// namespace scoped resources. +type scopeNaming struct { + scope meta.RESTScope + runtime.SelfLinker + itemPathFn func(name, namespace, resource, subresource string) bytes.Buffer + allNamespaces bool +} + +// scopeNaming implements ScopeNamer +var _ handlers.ScopeNamer = scopeNaming{} + +// Namespace returns the namespace from the path or the default. +func (n scopeNaming) Namespace(req *restful.Request) (namespace string, err error) { + if n.allNamespaces { + return "", nil + } + namespace = req.PathParameter(n.scope.ArgumentName()) + if len(namespace) == 0 { + // a URL was constructed without the namespace, or this method was invoked + // on an object without a namespace path parameter. + return "", fmt.Errorf("no namespace parameter found on request") + } + return namespace, nil +} + +// Name returns the name from the path, the namespace (or default), or an error if the +// name is empty. +func (n scopeNaming) Name(req *restful.Request) (namespace, name string, err error) { + namespace, _ = n.Namespace(req) + name = req.PathParameter("name") + if len(name) == 0 { + return "", "", errEmptyName + } + return +} + +func (n scopeNaming) reqResource(req *restful.Request) (resource, subresource string) { + return req.PathParameter("resource"), req.PathParameter("subresource") +} + +// GenerateLink returns the appropriate path and query to locate an object by its canonical path. +func (n scopeNaming) GenerateLink(req *restful.Request, obj runtime.Object) (uri string, err error) { + namespace, name, err := n.ObjectName(obj) + if err != nil { + return "", err + } + if len(namespace) == 0 && len(name) == 0 { + namespace, name, err = n.Name(req) + if err != nil { + return "", err + } + } + if len(name) == 0 { + return "", errEmptyName + } + + resource, subresource := n.reqResource(req) + + result := n.itemPathFn(name, namespace, resource, subresource) + return result.String(), nil +} + +// GenerateListLink returns the appropriate path and query to locate a list by its canonical path. +func (n scopeNaming) GenerateListLink(req *restful.Request) (uri string, err error) { + if len(req.Request.URL.RawPath) > 0 { + return req.Request.URL.RawPath, nil + } + return req.Request.URL.EscapedPath(), nil +} + +// ObjectName returns the name and namespace set on the object, or an error if the +// name cannot be returned. +// TODO: distinguish between objects with name/namespace and without via a specific error. +func (n scopeNaming) ObjectName(obj runtime.Object) (namespace, name string, err error) { + name, err = n.SelfLinker.Name(obj) + if err != nil { + return "", "", err + } + namespace, err = n.SelfLinker.Namespace(obj) + if err != nil { + return "", "", err + } + return namespace, name, err +} + +// An interface to see if an object supports swagger documentation as a method +type documentable interface { + SwaggerDoc() map[string]string +} + +// errEmptyName is returned when API requests do not fill the name section of the path. +var errEmptyName = errors.NewBadRequest("name must be provided") diff --git a/pkg/apiserver/installer/installer_test.go b/pkg/apiserver/installer/installer_test.go new file mode 100644 index 000000000..49a92cbe9 --- /dev/null +++ b/pkg/apiserver/installer/installer_test.go @@ -0,0 +1,79 @@ +/* +Copyright 2017 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 installer + +import ( + "bytes" + "testing" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/pkg/api" + + "github.com/emicklei/go-restful" +) + +type setTestSelfLinker struct { + t *testing.T + expectedSet string + name string + namespace string + called bool + err error +} + +func (s *setTestSelfLinker) Namespace(runtime.Object) (string, error) { return s.namespace, s.err } +func (s *setTestSelfLinker) Name(runtime.Object) (string, error) { return s.name, s.err } +func (s *setTestSelfLinker) SelfLink(runtime.Object) (string, error) { return "", s.err } +func (s *setTestSelfLinker) SetSelfLink(obj runtime.Object, selfLink string) error { + if e, a := s.expectedSet, selfLink; e != a { + s.t.Errorf("expected '%v', got '%v'", e, a) + } + s.called = true + return s.err +} + +func TestScopeNamingGenerateLink(t *testing.T) { + selfLinker := &setTestSelfLinker{ + t: t, + expectedSet: "/api/v1/namespaces/other/services/foo", + name: "foo", + namespace: "other", + } + s := scopeNaming{ + meta.RESTScopeNamespace, + selfLinker, + func(name, namespace, resource, subresource string) bytes.Buffer { + return *bytes.NewBufferString("/api/v1/namespaces/" + namespace + "/services/" + name) + }, + true, + } + service := &api.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "other", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + }, + } + _, err := s.GenerateLink(&restful.Request{}, service) + if err != nil { + t.Errorf("Unexpected error %v", err) + } +} + diff --git a/pkg/cmd/server/start.go b/pkg/cmd/server/start.go index 7988bba0d..7019f4f3d 100644 --- a/pkg/cmd/server/start.go +++ b/pkg/cmd/server/start.go @@ -60,12 +60,12 @@ func (o *CustomMetricsAdapterServerOptions) Complete() error { } func (o CustomMetricsAdapterServerOptions) Config() (*apiserver.Config, error) { - // TODO have a "real" external address - if err := o.SecureServing.MaybeDefaultWithSelfSignedCerts("localhost", net.ParseIP("127.0.0.1")); err != nil { + // TODO have a "real" external address (have an AdvertiseAddress?) + if err := o.SecureServing.MaybeDefaultWithSelfSignedCerts("localhost", nil, []net.IP{net.ParseIP("127.0.0.1")}); err != nil { return nil, fmt.Errorf("error creating self-signed certificates: %v", err) } - serverConfig := genericapiserver.NewConfig().WithSerializer(apiserver.Codecs) + serverConfig := genericapiserver.NewConfig(apiserver.Codecs) if err := o.SecureServing.ApplyTo(serverConfig); err != nil { return nil, err } diff --git a/pkg/registry/custom_metrics/reststorage.go b/pkg/registry/custom_metrics/reststorage.go index a208e220e..afcc2e427 100644 --- a/pkg/registry/custom_metrics/reststorage.go +++ b/pkg/registry/custom_metrics/reststorage.go @@ -19,23 +19,23 @@ package apiserver import ( "fmt" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" genericapirequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/registry/rest" - "k8s.io/metrics/pkg/apis/custom_metrics" + specificinstaller "k8s.io/custom-metrics-boilerplate/pkg/apiserver/installer/context" "k8s.io/custom-metrics-boilerplate/pkg/provider" + "k8s.io/metrics/pkg/apis/custom_metrics" ) type REST struct { cmProvider provider.CustomMetricsProvider } -var _ rest.KindProvider = &REST{} var _ rest.Storage = &REST{} -var _ rest.GetterWithOptions = &REST{} +var _ rest.Lister = &REST{} func NewREST(cmProvider provider.CustomMetricsProvider) *REST { return &REST{ @@ -46,52 +46,80 @@ func NewREST(cmProvider provider.CustomMetricsProvider) *REST { // Implement Storage func (r *REST) New() runtime.Object { - return &custom_metrics.MetricValueList{} + return &custom_metrics.MetricValue{} } -// Implement KindProvider +// Implement Lister -func (r *REST) Kind() string { - return "MetricValueList" +func (r *REST) NewList() runtime.Object { + return &custom_metrics.MetricValueList{} } -func (r *REST) Get(ctx genericapirequest.Context, name string, options runtime.Object) (runtime.Object, error) { - var err error +func (r *REST) List(ctx genericapirequest.Context, options *metainternalversion.ListOptions) (runtime.Object, error) { + // populate the label selector, defaulting to all selector := labels.Everything() - if options != nil { - listOpts := options.(*metav1.ListOptions) - if listOpts.LabelSelector != "" { - selector, err = labels.Parse(listOpts.LabelSelector) - if err != nil { - return nil, err - } + if options != nil && options.LabelSelector != nil { + selector = options.LabelSelector + } + + // 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) + name := "*" + if options != nil && options.FieldSelector != nil { + if nameMatch, required := options.FieldSelector.RequiresExactMatch("metadata.name"); required { + name = nameMatch } } namespace := genericapirequest.NamespaceValue(ctx) - resourceRaw, metricName, ok := genericapirequest.ResourceInformationFrom(ctx) + resourceRaw, metricName, ok := specificinstaller.ResourceInformationFrom(ctx) if !ok { return nil, fmt.Errorf("unable to get resource and metric name from request") } groupResource := schema.ParseGroupResource(resourceRaw) - if namespace == "" { - if name == "*" { - return r.cmProvider.GetRootScopedMetricBySelector(groupResource, selector, metricName) - } - - return r.cmProvider.GetRootScopedMetricByName(groupResource, name, metricName) + // handle metrics describing namespaces + if namespace != "" && resourceRaw == "metrics" { + // namespace-describing metrics have a path of /namespaces/$NS/metrics/$metric, + groupResource = schema.GroupResource{Resource: "namespaces"} + metricName = name + name = namespace + namespace = "" } + // handle namespaced and root metrics if name == "*" { - return r.cmProvider.GetNamespacedMetricBySelector(groupResource, namespace, selector, metricName) + return r.handleWildcardOp(namespace, groupResource, selector, metricName) + } else { + return r.handleIndividualOp(namespace, groupResource, name, metricName) + } +} + +func (r *REST) handleIndividualOp(namespace string, groupResource schema.GroupResource, name string, metricName string) (*custom_metrics.MetricValueList, error) { + var err error + var singleRes *custom_metrics.MetricValue + if namespace == "" { + singleRes, err = r.cmProvider.GetRootScopedMetricByName(groupResource, name, metricName) + } else { + singleRes, err = r.cmProvider.GetNamespacedMetricByName(groupResource, namespace, name, metricName) } - return r.cmProvider.GetNamespacedMetricByName(groupResource, namespace, name, metricName) + if err != nil { + return nil, err + } + + return &custom_metrics.MetricValueList{ + Items: []custom_metrics.MetricValue{*singleRes}, + }, nil } -func (r *REST) NewGetOptions() (runtime.Object, bool, string) { - return &metav1.ListOptions{}, false, "" +func (r *REST) handleWildcardOp(namespace string, groupResource schema.GroupResource, selector labels.Selector, metricName string) (*custom_metrics.MetricValueList, error) { + if namespace == "" { + return r.cmProvider.GetRootScopedMetricBySelector(groupResource, selector, metricName) + } else { + return r.cmProvider.GetNamespacedMetricBySelector(groupResource, namespace, selector, metricName) + } }