mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-31 07:20:13 +00:00
Merge pull request #81244 from liggitt/crd_startup_503
Return 503 for custom resource requests during server start
This commit is contained in:
commit
3f0a486cbf
@ -99,7 +99,14 @@ go_test(
|
|||||||
deps = [
|
deps = [
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/conversion:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/conversion:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/runtime/serializer/protobuf:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apiserver/pkg/endpoints/discovery:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
||||||
|
"//staging/src/k8s.io/client-go/tools/cache:go_default_library",
|
||||||
|
"//vendor/sigs.k8s.io/yaml:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -95,6 +95,7 @@ type crdHandler struct {
|
|||||||
customStorage atomic.Value
|
customStorage atomic.Value
|
||||||
|
|
||||||
crdLister listers.CustomResourceDefinitionLister
|
crdLister listers.CustomResourceDefinitionLister
|
||||||
|
hasSynced func() bool
|
||||||
|
|
||||||
delegate http.Handler
|
delegate http.Handler
|
||||||
restOptionsGetter generic.RESTOptionsGetter
|
restOptionsGetter generic.RESTOptionsGetter
|
||||||
@ -165,6 +166,7 @@ func NewCustomResourceDefinitionHandler(
|
|||||||
groupDiscoveryHandler: groupDiscoveryHandler,
|
groupDiscoveryHandler: groupDiscoveryHandler,
|
||||||
customStorage: atomic.Value{},
|
customStorage: atomic.Value{},
|
||||||
crdLister: crdInformer.Lister(),
|
crdLister: crdInformer.Lister(),
|
||||||
|
hasSynced: crdInformer.Informer().HasSynced,
|
||||||
delegate: delegate,
|
delegate: delegate,
|
||||||
restOptionsGetter: restOptionsGetter,
|
restOptionsGetter: restOptionsGetter,
|
||||||
admission: admission,
|
admission: admission,
|
||||||
@ -205,7 +207,10 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
ctx := req.Context()
|
ctx := req.Context()
|
||||||
requestInfo, ok := apirequest.RequestInfoFrom(ctx)
|
requestInfo, ok := apirequest.RequestInfoFrom(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
responsewriters.InternalError(w, req, fmt.Errorf("no RequestInfo found in the context"))
|
responsewriters.ErrorNegotiated(
|
||||||
|
apierrors.NewInternalError(fmt.Errorf("no RequestInfo found in the context")),
|
||||||
|
Codecs, schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}, w, req,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !requestInfo.IsResourceRequest {
|
if !requestInfo.IsResourceRequest {
|
||||||
@ -213,11 +218,19 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
// only match /apis/<group>/<version>
|
// only match /apis/<group>/<version>
|
||||||
// only registered under /apis
|
// only registered under /apis
|
||||||
if len(pathParts) == 3 {
|
if len(pathParts) == 3 {
|
||||||
|
if !r.hasSynced() {
|
||||||
|
responsewriters.ErrorNegotiated(serverStartingError(), Codecs, schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}, w, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
r.versionDiscoveryHandler.ServeHTTP(w, req)
|
r.versionDiscoveryHandler.ServeHTTP(w, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// only match /apis/<group>
|
// only match /apis/<group>
|
||||||
if len(pathParts) == 2 {
|
if len(pathParts) == 2 {
|
||||||
|
if !r.hasSynced() {
|
||||||
|
responsewriters.ErrorNegotiated(serverStartingError(), Codecs, schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}, w, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
r.groupDiscoveryHandler.ServeHTTP(w, req)
|
r.groupDiscoveryHandler.ServeHTTP(w, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -229,11 +242,20 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
crdName := requestInfo.Resource + "." + requestInfo.APIGroup
|
crdName := requestInfo.Resource + "." + requestInfo.APIGroup
|
||||||
crd, err := r.crdLister.Get(crdName)
|
crd, err := r.crdLister.Get(crdName)
|
||||||
if apierrors.IsNotFound(err) {
|
if apierrors.IsNotFound(err) {
|
||||||
|
if !r.hasSynced() {
|
||||||
|
responsewriters.ErrorNegotiated(serverStartingError(), Codecs, schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}, w, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
r.delegate.ServeHTTP(w, req)
|
r.delegate.ServeHTTP(w, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
utilruntime.HandleError(err)
|
||||||
|
responsewriters.ErrorNegotiated(
|
||||||
|
apierrors.NewInternalError(fmt.Errorf("error resolving resource")),
|
||||||
|
Codecs, schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}, w, req,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -272,7 +294,11 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
utilruntime.HandleError(err)
|
||||||
|
responsewriters.ErrorNegotiated(
|
||||||
|
apierrors.NewInternalError(fmt.Errorf("error resolving resource")),
|
||||||
|
Codecs, schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}, w, req,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !hasServedCRDVersion(crdInfo.spec, requestInfo.APIVersion) {
|
if !hasServedCRDVersion(crdInfo.spec, requestInfo.APIVersion) {
|
||||||
@ -296,7 +322,10 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
subresources, err := apiextensions.GetSubresourcesForVersion(crd, requestInfo.APIVersion)
|
subresources, err := apiextensions.GetSubresourcesForVersion(crd, requestInfo.APIVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utilruntime.HandleError(err)
|
utilruntime.HandleError(err)
|
||||||
http.Error(w, "the server could not properly serve the CR subresources", http.StatusInternalServerError)
|
responsewriters.ErrorNegotiated(
|
||||||
|
apierrors.NewInternalError(fmt.Errorf("could not properly serve the subresource")),
|
||||||
|
Codecs, schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}, w, req,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
@ -307,7 +336,10 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
case len(subresource) == 0:
|
case len(subresource) == 0:
|
||||||
handlerFunc = r.serveResource(w, req, requestInfo, crdInfo, terminating, supportedTypes)
|
handlerFunc = r.serveResource(w, req, requestInfo, crdInfo, terminating, supportedTypes)
|
||||||
default:
|
default:
|
||||||
http.Error(w, "the server could not find the requested resource", http.StatusNotFound)
|
responsewriters.ErrorNegotiated(
|
||||||
|
apierrors.NewNotFound(schema.GroupResource{Group: requestInfo.APIGroup, Resource: requestInfo.Resource}, requestInfo.Name),
|
||||||
|
Codecs, schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}, w, req,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if handlerFunc != nil {
|
if handlerFunc != nil {
|
||||||
@ -333,7 +365,9 @@ func (r *crdHandler) serveResource(w http.ResponseWriter, req *http.Request, req
|
|||||||
return handlers.ListResource(storage, storage, requestScope, forceWatch, r.minRequestTimeout)
|
return handlers.ListResource(storage, storage, requestScope, forceWatch, r.minRequestTimeout)
|
||||||
case "create":
|
case "create":
|
||||||
if terminating {
|
if terminating {
|
||||||
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
|
err := apierrors.NewMethodNotSupported(schema.GroupResource{Group: requestInfo.APIGroup, Resource: requestInfo.Resource}, requestInfo.Verb)
|
||||||
|
err.ErrStatus.Message = fmt.Sprintf("%v not allowed while custom resource definition is terminating", requestInfo.Verb)
|
||||||
|
responsewriters.ErrorNegotiated(err, Codecs, schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}, w, req)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return handlers.CreateResource(storage, requestScope, r.admission)
|
return handlers.CreateResource(storage, requestScope, r.admission)
|
||||||
@ -348,7 +382,10 @@ func (r *crdHandler) serveResource(w http.ResponseWriter, req *http.Request, req
|
|||||||
checkBody := true
|
checkBody := true
|
||||||
return handlers.DeleteCollection(storage, checkBody, requestScope, r.admission)
|
return handlers.DeleteCollection(storage, checkBody, requestScope, r.admission)
|
||||||
default:
|
default:
|
||||||
http.Error(w, fmt.Sprintf("unhandled verb %q", requestInfo.Verb), http.StatusMethodNotAllowed)
|
responsewriters.ErrorNegotiated(
|
||||||
|
apierrors.NewMethodNotSupported(schema.GroupResource{Group: requestInfo.APIGroup, Resource: requestInfo.Resource}, requestInfo.Verb),
|
||||||
|
Codecs, schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}, w, req,
|
||||||
|
)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -365,7 +402,10 @@ func (r *crdHandler) serveStatus(w http.ResponseWriter, req *http.Request, reque
|
|||||||
case "patch":
|
case "patch":
|
||||||
return handlers.PatchResource(storage, requestScope, r.admission, supportedTypes)
|
return handlers.PatchResource(storage, requestScope, r.admission, supportedTypes)
|
||||||
default:
|
default:
|
||||||
http.Error(w, fmt.Sprintf("unhandled verb %q", requestInfo.Verb), http.StatusMethodNotAllowed)
|
responsewriters.ErrorNegotiated(
|
||||||
|
apierrors.NewMethodNotSupported(schema.GroupResource{Group: requestInfo.APIGroup, Resource: requestInfo.Resource}, requestInfo.Verb),
|
||||||
|
Codecs, schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}, w, req,
|
||||||
|
)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -382,7 +422,10 @@ func (r *crdHandler) serveScale(w http.ResponseWriter, req *http.Request, reques
|
|||||||
case "patch":
|
case "patch":
|
||||||
return handlers.PatchResource(storage, requestScope, r.admission, supportedTypes)
|
return handlers.PatchResource(storage, requestScope, r.admission, supportedTypes)
|
||||||
default:
|
default:
|
||||||
http.Error(w, fmt.Sprintf("unhandled verb %q", requestInfo.Verb), http.StatusMethodNotAllowed)
|
responsewriters.ErrorNegotiated(
|
||||||
|
apierrors.NewMethodNotSupported(schema.GroupResource{Group: requestInfo.APIGroup, Resource: requestInfo.Resource}, requestInfo.Verb),
|
||||||
|
Codecs, schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}, w, req,
|
||||||
|
)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1125,3 +1168,15 @@ func hasServedCRDVersion(spec *apiextensions.CustomResourceDefinitionSpec, versi
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// serverStartingError returns a ServiceUnavailble error with a retry-after time
|
||||||
|
func serverStartingError() error {
|
||||||
|
err := apierrors.NewServiceUnavailable("server is starting")
|
||||||
|
if err.ErrStatus.Details == nil {
|
||||||
|
err.ErrStatus.Details = &metav1.StatusDetails{}
|
||||||
|
}
|
||||||
|
if err.ErrStatus.Details.RetryAfterSeconds == 0 {
|
||||||
|
err.ErrStatus.Details.RetryAfterSeconds = int32(10)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
@ -17,11 +17,23 @@ limitations under the License.
|
|||||||
package apiserver
|
package apiserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"sigs.k8s.io/yaml"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
|
"k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
|
||||||
|
listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/serializer/protobuf"
|
||||||
|
"k8s.io/apiserver/pkg/endpoints/discovery"
|
||||||
|
apirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
|
"k8s.io/client-go/tools/cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConvertFieldLabel(t *testing.T) {
|
func TestConvertFieldLabel(t *testing.T) {
|
||||||
@ -105,3 +117,301 @@ func TestConvertFieldLabel(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRouting(t *testing.T) {
|
||||||
|
hasSynced := false
|
||||||
|
|
||||||
|
crdIndexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
|
||||||
|
crdLister := listers.NewCustomResourceDefinitionLister(crdIndexer)
|
||||||
|
|
||||||
|
delegateCalled := false
|
||||||
|
delegate := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
delegateCalled = true
|
||||||
|
http.Error(w, "", 418)
|
||||||
|
})
|
||||||
|
customV1 := schema.GroupVersion{Group: "custom", Version: "v1"}
|
||||||
|
handler := &crdHandler{
|
||||||
|
crdLister: crdLister,
|
||||||
|
hasSynced: func() bool { return hasSynced },
|
||||||
|
delegate: delegate,
|
||||||
|
versionDiscoveryHandler: &versionDiscoveryHandler{
|
||||||
|
discovery: map[schema.GroupVersion]*discovery.APIVersionHandler{
|
||||||
|
customV1: discovery.NewAPIVersionHandler(Codecs, customV1, discovery.APIResourceListerFunc(func() []metav1.APIResource {
|
||||||
|
return nil
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
delegate: delegate,
|
||||||
|
},
|
||||||
|
groupDiscoveryHandler: &groupDiscoveryHandler{
|
||||||
|
discovery: map[string]*discovery.APIGroupHandler{
|
||||||
|
"custom": discovery.NewAPIGroupHandler(Codecs, metav1.APIGroup{
|
||||||
|
Name: customV1.Group,
|
||||||
|
Versions: []metav1.GroupVersionForDiscovery{{GroupVersion: customV1.String(), Version: customV1.Version}},
|
||||||
|
PreferredVersion: metav1.GroupVersionForDiscovery{GroupVersion: customV1.String(), Version: customV1.Version},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
delegate: delegate,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testcases := []struct {
|
||||||
|
Name string
|
||||||
|
Method string
|
||||||
|
Path string
|
||||||
|
Headers map[string]string
|
||||||
|
Body io.Reader
|
||||||
|
|
||||||
|
APIGroup string
|
||||||
|
APIVersion string
|
||||||
|
Verb string
|
||||||
|
Resource string
|
||||||
|
IsResourceRequest bool
|
||||||
|
|
||||||
|
HasSynced bool
|
||||||
|
|
||||||
|
ExpectStatus int
|
||||||
|
ExpectResponse func(*testing.T, *http.Response, []byte)
|
||||||
|
ExpectDelegateCalled bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "existing group discovery, presync",
|
||||||
|
Method: "GET",
|
||||||
|
Path: "/apis/custom",
|
||||||
|
APIGroup: "custom",
|
||||||
|
APIVersion: "",
|
||||||
|
HasSynced: false,
|
||||||
|
IsResourceRequest: false,
|
||||||
|
ExpectDelegateCalled: false,
|
||||||
|
ExpectStatus: 503,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "existing group discovery",
|
||||||
|
Method: "GET",
|
||||||
|
Path: "/apis/custom",
|
||||||
|
APIGroup: "custom",
|
||||||
|
APIVersion: "",
|
||||||
|
HasSynced: true,
|
||||||
|
IsResourceRequest: false,
|
||||||
|
ExpectDelegateCalled: false,
|
||||||
|
ExpectStatus: 200,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
Name: "nonexisting group discovery, presync",
|
||||||
|
Method: "GET",
|
||||||
|
Path: "/apis/other",
|
||||||
|
APIGroup: "other",
|
||||||
|
APIVersion: "",
|
||||||
|
HasSynced: false,
|
||||||
|
IsResourceRequest: false,
|
||||||
|
ExpectDelegateCalled: false,
|
||||||
|
ExpectStatus: 503,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "nonexisting group discovery",
|
||||||
|
Method: "GET",
|
||||||
|
Path: "/apis/other",
|
||||||
|
APIGroup: "other",
|
||||||
|
APIVersion: "",
|
||||||
|
HasSynced: true,
|
||||||
|
IsResourceRequest: false,
|
||||||
|
ExpectDelegateCalled: true,
|
||||||
|
ExpectStatus: 418,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
Name: "existing group version discovery, presync",
|
||||||
|
Method: "GET",
|
||||||
|
Path: "/apis/custom/v1",
|
||||||
|
APIGroup: "custom",
|
||||||
|
APIVersion: "v1",
|
||||||
|
HasSynced: false,
|
||||||
|
IsResourceRequest: false,
|
||||||
|
ExpectDelegateCalled: false,
|
||||||
|
ExpectStatus: 503,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "existing group version discovery",
|
||||||
|
Method: "GET",
|
||||||
|
Path: "/apis/custom/v1",
|
||||||
|
APIGroup: "custom",
|
||||||
|
APIVersion: "v1",
|
||||||
|
HasSynced: true,
|
||||||
|
IsResourceRequest: false,
|
||||||
|
ExpectDelegateCalled: false,
|
||||||
|
ExpectStatus: 200,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
Name: "nonexisting group version discovery, presync",
|
||||||
|
Method: "GET",
|
||||||
|
Path: "/apis/other/v1",
|
||||||
|
APIGroup: "other",
|
||||||
|
APIVersion: "v1",
|
||||||
|
HasSynced: false,
|
||||||
|
IsResourceRequest: false,
|
||||||
|
ExpectDelegateCalled: false,
|
||||||
|
ExpectStatus: 503,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "nonexisting group version discovery",
|
||||||
|
Method: "GET",
|
||||||
|
Path: "/apis/other/v1",
|
||||||
|
APIGroup: "other",
|
||||||
|
APIVersion: "v1",
|
||||||
|
HasSynced: true,
|
||||||
|
IsResourceRequest: false,
|
||||||
|
ExpectDelegateCalled: true,
|
||||||
|
ExpectStatus: 418,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
Name: "existing group, nonexisting version discovery, presync",
|
||||||
|
Method: "GET",
|
||||||
|
Path: "/apis/custom/v2",
|
||||||
|
APIGroup: "custom",
|
||||||
|
APIVersion: "v2",
|
||||||
|
HasSynced: false,
|
||||||
|
IsResourceRequest: false,
|
||||||
|
ExpectDelegateCalled: false,
|
||||||
|
ExpectStatus: 503,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "existing group, nonexisting version discovery",
|
||||||
|
Method: "GET",
|
||||||
|
Path: "/apis/custom/v2",
|
||||||
|
APIGroup: "custom",
|
||||||
|
APIVersion: "v2",
|
||||||
|
HasSynced: true,
|
||||||
|
IsResourceRequest: false,
|
||||||
|
ExpectDelegateCalled: true,
|
||||||
|
ExpectStatus: 418,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
Name: "nonexisting group, resource request, presync",
|
||||||
|
Method: "GET",
|
||||||
|
Path: "/apis/custom/v2/foos",
|
||||||
|
APIGroup: "custom",
|
||||||
|
APIVersion: "v2",
|
||||||
|
Verb: "list",
|
||||||
|
Resource: "foos",
|
||||||
|
HasSynced: false,
|
||||||
|
IsResourceRequest: true,
|
||||||
|
ExpectDelegateCalled: false,
|
||||||
|
ExpectStatus: 503,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "nonexisting group, resource request",
|
||||||
|
Method: "GET",
|
||||||
|
Path: "/apis/custom/v2/foos",
|
||||||
|
APIGroup: "custom",
|
||||||
|
APIVersion: "v2",
|
||||||
|
Verb: "list",
|
||||||
|
Resource: "foos",
|
||||||
|
HasSynced: true,
|
||||||
|
IsResourceRequest: true,
|
||||||
|
ExpectDelegateCalled: true,
|
||||||
|
ExpectStatus: 418,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testcases {
|
||||||
|
t.Run(tc.Name, func(t *testing.T) {
|
||||||
|
for _, contentType := range []string{"json", "yaml", "proto", "unknown"} {
|
||||||
|
t.Run(contentType, func(t *testing.T) {
|
||||||
|
delegateCalled = false
|
||||||
|
hasSynced = tc.HasSynced
|
||||||
|
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
req := httptest.NewRequest(tc.Method, tc.Path, tc.Body)
|
||||||
|
for k, v := range tc.Headers {
|
||||||
|
req.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectStatus := tc.ExpectStatus
|
||||||
|
switch contentType {
|
||||||
|
case "json":
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
case "yaml":
|
||||||
|
req.Header.Set("Accept", "application/yaml")
|
||||||
|
case "proto":
|
||||||
|
req.Header.Set("Accept", "application/vnd.kubernetes.protobuf, application/json")
|
||||||
|
case "unknown":
|
||||||
|
req.Header.Set("Accept", "application/vnd.kubernetes.unknown")
|
||||||
|
// rather than success, we'll get a not supported error
|
||||||
|
if expectStatus == 200 {
|
||||||
|
expectStatus = 406
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatalf("unknown content type %v", contentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
req = req.WithContext(apirequest.WithRequestInfo(req.Context(), &apirequest.RequestInfo{
|
||||||
|
Verb: tc.Verb,
|
||||||
|
Resource: tc.Resource,
|
||||||
|
APIGroup: tc.APIGroup,
|
||||||
|
APIVersion: tc.APIVersion,
|
||||||
|
IsResourceRequest: tc.IsResourceRequest,
|
||||||
|
Path: tc.Path,
|
||||||
|
}))
|
||||||
|
|
||||||
|
handler.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
if tc.ExpectDelegateCalled != delegateCalled {
|
||||||
|
t.Errorf("expected delegated called %v, got %v", tc.ExpectDelegateCalled, delegateCalled)
|
||||||
|
}
|
||||||
|
result := recorder.Result()
|
||||||
|
content, _ := ioutil.ReadAll(result.Body)
|
||||||
|
if e, a := expectStatus, result.StatusCode; e != a {
|
||||||
|
t.Log(string(content))
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if tc.ExpectResponse != nil {
|
||||||
|
tc.ExpectResponse(t, result, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure error responses come back with status objects in all encodings, including unknown encodings
|
||||||
|
if !delegateCalled && expectStatus >= 300 {
|
||||||
|
status := &metav1.Status{}
|
||||||
|
|
||||||
|
switch contentType {
|
||||||
|
// unknown accept headers fall back to json errors
|
||||||
|
case "json", "unknown":
|
||||||
|
if e, a := "application/json", result.Header.Get("Content-Type"); e != a {
|
||||||
|
t.Errorf("expected Content-Type %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(content, status); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
case "yaml":
|
||||||
|
if e, a := "application/yaml", result.Header.Get("Content-Type"); e != a {
|
||||||
|
t.Errorf("expected Content-Type %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if err := yaml.Unmarshal(content, status); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
case "proto":
|
||||||
|
if e, a := "application/vnd.kubernetes.protobuf", result.Header.Get("Content-Type"); e != a {
|
||||||
|
t.Errorf("expected Content-Type %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if _, _, err := protobuf.NewSerializer(Scheme, Scheme).Decode(content, nil, status); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatalf("unknown content type %v", contentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, a := metav1.Unversioned.WithKind("Status"), status.GroupVersionKind(); e != a {
|
||||||
|
t.Errorf("expected %#v, got %#v", e, a)
|
||||||
|
}
|
||||||
|
if int(status.Code) != int(expectStatus) {
|
||||||
|
t.Errorf("expected %v, got %v", expectStatus, status.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user