mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-27 21:47:07 +00:00
Merge pull request #45182 from deads2k/tpr-08-simple-serving
Automatic merge from submit-queue (batch tested with PRs 45182, 45429) CustomResources in separate API server Builds on https://github.com/kubernetes/kubernetes/pull/45115. This adds a basic handler for custom resources. No status handling, no finalizers, no controllers, but basic CRUD runs to allow @enisoc and others to start considering migration. @kubernetes/sig-api-machinery-misc
This commit is contained in:
commit
6c4663635c
@ -408,6 +408,7 @@ staging/src/k8s.io/kube-apiextensions-server/pkg/client/informers/internalversio
|
|||||||
staging/src/k8s.io/kube-apiextensions-server/pkg/client/informers/internalversion/apiextensions/internalversion
|
staging/src/k8s.io/kube-apiextensions-server/pkg/client/informers/internalversion/apiextensions/internalversion
|
||||||
staging/src/k8s.io/kube-apiextensions-server/pkg/client/listers/apiextensions/internalversion
|
staging/src/k8s.io/kube-apiextensions-server/pkg/client/listers/apiextensions/internalversion
|
||||||
staging/src/k8s.io/kube-apiextensions-server/pkg/client/listers/apiextensions/v1alpha1
|
staging/src/k8s.io/kube-apiextensions-server/pkg/client/listers/apiextensions/v1alpha1
|
||||||
|
staging/src/k8s.io/kube-apiextensions-server/test/integration
|
||||||
staging/src/k8s.io/metrics/pkg/apis/custom_metrics/install
|
staging/src/k8s.io/metrics/pkg/apis/custom_metrics/install
|
||||||
staging/src/k8s.io/metrics/pkg/apis/metrics/install
|
staging/src/k8s.io/metrics/pkg/apis/metrics/install
|
||||||
staging/src/k8s.io/sample-apiserver
|
staging/src/k8s.io/sample-apiserver
|
||||||
|
@ -44,6 +44,9 @@ kube::test::find_integration_test_dirs() {
|
|||||||
find test/integration/ -name '*_test.go' -print0 \
|
find test/integration/ -name '*_test.go' -print0 \
|
||||||
| xargs -0n1 dirname | sed "s|^|${KUBE_GO_PACKAGE}/|" \
|
| xargs -0n1 dirname | sed "s|^|${KUBE_GO_PACKAGE}/|" \
|
||||||
| LC_ALL=C sort -u
|
| LC_ALL=C sort -u
|
||||||
|
find vendor/k8s.io/kube-apiextensions-server/test/integration/ -name '*_test.go' -print0 \
|
||||||
|
| xargs -0n1 dirname | sed "s|^|${KUBE_GO_PACKAGE}/|" \
|
||||||
|
| LC_ALL=C sort -u
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,7 +72,11 @@ kube::test::find_dirs() {
|
|||||||
find ./staging/src/k8s.io/kube-aggregator -name '*_test.go' \
|
find ./staging/src/k8s.io/kube-aggregator -name '*_test.go' \
|
||||||
-name '*_test.go' -print0 | xargs -0n1 dirname | sed 's|^\./staging/src/|./vendor/|' | LC_ALL=C sort -u
|
-name '*_test.go' -print0 | xargs -0n1 dirname | sed 's|^\./staging/src/|./vendor/|' | LC_ALL=C sort -u
|
||||||
|
|
||||||
find ./staging/src/k8s.io/kube-apiextensions-server -name '*_test.go' \
|
find ./staging/src/k8s.io/kube-apiextensions-server -not \( \
|
||||||
|
\( \
|
||||||
|
-o -path './test/integration/*' \
|
||||||
|
\) -prune \
|
||||||
|
\) -name '*_test.go' \
|
||||||
-name '*_test.go' -print0 | xargs -0n1 dirname | sed 's|^\./staging/src/|./vendor/|' | LC_ALL=C sort -u
|
-name '*_test.go' -print0 | xargs -0n1 dirname | sed 's|^\./staging/src/|./vendor/|' | LC_ALL=C sort -u
|
||||||
|
|
||||||
find ./staging/src/k8s.io/sample-apiserver -name '*_test.go' \
|
find ./staging/src/k8s.io/sample-apiserver -name '*_test.go' \
|
||||||
|
@ -28,15 +28,15 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||||
)
|
)
|
||||||
|
|
||||||
// apiGroupHandler creates a webservice serving the supported versions, preferred version, and name
|
// APIGroupHandler creates a webservice serving the supported versions, preferred version, and name
|
||||||
// of a group. E.g., such a web service will be registered at /apis/extensions.
|
// of a group. E.g., such a web service will be registered at /apis/extensions.
|
||||||
type apiGroupHandler struct {
|
type APIGroupHandler struct {
|
||||||
serializer runtime.NegotiatedSerializer
|
serializer runtime.NegotiatedSerializer
|
||||||
|
|
||||||
group metav1.APIGroup
|
group metav1.APIGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAPIGroupHandler(serializer runtime.NegotiatedSerializer, group metav1.APIGroup) *apiGroupHandler {
|
func NewAPIGroupHandler(serializer runtime.NegotiatedSerializer, group metav1.APIGroup) *APIGroupHandler {
|
||||||
if keepUnversioned(group.Name) {
|
if keepUnversioned(group.Name) {
|
||||||
// Because in release 1.1, /apis/extensions returns response with empty
|
// Because in release 1.1, /apis/extensions returns response with empty
|
||||||
// APIVersion, we use stripVersionNegotiatedSerializer to keep the
|
// APIVersion, we use stripVersionNegotiatedSerializer to keep the
|
||||||
@ -44,13 +44,13 @@ func NewAPIGroupHandler(serializer runtime.NegotiatedSerializer, group metav1.AP
|
|||||||
serializer = stripVersionNegotiatedSerializer{serializer}
|
serializer = stripVersionNegotiatedSerializer{serializer}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &apiGroupHandler{
|
return &APIGroupHandler{
|
||||||
serializer: serializer,
|
serializer: serializer,
|
||||||
group: group,
|
group: group,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *apiGroupHandler) WebService() *restful.WebService {
|
func (s *APIGroupHandler) WebService() *restful.WebService {
|
||||||
mediaTypes, _ := negotiation.MediaTypesForSerializer(s.serializer)
|
mediaTypes, _ := negotiation.MediaTypesForSerializer(s.serializer)
|
||||||
ws := new(restful.WebService)
|
ws := new(restful.WebService)
|
||||||
ws.Path(APIGroupPrefix + "/" + s.group.Name)
|
ws.Path(APIGroupPrefix + "/" + s.group.Name)
|
||||||
@ -65,6 +65,10 @@ func (s *apiGroupHandler) WebService() *restful.WebService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handle returns a handler which will return the api.GroupAndVersion of the group.
|
// handle returns a handler which will return the api.GroupAndVersion of the group.
|
||||||
func (s *apiGroupHandler) handle(req *restful.Request, resp *restful.Response) {
|
func (s *APIGroupHandler) handle(req *restful.Request, resp *restful.Response) {
|
||||||
responsewriters.WriteObjectNegotiated(s.serializer, schema.GroupVersion{}, resp.ResponseWriter, req.Request, http.StatusOK, &s.group)
|
s.ServeHTTP(resp.ResponseWriter, req.Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *APIGroupHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
|
responsewriters.WriteObjectNegotiated(s.serializer, schema.GroupVersion{}, w, req, http.StatusOK, &s.group)
|
||||||
}
|
}
|
||||||
|
@ -32,16 +32,22 @@ type APIResourceLister interface {
|
|||||||
ListAPIResources() []metav1.APIResource
|
ListAPIResources() []metav1.APIResource
|
||||||
}
|
}
|
||||||
|
|
||||||
// apiVersionHandler creates a webservice serving the supported resources for the version
|
type APIResourceListerFunc func() []metav1.APIResource
|
||||||
|
|
||||||
|
func (f APIResourceListerFunc) ListAPIResources() []metav1.APIResource {
|
||||||
|
return f()
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIVersionHandler creates a webservice serving the supported resources for the version
|
||||||
// E.g., such a web service will be registered at /apis/extensions/v1beta1.
|
// E.g., such a web service will be registered at /apis/extensions/v1beta1.
|
||||||
type apiVersionHandler struct {
|
type APIVersionHandler struct {
|
||||||
serializer runtime.NegotiatedSerializer
|
serializer runtime.NegotiatedSerializer
|
||||||
|
|
||||||
groupVersion schema.GroupVersion
|
groupVersion schema.GroupVersion
|
||||||
apiResourceLister APIResourceLister
|
apiResourceLister APIResourceLister
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAPIVersionHandler(serializer runtime.NegotiatedSerializer, groupVersion schema.GroupVersion, apiResourceLister APIResourceLister) *apiVersionHandler {
|
func NewAPIVersionHandler(serializer runtime.NegotiatedSerializer, groupVersion schema.GroupVersion, apiResourceLister APIResourceLister) *APIVersionHandler {
|
||||||
if keepUnversioned(groupVersion.Group) {
|
if keepUnversioned(groupVersion.Group) {
|
||||||
// Because in release 1.1, /apis/extensions returns response with empty
|
// Because in release 1.1, /apis/extensions returns response with empty
|
||||||
// APIVersion, we use stripVersionNegotiatedSerializer to keep the
|
// APIVersion, we use stripVersionNegotiatedSerializer to keep the
|
||||||
@ -49,14 +55,14 @@ func NewAPIVersionHandler(serializer runtime.NegotiatedSerializer, groupVersion
|
|||||||
serializer = stripVersionNegotiatedSerializer{serializer}
|
serializer = stripVersionNegotiatedSerializer{serializer}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &apiVersionHandler{
|
return &APIVersionHandler{
|
||||||
serializer: serializer,
|
serializer: serializer,
|
||||||
groupVersion: groupVersion,
|
groupVersion: groupVersion,
|
||||||
apiResourceLister: apiResourceLister,
|
apiResourceLister: apiResourceLister,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *apiVersionHandler) AddToWebService(ws *restful.WebService) {
|
func (s *APIVersionHandler) AddToWebService(ws *restful.WebService) {
|
||||||
mediaTypes, _ := negotiation.MediaTypesForSerializer(s.serializer)
|
mediaTypes, _ := negotiation.MediaTypesForSerializer(s.serializer)
|
||||||
ws.Route(ws.GET("/").To(s.handle).
|
ws.Route(ws.GET("/").To(s.handle).
|
||||||
Doc("get available resources").
|
Doc("get available resources").
|
||||||
@ -67,7 +73,11 @@ func (s *apiVersionHandler) AddToWebService(ws *restful.WebService) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handle returns a handler which will return the api.VersionAndVersion of the group.
|
// handle returns a handler which will return the api.VersionAndVersion of the group.
|
||||||
func (s *apiVersionHandler) handle(req *restful.Request, resp *restful.Response) {
|
func (s *APIVersionHandler) handle(req *restful.Request, resp *restful.Response) {
|
||||||
responsewriters.WriteObjectNegotiated(s.serializer, schema.GroupVersion{}, resp.ResponseWriter, req.Request, http.StatusOK,
|
s.ServeHTTP(resp.ResponseWriter, req.Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *APIVersionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
|
responsewriters.WriteObjectNegotiated(s.serializer, schema.GroupVersion{}, w, req, http.StatusOK,
|
||||||
&metav1.APIResourceList{GroupVersion: s.groupVersion.String(), APIResources: s.apiResourceLister.ListAPIResources()})
|
&metav1.APIResourceList{GroupVersion: s.groupVersion.String(), APIResources: s.apiResourceLister.ListAPIResources()})
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: apiregistration.k8s.io/v1alpha1
|
||||||
|
kind: APIService
|
||||||
|
metadata:
|
||||||
|
name: v1alpha1.mygroup.example.com
|
||||||
|
spec:
|
||||||
|
insecureSkipTLSVerify: true
|
||||||
|
group: mygroup.example.com
|
||||||
|
priority: 500
|
||||||
|
service:
|
||||||
|
name: api
|
||||||
|
namespace: kube-apiextensions
|
||||||
|
version: v1alpha1
|
@ -5,6 +5,7 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
group: mygroup.example.com
|
group: mygroup.example.com
|
||||||
version: v1alpha1
|
version: v1alpha1
|
||||||
|
scope: Namespaced
|
||||||
names:
|
names:
|
||||||
name: noxus
|
name: noxus
|
||||||
singular: noxu
|
singular: noxu
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: mygroup.example.com/v1alpha1
|
||||||
|
kind: Noxu
|
||||||
|
metadata:
|
||||||
|
name: alfa-noxu
|
||||||
|
spec:
|
||||||
|
key: value
|
@ -33,7 +33,7 @@ type CustomResourceSpec struct {
|
|||||||
|
|
||||||
// CustomResourceNames indicates the names to serve this CustomResource
|
// CustomResourceNames indicates the names to serve this CustomResource
|
||||||
type CustomResourceNames struct {
|
type CustomResourceNames struct {
|
||||||
// Plural is the plural name of the resource to serve. It must match the name of the TPR-registration
|
// Plural is the plural name of the resource to serve. It must match the name of the CustomResource-registration
|
||||||
// too: plural.group and it must be all lowercase.
|
// too: plural.group and it must be all lowercase.
|
||||||
Plural string
|
Plural string
|
||||||
// Singular is the singular name of the resource. It must be all lowercase Defaults to lowercased <kind>
|
// Singular is the singular name of the resource. It must be all lowercase Defaults to lowercased <kind>
|
||||||
|
@ -33,7 +33,7 @@ type CustomResourceSpec struct {
|
|||||||
|
|
||||||
// CustomResourceNames indicates the names to serve this CustomResource
|
// CustomResourceNames indicates the names to serve this CustomResource
|
||||||
type CustomResourceNames struct {
|
type CustomResourceNames struct {
|
||||||
// Plural is the plural name of the resource to serve. It must match the name of the TPR-registration
|
// Plural is the plural name of the resource to serve. It must match the name of the CustomResource-registration
|
||||||
// too: plural.group and it must be all lowercase.
|
// too: plural.group and it must be all lowercase.
|
||||||
Plural string `json:"plural" protobuf:"bytes,1,opt,name=plural"`
|
Plural string `json:"plural" protobuf:"bytes,1,opt,name=plural"`
|
||||||
// Singular is the singular name of the resource. It must be all lowercase Defaults to lowercased <kind>
|
// Singular is the singular name of the resource. It must be all lowercase Defaults to lowercased <kind>
|
||||||
|
@ -9,18 +9,42 @@ load(
|
|||||||
|
|
||||||
go_library(
|
go_library(
|
||||||
name = "go_default_library",
|
name = "go_default_library",
|
||||||
srcs = ["apiserver.go"],
|
srcs = [
|
||||||
|
"apiserver.go",
|
||||||
|
"customresource_discovery.go",
|
||||||
|
"customresource_discovery_controller.go",
|
||||||
|
"customresource_handler.go",
|
||||||
|
],
|
||||||
tags = ["automanaged"],
|
tags = ["automanaged"],
|
||||||
deps = [
|
deps = [
|
||||||
|
"//vendor/github.com/golang/glog:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/apimachinery/announced:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/apimachinery/announced:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/apimachinery/registered:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/apimachinery/registered:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/labels:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer/json:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/version:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/version:go_default_library",
|
||||||
|
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||||
|
"//vendor/k8s.io/apiserver/pkg/endpoints/discovery:go_default_library",
|
||||||
|
"//vendor/k8s.io/apiserver/pkg/endpoints/handlers:go_default_library",
|
||||||
|
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
||||||
|
"//vendor/k8s.io/apiserver/pkg/registry/generic:go_default_library",
|
||||||
|
"//vendor/k8s.io/apiserver/pkg/registry/generic/registry:go_default_library",
|
||||||
"//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library",
|
"//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library",
|
||||||
"//vendor/k8s.io/apiserver/pkg/server:go_default_library",
|
"//vendor/k8s.io/apiserver/pkg/server:go_default_library",
|
||||||
|
"//vendor/k8s.io/apiserver/pkg/storage/storagebackend:go_default_library",
|
||||||
|
"//vendor/k8s.io/client-go/discovery:go_default_library",
|
||||||
|
"//vendor/k8s.io/client-go/tools/cache:go_default_library",
|
||||||
|
"//vendor/k8s.io/client-go/util/workqueue:go_default_library",
|
||||||
"//vendor/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions:go_default_library",
|
"//vendor/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions:go_default_library",
|
||||||
"//vendor/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/install:go_default_library",
|
"//vendor/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/install:go_default_library",
|
||||||
"//vendor/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/v1alpha1:go_default_library",
|
"//vendor/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/v1alpha1:go_default_library",
|
||||||
@ -28,6 +52,9 @@ go_library(
|
|||||||
"//vendor/k8s.io/kube-apiextensions-server/pkg/client/clientset/internalclientset:go_default_library",
|
"//vendor/k8s.io/kube-apiextensions-server/pkg/client/clientset/internalclientset:go_default_library",
|
||||||
"//vendor/k8s.io/kube-apiextensions-server/pkg/client/informers/externalversions:go_default_library",
|
"//vendor/k8s.io/kube-apiextensions-server/pkg/client/informers/externalversions:go_default_library",
|
||||||
"//vendor/k8s.io/kube-apiextensions-server/pkg/client/informers/internalversion:go_default_library",
|
"//vendor/k8s.io/kube-apiextensions-server/pkg/client/informers/internalversion:go_default_library",
|
||||||
|
"//vendor/k8s.io/kube-apiextensions-server/pkg/client/informers/internalversion/apiextensions/internalversion:go_default_library",
|
||||||
|
"//vendor/k8s.io/kube-apiextensions-server/pkg/client/listers/apiextensions/internalversion:go_default_library",
|
||||||
"//vendor/k8s.io/kube-apiextensions-server/pkg/registry/customresource:go_default_library",
|
"//vendor/k8s.io/kube-apiextensions-server/pkg/registry/customresource:go_default_library",
|
||||||
|
"//vendor/k8s.io/kube-apiextensions-server/pkg/registry/customresourcestorage:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -17,6 +17,9 @@ limitations under the License.
|
|||||||
package apiserver
|
package apiserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/apimachinery/announced"
|
"k8s.io/apimachinery/pkg/apimachinery/announced"
|
||||||
"k8s.io/apimachinery/pkg/apimachinery/registered"
|
"k8s.io/apimachinery/pkg/apimachinery/registered"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
@ -24,17 +27,20 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||||
"k8s.io/apimachinery/pkg/version"
|
"k8s.io/apimachinery/pkg/version"
|
||||||
|
"k8s.io/apiserver/pkg/endpoints/discovery"
|
||||||
|
genericregistry "k8s.io/apiserver/pkg/registry/generic"
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||||
|
|
||||||
"k8s.io/kube-apiextensions-server/pkg/apis/apiextensions"
|
"k8s.io/kube-apiextensions-server/pkg/apis/apiextensions"
|
||||||
"k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/install"
|
"k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/install"
|
||||||
"k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/v1alpha1"
|
"k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/v1alpha1"
|
||||||
|
"k8s.io/kube-apiextensions-server/pkg/client/clientset/internalclientset"
|
||||||
|
internalinformers "k8s.io/kube-apiextensions-server/pkg/client/informers/internalversion"
|
||||||
"k8s.io/kube-apiextensions-server/pkg/registry/customresource"
|
"k8s.io/kube-apiextensions-server/pkg/registry/customresource"
|
||||||
|
|
||||||
// make sure the generated client works
|
// make sure the generated client works
|
||||||
_ "k8s.io/kube-apiextensions-server/pkg/client/clientset/clientset"
|
_ "k8s.io/kube-apiextensions-server/pkg/client/clientset/clientset"
|
||||||
_ "k8s.io/kube-apiextensions-server/pkg/client/clientset/internalclientset"
|
|
||||||
_ "k8s.io/kube-apiextensions-server/pkg/client/informers/externalversions"
|
_ "k8s.io/kube-apiextensions-server/pkg/client/informers/externalversions"
|
||||||
_ "k8s.io/kube-apiextensions-server/pkg/client/informers/internalversion"
|
_ "k8s.io/kube-apiextensions-server/pkg/client/informers/internalversion"
|
||||||
)
|
)
|
||||||
@ -64,6 +70,8 @@ func init() {
|
|||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
GenericConfig *genericapiserver.Config
|
GenericConfig *genericapiserver.Config
|
||||||
|
|
||||||
|
CustomResourceRESTOptionsGetter genericregistry.RESTOptionsGetter
|
||||||
}
|
}
|
||||||
|
|
||||||
type CustomResources struct {
|
type CustomResources struct {
|
||||||
@ -76,6 +84,7 @@ type completedConfig struct {
|
|||||||
|
|
||||||
// Complete fills in any fields not set that are required to have valid data. It's mutating the receiver.
|
// Complete fills in any fields not set that are required to have valid data. It's mutating the receiver.
|
||||||
func (c *Config) Complete() completedConfig {
|
func (c *Config) Complete() completedConfig {
|
||||||
|
c.GenericConfig.EnableDiscovery = false
|
||||||
c.GenericConfig.Complete()
|
c.GenericConfig.Complete()
|
||||||
|
|
||||||
c.GenericConfig.Version = &version.Info{
|
c.GenericConfig.Version = &version.Info{
|
||||||
@ -92,7 +101,7 @@ func (c *Config) SkipComplete() completedConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new instance of CustomResources from the given config.
|
// New returns a new instance of CustomResources from the given config.
|
||||||
func (c completedConfig) New() (*CustomResources, error) {
|
func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget, stopCh <-chan struct{}) (*CustomResources, error) {
|
||||||
genericServer, err := c.Config.GenericConfig.SkipComplete().New() // completion is done in Complete, no need for a second time
|
genericServer, err := c.Config.GenericConfig.SkipComplete().New() // completion is done in Complete, no need for a second time
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -112,5 +121,47 @@ func (c completedConfig) New() (*CustomResources, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customResourceClient, err := internalclientset.NewForConfig(s.GenericAPIServer.LoopbackClientConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
customResourceInformers := internalinformers.NewSharedInformerFactory(customResourceClient, 5*time.Minute)
|
||||||
|
|
||||||
|
delegateHandler := delegationTarget.UnprotectedHandler()
|
||||||
|
if delegateHandler == nil {
|
||||||
|
delegateHandler = http.NotFoundHandler()
|
||||||
|
}
|
||||||
|
|
||||||
|
versionDiscoveryHandler := &versionDiscoveryHandler{
|
||||||
|
discovery: map[schema.GroupVersion]*discovery.APIVersionHandler{},
|
||||||
|
delegate: delegateHandler,
|
||||||
|
}
|
||||||
|
groupDiscoveryHandler := &groupDiscoveryHandler{
|
||||||
|
discovery: map[string]*discovery.APIGroupHandler{},
|
||||||
|
delegate: delegateHandler,
|
||||||
|
}
|
||||||
|
customResourceHandler := NewCustomResourceHandler(
|
||||||
|
versionDiscoveryHandler,
|
||||||
|
groupDiscoveryHandler,
|
||||||
|
s.GenericAPIServer.RequestContextMapper(),
|
||||||
|
customResourceInformers.Apiextensions().InternalVersion().CustomResources().Lister(),
|
||||||
|
delegationTarget.UnprotectedHandler(),
|
||||||
|
c.CustomResourceRESTOptionsGetter,
|
||||||
|
c.GenericConfig.AdmissionControl,
|
||||||
|
)
|
||||||
|
s.GenericAPIServer.FallThroughHandler.Handle("/apis", customResourceHandler)
|
||||||
|
s.GenericAPIServer.FallThroughHandler.HandlePrefix("/apis/", customResourceHandler)
|
||||||
|
|
||||||
|
customResourceController := NewDiscoveryController(customResourceInformers.Apiextensions().InternalVersion().CustomResources(), versionDiscoveryHandler, groupDiscoveryHandler)
|
||||||
|
|
||||||
|
s.GenericAPIServer.AddPostStartHook("start-apiextensions-informers", func(context genericapiserver.PostStartHookContext) error {
|
||||||
|
customResourceInformers.Start(stopCh)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
s.GenericAPIServer.AddPostStartHook("start-apiextensions-controllers", func(context genericapiserver.PostStartHookContext) error {
|
||||||
|
go customResourceController.Run(stopCh)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
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 apiserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apiserver/pkg/endpoints/discovery"
|
||||||
|
)
|
||||||
|
|
||||||
|
type versionDiscoveryHandler struct {
|
||||||
|
// TODO, writing is infrequent, optimize this
|
||||||
|
discoveryLock sync.RWMutex
|
||||||
|
discovery map[schema.GroupVersion]*discovery.APIVersionHandler
|
||||||
|
|
||||||
|
delegate http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *versionDiscoveryHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
|
pathParts := splitPath(req.URL.Path)
|
||||||
|
// only match /apis/<group>/<version>
|
||||||
|
if len(pathParts) != 3 || pathParts[0] != "apis" {
|
||||||
|
r.delegate.ServeHTTP(w, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
discovery, ok := r.getDiscovery(schema.GroupVersion{Group: pathParts[1], Version: pathParts[2]})
|
||||||
|
if !ok {
|
||||||
|
r.delegate.ServeHTTP(w, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
discovery.ServeHTTP(w, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *versionDiscoveryHandler) getDiscovery(gv schema.GroupVersion) (*discovery.APIVersionHandler, bool) {
|
||||||
|
r.discoveryLock.RLock()
|
||||||
|
defer r.discoveryLock.RUnlock()
|
||||||
|
|
||||||
|
ret, ok := r.discovery[gv]
|
||||||
|
return ret, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *versionDiscoveryHandler) setDiscovery(gv schema.GroupVersion, discovery *discovery.APIVersionHandler) {
|
||||||
|
r.discoveryLock.Lock()
|
||||||
|
defer r.discoveryLock.Unlock()
|
||||||
|
|
||||||
|
r.discovery[gv] = discovery
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *versionDiscoveryHandler) unsetDiscovery(gv schema.GroupVersion) {
|
||||||
|
r.discoveryLock.Lock()
|
||||||
|
defer r.discoveryLock.Unlock()
|
||||||
|
|
||||||
|
delete(r.discovery, gv)
|
||||||
|
}
|
||||||
|
|
||||||
|
type groupDiscoveryHandler struct {
|
||||||
|
// TODO, writing is infrequent, optimize this
|
||||||
|
discoveryLock sync.RWMutex
|
||||||
|
discovery map[string]*discovery.APIGroupHandler
|
||||||
|
|
||||||
|
delegate http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *groupDiscoveryHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
|
pathParts := splitPath(req.URL.Path)
|
||||||
|
// only match /apis/<group>
|
||||||
|
if len(pathParts) != 2 || pathParts[0] != "apis" {
|
||||||
|
r.delegate.ServeHTTP(w, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
discovery, ok := r.getDiscovery(pathParts[1])
|
||||||
|
if !ok {
|
||||||
|
r.delegate.ServeHTTP(w, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
discovery.ServeHTTP(w, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *groupDiscoveryHandler) getDiscovery(group string) (*discovery.APIGroupHandler, bool) {
|
||||||
|
r.discoveryLock.RLock()
|
||||||
|
defer r.discoveryLock.RUnlock()
|
||||||
|
|
||||||
|
ret, ok := r.discovery[group]
|
||||||
|
return ret, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *groupDiscoveryHandler) setDiscovery(group string, discovery *discovery.APIGroupHandler) {
|
||||||
|
r.discoveryLock.Lock()
|
||||||
|
defer r.discoveryLock.Unlock()
|
||||||
|
|
||||||
|
r.discovery[group] = discovery
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *groupDiscoveryHandler) unsetDiscovery(group string) {
|
||||||
|
r.discoveryLock.Lock()
|
||||||
|
defer r.discoveryLock.Unlock()
|
||||||
|
|
||||||
|
delete(r.discovery, group)
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitPath returns the segments for a URL path.
|
||||||
|
func splitPath(path string) []string {
|
||||||
|
path = strings.Trim(path, "/")
|
||||||
|
if path == "" {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
return strings.Split(path, "/")
|
||||||
|
}
|
@ -0,0 +1,211 @@
|
|||||||
|
/*
|
||||||
|
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 apiserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang/glog"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
"k8s.io/apiserver/pkg/endpoints/discovery"
|
||||||
|
"k8s.io/client-go/tools/cache"
|
||||||
|
"k8s.io/client-go/util/workqueue"
|
||||||
|
|
||||||
|
"k8s.io/kube-apiextensions-server/pkg/apis/apiextensions"
|
||||||
|
informers "k8s.io/kube-apiextensions-server/pkg/client/informers/internalversion/apiextensions/internalversion"
|
||||||
|
listers "k8s.io/kube-apiextensions-server/pkg/client/listers/apiextensions/internalversion"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DiscoveryController struct {
|
||||||
|
versionHandler *versionDiscoveryHandler
|
||||||
|
groupHandler *groupDiscoveryHandler
|
||||||
|
|
||||||
|
customResourceLister listers.CustomResourceLister
|
||||||
|
customResourcesSynced cache.InformerSynced
|
||||||
|
|
||||||
|
// To allow injection for testing.
|
||||||
|
syncFn func(version schema.GroupVersion) error
|
||||||
|
|
||||||
|
queue workqueue.RateLimitingInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDiscoveryController(customResourceInformer informers.CustomResourceInformer, versionHandler *versionDiscoveryHandler, groupHandler *groupDiscoveryHandler) *DiscoveryController {
|
||||||
|
c := &DiscoveryController{
|
||||||
|
versionHandler: versionHandler,
|
||||||
|
groupHandler: groupHandler,
|
||||||
|
customResourceLister: customResourceInformer.Lister(),
|
||||||
|
customResourcesSynced: customResourceInformer.Informer().HasSynced,
|
||||||
|
|
||||||
|
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "DiscoveryController"),
|
||||||
|
}
|
||||||
|
|
||||||
|
customResourceInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||||
|
AddFunc: c.addCustomResource,
|
||||||
|
UpdateFunc: c.updateCustomResource,
|
||||||
|
DeleteFunc: c.deleteCustomResource,
|
||||||
|
})
|
||||||
|
|
||||||
|
c.syncFn = c.sync
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DiscoveryController) sync(version schema.GroupVersion) error {
|
||||||
|
|
||||||
|
apiVersionsForDiscovery := []metav1.GroupVersionForDiscovery{}
|
||||||
|
apiResourcesForDiscovery := []metav1.APIResource{}
|
||||||
|
|
||||||
|
customResources, err := c.customResourceLister.List(labels.Everything())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
foundVersion := false
|
||||||
|
foundGroup := false
|
||||||
|
for _, customResource := range customResources {
|
||||||
|
// TODO add status checking
|
||||||
|
|
||||||
|
if customResource.Spec.Group != version.Group {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
foundGroup = true
|
||||||
|
apiVersionsForDiscovery = append(apiVersionsForDiscovery, metav1.GroupVersionForDiscovery{
|
||||||
|
GroupVersion: customResource.Spec.Group + "/" + customResource.Spec.Version,
|
||||||
|
Version: customResource.Spec.Version,
|
||||||
|
})
|
||||||
|
|
||||||
|
if customResource.Spec.Version != version.Version {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
foundVersion = true
|
||||||
|
|
||||||
|
apiResourcesForDiscovery = append(apiResourcesForDiscovery, metav1.APIResource{
|
||||||
|
Name: customResource.Spec.Names.Plural,
|
||||||
|
SingularName: customResource.Spec.Names.Singular,
|
||||||
|
Namespaced: customResource.Spec.Scope == apiextensions.NamespaceScoped,
|
||||||
|
Kind: customResource.Spec.Names.Kind,
|
||||||
|
Verbs: metav1.Verbs([]string{"delete", "deletecollection", "get", "list", "patch", "create", "update", "watch"}),
|
||||||
|
ShortNames: customResource.Spec.Names.ShortNames,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundGroup {
|
||||||
|
c.groupHandler.unsetDiscovery(version.Group)
|
||||||
|
c.versionHandler.unsetDiscovery(version)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
apiGroup := metav1.APIGroup{
|
||||||
|
Name: version.Group,
|
||||||
|
Versions: apiVersionsForDiscovery,
|
||||||
|
// the preferred versions for a group is arbitrary since there cannot be duplicate resources
|
||||||
|
PreferredVersion: apiVersionsForDiscovery[0],
|
||||||
|
}
|
||||||
|
c.groupHandler.setDiscovery(version.Group, discovery.NewAPIGroupHandler(Codecs, apiGroup))
|
||||||
|
|
||||||
|
if !foundVersion {
|
||||||
|
c.versionHandler.unsetDiscovery(version)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c.versionHandler.setDiscovery(version, discovery.NewAPIVersionHandler(Codecs, version, discovery.APIResourceListerFunc(func() []metav1.APIResource {
|
||||||
|
return apiResourcesForDiscovery
|
||||||
|
})))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DiscoveryController) Run(stopCh <-chan struct{}) {
|
||||||
|
defer utilruntime.HandleCrash()
|
||||||
|
defer c.queue.ShutDown()
|
||||||
|
defer glog.Infof("Shutting down DiscoveryController")
|
||||||
|
|
||||||
|
glog.Infof("Starting DiscoveryController")
|
||||||
|
|
||||||
|
if !cache.WaitForCacheSync(stopCh, c.customResourcesSynced) {
|
||||||
|
utilruntime.HandleError(fmt.Errorf("timed out waiting for caches to sync"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// only start one worker thread since its a slow moving API
|
||||||
|
go wait.Until(c.runWorker, time.Second, stopCh)
|
||||||
|
|
||||||
|
<-stopCh
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DiscoveryController) runWorker() {
|
||||||
|
for c.processNextWorkItem() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// processNextWorkItem deals with one key off the queue. It returns false when it's time to quit.
|
||||||
|
func (c *DiscoveryController) processNextWorkItem() bool {
|
||||||
|
key, quit := c.queue.Get()
|
||||||
|
if quit {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer c.queue.Done(key)
|
||||||
|
|
||||||
|
err := c.syncFn(key.(schema.GroupVersion))
|
||||||
|
if err == nil {
|
||||||
|
c.queue.Forget(key)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
utilruntime.HandleError(fmt.Errorf("%v failed with: %v", key, err))
|
||||||
|
c.queue.AddRateLimited(key)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DiscoveryController) enqueue(obj *apiextensions.CustomResource) {
|
||||||
|
c.queue.Add(schema.GroupVersion{Group: obj.Spec.Group, Version: obj.Spec.Version})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DiscoveryController) addCustomResource(obj interface{}) {
|
||||||
|
castObj := obj.(*apiextensions.CustomResource)
|
||||||
|
glog.V(4).Infof("Adding customresource %s", castObj.Name)
|
||||||
|
c.enqueue(castObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DiscoveryController) updateCustomResource(obj, _ interface{}) {
|
||||||
|
castObj := obj.(*apiextensions.CustomResource)
|
||||||
|
glog.V(4).Infof("Updating customresource %s", castObj.Name)
|
||||||
|
c.enqueue(castObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DiscoveryController) deleteCustomResource(obj interface{}) {
|
||||||
|
castObj, ok := obj.(*apiextensions.CustomResource)
|
||||||
|
if !ok {
|
||||||
|
tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
|
||||||
|
if !ok {
|
||||||
|
glog.Errorf("Couldn't get object from tombstone %#v", obj)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
castObj, ok = tombstone.Obj.(*apiextensions.CustomResource)
|
||||||
|
if !ok {
|
||||||
|
glog.Errorf("Tombstone contained object that is not expected %#v", obj)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
glog.V(4).Infof("Deleting customresource %q", castObj.Name)
|
||||||
|
c.enqueue(castObj)
|
||||||
|
}
|
@ -0,0 +1,372 @@
|
|||||||
|
/*
|
||||||
|
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 apiserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/serializer/json"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
|
"k8s.io/apiserver/pkg/admission"
|
||||||
|
"k8s.io/apiserver/pkg/endpoints/handlers"
|
||||||
|
apirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
|
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
|
||||||
|
"k8s.io/apiserver/pkg/storage/storagebackend"
|
||||||
|
"k8s.io/client-go/discovery"
|
||||||
|
|
||||||
|
"k8s.io/kube-apiextensions-server/pkg/apis/apiextensions"
|
||||||
|
listers "k8s.io/kube-apiextensions-server/pkg/client/listers/apiextensions/internalversion"
|
||||||
|
"k8s.io/kube-apiextensions-server/pkg/registry/customresourcestorage"
|
||||||
|
)
|
||||||
|
|
||||||
|
// customResourceHandler serves the `/apis` endpoint.
|
||||||
|
// This is registered as a filter so that it never collides with any explictly registered endpoints
|
||||||
|
type customResourceHandler struct {
|
||||||
|
versionDiscoveryHandler *versionDiscoveryHandler
|
||||||
|
groupDiscoveryHandler *groupDiscoveryHandler
|
||||||
|
|
||||||
|
customStorageLock sync.Mutex
|
||||||
|
// customStorage contains a customResourceStorageMap
|
||||||
|
customStorage atomic.Value
|
||||||
|
|
||||||
|
requestContextMapper apirequest.RequestContextMapper
|
||||||
|
|
||||||
|
customResourceLister listers.CustomResourceLister
|
||||||
|
|
||||||
|
delegate http.Handler
|
||||||
|
restOptionsGetter generic.RESTOptionsGetter
|
||||||
|
admission admission.Interface
|
||||||
|
}
|
||||||
|
|
||||||
|
// customResourceInfo stores enough information to serve the storage for the custom resource
|
||||||
|
type customResourceInfo struct {
|
||||||
|
storage *customresourcestorage.REST
|
||||||
|
requestScope handlers.RequestScope
|
||||||
|
}
|
||||||
|
|
||||||
|
// customResourceStorageMap goes from customresource to its storage
|
||||||
|
type customResourceStorageMap map[types.UID]*customResourceInfo
|
||||||
|
|
||||||
|
func NewCustomResourceHandler(
|
||||||
|
versionDiscoveryHandler *versionDiscoveryHandler,
|
||||||
|
groupDiscoveryHandler *groupDiscoveryHandler,
|
||||||
|
requestContextMapper apirequest.RequestContextMapper,
|
||||||
|
customResourceLister listers.CustomResourceLister,
|
||||||
|
delegate http.Handler,
|
||||||
|
restOptionsGetter generic.RESTOptionsGetter,
|
||||||
|
admission admission.Interface) *customResourceHandler {
|
||||||
|
ret := &customResourceHandler{
|
||||||
|
versionDiscoveryHandler: versionDiscoveryHandler,
|
||||||
|
groupDiscoveryHandler: groupDiscoveryHandler,
|
||||||
|
customStorage: atomic.Value{},
|
||||||
|
requestContextMapper: requestContextMapper,
|
||||||
|
customResourceLister: customResourceLister,
|
||||||
|
delegate: delegate,
|
||||||
|
restOptionsGetter: restOptionsGetter,
|
||||||
|
admission: admission,
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.customStorage.Store(customResourceStorageMap{})
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *customResourceHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
|
ctx, ok := r.requestContextMapper.Get(req)
|
||||||
|
if !ok {
|
||||||
|
// programmer error
|
||||||
|
panic("missing context")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
requestInfo, ok := apirequest.RequestInfoFrom(ctx)
|
||||||
|
if !ok {
|
||||||
|
// programmer error
|
||||||
|
panic("missing requestInfo")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !requestInfo.IsResourceRequest {
|
||||||
|
pathParts := splitPath(requestInfo.Path)
|
||||||
|
// only match /apis/<group>/<version>
|
||||||
|
// only registered under /apis
|
||||||
|
if len(pathParts) == 3 {
|
||||||
|
r.versionDiscoveryHandler.ServeHTTP(w, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// only match /apis/<group>
|
||||||
|
if len(pathParts) == 2 {
|
||||||
|
r.groupDiscoveryHandler.ServeHTTP(w, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.delegate.ServeHTTP(w, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(requestInfo.Subresource) > 0 {
|
||||||
|
http.NotFound(w, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
customResourceName := requestInfo.Resource + "." + requestInfo.APIGroup
|
||||||
|
customResource, err := r.customResourceLister.Get(customResourceName)
|
||||||
|
if apierrors.IsNotFound(err) {
|
||||||
|
r.delegate.ServeHTTP(w, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if customResource.Spec.Version != requestInfo.APIVersion {
|
||||||
|
r.delegate.ServeHTTP(w, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO this is the point to do the condition checks
|
||||||
|
|
||||||
|
customResourceInfo := r.getServingInfoFor(customResource)
|
||||||
|
storage := customResourceInfo.storage
|
||||||
|
requestScope := customResourceInfo.requestScope
|
||||||
|
minRequestTimeout := 1 * time.Minute
|
||||||
|
|
||||||
|
switch requestInfo.Verb {
|
||||||
|
case "get":
|
||||||
|
handler := handlers.GetResource(storage, storage, requestScope)
|
||||||
|
handler(w, req)
|
||||||
|
return
|
||||||
|
case "list":
|
||||||
|
forceWatch := false
|
||||||
|
handler := handlers.ListResource(storage, storage, requestScope, forceWatch, minRequestTimeout)
|
||||||
|
handler(w, req)
|
||||||
|
return
|
||||||
|
case "watch":
|
||||||
|
forceWatch := true
|
||||||
|
handler := handlers.ListResource(storage, storage, requestScope, forceWatch, minRequestTimeout)
|
||||||
|
handler(w, req)
|
||||||
|
return
|
||||||
|
case "create":
|
||||||
|
handler := handlers.CreateResource(storage, requestScope, discovery.NewUnstructuredObjectTyper(nil), r.admission)
|
||||||
|
handler(w, req)
|
||||||
|
return
|
||||||
|
case "update":
|
||||||
|
handler := handlers.UpdateResource(storage, requestScope, discovery.NewUnstructuredObjectTyper(nil), r.admission)
|
||||||
|
handler(w, req)
|
||||||
|
return
|
||||||
|
case "patch":
|
||||||
|
handler := handlers.PatchResource(storage, requestScope, r.admission, unstructured.UnstructuredObjectConverter{})
|
||||||
|
handler(w, req)
|
||||||
|
return
|
||||||
|
case "delete":
|
||||||
|
allowsOptions := true
|
||||||
|
handler := handlers.DeleteResource(storage, allowsOptions, requestScope, r.admission)
|
||||||
|
handler(w, req)
|
||||||
|
return
|
||||||
|
|
||||||
|
default:
|
||||||
|
http.Error(w, fmt.Sprintf("unhandled verb %q", requestInfo.Verb), http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeDeadStorage removes REST storage that isn't being used
|
||||||
|
func (r *customResourceHandler) removeDeadStorage() {
|
||||||
|
// these don't have to be live. A snapshot is fine
|
||||||
|
// if we wrongly delete, that's ok. The rest storage will be recreated on the next request
|
||||||
|
// if we wrongly miss one, that's ok. We'll get it next time
|
||||||
|
storageMap := r.customStorage.Load().(customResourceStorageMap)
|
||||||
|
allCustomResources, err := r.customResourceLister.List(labels.Everything())
|
||||||
|
if err != nil {
|
||||||
|
utilruntime.HandleError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for uid := range storageMap {
|
||||||
|
found := false
|
||||||
|
for _, customResource := range allCustomResources {
|
||||||
|
if customResource.UID == uid {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
delete(storageMap, uid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.customStorageLock.Lock()
|
||||||
|
defer r.customStorageLock.Unlock()
|
||||||
|
|
||||||
|
r.customStorage.Store(storageMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *customResourceHandler) getServingInfoFor(customResource *apiextensions.CustomResource) *customResourceInfo {
|
||||||
|
storageMap := r.customStorage.Load().(customResourceStorageMap)
|
||||||
|
ret, ok := storageMap[customResource.UID]
|
||||||
|
if ok {
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
r.customStorageLock.Lock()
|
||||||
|
defer r.customStorageLock.Unlock()
|
||||||
|
|
||||||
|
ret, ok = storageMap[customResource.UID]
|
||||||
|
if ok {
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
storage := customresourcestorage.NewREST(
|
||||||
|
schema.GroupResource{Group: customResource.Spec.Group, Resource: customResource.Spec.Names.Plural},
|
||||||
|
schema.GroupVersionKind{Group: customResource.Spec.Group, Version: customResource.Spec.Version, Kind: customResource.Spec.Names.ListKind},
|
||||||
|
UnstructuredCopier{},
|
||||||
|
customresourcestorage.NewStrategy(discovery.NewUnstructuredObjectTyper(nil), customResource.Spec.Scope == apiextensions.NamespaceScoped),
|
||||||
|
r.restOptionsGetter,
|
||||||
|
)
|
||||||
|
|
||||||
|
parameterScheme := runtime.NewScheme()
|
||||||
|
parameterScheme.AddUnversionedTypes(schema.GroupVersion{Group: customResource.Spec.Group, Version: customResource.Spec.Version},
|
||||||
|
&metav1.ListOptions{},
|
||||||
|
&metav1.ExportOptions{},
|
||||||
|
&metav1.GetOptions{},
|
||||||
|
&metav1.DeleteOptions{},
|
||||||
|
)
|
||||||
|
parameterScheme.AddGeneratedDeepCopyFuncs(metav1.GetGeneratedDeepCopyFuncs()...)
|
||||||
|
parameterCodec := runtime.NewParameterCodec(parameterScheme)
|
||||||
|
|
||||||
|
requestScope := handlers.RequestScope{
|
||||||
|
Namer: handlers.ContextBasedNaming{
|
||||||
|
GetContext: func(req *http.Request) apirequest.Context {
|
||||||
|
ret, _ := r.requestContextMapper.Get(req)
|
||||||
|
return ret
|
||||||
|
},
|
||||||
|
SelfLinker: meta.NewAccessor(),
|
||||||
|
ClusterScoped: customResource.Spec.Scope == apiextensions.ClusterScoped,
|
||||||
|
},
|
||||||
|
ContextFunc: func(req *http.Request) apirequest.Context {
|
||||||
|
ret, _ := r.requestContextMapper.Get(req)
|
||||||
|
return ret
|
||||||
|
},
|
||||||
|
|
||||||
|
Serializer: UnstructuredNegotiatedSerializer{},
|
||||||
|
ParameterCodec: parameterCodec,
|
||||||
|
|
||||||
|
Creater: UnstructuredCreator{},
|
||||||
|
Convertor: unstructured.UnstructuredObjectConverter{},
|
||||||
|
Defaulter: UnstructuredDefaulter{},
|
||||||
|
Copier: UnstructuredCopier{},
|
||||||
|
Typer: discovery.NewUnstructuredObjectTyper(nil),
|
||||||
|
UnsafeConvertor: unstructured.UnstructuredObjectConverter{},
|
||||||
|
|
||||||
|
Resource: schema.GroupVersionResource{Group: customResource.Spec.Group, Version: customResource.Spec.Version, Resource: customResource.Spec.Names.Plural},
|
||||||
|
Kind: schema.GroupVersionKind{Group: customResource.Spec.Group, Version: customResource.Spec.Version, Kind: customResource.Spec.Names.Kind},
|
||||||
|
Subresource: "",
|
||||||
|
|
||||||
|
MetaGroupVersion: metav1.SchemeGroupVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = &customResourceInfo{
|
||||||
|
storage: storage,
|
||||||
|
requestScope: requestScope,
|
||||||
|
}
|
||||||
|
storageMap[customResource.UID] = ret
|
||||||
|
r.customStorage.Store(storageMap)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnstructuredNegotiatedSerializer struct{}
|
||||||
|
|
||||||
|
func (s UnstructuredNegotiatedSerializer) SupportedMediaTypes() []runtime.SerializerInfo {
|
||||||
|
return []runtime.SerializerInfo{
|
||||||
|
{
|
||||||
|
MediaType: "application/json",
|
||||||
|
EncodesAsText: true,
|
||||||
|
Serializer: json.NewSerializer(json.DefaultMetaFactory, UnstructuredCreator{}, discovery.NewUnstructuredObjectTyper(nil), false),
|
||||||
|
PrettySerializer: json.NewSerializer(json.DefaultMetaFactory, UnstructuredCreator{}, discovery.NewUnstructuredObjectTyper(nil), true),
|
||||||
|
StreamSerializer: &runtime.StreamSerializerInfo{
|
||||||
|
EncodesAsText: true,
|
||||||
|
Serializer: json.NewSerializer(json.DefaultMetaFactory, UnstructuredCreator{}, discovery.NewUnstructuredObjectTyper(nil), false),
|
||||||
|
Framer: json.Framer,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s UnstructuredNegotiatedSerializer) EncoderForVersion(serializer runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder {
|
||||||
|
return unstructured.UnstructuredJSONScheme
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s UnstructuredNegotiatedSerializer) DecoderToVersion(serializer runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder {
|
||||||
|
return unstructured.UnstructuredJSONScheme
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnstructuredCreator struct{}
|
||||||
|
|
||||||
|
func (UnstructuredCreator) New(kind schema.GroupVersionKind) (runtime.Object, error) {
|
||||||
|
ret := &unstructured.Unstructured{}
|
||||||
|
ret.SetGroupVersionKind(kind)
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnstructuredCopier struct{}
|
||||||
|
|
||||||
|
func (UnstructuredCopier) Copy(obj runtime.Object) (runtime.Object, error) {
|
||||||
|
// serialize and deserialize to ensure a clean copy
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
err := unstructured.UnstructuredJSONScheme.Encode(obj, buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out := &unstructured.Unstructured{}
|
||||||
|
result, _, err := unstructured.UnstructuredJSONScheme.Decode(buf.Bytes(), nil, out)
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnstructuredDefaulter struct{}
|
||||||
|
|
||||||
|
func (UnstructuredDefaulter) Default(in runtime.Object) {}
|
||||||
|
|
||||||
|
type CustomResourceRESTOptionsGetter struct {
|
||||||
|
StorageConfig storagebackend.Config
|
||||||
|
StoragePrefix string
|
||||||
|
EnableWatchCache bool
|
||||||
|
EnableGarbageCollection bool
|
||||||
|
DeleteCollectionWorkers int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t CustomResourceRESTOptionsGetter) GetRESTOptions(resource schema.GroupResource) (generic.RESTOptions, error) {
|
||||||
|
ret := generic.RESTOptions{
|
||||||
|
StorageConfig: &t.StorageConfig,
|
||||||
|
Decorator: generic.UndecoratedStorage,
|
||||||
|
EnableGarbageCollection: t.EnableGarbageCollection,
|
||||||
|
DeleteCollectionWorkers: t.DeleteCollectionWorkers,
|
||||||
|
ResourcePrefix: t.StoragePrefix + "/" + resource.Group + "/" + resource.Resource,
|
||||||
|
}
|
||||||
|
if t.EnableWatchCache {
|
||||||
|
ret.Decorator = genericregistry.StorageWithCacher
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
|
}
|
@ -13,6 +13,7 @@ go_library(
|
|||||||
tags = ["automanaged"],
|
tags = ["automanaged"],
|
||||||
deps = [
|
deps = [
|
||||||
"//vendor/github.com/spf13/cobra:go_default_library",
|
"//vendor/github.com/spf13/cobra:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
|
||||||
"//vendor/k8s.io/apiserver/pkg/server:go_default_library",
|
"//vendor/k8s.io/apiserver/pkg/server:go_default_library",
|
||||||
"//vendor/k8s.io/apiserver/pkg/server/options:go_default_library",
|
"//vendor/k8s.io/apiserver/pkg/server/options:go_default_library",
|
||||||
"//vendor/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/v1alpha1:go_default_library",
|
"//vendor/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/v1alpha1:go_default_library",
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||||
genericoptions "k8s.io/apiserver/pkg/server/options"
|
genericoptions "k8s.io/apiserver/pkg/server/options"
|
||||||
"k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/v1alpha1"
|
"k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/v1alpha1"
|
||||||
@ -94,8 +95,19 @@ func (o CustomResourcesServerOptions) Config() (*apiserver.Config, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customResourceRESTOptionsGetter := apiserver.CustomResourceRESTOptionsGetter{
|
||||||
|
StorageConfig: o.RecommendedOptions.Etcd.StorageConfig,
|
||||||
|
StoragePrefix: o.RecommendedOptions.Etcd.StorageConfig.Prefix,
|
||||||
|
EnableWatchCache: o.RecommendedOptions.Etcd.EnableWatchCache,
|
||||||
|
EnableGarbageCollection: o.RecommendedOptions.Etcd.EnableGarbageCollection,
|
||||||
|
DeleteCollectionWorkers: o.RecommendedOptions.Etcd.DeleteCollectionWorkers,
|
||||||
|
}
|
||||||
|
customResourceRESTOptionsGetter.StorageConfig.Codec = unstructured.UnstructuredJSONScheme
|
||||||
|
customResourceRESTOptionsGetter.StorageConfig.Copier = apiserver.UnstructuredCopier{}
|
||||||
|
|
||||||
config := &apiserver.Config{
|
config := &apiserver.Config{
|
||||||
GenericConfig: serverConfig,
|
GenericConfig: serverConfig,
|
||||||
|
CustomResourceRESTOptionsGetter: customResourceRESTOptionsGetter,
|
||||||
}
|
}
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
@ -106,7 +118,7 @@ func (o CustomResourcesServerOptions) RunCustomResourcesServer(stopCh <-chan str
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
server, err := config.Complete().New()
|
server, err := config.Complete().New(genericapiserver.EmptyDelegate, stopCh)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
package(default_visibility = ["//visibility:public"])
|
||||||
|
|
||||||
|
licenses(["notice"])
|
||||||
|
|
||||||
|
load(
|
||||||
|
"@io_bazel_rules_go//go:def.bzl",
|
||||||
|
"go_library",
|
||||||
|
)
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "go_default_library",
|
||||||
|
srcs = [
|
||||||
|
"etcd.go",
|
||||||
|
"strategy.go",
|
||||||
|
],
|
||||||
|
tags = ["automanaged"],
|
||||||
|
deps = [
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/api/validation:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/fields:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/labels:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
||||||
|
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
||||||
|
"//vendor/k8s.io/apiserver/pkg/registry/generic:go_default_library",
|
||||||
|
"//vendor/k8s.io/apiserver/pkg/registry/generic/registry:go_default_library",
|
||||||
|
"//vendor/k8s.io/apiserver/pkg/storage:go_default_library",
|
||||||
|
"//vendor/k8s.io/apiserver/pkg/storage/names:go_default_library",
|
||||||
|
],
|
||||||
|
)
|
@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
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 customresourcestorage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
|
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
// rest implements a RESTStorage for API services against etcd
|
||||||
|
type REST struct {
|
||||||
|
*genericregistry.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewREST returns a RESTStorage object that will work against API services.
|
||||||
|
func NewREST(resource schema.GroupResource, listKind schema.GroupVersionKind, copier runtime.ObjectCopier, strategy CustomResourceStorageStrategy, optsGetter generic.RESTOptionsGetter) *REST {
|
||||||
|
store := &genericregistry.Store{
|
||||||
|
Copier: copier,
|
||||||
|
NewFunc: func() runtime.Object { return &unstructured.Unstructured{} },
|
||||||
|
NewListFunc: func() runtime.Object {
|
||||||
|
// lists are never stored, only manufactured, so stomp in the right kind
|
||||||
|
ret := &unstructured.UnstructuredList{}
|
||||||
|
ret.SetGroupVersionKind(listKind)
|
||||||
|
return ret
|
||||||
|
},
|
||||||
|
ObjectNameFunc: func(obj runtime.Object) (string, error) {
|
||||||
|
accessor, err := meta.Accessor(obj)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return accessor.GetName(), nil
|
||||||
|
},
|
||||||
|
PredicateFunc: strategy.MatchCustomResourceStorage,
|
||||||
|
QualifiedResource: resource,
|
||||||
|
|
||||||
|
CreateStrategy: strategy,
|
||||||
|
UpdateStrategy: strategy,
|
||||||
|
DeleteStrategy: strategy,
|
||||||
|
}
|
||||||
|
options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: strategy.GetAttrs}
|
||||||
|
if err := store.CompleteWithOptions(options); err != nil {
|
||||||
|
panic(err) // TODO: Propagate error up
|
||||||
|
}
|
||||||
|
return &REST{store}
|
||||||
|
}
|
@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
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 customresourcestorage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
"k8s.io/apimachinery/pkg/api/validation"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
|
"k8s.io/apiserver/pkg/storage"
|
||||||
|
"k8s.io/apiserver/pkg/storage/names"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CustomResourceStorageStrategy struct {
|
||||||
|
runtime.ObjectTyper
|
||||||
|
names.NameGenerator
|
||||||
|
|
||||||
|
namespaceScoped bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStrategy(typer runtime.ObjectTyper, namespaceScoped bool) CustomResourceStorageStrategy {
|
||||||
|
return CustomResourceStorageStrategy{typer, names.SimpleNameGenerator, namespaceScoped}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a CustomResourceStorageStrategy) NamespaceScoped() bool {
|
||||||
|
return a.namespaceScoped
|
||||||
|
}
|
||||||
|
|
||||||
|
func (CustomResourceStorageStrategy) PrepareForCreate(ctx genericapirequest.Context, obj runtime.Object) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (CustomResourceStorageStrategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime.Object) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a CustomResourceStorageStrategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList {
|
||||||
|
accessor, err := meta.Accessor(obj)
|
||||||
|
if err != nil {
|
||||||
|
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
|
||||||
|
}
|
||||||
|
|
||||||
|
return validation.ValidateObjectMetaAccessor(accessor, a.namespaceScoped, validation.NameIsDNSSubdomain, field.NewPath("metadata"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (CustomResourceStorageStrategy) AllowCreateOnUpdate() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (CustomResourceStorageStrategy) AllowUnconditionalUpdate() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (CustomResourceStorageStrategy) Canonicalize(obj runtime.Object) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (CustomResourceStorageStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
|
||||||
|
objAccessor, err := meta.Accessor(obj)
|
||||||
|
if err != nil {
|
||||||
|
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
|
||||||
|
}
|
||||||
|
oldAccessor, err := meta.Accessor(old)
|
||||||
|
if err != nil {
|
||||||
|
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
|
||||||
|
}
|
||||||
|
|
||||||
|
return validation.ValidateObjectMetaAccessorUpdate(objAccessor, oldAccessor, field.NewPath("metadata"))
|
||||||
|
|
||||||
|
return field.ErrorList{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a CustomResourceStorageStrategy) GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) {
|
||||||
|
accessor, err := meta.Accessor(obj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return labels.Set(accessor.GetLabels()), objectMetaFieldsSet(accessor, a.namespaceScoped), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// objectMetaFieldsSet returns a fields that represent the ObjectMeta.
|
||||||
|
func objectMetaFieldsSet(objectMeta metav1.Object, namespaceScoped bool) fields.Set {
|
||||||
|
if namespaceScoped {
|
||||||
|
return fields.Set{
|
||||||
|
"metadata.name": objectMeta.GetName(),
|
||||||
|
"metadata.namespace": objectMeta.GetNamespace(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fields.Set{
|
||||||
|
"metadata.name": objectMeta.GetName(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a CustomResourceStorageStrategy) MatchCustomResourceStorage(label labels.Selector, field fields.Selector) storage.SelectionPredicate {
|
||||||
|
return storage.SelectionPredicate{
|
||||||
|
Label: label,
|
||||||
|
Field: field,
|
||||||
|
GetAttrs: a.GetAttrs,
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package(default_visibility = ["//visibility:public"])
|
||||||
|
|
||||||
|
licenses(["notice"])
|
||||||
|
|
||||||
|
load(
|
||||||
|
"@io_bazel_rules_go//go:def.bzl",
|
||||||
|
"go_test",
|
||||||
|
)
|
||||||
|
|
||||||
|
go_test(
|
||||||
|
name = "go_default_test",
|
||||||
|
srcs = ["basic_test.go"],
|
||||||
|
tags = [
|
||||||
|
"automanaged",
|
||||||
|
"integration",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/watch:go_default_library",
|
||||||
|
"//vendor/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/v1alpha1:go_default_library",
|
||||||
|
"//vendor/k8s.io/kube-apiextensions-server/test/integration/testserver:go_default_library",
|
||||||
|
],
|
||||||
|
)
|
@ -0,0 +1,19 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDEzCCAfugAwIBAgIBATANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRsb2Nh
|
||||||
|
bGhvc3RAMTQ5MzY2NDQ4OTAeFw0xNzA1MDExODQ4MDlaFw0xODA1MDExODQ4MDla
|
||||||
|
MB8xHTAbBgNVBAMMFGxvY2FsaG9zdEAxNDkzNjY0NDg5MIIBIjANBgkqhkiG9w0B
|
||||||
|
AQEFAAOCAQ8AMIIBCgKCAQEAy6tMZcDbG1J4vbX+YdiPswxqO+hX1r+i9+DFb1N/
|
||||||
|
xyodBbprn1Mmd2k1lv3AkiZKm38v7dgzQ9/teA8Jm/1tyjjZSV/CxsZZWNuukPGU
|
||||||
|
ykEtn4mvkb5tOI1159ieTBiL4mKx5VNq8DkIpy9CT22Ud9dHkJaxJHcIF601hXHg
|
||||||
|
GIRla/6CRlkY/GFUItl1oij4sgzXRTS2pdv8lsmt2s7dXj737l10QCz9YDVuGSfu
|
||||||
|
rYoHGwY5ofYYFWzscD7Ds4O0tPdu4mSPIu753K7nB3ilfBi+tUWcSXpw9wE4+hIF
|
||||||
|
a1In8jnM+lw5/j/UoghrCtQ54BGWzpivPPXKv2dlNIOPiwIDAQABo1owWDAOBgNV
|
||||||
|
HQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUwAwEB
|
||||||
|
/zAgBgNVHREEGTAXgglsb2NhbGhvc3SHBH8AAAGHBH8AAAEwDQYJKoZIhvcNAQEL
|
||||||
|
BQADggEBAJbxTi/0Joxx/oja4QDksbWroip0qVJKh1ic7ryai52aSBTcHMF9pWiL
|
||||||
|
047lL3sL0sN0YavXPUiow4PMTQm14W01ciwuZj5DCCaXnmnGtBy0fy8ifUdQoD/J
|
||||||
|
9pvLQMWAsx+GP2XzY+KxYFQairKS7BehEF/d24TgNHPskgc2p2XgK3Z7Ipp7hQrj
|
||||||
|
yZiTNromeULT12d5Zuwf+IeDp3aopGyhxCTOoc+RCz4MKLfKov40xjlaA4jVWazd
|
||||||
|
ccHWnagwM5lDlXnmCqZRVvyOWaUulJCEzRFfRTHFxKgj6DSPNt00wHXNmQUvjhN/
|
||||||
|
YXFAkfKQQEs3qQRXoHAXKquplnLgjyA=
|
||||||
|
-----END CERTIFICATE-----
|
@ -0,0 +1,27 @@
|
|||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEpQIBAAKCAQEAy6tMZcDbG1J4vbX+YdiPswxqO+hX1r+i9+DFb1N/xyodBbpr
|
||||||
|
n1Mmd2k1lv3AkiZKm38v7dgzQ9/teA8Jm/1tyjjZSV/CxsZZWNuukPGUykEtn4mv
|
||||||
|
kb5tOI1159ieTBiL4mKx5VNq8DkIpy9CT22Ud9dHkJaxJHcIF601hXHgGIRla/6C
|
||||||
|
RlkY/GFUItl1oij4sgzXRTS2pdv8lsmt2s7dXj737l10QCz9YDVuGSfurYoHGwY5
|
||||||
|
ofYYFWzscD7Ds4O0tPdu4mSPIu753K7nB3ilfBi+tUWcSXpw9wE4+hIFa1In8jnM
|
||||||
|
+lw5/j/UoghrCtQ54BGWzpivPPXKv2dlNIOPiwIDAQABAoIBABy4qWtoCP4PYUuP
|
||||||
|
kLIHsiwTwh90on58Q+Uk43LRmaFihPk70tWDCleolJAYdMGneLn4869c383glEJs
|
||||||
|
DHTdBlCQN8QrJvKVIiBvymxSRSNIkcB/0CyDaC+jc08gsyIUDBX+yQuH+fqqcFfz
|
||||||
|
SCyfTWKhD0yKk6yKxK9iE7wf1PRf5uLtJD6x1vV0NBmsHH++feODjNVsHDRvnwy6
|
||||||
|
3KXkgSvfCTQ7qnQPZ/MSsRxWRdMBCnhaQq9qRnJ8bv8XotrCsEG5laMybriyJYXX
|
||||||
|
wvr9Dt04ciUD/g3qwIPy1ygMAKE9ya8hivSRURptZxz9SKCenWWihsfIzk4uyOi+
|
||||||
|
sVDkJVECgYEA2981ZhbkruG5JhOsWVXTxDlXOrjI0pUqfej0WEp8lwdBMOrTXFs1
|
||||||
|
cB8kdSocVC5GnMTbg6bqJrfglbNDOA7sUA8E8APLFFUAAvPwVfnrdDWk+0jK7atu
|
||||||
|
2sixQGeIB97Y6ojWfSbjyA/0p3Z0zCTfP5SR6xt4hVWhInB11m5IpO0CgYEA7SKH
|
||||||
|
bfWPZ0xfkFdM2X2cWQjt4zYHIUQqAKLPs2OjPTBC8PioNiZlvZq9bNseYR+eyIg6
|
||||||
|
P9Kwe4KV/hzOSScf7JYpsN+YQ1+4Y+E63BkhAHXyz7y/vD+DA/Z83oKbelQtJqwq
|
||||||
|
W+Zo1OGJrfZwEKK5JWN9HF+KI9Z1iMyZoyw8L1cCgYEAtaLDfj7TVBVs2qPN8U8R
|
||||||
|
zjyAbyZP4IcRv0o+8OE345w+oqabTOScVK+lcpUDKhfAhamqniu5q5qjkYextBG/
|
||||||
|
7rM5pP29OmKty8KxfJUlia73SA9udMD2pw68PzRIEBhsofPBHUqPSarEtcMJ4ctk
|
||||||
|
EiYuFUdwXNXMc6Lr9eTNZlECgYEAsfFJIvAzjdY3l76KwmGJox4aNHdkXkgiJJwH
|
||||||
|
s5s+8Tl34g8VWpzxl5e4MSkz4LmzktL2stHM8MGLAEZpXWdog0YjPsBqJ5R6byih
|
||||||
|
3GtW4lufutbuIbqe+6hJB0eGmAL2ZqCmoJODcstTXyEf8rvIpw/C4DmpFT9mryKo
|
||||||
|
31LgTr0CgYEAuxusmnR2vzZP/RjpjzmZcvIHf4xORG+SXlg3BXsSEd6+g3Rqiy5t
|
||||||
|
Q0UkHHwYnYurBmJ2HL1LG9mZwU89D00F/4mJpJuWfwqtqvodIRZ7bimyGGbvKZ1t
|
||||||
|
BGLmUssF5MYn75v7E5opxcc51aieW8nUQbop/PPMvWsYLrL/mcJNBpA=
|
||||||
|
-----END RSA PRIVATE KEY-----
|
@ -0,0 +1,189 @@
|
|||||||
|
/*
|
||||||
|
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 integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
|
apiextensionsv1alpha1 "k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/v1alpha1"
|
||||||
|
"k8s.io/kube-apiextensions-server/test/integration/testserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestServerUp(t *testing.T) {
|
||||||
|
stopCh, _, _, err := testserver.StartDefaultServer()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer close(stopCh)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimpleCRUD(t *testing.T) {
|
||||||
|
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer close(stopCh)
|
||||||
|
|
||||||
|
noxuDefinition := testserver.NewNoxuCustomResourceDefinition()
|
||||||
|
noxuVersionClient, err := testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ns := "not-the-default"
|
||||||
|
noxuNamespacedResourceClient := noxuVersionClient.Resource(&metav1.APIResource{
|
||||||
|
Name: noxuDefinition.Spec.Names.Plural,
|
||||||
|
Namespaced: noxuDefinition.Spec.Scope == apiextensionsv1alpha1.NamespaceScoped,
|
||||||
|
}, ns)
|
||||||
|
initialList, err := noxuNamespacedResourceClient.List(metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if e, a := 0, len(initialList.(*unstructured.UnstructuredList).Items); e != a {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
initialListTypeMeta, err := meta.TypeAccessor(initialList)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if e, a := noxuDefinition.Spec.Group+"/"+noxuDefinition.Spec.Version, initialListTypeMeta.GetAPIVersion(); e != a {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if e, a := noxuDefinition.Spec.Names.ListKind, initialListTypeMeta.GetKind(); e != a {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
initialListListMeta, err := meta.ListAccessor(initialList)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
noxuNamespacedWatch, err := noxuNamespacedResourceClient.Watch(metav1.ListOptions{ResourceVersion: initialListListMeta.GetResourceVersion()})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer noxuNamespacedWatch.Stop()
|
||||||
|
|
||||||
|
noxuInstanceToCreate := testserver.NewNoxuInstance(ns, "foo")
|
||||||
|
createdNoxuInstance, err := noxuNamespacedResourceClient.Create(noxuInstanceToCreate)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("%#v", createdNoxuInstance)
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
createdObjectMeta, err := meta.Accessor(createdNoxuInstance)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// it should have a UUID
|
||||||
|
if len(createdObjectMeta.GetUID()) == 0 {
|
||||||
|
t.Errorf("missing uuid: %#v", createdNoxuInstance)
|
||||||
|
}
|
||||||
|
createdTypeMeta, err := meta.TypeAccessor(createdNoxuInstance)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if e, a := noxuDefinition.Spec.Group+"/"+noxuDefinition.Spec.Version, createdTypeMeta.GetAPIVersion(); e != a {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if e, a := noxuDefinition.Spec.Names.Kind, createdTypeMeta.GetKind(); e != a {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case watchEvent := <-noxuNamespacedWatch.ResultChan():
|
||||||
|
if e, a := watch.Added, watchEvent.Type; e != a {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
createdObjectMeta, err := meta.Accessor(watchEvent.Object)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// it should have a UUID
|
||||||
|
if len(createdObjectMeta.GetUID()) == 0 {
|
||||||
|
t.Errorf("missing uuid: %#v", watchEvent.Object)
|
||||||
|
}
|
||||||
|
createdTypeMeta, err := meta.TypeAccessor(watchEvent.Object)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if e, a := noxuDefinition.Spec.Group+"/"+noxuDefinition.Spec.Version, createdTypeMeta.GetAPIVersion(); e != a {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if e, a := noxuDefinition.Spec.Names.Kind, createdTypeMeta.GetKind(); e != a {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Errorf("missing watch event")
|
||||||
|
}
|
||||||
|
|
||||||
|
gottenNoxuInstance, err := noxuNamespacedResourceClient.Get("foo")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if e, a := createdNoxuInstance, gottenNoxuInstance; !reflect.DeepEqual(e, a) {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
listWithItem, err := noxuNamespacedResourceClient.List(metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if e, a := 1, len(listWithItem.(*unstructured.UnstructuredList).Items); e != a {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if e, a := *createdNoxuInstance, listWithItem.(*unstructured.UnstructuredList).Items[0]; !reflect.DeepEqual(e, a) {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := noxuNamespacedResourceClient.Delete("foo", nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
listWithoutItem, err := noxuNamespacedResourceClient.List(metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if e, a := 0, len(listWithoutItem.(*unstructured.UnstructuredList).Items); e != a {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case watchEvent := <-noxuNamespacedWatch.ResultChan():
|
||||||
|
if e, a := watch.Deleted, watchEvent.Type; e != a {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
deletedObjectMeta, err := meta.Accessor(watchEvent.Object)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// it should have a UUID
|
||||||
|
if e, a := createdObjectMeta.GetUID(), deletedObjectMeta.GetUID(); e != a {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Errorf("missing watch event")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package(default_visibility = ["//visibility:public"])
|
||||||
|
|
||||||
|
licenses(["notice"])
|
||||||
|
|
||||||
|
load(
|
||||||
|
"@io_bazel_rules_go//go:def.bzl",
|
||||||
|
"go_library",
|
||||||
|
)
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "go_default_library",
|
||||||
|
srcs = [
|
||||||
|
"resources.go",
|
||||||
|
"start.go",
|
||||||
|
],
|
||||||
|
tags = ["automanaged"],
|
||||||
|
deps = [
|
||||||
|
"//vendor/github.com/pborman/uuid:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
||||||
|
"//vendor/k8s.io/apiserver/pkg/authorization/authorizerfactory:go_default_library",
|
||||||
|
"//vendor/k8s.io/apiserver/pkg/server:go_default_library",
|
||||||
|
"//vendor/k8s.io/client-go/dynamic:go_default_library",
|
||||||
|
"//vendor/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/v1alpha1:go_default_library",
|
||||||
|
"//vendor/k8s.io/kube-apiextensions-server/pkg/apiserver:go_default_library",
|
||||||
|
"//vendor/k8s.io/kube-apiextensions-server/pkg/client/clientset/clientset:go_default_library",
|
||||||
|
"//vendor/k8s.io/kube-apiextensions-server/pkg/cmd/server:go_default_library",
|
||||||
|
],
|
||||||
|
)
|
@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
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 testserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
|
apiextensionsv1alpha1 "k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/v1alpha1"
|
||||||
|
"k8s.io/kube-apiextensions-server/pkg/client/clientset/clientset"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewNoxuCustomResourceDefinition() *apiextensionsv1alpha1.CustomResource {
|
||||||
|
return &apiextensionsv1alpha1.CustomResource{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "noxus.mygroup.example.com"},
|
||||||
|
Spec: apiextensionsv1alpha1.CustomResourceSpec{
|
||||||
|
Group: "mygroup.example.com",
|
||||||
|
Version: "v1alpha1",
|
||||||
|
Names: apiextensionsv1alpha1.CustomResourceNames{
|
||||||
|
Plural: "noxus",
|
||||||
|
Singular: "nonenglishnoxu",
|
||||||
|
Kind: "WishIHadChosenNoxu",
|
||||||
|
ListKind: "NoxuItemList",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNoxuInstance(namespace, name string) *unstructured.Unstructured {
|
||||||
|
return &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "mygroup.example.com/v1alpha1",
|
||||||
|
"kind": "WishIHadChosenNoxu",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"namespace": namespace,
|
||||||
|
"name": name,
|
||||||
|
},
|
||||||
|
"content": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateNewCustomResourceDefinition(customResource *apiextensionsv1alpha1.CustomResource, apiExtensionsClient clientset.Interface, clientPool dynamic.ClientPool) (*dynamic.Client, error) {
|
||||||
|
_, err := apiExtensionsClient.Apiextensions().CustomResources().Create(customResource)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait until the resource appears in discovery
|
||||||
|
err = wait.PollImmediate(30*time.Millisecond, 30*time.Second, func() (bool, error) {
|
||||||
|
resourceList, err := apiExtensionsClient.Discovery().ServerResourcesForGroupVersion(customResource.Spec.Group + "/" + customResource.Spec.Version)
|
||||||
|
if err != nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
for _, resource := range resourceList.APIResources {
|
||||||
|
if resource.Name == customResource.Spec.Names.Plural {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamicClient, err := clientPool.ClientForGroupVersionResource(schema.GroupVersionResource{Group: customResource.Spec.Group, Version: customResource.Spec.Version, Resource: customResource.Spec.Names.Plural})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return dynamicClient, nil
|
||||||
|
}
|
@ -0,0 +1,178 @@
|
|||||||
|
/*
|
||||||
|
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 testserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pborman/uuid"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
"k8s.io/apiserver/pkg/authorization/authorizerfactory"
|
||||||
|
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
|
extensionsapiserver "k8s.io/kube-apiextensions-server/pkg/apiserver"
|
||||||
|
"k8s.io/kube-apiextensions-server/pkg/client/clientset/clientset"
|
||||||
|
"k8s.io/kube-apiextensions-server/pkg/cmd/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DefaultServerConfig() (*extensionsapiserver.Config, error) {
|
||||||
|
port, err := FindFreeLocalPort()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
options := server.NewCustomResourcesServerOptions(os.Stdout, os.Stderr)
|
||||||
|
options.RecommendedOptions.Audit.Path = "-"
|
||||||
|
options.RecommendedOptions.SecureServing.BindPort = port
|
||||||
|
options.RecommendedOptions.Authentication.SkipInClusterLookup = true
|
||||||
|
options.RecommendedOptions.SecureServing.BindAddress = net.ParseIP("127.0.0.1")
|
||||||
|
etcdURL, ok := os.LookupEnv("KUBE_INTEGRATION_ETCD_URL")
|
||||||
|
if !ok {
|
||||||
|
etcdURL = "http://127.0.0.1:2379"
|
||||||
|
}
|
||||||
|
options.RecommendedOptions.Etcd.StorageConfig.ServerList = []string{etcdURL}
|
||||||
|
options.RecommendedOptions.Etcd.StorageConfig.Prefix = uuid.New()
|
||||||
|
|
||||||
|
// TODO stop copying this
|
||||||
|
// because there isn't currently a way to disable authentication or authorization from options
|
||||||
|
// explode options.Config here
|
||||||
|
genericConfig := genericapiserver.NewConfig(extensionsapiserver.Codecs)
|
||||||
|
genericConfig.Authenticator = nil
|
||||||
|
genericConfig.Authorizer = authorizerfactory.NewAlwaysAllowAuthorizer()
|
||||||
|
|
||||||
|
if err := options.RecommendedOptions.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)
|
||||||
|
}
|
||||||
|
if err := options.RecommendedOptions.Etcd.ApplyTo(genericConfig); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := options.RecommendedOptions.SecureServing.ApplyTo(genericConfig); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := options.RecommendedOptions.Audit.ApplyTo(genericConfig); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := options.RecommendedOptions.Features.ApplyTo(genericConfig); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
customResourceRESTOptionsGetter := extensionsapiserver.CustomResourceRESTOptionsGetter{
|
||||||
|
StorageConfig: options.RecommendedOptions.Etcd.StorageConfig,
|
||||||
|
StoragePrefix: options.RecommendedOptions.Etcd.StorageConfig.Prefix,
|
||||||
|
EnableWatchCache: options.RecommendedOptions.Etcd.EnableWatchCache,
|
||||||
|
EnableGarbageCollection: options.RecommendedOptions.Etcd.EnableGarbageCollection,
|
||||||
|
DeleteCollectionWorkers: options.RecommendedOptions.Etcd.DeleteCollectionWorkers,
|
||||||
|
}
|
||||||
|
customResourceRESTOptionsGetter.StorageConfig.Codec = unstructured.UnstructuredJSONScheme
|
||||||
|
customResourceRESTOptionsGetter.StorageConfig.Copier = extensionsapiserver.UnstructuredCopier{}
|
||||||
|
|
||||||
|
config := &extensionsapiserver.Config{
|
||||||
|
GenericConfig: genericConfig,
|
||||||
|
CustomResourceRESTOptionsGetter: customResourceRESTOptionsGetter,
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartServer(config *extensionsapiserver.Config) (chan struct{}, clientset.Interface, dynamic.ClientPool, error) {
|
||||||
|
stopCh := make(chan struct{})
|
||||||
|
server, err := config.Complete().New(genericapiserver.EmptyDelegate, stopCh)
|
||||||
|
if err != nil {
|
||||||
|
close(stopCh)
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
err := server.GenericAPIServer.PrepareRun().Run(stopCh)
|
||||||
|
if err != nil {
|
||||||
|
close(stopCh)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// wait until the server is healthy
|
||||||
|
err = wait.PollImmediate(30*time.Millisecond, 30*time.Second, func() (bool, error) {
|
||||||
|
healthClient, err := clientset.NewForConfig(server.GenericAPIServer.LoopbackClientConfig)
|
||||||
|
if err != nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
healthResult := healthClient.Discovery().RESTClient().Get().AbsPath("/healthz").Do()
|
||||||
|
if healthResult.Error() != nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
rawHealth, err := healthResult.Raw()
|
||||||
|
if err != nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if string(rawHealth) != "ok" {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
close(stopCh)
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
apiExtensionsClient, err := clientset.NewForConfig(server.GenericAPIServer.LoopbackClientConfig)
|
||||||
|
if err != nil {
|
||||||
|
close(stopCh)
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, _ := apiExtensionsClient.Discovery().RESTClient().Get().AbsPath("/apis/apiextensions.k8s.io/v1alpha1").DoRaw()
|
||||||
|
fmt.Print(string(bytes))
|
||||||
|
|
||||||
|
return stopCh, apiExtensionsClient, dynamic.NewDynamicClientPool(server.GenericAPIServer.LoopbackClientConfig), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartDefaultServer() (chan struct{}, clientset.Interface, dynamic.ClientPool, error) {
|
||||||
|
config, err := DefaultServerConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return StartServer(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindFreeLocalPort returns the number of an available port number on
|
||||||
|
// the loopback interface. Useful for determining the port to launch
|
||||||
|
// a server on. Error handling required - there is a non-zero chance
|
||||||
|
// that the returned port number will be bound by another process
|
||||||
|
// after this function returns.
|
||||||
|
func FindFreeLocalPort() (int, error) {
|
||||||
|
l, err := net.Listen("tcp", ":0")
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer l.Close()
|
||||||
|
_, portStr, err := net.SplitHostPort(l.Addr().String())
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
port, err := strconv.Atoi(portStr)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return port, nil
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user