mirror of https://github.com/k3s-io/k3s
477 lines
18 KiB
Go
477 lines
18 KiB
Go
/*
|
|
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)
|
|
}
|
|
}
|
|
}
|