k8s.io/apiserver@v0.31.1/pkg/endpoints/request/requestinfo_test.go (about)

     1  /*
     2  Copyright 2016 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package request
    18  
    19  import (
    20  	"fmt"
    21  	"net/http"
    22  	"reflect"
    23  	"strings"
    24  	"testing"
    25  
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/util/sets"
    28  	genericfeatures "k8s.io/apiserver/pkg/features"
    29  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    30  	featuregatetesting "k8s.io/component-base/featuregate/testing"
    31  )
    32  
    33  func TestGetAPIRequestInfo(t *testing.T) {
    34  	namespaceAll := metav1.NamespaceAll
    35  	successCases := []struct {
    36  		method              string
    37  		url                 string
    38  		expectedVerb        string
    39  		expectedAPIPrefix   string
    40  		expectedAPIGroup    string
    41  		expectedAPIVersion  string
    42  		expectedNamespace   string
    43  		expectedResource    string
    44  		expectedSubresource string
    45  		expectedName        string
    46  		expectedParts       []string
    47  	}{
    48  
    49  		// resource paths
    50  		{"GET", "/api/v1/namespaces", "list", "api", "", "v1", "", "namespaces", "", "", []string{"namespaces"}},
    51  		{"GET", "/api/v1/namespaces/other", "get", "api", "", "v1", "other", "namespaces", "", "other", []string{"namespaces", "other"}},
    52  
    53  		{"GET", "/api/v1/namespaces/other/pods", "list", "api", "", "v1", "other", "pods", "", "", []string{"pods"}},
    54  		{"GET", "/api/v1/namespaces/other/pods/foo", "get", "api", "", "v1", "other", "pods", "", "foo", []string{"pods", "foo"}},
    55  		{"HEAD", "/api/v1/namespaces/other/pods/foo", "get", "api", "", "v1", "other", "pods", "", "foo", []string{"pods", "foo"}},
    56  		{"GET", "/api/v1/pods", "list", "api", "", "v1", namespaceAll, "pods", "", "", []string{"pods"}},
    57  		{"HEAD", "/api/v1/pods", "list", "api", "", "v1", namespaceAll, "pods", "", "", []string{"pods"}},
    58  		{"GET", "/api/v1/namespaces/other/pods/foo", "get", "api", "", "v1", "other", "pods", "", "foo", []string{"pods", "foo"}},
    59  		{"GET", "/api/v1/namespaces/other/pods", "list", "api", "", "v1", "other", "pods", "", "", []string{"pods"}},
    60  
    61  		// special verbs
    62  		{"GET", "/api/v1/proxy/namespaces/other/pods/foo", "proxy", "api", "", "v1", "other", "pods", "", "foo", []string{"pods", "foo"}},
    63  		{"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"}},
    64  		{"GET", "/api/v1/watch/pods", "watch", "api", "", "v1", namespaceAll, "pods", "", "", []string{"pods"}},
    65  		{"GET", "/api/v1/pods?watch=true", "watch", "api", "", "v1", namespaceAll, "pods", "", "", []string{"pods"}},
    66  		{"GET", "/api/v1/pods?watch=false", "list", "api", "", "v1", namespaceAll, "pods", "", "", []string{"pods"}},
    67  		{"GET", "/api/v1/watch/namespaces/other/pods", "watch", "api", "", "v1", "other", "pods", "", "", []string{"pods"}},
    68  		{"GET", "/api/v1/namespaces/other/pods?watch=1", "watch", "api", "", "v1", "other", "pods", "", "", []string{"pods"}},
    69  		{"GET", "/api/v1/namespaces/other/pods?watch=0", "list", "api", "", "v1", "other", "pods", "", "", []string{"pods"}},
    70  
    71  		// subresource identification
    72  		{"GET", "/api/v1/namespaces/other/pods/foo/status", "get", "api", "", "v1", "other", "pods", "status", "foo", []string{"pods", "foo", "status"}},
    73  		{"GET", "/api/v1/namespaces/other/pods/foo/proxy/subpath", "get", "api", "", "v1", "other", "pods", "proxy", "foo", []string{"pods", "foo", "proxy", "subpath"}},
    74  		{"PUT", "/api/v1/namespaces/other/finalize", "update", "api", "", "v1", "other", "namespaces", "finalize", "other", []string{"namespaces", "other", "finalize"}},
    75  		{"PUT", "/api/v1/namespaces/other/status", "update", "api", "", "v1", "other", "namespaces", "status", "other", []string{"namespaces", "other", "status"}},
    76  
    77  		// verb identification
    78  		{"PATCH", "/api/v1/namespaces/other/pods/foo", "patch", "api", "", "v1", "other", "pods", "", "foo", []string{"pods", "foo"}},
    79  		{"DELETE", "/api/v1/namespaces/other/pods/foo", "delete", "api", "", "v1", "other", "pods", "", "foo", []string{"pods", "foo"}},
    80  		{"POST", "/api/v1/namespaces/other/pods", "create", "api", "", "v1", "other", "pods", "", "", []string{"pods"}},
    81  
    82  		// deletecollection verb identification
    83  		{"DELETE", "/api/v1/nodes", "deletecollection", "api", "", "v1", "", "nodes", "", "", []string{"nodes"}},
    84  		{"DELETE", "/api/v1/namespaces", "deletecollection", "api", "", "v1", "", "namespaces", "", "", []string{"namespaces"}},
    85  		{"DELETE", "/api/v1/namespaces/other/pods", "deletecollection", "api", "", "v1", "other", "pods", "", "", []string{"pods"}},
    86  		{"DELETE", "/apis/extensions/v1/namespaces/other/pods", "deletecollection", "api", "extensions", "v1", "other", "pods", "", "", []string{"pods"}},
    87  
    88  		// api group identification
    89  		{"POST", "/apis/extensions/v1/namespaces/other/pods", "create", "api", "extensions", "v1", "other", "pods", "", "", []string{"pods"}},
    90  
    91  		// api version identification
    92  		{"POST", "/apis/extensions/v1beta3/namespaces/other/pods", "create", "api", "extensions", "v1beta3", "other", "pods", "", "", []string{"pods"}},
    93  	}
    94  
    95  	resolver := newTestRequestInfoResolver()
    96  
    97  	for _, successCase := range successCases {
    98  		req, _ := http.NewRequest(successCase.method, successCase.url, nil)
    99  
   100  		apiRequestInfo, err := resolver.NewRequestInfo(req)
   101  		if err != nil {
   102  			t.Errorf("Unexpected error for url: %s %v", successCase.url, err)
   103  		}
   104  		if !apiRequestInfo.IsResourceRequest {
   105  			t.Errorf("Expected resource request")
   106  		}
   107  		if successCase.expectedVerb != apiRequestInfo.Verb {
   108  			t.Errorf("Unexpected verb for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedVerb, apiRequestInfo.Verb)
   109  		}
   110  		if successCase.expectedAPIVersion != apiRequestInfo.APIVersion {
   111  			t.Errorf("Unexpected apiVersion for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedAPIVersion, apiRequestInfo.APIVersion)
   112  		}
   113  		if successCase.expectedNamespace != apiRequestInfo.Namespace {
   114  			t.Errorf("Unexpected namespace for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedNamespace, apiRequestInfo.Namespace)
   115  		}
   116  		if successCase.expectedResource != apiRequestInfo.Resource {
   117  			t.Errorf("Unexpected resource for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedResource, apiRequestInfo.Resource)
   118  		}
   119  		if successCase.expectedSubresource != apiRequestInfo.Subresource {
   120  			t.Errorf("Unexpected resource for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedSubresource, apiRequestInfo.Subresource)
   121  		}
   122  		if successCase.expectedName != apiRequestInfo.Name {
   123  			t.Errorf("Unexpected name for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedName, apiRequestInfo.Name)
   124  		}
   125  		if !reflect.DeepEqual(successCase.expectedParts, apiRequestInfo.Parts) {
   126  			t.Errorf("Unexpected parts for url: %s, expected: %v, actual: %v", successCase.url, successCase.expectedParts, apiRequestInfo.Parts)
   127  		}
   128  	}
   129  
   130  	errorCases := map[string]string{
   131  		"no resource path":            "/",
   132  		"just apiversion":             "/api/version/",
   133  		"just prefix, group, version": "/apis/group/version/",
   134  		"apiversion with no resource": "/api/version/",
   135  		"bad prefix":                  "/badprefix/version/resource",
   136  		"missing api group":           "/apis/version/resource",
   137  	}
   138  	for k, v := range errorCases {
   139  		req, err := http.NewRequest("GET", v, nil)
   140  		if err != nil {
   141  			t.Errorf("Unexpected error %v", err)
   142  		}
   143  		apiRequestInfo, err := resolver.NewRequestInfo(req)
   144  		if err != nil {
   145  			t.Errorf("%s: Unexpected error %v", k, err)
   146  		}
   147  		if apiRequestInfo.IsResourceRequest {
   148  			t.Errorf("%s: expected non-resource request", k)
   149  		}
   150  	}
   151  }
   152  
   153  func TestGetNonAPIRequestInfo(t *testing.T) {
   154  	tests := map[string]struct {
   155  		url      string
   156  		expected bool
   157  	}{
   158  		"simple groupless":  {"/api/version/resource", true},
   159  		"simple group":      {"/apis/group/version/resource/name/subresource", true},
   160  		"more steps":        {"/api/version/resource/name/subresource", true},
   161  		"group list":        {"/apis/batch/v1/job", true},
   162  		"group get":         {"/apis/batch/v1/job/foo", true},
   163  		"group subresource": {"/apis/batch/v1/job/foo/scale", true},
   164  
   165  		"bad root":                     {"/not-api/version/resource", false},
   166  		"group without enough steps":   {"/apis/extensions/v1beta1", false},
   167  		"group without enough steps 2": {"/apis/extensions/v1beta1/", false},
   168  		"not enough steps":             {"/api/version", false},
   169  		"one step":                     {"/api", false},
   170  		"zero step":                    {"/", false},
   171  		"empty":                        {"", false},
   172  	}
   173  
   174  	resolver := newTestRequestInfoResolver()
   175  
   176  	for testName, tc := range tests {
   177  		req, _ := http.NewRequest("GET", tc.url, nil)
   178  
   179  		apiRequestInfo, err := resolver.NewRequestInfo(req)
   180  		if err != nil {
   181  			t.Errorf("%s: Unexpected error %v", testName, err)
   182  		}
   183  		if e, a := tc.expected, apiRequestInfo.IsResourceRequest; e != a {
   184  			t.Errorf("%s: expected %v, actual %v", testName, e, a)
   185  		}
   186  	}
   187  }
   188  
   189  func newTestRequestInfoResolver() *RequestInfoFactory {
   190  	return &RequestInfoFactory{
   191  		APIPrefixes:          sets.NewString("api", "apis"),
   192  		GrouplessAPIPrefixes: sets.NewString("api"),
   193  	}
   194  }
   195  
   196  func TestSelectorParsing(t *testing.T) {
   197  	tests := []struct {
   198  		name                  string
   199  		method                string
   200  		url                   string
   201  		expectedName          string
   202  		expectedErr           error
   203  		expectedVerb          string
   204  		expectedFieldSelector string
   205  		expectedLabelSelector string
   206  	}{
   207  		{
   208  			name:                  "no selector",
   209  			method:                "GET",
   210  			url:                   "/apis/group/version/resource",
   211  			expectedVerb:          "list",
   212  			expectedFieldSelector: "",
   213  		},
   214  		{
   215  			name:                  "metadata.name selector",
   216  			method:                "GET",
   217  			url:                   "/apis/group/version/resource?fieldSelector=metadata.name=name1",
   218  			expectedName:          "name1",
   219  			expectedVerb:          "list",
   220  			expectedFieldSelector: "metadata.name=name1",
   221  		},
   222  		{
   223  			name:                  "metadata.name selector with watch",
   224  			method:                "GET",
   225  			url:                   "/apis/group/version/resource?watch=true&fieldSelector=metadata.name=name1",
   226  			expectedName:          "name1",
   227  			expectedVerb:          "watch",
   228  			expectedFieldSelector: "metadata.name=name1",
   229  		},
   230  		{
   231  			name:                  "random selector",
   232  			method:                "GET",
   233  			url:                   "/apis/group/version/resource?fieldSelector=foo=bar&labelSelector=baz=qux",
   234  			expectedName:          "",
   235  			expectedVerb:          "list",
   236  			expectedFieldSelector: "foo=bar",
   237  			expectedLabelSelector: "baz=qux",
   238  		},
   239  		{
   240  			name:                  "invalid selector with metadata.name",
   241  			method:                "GET",
   242  			url:                   "/apis/group/version/resource?fieldSelector=metadata.name=name1,foo",
   243  			expectedName:          "",
   244  			expectedErr:           fmt.Errorf("invalid selector"),
   245  			expectedVerb:          "list",
   246  			expectedFieldSelector: "metadata.name=name1,foo",
   247  		},
   248  		{
   249  			name:                  "invalid selector with metadata.name with watch",
   250  			method:                "GET",
   251  			url:                   "/apis/group/version/resource?fieldSelector=metadata.name=name1,foo&watch=true",
   252  			expectedName:          "",
   253  			expectedErr:           fmt.Errorf("invalid selector"),
   254  			expectedVerb:          "watch",
   255  			expectedFieldSelector: "metadata.name=name1,foo",
   256  		},
   257  		{
   258  			name:                  "invalid selector with metadata.name with watch false",
   259  			method:                "GET",
   260  			url:                   "/apis/group/version/resource?fieldSelector=metadata.name=name1,foo&watch=false",
   261  			expectedName:          "",
   262  			expectedErr:           fmt.Errorf("invalid selector"),
   263  			expectedVerb:          "list",
   264  			expectedFieldSelector: "metadata.name=name1,foo",
   265  		},
   266  		{
   267  			name:                  "selector on deletecollection is honored",
   268  			method:                "DELETE",
   269  			url:                   "/apis/group/version/resource?fieldSelector=foo=bar&labelSelector=baz=qux",
   270  			expectedName:          "",
   271  			expectedVerb:          "deletecollection",
   272  			expectedFieldSelector: "foo=bar",
   273  			expectedLabelSelector: "baz=qux",
   274  		},
   275  		{
   276  			name:                  "selector on repeated param matches parsed param",
   277  			method:                "GET",
   278  			url:                   "/apis/group/version/resource?fieldSelector=metadata.name=foo&fieldSelector=metadata.name=bar&labelSelector=foo=bar&labelSelector=foo=baz",
   279  			expectedName:          "foo",
   280  			expectedVerb:          "list",
   281  			expectedFieldSelector: "metadata.name=foo",
   282  			expectedLabelSelector: "foo=bar",
   283  		},
   284  		{
   285  			name:                  "selector on other verb is ignored",
   286  			method:                "GET",
   287  			url:                   "/apis/group/version/resource/name?fieldSelector=foo=bar&labelSelector=foo=bar",
   288  			expectedName:          "name",
   289  			expectedVerb:          "get",
   290  			expectedFieldSelector: "",
   291  			expectedLabelSelector: "",
   292  		},
   293  		{
   294  			name:                  "selector on deprecated root type watch is not parsed",
   295  			method:                "GET",
   296  			url:                   "/apis/group/version/watch/resource?fieldSelector=metadata.name=foo&labelSelector=foo=bar",
   297  			expectedName:          "",
   298  			expectedVerb:          "watch",
   299  			expectedFieldSelector: "",
   300  			expectedLabelSelector: "",
   301  		},
   302  		{
   303  			name:                  "selector on deprecated root item watch is not parsed",
   304  			method:                "GET",
   305  			url:                   "/apis/group/version/watch/resource/name?fieldSelector=metadata.name=foo&labelSelector=foo=bar",
   306  			expectedName:          "name",
   307  			expectedVerb:          "watch",
   308  			expectedFieldSelector: "",
   309  			expectedLabelSelector: "",
   310  		},
   311  	}
   312  
   313  	resolver := newTestRequestInfoResolver()
   314  
   315  	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true)
   316  
   317  	for _, tc := range tests {
   318  		req, _ := http.NewRequest(tc.method, tc.url, nil)
   319  
   320  		apiRequestInfo, err := resolver.NewRequestInfo(req)
   321  		if err != nil {
   322  			if tc.expectedErr == nil || !strings.Contains(err.Error(), tc.expectedErr.Error()) {
   323  				t.Errorf("%s: Unexpected error %v", tc.name, err)
   324  			}
   325  		}
   326  		if e, a := tc.expectedName, apiRequestInfo.Name; e != a {
   327  			t.Errorf("%s: expected %v, actual %v", tc.name, e, a)
   328  		}
   329  		if e, a := tc.expectedVerb, apiRequestInfo.Verb; e != a {
   330  			t.Errorf("%s: expected verb %v, actual %v", tc.name, e, a)
   331  		}
   332  		if e, a := tc.expectedFieldSelector, apiRequestInfo.FieldSelector; e != a {
   333  			t.Errorf("%s: expected fieldSelector %v, actual %v", tc.name, e, a)
   334  		}
   335  		if e, a := tc.expectedLabelSelector, apiRequestInfo.LabelSelector; e != a {
   336  			t.Errorf("%s: expected labelSelector %v, actual %v", tc.name, e, a)
   337  		}
   338  	}
   339  }