123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476 |
- /*
- Copyright 2014 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"
- "io/ioutil"
- "net/http"
- "net/http/httptest"
- "reflect"
- "regexp"
- "strings"
- "sync"
- "testing"
- "time"
- "k8s.io/kubernetes/pkg/api"
- "k8s.io/kubernetes/pkg/api/errors"
- "k8s.io/kubernetes/pkg/api/testapi"
- "k8s.io/kubernetes/pkg/apis/extensions"
- "k8s.io/kubernetes/pkg/auth/authorizer"
- "k8s.io/kubernetes/pkg/util/sets"
- )
- type fakeRL bool
- func (fakeRL) Stop() {}
- func (f fakeRL) TryAccept() bool { return bool(f) }
- func (f fakeRL) Accept() {}
- func expectHTTP(url string, code int) error {
- r, err := http.Get(url)
- if err != nil {
- return fmt.Errorf("unexpected error: %v", err)
- }
- if r.StatusCode != code {
- return fmt.Errorf("unexpected response: %v", r.StatusCode)
- }
- return nil
- }
- func getPath(resource, namespace, name string) string {
- return testapi.Default.ResourcePath(resource, namespace, name)
- }
- func pathWithPrefix(prefix, resource, namespace, name string) string {
- return testapi.Default.ResourcePathWithPrefix(prefix, resource, namespace, name)
- }
- // Tests that MaxInFlightLimit works, i.e.
- // - "long" requests such as proxy or watch, identified by regexp are not accounted despite
- // hanging for the long time,
- // - "short" requests are correctly accounted, i.e. there can be only size of channel passed to the
- // constructor in flight at any given moment,
- // - subsequent "short" requests are rejected instantly with appropriate error,
- // - subsequent "long" requests are handled normally,
- // - we correctly recover after some "short" requests finish, i.e. we can process new ones.
- func TestMaxInFlight(t *testing.T) {
- const AllowedInflightRequestsNo = 3
- // Size of inflightRequestsChannel determines how many concurrent inflight requests
- // are allowed.
- inflightRequestsChannel := make(chan bool, AllowedInflightRequestsNo)
- // notAccountedPathsRegexp specifies paths requests to which we don't account into
- // requests in flight.
- notAccountedPathsRegexp := regexp.MustCompile(".*\\/watch")
- longRunningRequestCheck := BasicLongRunningRequestCheck(notAccountedPathsRegexp, map[string]string{"watch": "true"})
- // Calls is used to wait until all server calls are received. We are sending
- // AllowedInflightRequestsNo of 'long' not-accounted requests and the same number of
- // 'short' accounted ones.
- calls := &sync.WaitGroup{}
- calls.Add(AllowedInflightRequestsNo * 2)
- // Responses is used to wait until all responses are
- // received. This prevents some async requests getting EOF
- // errors from prematurely closing the server
- responses := sync.WaitGroup{}
- responses.Add(AllowedInflightRequestsNo * 2)
- // Block is used to keep requests in flight for as long as we need to. All requests will
- // be unblocked at the same time.
- block := sync.WaitGroup{}
- block.Add(1)
- server := httptest.NewServer(
- MaxInFlightLimit(
- inflightRequestsChannel,
- longRunningRequestCheck,
- http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- // A short, accounted request that does not wait for block WaitGroup.
- if strings.Contains(r.URL.Path, "dontwait") {
- return
- }
- if calls != nil {
- calls.Done()
- }
- block.Wait()
- }),
- ),
- )
- defer server.Close()
- // These should hang, but not affect accounting. use a query param match
- for i := 0; i < AllowedInflightRequestsNo; i++ {
- // These should hang waiting on block...
- go func() {
- if err := expectHTTP(server.URL+"/foo/bar?watch=true", http.StatusOK); err != nil {
- t.Error(err)
- }
- responses.Done()
- }()
- }
- // Check that sever is not saturated by not-accounted calls
- if err := expectHTTP(server.URL+"/dontwait", http.StatusOK); err != nil {
- t.Error(err)
- }
- // These should hang and be accounted, i.e. saturate the server
- for i := 0; i < AllowedInflightRequestsNo; i++ {
- // These should hang waiting on block...
- go func() {
- if err := expectHTTP(server.URL, http.StatusOK); err != nil {
- t.Error(err)
- }
- responses.Done()
- }()
- }
- // We wait for all calls to be received by the server
- calls.Wait()
- // Disable calls notifications in the server
- calls = nil
- // Do this multiple times to show that it rate limit rejected requests don't block.
- for i := 0; i < 2; i++ {
- if err := expectHTTP(server.URL, errors.StatusTooManyRequests); err != nil {
- t.Error(err)
- }
- }
- // Validate that non-accounted URLs still work. use a path regex match
- if err := expectHTTP(server.URL+"/dontwait/watch", http.StatusOK); err != nil {
- t.Error(err)
- }
- // Let all hanging requests finish
- block.Done()
- // Show that we recover from being blocked up.
- // Too avoid flakyness we need to wait until at least one of the requests really finishes.
- responses.Wait()
- if err := expectHTTP(server.URL, http.StatusOK); err != nil {
- t.Error(err)
- }
- }
- func TestReadOnly(t *testing.T) {
- server := httptest.NewServer(ReadOnly(http.HandlerFunc(
- func(w http.ResponseWriter, req *http.Request) {
- if req.Method != "GET" {
- t.Errorf("Unexpected call: %v", req.Method)
- }
- },
- )))
- defer server.Close()
- for _, verb := range []string{"GET", "POST", "PUT", "DELETE", "CREATE"} {
- req, err := http.NewRequest(verb, server.URL, nil)
- if err != nil {
- t.Fatalf("Couldn't make request: %v", err)
- }
- http.DefaultClient.Do(req)
- }
- }
- func TestTimeout(t *testing.T) {
- sendResponse := make(chan struct{}, 1)
- writeErrors := make(chan error, 1)
- timeout := make(chan time.Time, 1)
- resp := "test response"
- timeoutResp := "test timeout"
- ts := httptest.NewServer(TimeoutHandler(http.HandlerFunc(
- func(w http.ResponseWriter, r *http.Request) {
- <-sendResponse
- _, err := w.Write([]byte(resp))
- writeErrors <- err
- }),
- func(*http.Request) (<-chan time.Time, string) {
- return timeout, timeoutResp
- }))
- defer ts.Close()
- // No timeouts
- sendResponse <- struct{}{}
- res, err := http.Get(ts.URL)
- if err != nil {
- t.Error(err)
- }
- if res.StatusCode != http.StatusOK {
- t.Errorf("got res.StatusCode %d; expected %d", res.StatusCode, http.StatusOK)
- }
- body, _ := ioutil.ReadAll(res.Body)
- if string(body) != resp {
- t.Errorf("got body %q; expected %q", string(body), resp)
- }
- if err := <-writeErrors; err != nil {
- t.Errorf("got unexpected Write error on first request: %v", err)
- }
- // Times out
- timeout <- time.Time{}
- res, err = http.Get(ts.URL)
- if err != nil {
- t.Error(err)
- }
- if res.StatusCode != http.StatusGatewayTimeout {
- t.Errorf("got res.StatusCode %d; expected %d", res.StatusCode, http.StatusServiceUnavailable)
- }
- body, _ = ioutil.ReadAll(res.Body)
- if string(body) != timeoutResp {
- t.Errorf("got body %q; expected %q", string(body), timeoutResp)
- }
- // Now try to send a response
- sendResponse <- struct{}{}
- if err := <-writeErrors; err != http.ErrHandlerTimeout {
- t.Errorf("got Write error of %v; expected %v", err, http.ErrHandlerTimeout)
- }
- }
- func TestGetAttribs(t *testing.T) {
- r := &requestAttributeGetter{api.NewRequestContextMapper(), &RequestInfoResolver{sets.NewString("api", "apis"), sets.NewString("api")}}
- testcases := map[string]struct {
- Verb string
- Path string
- ExpectedAttributes *authorizer.AttributesRecord
- }{
- "non-resource root": {
- Verb: "POST",
- Path: "/",
- ExpectedAttributes: &authorizer.AttributesRecord{
- Verb: "post",
- Path: "/",
- },
- },
- "non-resource api prefix": {
- Verb: "GET",
- Path: "/api/",
- ExpectedAttributes: &authorizer.AttributesRecord{
- Verb: "get",
- Path: "/api/",
- },
- },
- "non-resource group api prefix": {
- Verb: "GET",
- Path: "/apis/extensions/",
- ExpectedAttributes: &authorizer.AttributesRecord{
- Verb: "get",
- Path: "/apis/extensions/",
- },
- },
- "resource": {
- Verb: "POST",
- Path: "/api/v1/nodes/mynode",
- ExpectedAttributes: &authorizer.AttributesRecord{
- Verb: "create",
- Path: "/api/v1/nodes/mynode",
- ResourceRequest: true,
- Resource: "nodes",
- APIVersion: "v1",
- Name: "mynode",
- },
- },
- "namespaced resource": {
- Verb: "PUT",
- Path: "/api/v1/namespaces/myns/pods/mypod",
- ExpectedAttributes: &authorizer.AttributesRecord{
- Verb: "update",
- Path: "/api/v1/namespaces/myns/pods/mypod",
- ResourceRequest: true,
- Namespace: "myns",
- Resource: "pods",
- APIVersion: "v1",
- Name: "mypod",
- },
- },
- "API group resource": {
- Verb: "GET",
- Path: "/apis/extensions/v1beta1/namespaces/myns/jobs",
- ExpectedAttributes: &authorizer.AttributesRecord{
- Verb: "list",
- Path: "/apis/extensions/v1beta1/namespaces/myns/jobs",
- ResourceRequest: true,
- APIGroup: extensions.GroupName,
- APIVersion: "v1beta1",
- Namespace: "myns",
- Resource: "jobs",
- },
- },
- }
- for k, tc := range testcases {
- req, _ := http.NewRequest(tc.Verb, tc.Path, nil)
- attribs := r.GetAttribs(req)
- if !reflect.DeepEqual(attribs, tc.ExpectedAttributes) {
- t.Errorf("%s: expected\n\t%#v\ngot\n\t%#v", k, tc.ExpectedAttributes, attribs)
- }
- }
- }
- func TestGetAPIRequestInfo(t *testing.T) {
- successCases := []struct {
- method string
- url string
- expectedVerb string
- expectedAPIPrefix string
- expectedAPIGroup string
- expectedAPIVersion string
- expectedNamespace string
- expectedResource string
- expectedSubresource string
- expectedName string
- expectedParts []string
- }{
- // resource paths
- {"GET", "/api/v1/namespaces", "list", "api", "", "v1", "", "namespaces", "", "", []string{"namespaces"}},
- {"GET", "/api/v1/namespaces/other", "get", "api", "", "v1", "other", "namespaces", "", "other", []string{"namespaces", "other"}},
- {"GET", "/api/v1/namespaces/other/pods", "list", "api", "", "v1", "other", "pods", "", "", []string{"pods"}},
- {"GET", "/api/v1/namespaces/other/pods/foo", "get", "api", "", "v1", "other", "pods", "", "foo", []string{"pods", "foo"}},
- {"HEAD", "/api/v1/namespaces/other/pods/foo", "get", "api", "", "v1", "other", "pods", "", "foo", []string{"pods", "foo"}},
- {"GET", "/api/v1/pods", "list", "api", "", "v1", api.NamespaceAll, "pods", "", "", []string{"pods"}},
- {"HEAD", "/api/v1/pods", "list", "api", "", "v1", api.NamespaceAll, "pods", "", "", []string{"pods"}},
- {"GET", "/api/v1/namespaces/other/pods/foo", "get", "api", "", "v1", "other", "pods", "", "foo", []string{"pods", "foo"}},
- {"GET", "/api/v1/namespaces/other/pods", "list", "api", "", "v1", "other", "pods", "", "", []string{"pods"}},
- // special verbs
- {"GET", "/api/v1/proxy/namespaces/other/pods/foo", "proxy", "api", "", "v1", "other", "pods", "", "foo", []string{"pods", "foo"}},
- {"GET", "/api/v1/proxy/namespaces/other/pods/foo/subpath/not/a/subresource", "proxy", "api", "", "v1", "other", "pods", "", "foo", []string{"pods", "foo", "subpath", "not", "a", "subresource"}},
- {"GET", "/api/v1/redirect/namespaces/other/pods/foo", "redirect", "api", "", "v1", "other", "pods", "", "foo", []string{"pods", "foo"}},
- {"GET", "/api/v1/redirect/namespaces/other/pods/foo/subpath/not/a/subresource", "redirect", "api", "", "v1", "other", "pods", "", "foo", []string{"pods", "foo", "subpath", "not", "a", "subresource"}},
- {"GET", "/api/v1/watch/pods", "watch", "api", "", "v1", api.NamespaceAll, "pods", "", "", []string{"pods"}},
- {"GET", "/api/v1/watch/namespaces/other/pods", "watch", "api", "", "v1", "other", "pods", "", "", []string{"pods"}},
- // subresource identification
- {"GET", "/api/v1/namespaces/other/pods/foo/status", "get", "api", "", "v1", "other", "pods", "status", "foo", []string{"pods", "foo", "status"}},
- {"GET", "/api/v1/namespaces/other/pods/foo/proxy/subpath", "get", "api", "", "v1", "other", "pods", "proxy", "foo", []string{"pods", "foo", "proxy", "subpath"}},
- {"PUT", "/api/v1/namespaces/other/finalize", "update", "api", "", "v1", "other", "namespaces", "finalize", "other", []string{"namespaces", "other", "finalize"}},
- {"PUT", "/api/v1/namespaces/other/status", "update", "api", "", "v1", "other", "namespaces", "status", "other", []string{"namespaces", "other", "status"}},
- // verb identification
- {"PATCH", "/api/v1/namespaces/other/pods/foo", "patch", "api", "", "v1", "other", "pods", "", "foo", []string{"pods", "foo"}},
- {"DELETE", "/api/v1/namespaces/other/pods/foo", "delete", "api", "", "v1", "other", "pods", "", "foo", []string{"pods", "foo"}},
- {"POST", "/api/v1/namespaces/other/pods", "create", "api", "", "v1", "other", "pods", "", "", []string{"pods"}},
- // deletecollection verb identification
- {"DELETE", "/api/v1/nodes", "deletecollection", "api", "", "v1", "", "nodes", "", "", []string{"nodes"}},
- {"DELETE", "/api/v1/namespaces", "deletecollection", "api", "", "v1", "", "namespaces", "", "", []string{"namespaces"}},
- {"DELETE", "/api/v1/namespaces/other/pods", "deletecollection", "api", "", "v1", "other", "pods", "", "", []string{"pods"}},
- {"DELETE", "/apis/extensions/v1/namespaces/other/pods", "deletecollection", "api", "extensions", "v1", "other", "pods", "", "", []string{"pods"}},
- // api group identification
- {"POST", "/apis/extensions/v1/namespaces/other/pods", "create", "api", "extensions", "v1", "other", "pods", "", "", []string{"pods"}},
- // api version identification
- {"POST", "/apis/extensions/v1beta3/namespaces/other/pods", "create", "api", "extensions", "v1beta3", "other", "pods", "", "", []string{"pods"}},
- }
- requestInfoResolver := newTestRequestInfoResolver()
- for _, successCase := range successCases {
- req, _ := http.NewRequest(successCase.method, successCase.url, nil)
- apiRequestInfo, err := requestInfoResolver.GetRequestInfo(req)
- if err != nil {
- t.Errorf("Unexpected error for url: %s %v", successCase.url, err)
- }
- if !apiRequestInfo.IsResourceRequest {
- t.Errorf("Expected resource request")
- }
- if successCase.expectedVerb != apiRequestInfo.Verb {
- t.Errorf("Unexpected verb for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedVerb, apiRequestInfo.Verb)
- }
- if successCase.expectedAPIVersion != apiRequestInfo.APIVersion {
- t.Errorf("Unexpected apiVersion for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedAPIVersion, apiRequestInfo.APIVersion)
- }
- if successCase.expectedNamespace != apiRequestInfo.Namespace {
- t.Errorf("Unexpected namespace for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedNamespace, apiRequestInfo.Namespace)
- }
- if successCase.expectedResource != apiRequestInfo.Resource {
- t.Errorf("Unexpected resource for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedResource, apiRequestInfo.Resource)
- }
- if successCase.expectedSubresource != apiRequestInfo.Subresource {
- t.Errorf("Unexpected resource for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedSubresource, apiRequestInfo.Subresource)
- }
- if successCase.expectedName != apiRequestInfo.Name {
- t.Errorf("Unexpected name for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedName, apiRequestInfo.Name)
- }
- if !reflect.DeepEqual(successCase.expectedParts, apiRequestInfo.Parts) {
- t.Errorf("Unexpected parts for url: %s, expected: %v, actual: %v", successCase.url, successCase.expectedParts, apiRequestInfo.Parts)
- }
- }
- errorCases := map[string]string{
- "no resource path": "/",
- "just apiversion": "/api/version/",
- "just prefix, group, version": "/apis/group/version/",
- "apiversion with no resource": "/api/version/",
- "bad prefix": "/badprefix/version/resource",
- "missing api group": "/apis/version/resource",
- }
- for k, v := range errorCases {
- req, err := http.NewRequest("GET", v, nil)
- if err != nil {
- t.Errorf("Unexpected error %v", err)
- }
- apiRequestInfo, err := requestInfoResolver.GetRequestInfo(req)
- if err != nil {
- t.Errorf("%s: Unexpected error %v", k, err)
- }
- if apiRequestInfo.IsResourceRequest {
- t.Errorf("%s: expected non-resource request", k)
- }
- }
- }
- func TestGetNonAPIRequestInfo(t *testing.T) {
- tests := map[string]struct {
- url string
- expected bool
- }{
- "simple groupless": {"/api/version/resource", true},
- "simple group": {"/apis/group/version/resource/name/subresource", true},
- "more steps": {"/api/version/resource/name/subresource", true},
- "group list": {"/apis/extensions/v1beta1/job", true},
- "group get": {"/apis/extensions/v1beta1/job/foo", true},
- "group subresource": {"/apis/extensions/v1beta1/job/foo/scale", true},
- "bad root": {"/not-api/version/resource", false},
- "group without enough steps": {"/apis/extensions/v1beta1", false},
- "group without enough steps 2": {"/apis/extensions/v1beta1/", false},
- "not enough steps": {"/api/version", false},
- "one step": {"/api", false},
- "zero step": {"/", false},
- "empty": {"", false},
- }
- requestInfoResolver := newTestRequestInfoResolver()
- for testName, tc := range tests {
- req, _ := http.NewRequest("GET", tc.url, nil)
- apiRequestInfo, err := requestInfoResolver.GetRequestInfo(req)
- if err != nil {
- t.Errorf("%s: Unexpected error %v", testName, err)
- }
- if e, a := tc.expected, apiRequestInfo.IsResourceRequest; e != a {
- t.Errorf("%s: expected %v, actual %v", testName, e, a)
- }
- }
- }
|