...

Source file src/k8s.io/kubectl/pkg/proxy/proxy_server_test.go

Documentation: k8s.io/kubectl/pkg/proxy

     1  /*
     2  Copyright 2014 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 proxy
    18  
    19  import (
    20  	"fmt"
    21  	"io"
    22  	"net/http"
    23  	"net/http/httptest"
    24  	"net/url"
    25  	"os"
    26  	"path/filepath"
    27  	"strings"
    28  	"testing"
    29  
    30  	"k8s.io/apimachinery/pkg/util/proxy"
    31  	"k8s.io/client-go/rest"
    32  )
    33  
    34  func TestAccept(t *testing.T) {
    35  	tests := []struct {
    36  		name          string
    37  		acceptPaths   string
    38  		rejectPaths   string
    39  		acceptHosts   string
    40  		rejectMethods string
    41  		path          string
    42  		host          string
    43  		method        string
    44  		expectAccept  bool
    45  	}{
    46  
    47  		{
    48  			name:          "test1",
    49  			acceptPaths:   DefaultPathAcceptRE,
    50  			rejectPaths:   DefaultPathRejectRE,
    51  			acceptHosts:   DefaultHostAcceptRE,
    52  			rejectMethods: DefaultMethodRejectRE,
    53  			path:          "",
    54  			host:          "127.0.0.1",
    55  			method:        "GET",
    56  			expectAccept:  true,
    57  		},
    58  		{
    59  			name:          "test2",
    60  			acceptPaths:   DefaultPathAcceptRE,
    61  			rejectPaths:   DefaultPathRejectRE,
    62  			acceptHosts:   DefaultHostAcceptRE,
    63  			rejectMethods: DefaultMethodRejectRE,
    64  			path:          "/api/v1/pods",
    65  			host:          "127.0.0.1",
    66  			method:        "GET",
    67  			expectAccept:  true,
    68  		},
    69  		{
    70  			name:          "test3",
    71  			acceptPaths:   DefaultPathAcceptRE,
    72  			rejectPaths:   DefaultPathRejectRE,
    73  			acceptHosts:   DefaultHostAcceptRE,
    74  			rejectMethods: DefaultMethodRejectRE,
    75  			path:          "/api/v1/pods",
    76  			host:          "localhost",
    77  			method:        "GET",
    78  			expectAccept:  true,
    79  		},
    80  		{
    81  			name:          "test4",
    82  			acceptPaths:   DefaultPathAcceptRE,
    83  			rejectPaths:   DefaultPathRejectRE,
    84  			acceptHosts:   DefaultHostAcceptRE,
    85  			rejectMethods: DefaultMethodRejectRE,
    86  			path:          "/api/v1/namespaces/default/pods/foo",
    87  			host:          "localhost",
    88  			method:        "GET",
    89  			expectAccept:  true,
    90  		},
    91  		{
    92  			name:          "test5",
    93  			acceptPaths:   DefaultPathAcceptRE,
    94  			rejectPaths:   DefaultPathRejectRE,
    95  			acceptHosts:   DefaultHostAcceptRE,
    96  			rejectMethods: DefaultMethodRejectRE,
    97  			path:          "/api/v1/namespaces/default/pods/attachfoo",
    98  			host:          "localhost",
    99  			method:        "GET",
   100  			expectAccept:  true,
   101  		},
   102  		{
   103  			name:          "test7",
   104  			acceptPaths:   DefaultPathAcceptRE,
   105  			rejectPaths:   DefaultPathRejectRE,
   106  			acceptHosts:   DefaultHostAcceptRE,
   107  			rejectMethods: DefaultMethodRejectRE,
   108  			path:          "/api/v1/namespaces/default/pods/execfoo",
   109  			host:          "localhost",
   110  			method:        "GET",
   111  			expectAccept:  true,
   112  		},
   113  		{
   114  			name:          "test8",
   115  			acceptPaths:   DefaultPathAcceptRE,
   116  			rejectPaths:   DefaultPathRejectRE,
   117  			acceptHosts:   DefaultHostAcceptRE,
   118  			rejectMethods: DefaultMethodRejectRE,
   119  			path:          "/api/v1/namespaces/default/pods/foo/exec",
   120  			host:          "127.0.0.1",
   121  			method:        "GET",
   122  			expectAccept:  false,
   123  		},
   124  		{
   125  			name:          "test9",
   126  			acceptPaths:   DefaultPathAcceptRE,
   127  			rejectPaths:   DefaultPathRejectRE,
   128  			acceptHosts:   DefaultHostAcceptRE,
   129  			rejectMethods: DefaultMethodRejectRE,
   130  			path:          "/api/v1/namespaces/default/pods/foo/attach",
   131  			host:          "127.0.0.1",
   132  			method:        "GET",
   133  			expectAccept:  false,
   134  		},
   135  		{
   136  			name:          "test10",
   137  			acceptPaths:   DefaultPathAcceptRE,
   138  			rejectPaths:   DefaultPathRejectRE,
   139  			acceptHosts:   DefaultHostAcceptRE,
   140  			rejectMethods: DefaultMethodRejectRE,
   141  			path:          "/api/v1/pods",
   142  			host:          "evil.com",
   143  			method:        "GET",
   144  			expectAccept:  false,
   145  		},
   146  		{
   147  			name:          "test11",
   148  			acceptPaths:   DefaultPathAcceptRE,
   149  			rejectPaths:   DefaultPathRejectRE,
   150  			acceptHosts:   DefaultHostAcceptRE,
   151  			rejectMethods: DefaultMethodRejectRE,
   152  			path:          "/api/v1/pods",
   153  			host:          "localhost.evil.com",
   154  			method:        "GET",
   155  			expectAccept:  false,
   156  		},
   157  		{
   158  			name:          "test12",
   159  			acceptPaths:   DefaultPathAcceptRE,
   160  			rejectPaths:   DefaultPathRejectRE,
   161  			acceptHosts:   DefaultHostAcceptRE,
   162  			rejectMethods: DefaultMethodRejectRE,
   163  			path:          "/api/v1/pods",
   164  			host:          "127a0b0c1",
   165  			method:        "GET",
   166  			expectAccept:  false,
   167  		},
   168  		{
   169  			name:          "test13",
   170  			acceptPaths:   DefaultPathAcceptRE,
   171  			rejectPaths:   DefaultPathRejectRE,
   172  			acceptHosts:   DefaultHostAcceptRE,
   173  			rejectMethods: DefaultMethodRejectRE,
   174  			path:          "/ui",
   175  			host:          "localhost",
   176  			method:        "GET",
   177  			expectAccept:  true,
   178  		},
   179  		{
   180  			name:          "test14",
   181  			acceptPaths:   DefaultPathAcceptRE,
   182  			rejectPaths:   DefaultPathRejectRE,
   183  			acceptHosts:   DefaultHostAcceptRE,
   184  			rejectMethods: DefaultMethodRejectRE,
   185  			path:          "/api/v1/pods",
   186  			host:          "localhost",
   187  			method:        "POST",
   188  			expectAccept:  true,
   189  		},
   190  		{
   191  			name:          "test15",
   192  			acceptPaths:   DefaultPathAcceptRE,
   193  			rejectPaths:   DefaultPathRejectRE,
   194  			acceptHosts:   DefaultHostAcceptRE,
   195  			rejectMethods: DefaultMethodRejectRE,
   196  			path:          "/api/v1/namespaces/default/pods/somepod",
   197  			host:          "localhost",
   198  			method:        "PUT",
   199  			expectAccept:  true,
   200  		},
   201  		{
   202  			name:          "test16",
   203  			acceptPaths:   DefaultPathAcceptRE,
   204  			rejectPaths:   DefaultPathRejectRE,
   205  			acceptHosts:   DefaultHostAcceptRE,
   206  			rejectMethods: DefaultMethodRejectRE,
   207  			path:          "/api/v1/namespaces/default/pods/somepod",
   208  			host:          "localhost",
   209  			method:        "PATCH",
   210  			expectAccept:  true,
   211  		},
   212  		{
   213  			name:          "test17",
   214  			acceptPaths:   DefaultPathAcceptRE,
   215  			rejectPaths:   DefaultPathRejectRE,
   216  			acceptHosts:   DefaultHostAcceptRE,
   217  			rejectMethods: "GET",
   218  			path:          "/api/v1/pods",
   219  			host:          "127.0.0.1",
   220  			method:        "GET",
   221  			expectAccept:  false,
   222  		},
   223  		{
   224  			name:          "test18",
   225  			acceptPaths:   DefaultPathAcceptRE,
   226  			rejectPaths:   DefaultPathRejectRE,
   227  			acceptHosts:   DefaultHostAcceptRE,
   228  			rejectMethods: "POST",
   229  			path:          "/api/v1/pods",
   230  			host:          "localhost",
   231  			method:        "POST",
   232  			expectAccept:  false,
   233  		},
   234  		{
   235  			name:          "test19",
   236  			acceptPaths:   DefaultPathAcceptRE,
   237  			rejectPaths:   DefaultPathRejectRE,
   238  			acceptHosts:   DefaultHostAcceptRE,
   239  			rejectMethods: "PUT",
   240  			path:          "/api/v1/namespaces/default/pods/somepod",
   241  			host:          "localhost",
   242  			method:        "PUT",
   243  			expectAccept:  false,
   244  		},
   245  		{
   246  			name:          "test20",
   247  			acceptPaths:   DefaultPathAcceptRE,
   248  			rejectPaths:   DefaultPathRejectRE,
   249  			acceptHosts:   DefaultHostAcceptRE,
   250  			rejectMethods: "PATCH",
   251  			path:          "/api/v1/namespaces/default/pods/somepod",
   252  			host:          "localhost",
   253  			method:        "PATCH",
   254  			expectAccept:  false,
   255  		},
   256  		{
   257  			name:          "test21",
   258  			acceptPaths:   DefaultPathAcceptRE,
   259  			rejectPaths:   DefaultPathRejectRE,
   260  			acceptHosts:   DefaultHostAcceptRE,
   261  			rejectMethods: "POST,PUT,PATCH",
   262  			path:          "/api/v1/namespaces/default/pods/somepod",
   263  			host:          "localhost",
   264  			method:        "PATCH",
   265  			expectAccept:  false,
   266  		},
   267  		{
   268  			name:          "test22",
   269  			acceptPaths:   DefaultPathAcceptRE,
   270  			rejectPaths:   DefaultPathRejectRE,
   271  			acceptHosts:   DefaultHostAcceptRE,
   272  			rejectMethods: "POST,PUT,PATCH",
   273  			path:          "/api/v1/namespaces/default/pods/somepod",
   274  			host:          "localhost",
   275  			method:        "PUT",
   276  			expectAccept:  false,
   277  		},
   278  		{
   279  			name:          "test23",
   280  			acceptPaths:   DefaultPathAcceptRE,
   281  			rejectPaths:   DefaultPathRejectRE,
   282  			acceptHosts:   DefaultHostAcceptRE,
   283  			rejectMethods: DefaultMethodRejectRE,
   284  			path:          "/api/v1/namespaces/default/pods/somepod/exec",
   285  			host:          "localhost",
   286  			method:        "POST",
   287  			expectAccept:  false,
   288  		},
   289  		{
   290  			name:          "test24",
   291  			acceptPaths:   DefaultPathAcceptRE,
   292  			rejectPaths:   "",
   293  			acceptHosts:   DefaultHostAcceptRE,
   294  			rejectMethods: DefaultMethodRejectRE,
   295  			path:          "/api/v1/namespaces/default/pods/somepod/exec",
   296  			host:          "localhost",
   297  			method:        "POST",
   298  			expectAccept:  true,
   299  		},
   300  	}
   301  	for _, tt := range tests {
   302  		t.Run(tt.name, func(t *testing.T) {
   303  			filter := &FilterServer{
   304  				AcceptPaths:   MakeRegexpArrayOrDie(tt.acceptPaths),
   305  				RejectPaths:   MakeRegexpArrayOrDie(tt.rejectPaths),
   306  				AcceptHosts:   MakeRegexpArrayOrDie(tt.acceptHosts),
   307  				RejectMethods: MakeRegexpArrayOrDie(tt.rejectMethods),
   308  			}
   309  			accept := filter.accept(tt.method, tt.path, tt.host)
   310  			if accept != tt.expectAccept {
   311  				t.Errorf("expected: %v, got %v for %#v", tt.expectAccept, accept, tt)
   312  			}
   313  		})
   314  	}
   315  }
   316  
   317  func TestRegexpMatch(t *testing.T) {
   318  	tests := []struct {
   319  		name        string
   320  		str         string
   321  		regexps     string
   322  		expectMatch bool
   323  	}{
   324  		{
   325  			name:        "test1",
   326  			str:         "foo",
   327  			regexps:     "bar,.*",
   328  			expectMatch: true,
   329  		},
   330  		{
   331  			name:        "test2",
   332  			str:         "foo",
   333  			regexps:     "bar,fo.*",
   334  			expectMatch: true,
   335  		},
   336  		{
   337  			name:        "test3",
   338  			str:         "bar",
   339  			regexps:     "bar,fo.*",
   340  			expectMatch: true,
   341  		},
   342  		{
   343  			name:        "test4",
   344  			str:         "baz",
   345  			regexps:     "bar,fo.*",
   346  			expectMatch: false,
   347  		},
   348  	}
   349  	for _, tt := range tests {
   350  		t.Run(tt.name, func(t *testing.T) {
   351  			match := matchesRegexp(tt.str, MakeRegexpArrayOrDie(tt.regexps))
   352  			if tt.expectMatch != match {
   353  				t.Errorf("expected: %v, found: %v, for %s and %v", tt.expectMatch, match, tt.str, tt.regexps)
   354  			}
   355  		})
   356  	}
   357  }
   358  
   359  func TestFileServing(t *testing.T) {
   360  	const (
   361  		fname = "test.txt"
   362  		data  = "This is test data"
   363  	)
   364  	dir, err := os.MkdirTemp("", "data")
   365  	if err != nil {
   366  		t.Fatalf("error creating tmp dir: %v", err)
   367  	}
   368  	defer os.RemoveAll(dir)
   369  	if err := os.WriteFile(filepath.Join(dir, fname), []byte(data), 0755); err != nil {
   370  		t.Fatalf("error writing tmp file: %v", err)
   371  	}
   372  
   373  	const prefix = "/foo/"
   374  	handler := newFileHandler(prefix, dir)
   375  	server := httptest.NewServer(handler)
   376  	defer server.Close()
   377  
   378  	url := server.URL + prefix + fname
   379  	res, err := http.Get(url)
   380  	if err != nil {
   381  		t.Fatalf("http.Get(%q) error: %v", url, err)
   382  	}
   383  	defer res.Body.Close()
   384  
   385  	if res.StatusCode != http.StatusOK {
   386  		t.Errorf("res.StatusCode = %d; want %d", res.StatusCode, http.StatusOK)
   387  	}
   388  	b, err := io.ReadAll(res.Body)
   389  	if err != nil {
   390  		t.Fatalf("error reading resp body: %v", err)
   391  	}
   392  	if string(b) != data {
   393  		t.Errorf("have %q; want %q", string(b), data)
   394  	}
   395  }
   396  
   397  func newProxy(target *url.URL) http.Handler {
   398  	p := proxy.NewUpgradeAwareHandler(target, http.DefaultTransport, false, false, &responder{})
   399  	p.UseRequestLocation = true
   400  	return p
   401  }
   402  
   403  func TestAPIRequests(t *testing.T) {
   404  	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   405  		b, err := io.ReadAll(r.Body)
   406  		if err != nil {
   407  			http.Error(w, err.Error(), http.StatusInternalServerError)
   408  			return
   409  		}
   410  		fmt.Fprintf(w, "%s %s %s", r.Method, r.RequestURI, string(b))
   411  	}))
   412  	defer ts.Close()
   413  
   414  	// httptest.NewServer should always generate a valid URL.
   415  	target, _ := url.Parse(ts.URL)
   416  	target.Path = "/"
   417  	proxy := newProxy(target)
   418  
   419  	tests := []struct{ name, method, body string }{
   420  		{"test1", "GET", ""},
   421  		{"test2", "DELETE", ""},
   422  		{"test3", "POST", "test payload"},
   423  		{"test4", "PUT", "test payload"},
   424  	}
   425  
   426  	const path = "/api/test?fields=ID%3Dfoo&labels=key%3Dvalue"
   427  	for i, tt := range tests {
   428  		t.Run(tt.name, func(t *testing.T) {
   429  			r, err := http.NewRequest(tt.method, path, strings.NewReader(tt.body))
   430  			if err != nil {
   431  				t.Errorf("error creating request: %v", err)
   432  				return
   433  			}
   434  			w := httptest.NewRecorder()
   435  			proxy.ServeHTTP(w, r)
   436  			if w.Code != http.StatusOK {
   437  				t.Errorf("%d: proxy.ServeHTTP w.Code = %d; want %d", i, w.Code, http.StatusOK)
   438  			}
   439  			want := strings.Join([]string{tt.method, path, tt.body}, " ")
   440  			if w.Body.String() != want {
   441  				t.Errorf("%d: response body = %q; want %q", i, w.Body.String(), want)
   442  			}
   443  		})
   444  	}
   445  }
   446  
   447  func TestPathHandling(t *testing.T) {
   448  	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   449  		fmt.Fprint(w, r.URL.Path)
   450  	}))
   451  	defer ts.Close()
   452  
   453  	table := []struct {
   454  		name       string
   455  		prefix     string
   456  		reqPath    string
   457  		expectPath string
   458  		appendPath bool
   459  	}{
   460  		{"test1", "/api/", "/metrics", "404 page not found\n", false},
   461  		{"test2", "/api/", "/api/metrics", "/api/metrics", false},
   462  		{"test3", "/api/", "/api/v1/pods/", "/api/v1/pods/", false},
   463  		{"test4", "/", "/metrics", "/metrics", false},
   464  		{"test5", "/", "/api/v1/pods/", "/api/v1/pods/", false},
   465  		{"test6", "/custom/", "/metrics", "404 page not found\n", false},
   466  		{"test7", "/custom/", "/api/metrics", "404 page not found\n", false},
   467  		{"test8", "/custom/", "/api/v1/pods/", "404 page not found\n", false},
   468  		{"test9", "/custom/", "/custom/api/metrics", "/api/metrics", false},
   469  		{"test10", "/custom/", "/custom/api/v1/pods/", "/api/v1/pods/", false},
   470  		{"test11", "/custom/", "/custom/api/v1/services/", "/api/v1/services/", true},
   471  	}
   472  
   473  	cc := &rest.Config{
   474  		Host: ts.URL,
   475  	}
   476  
   477  	for _, tt := range table {
   478  		t.Run(tt.name, func(t *testing.T) {
   479  			p, err := NewServer("", tt.prefix, "/not/used/for/this/test", nil, cc, 0, tt.appendPath)
   480  			if err != nil {
   481  				t.Fatalf("%#v: %v", tt, err)
   482  			}
   483  			pts := httptest.NewServer(p.handler)
   484  			defer pts.Close()
   485  
   486  			r, err := http.Get(pts.URL + tt.reqPath)
   487  			if err != nil {
   488  				t.Fatalf("%#v: %v", tt, err)
   489  			}
   490  			body, err := io.ReadAll(r.Body)
   491  			r.Body.Close()
   492  			if err != nil {
   493  				t.Fatalf("%#v: %v", tt, err)
   494  			}
   495  			if e, a := tt.expectPath, string(body); e != a {
   496  				t.Errorf("%#v: Wanted %q, got %q", tt, e, a)
   497  			}
   498  		})
   499  	}
   500  }
   501  
   502  func TestExtractHost(t *testing.T) {
   503  	fixtures := map[string]string{
   504  		"localhost:8085": "localhost",
   505  		"marmalade":      "marmalade",
   506  	}
   507  	for header, expected := range fixtures {
   508  		host := extractHost(header)
   509  		if host != expected {
   510  			t.Fatalf("%s != %s", host, expected)
   511  		}
   512  	}
   513  }
   514  

View as plain text