k8s.io/kubernetes@v1.29.3/pkg/kubelet/server/server_test.go (about) 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 server 18 19 import ( 20 "context" 21 "crypto/tls" 22 "errors" 23 "fmt" 24 "io" 25 "net" 26 "net/http" 27 "net/http/httptest" 28 "net/http/httputil" 29 "net/url" 30 "reflect" 31 "strconv" 32 "strings" 33 "testing" 34 "time" 35 36 cadvisorapi "github.com/google/cadvisor/info/v1" 37 cadvisorapiv2 "github.com/google/cadvisor/info/v2" 38 "github.com/stretchr/testify/assert" 39 "github.com/stretchr/testify/require" 40 oteltrace "go.opentelemetry.io/otel/trace" 41 v1 "k8s.io/api/core/v1" 42 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 43 "k8s.io/apimachinery/pkg/types" 44 "k8s.io/apimachinery/pkg/util/httpstream" 45 "k8s.io/apimachinery/pkg/util/httpstream/spdy" 46 "k8s.io/apiserver/pkg/authentication/authenticator" 47 "k8s.io/apiserver/pkg/authentication/user" 48 "k8s.io/apiserver/pkg/authorization/authorizer" 49 "k8s.io/client-go/tools/record" 50 "k8s.io/client-go/tools/remotecommand" 51 runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1" 52 statsapi "k8s.io/kubelet/pkg/apis/stats/v1alpha1" 53 api "k8s.io/kubernetes/pkg/apis/core" 54 "k8s.io/utils/pointer" 55 56 // Do some initialization to decode the query parameters correctly. 57 utilfeature "k8s.io/apiserver/pkg/util/feature" 58 featuregatetesting "k8s.io/component-base/featuregate/testing" 59 "k8s.io/kubelet/pkg/cri/streaming" 60 "k8s.io/kubelet/pkg/cri/streaming/portforward" 61 remotecommandserver "k8s.io/kubelet/pkg/cri/streaming/remotecommand" 62 _ "k8s.io/kubernetes/pkg/apis/core/install" 63 "k8s.io/kubernetes/pkg/features" 64 kubeletconfiginternal "k8s.io/kubernetes/pkg/kubelet/apis/config" 65 "k8s.io/kubernetes/pkg/kubelet/cm" 66 "k8s.io/kubernetes/pkg/kubelet/server/stats" 67 "k8s.io/kubernetes/pkg/volume" 68 ) 69 70 const ( 71 testUID = "9b01b80f-8fb4-11e4-95ab-4200af06647" 72 testContainerID = "container789" 73 testPodSandboxID = "pod0987" 74 ) 75 76 type fakeKubelet struct { 77 podByNameFunc func(namespace, name string) (*v1.Pod, bool) 78 containerInfoFunc func(ctx context.Context, podFullName string, uid types.UID, containerName string, req *cadvisorapi.ContainerInfoRequest) (*cadvisorapi.ContainerInfo, error) 79 rawInfoFunc func(query *cadvisorapi.ContainerInfoRequest) (map[string]*cadvisorapi.ContainerInfo, error) 80 machineInfoFunc func() (*cadvisorapi.MachineInfo, error) 81 podsFunc func() []*v1.Pod 82 runningPodsFunc func(ctx context.Context) ([]*v1.Pod, error) 83 logFunc func(w http.ResponseWriter, req *http.Request) 84 runFunc func(podFullName string, uid types.UID, containerName string, cmd []string) ([]byte, error) 85 getExecCheck func(string, types.UID, string, []string, remotecommandserver.Options) 86 getAttachCheck func(string, types.UID, string, remotecommandserver.Options) 87 getPortForwardCheck func(string, string, types.UID, portforward.V4Options) 88 89 containerLogsFunc func(ctx context.Context, podFullName, containerName string, logOptions *v1.PodLogOptions, stdout, stderr io.Writer) error 90 hostnameFunc func() string 91 resyncInterval time.Duration 92 loopEntryTime time.Time 93 plegHealth bool 94 streamingRuntime streaming.Server 95 } 96 97 func (fk *fakeKubelet) ResyncInterval() time.Duration { 98 return fk.resyncInterval 99 } 100 101 func (fk *fakeKubelet) LatestLoopEntryTime() time.Time { 102 return fk.loopEntryTime 103 } 104 105 func (fk *fakeKubelet) GetPodByName(namespace, name string) (*v1.Pod, bool) { 106 return fk.podByNameFunc(namespace, name) 107 } 108 109 func (fk *fakeKubelet) GetRequestedContainersInfo(containerName string, options cadvisorapiv2.RequestOptions) (map[string]*cadvisorapi.ContainerInfo, error) { 110 return map[string]*cadvisorapi.ContainerInfo{}, nil 111 } 112 113 func (fk *fakeKubelet) GetContainerInfo(ctx context.Context, podFullName string, uid types.UID, containerName string, req *cadvisorapi.ContainerInfoRequest) (*cadvisorapi.ContainerInfo, error) { 114 return fk.containerInfoFunc(ctx, podFullName, uid, containerName, req) 115 } 116 117 func (fk *fakeKubelet) GetRawContainerInfo(containerName string, req *cadvisorapi.ContainerInfoRequest, subcontainers bool) (map[string]*cadvisorapi.ContainerInfo, error) { 118 return fk.rawInfoFunc(req) 119 } 120 121 func (fk *fakeKubelet) GetCachedMachineInfo() (*cadvisorapi.MachineInfo, error) { 122 return fk.machineInfoFunc() 123 } 124 125 func (*fakeKubelet) GetVersionInfo() (*cadvisorapi.VersionInfo, error) { 126 return &cadvisorapi.VersionInfo{}, nil 127 } 128 129 func (fk *fakeKubelet) GetPods() []*v1.Pod { 130 return fk.podsFunc() 131 } 132 133 func (fk *fakeKubelet) GetRunningPods(ctx context.Context) ([]*v1.Pod, error) { 134 return fk.runningPodsFunc(ctx) 135 } 136 137 func (fk *fakeKubelet) ServeLogs(w http.ResponseWriter, req *http.Request) { 138 fk.logFunc(w, req) 139 } 140 141 func (fk *fakeKubelet) GetKubeletContainerLogs(ctx context.Context, podFullName, containerName string, logOptions *v1.PodLogOptions, stdout, stderr io.Writer) error { 142 return fk.containerLogsFunc(ctx, podFullName, containerName, logOptions, stdout, stderr) 143 } 144 145 func (fk *fakeKubelet) GetHostname() string { 146 return fk.hostnameFunc() 147 } 148 149 func (fk *fakeKubelet) RunInContainer(_ context.Context, podFullName string, uid types.UID, containerName string, cmd []string) ([]byte, error) { 150 return fk.runFunc(podFullName, uid, containerName, cmd) 151 } 152 153 func (fk *fakeKubelet) CheckpointContainer(_ context.Context, podUID types.UID, podFullName, containerName string, options *runtimeapi.CheckpointContainerRequest) error { 154 if containerName == "checkpointingFailure" { 155 return fmt.Errorf("Returning error for test") 156 } 157 return nil 158 } 159 160 func (fk *fakeKubelet) ListMetricDescriptors(ctx context.Context) ([]*runtimeapi.MetricDescriptor, error) { 161 return nil, nil 162 } 163 164 func (fk *fakeKubelet) ListPodSandboxMetrics(ctx context.Context) ([]*runtimeapi.PodSandboxMetrics, error) { 165 return nil, nil 166 } 167 168 type fakeRuntime struct { 169 execFunc func(string, []string, io.Reader, io.WriteCloser, io.WriteCloser, bool, <-chan remotecommand.TerminalSize) error 170 attachFunc func(string, io.Reader, io.WriteCloser, io.WriteCloser, bool, <-chan remotecommand.TerminalSize) error 171 portForwardFunc func(string, int32, io.ReadWriteCloser) error 172 } 173 174 func (f *fakeRuntime) Exec(_ context.Context, containerID string, cmd []string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error { 175 return f.execFunc(containerID, cmd, stdin, stdout, stderr, tty, resize) 176 } 177 178 func (f *fakeRuntime) Attach(_ context.Context, containerID string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error { 179 return f.attachFunc(containerID, stdin, stdout, stderr, tty, resize) 180 } 181 182 func (f *fakeRuntime) PortForward(_ context.Context, podSandboxID string, port int32, stream io.ReadWriteCloser) error { 183 return f.portForwardFunc(podSandboxID, port, stream) 184 } 185 186 type testStreamingServer struct { 187 streaming.Server 188 fakeRuntime *fakeRuntime 189 testHTTPServer *httptest.Server 190 } 191 192 func newTestStreamingServer(streamIdleTimeout time.Duration) (s *testStreamingServer, err error) { 193 s = &testStreamingServer{} 194 s.testHTTPServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 195 s.ServeHTTP(w, r) 196 })) 197 defer func() { 198 if err != nil { 199 s.testHTTPServer.Close() 200 } 201 }() 202 203 testURL, err := url.Parse(s.testHTTPServer.URL) 204 if err != nil { 205 return nil, err 206 } 207 208 s.fakeRuntime = &fakeRuntime{} 209 config := streaming.DefaultConfig 210 config.BaseURL = testURL 211 if streamIdleTimeout != 0 { 212 config.StreamIdleTimeout = streamIdleTimeout 213 } 214 s.Server, err = streaming.NewServer(config, s.fakeRuntime) 215 if err != nil { 216 return nil, err 217 } 218 return s, nil 219 } 220 221 func (fk *fakeKubelet) GetExec(_ context.Context, podFullName string, podUID types.UID, containerName string, cmd []string, streamOpts remotecommandserver.Options) (*url.URL, error) { 222 if fk.getExecCheck != nil { 223 fk.getExecCheck(podFullName, podUID, containerName, cmd, streamOpts) 224 } 225 // Always use testContainerID 226 resp, err := fk.streamingRuntime.GetExec(&runtimeapi.ExecRequest{ 227 ContainerId: testContainerID, 228 Cmd: cmd, 229 Tty: streamOpts.TTY, 230 Stdin: streamOpts.Stdin, 231 Stdout: streamOpts.Stdout, 232 Stderr: streamOpts.Stderr, 233 }) 234 if err != nil { 235 return nil, err 236 } 237 return url.Parse(resp.GetUrl()) 238 } 239 240 func (fk *fakeKubelet) GetAttach(_ context.Context, podFullName string, podUID types.UID, containerName string, streamOpts remotecommandserver.Options) (*url.URL, error) { 241 if fk.getAttachCheck != nil { 242 fk.getAttachCheck(podFullName, podUID, containerName, streamOpts) 243 } 244 // Always use testContainerID 245 resp, err := fk.streamingRuntime.GetAttach(&runtimeapi.AttachRequest{ 246 ContainerId: testContainerID, 247 Tty: streamOpts.TTY, 248 Stdin: streamOpts.Stdin, 249 Stdout: streamOpts.Stdout, 250 Stderr: streamOpts.Stderr, 251 }) 252 if err != nil { 253 return nil, err 254 } 255 return url.Parse(resp.GetUrl()) 256 } 257 258 func (fk *fakeKubelet) GetPortForward(ctx context.Context, podName, podNamespace string, podUID types.UID, portForwardOpts portforward.V4Options) (*url.URL, error) { 259 if fk.getPortForwardCheck != nil { 260 fk.getPortForwardCheck(podName, podNamespace, podUID, portForwardOpts) 261 } 262 // Always use testPodSandboxID 263 resp, err := fk.streamingRuntime.GetPortForward(&runtimeapi.PortForwardRequest{ 264 PodSandboxId: testPodSandboxID, 265 Port: portForwardOpts.Ports, 266 }) 267 if err != nil { 268 return nil, err 269 } 270 return url.Parse(resp.GetUrl()) 271 } 272 273 // Unused functions 274 func (*fakeKubelet) GetNode() (*v1.Node, error) { return nil, nil } 275 func (*fakeKubelet) GetNodeConfig() cm.NodeConfig { return cm.NodeConfig{} } 276 func (*fakeKubelet) GetPodCgroupRoot() string { return "" } 277 func (*fakeKubelet) GetPodByCgroupfs(cgroupfs string) (*v1.Pod, bool) { return nil, false } 278 func (fk *fakeKubelet) ListVolumesForPod(podUID types.UID) (map[string]volume.Volume, bool) { 279 return map[string]volume.Volume{}, true 280 } 281 func (*fakeKubelet) ListBlockVolumesForPod(podUID types.UID) (map[string]volume.BlockVolume, bool) { 282 return map[string]volume.BlockVolume{}, true 283 } 284 func (*fakeKubelet) RootFsStats() (*statsapi.FsStats, error) { return nil, nil } 285 func (*fakeKubelet) ListPodStats(_ context.Context) ([]statsapi.PodStats, error) { return nil, nil } 286 func (*fakeKubelet) ListPodStatsAndUpdateCPUNanoCoreUsage(_ context.Context) ([]statsapi.PodStats, error) { 287 return nil, nil 288 } 289 func (*fakeKubelet) ListPodCPUAndMemoryStats(_ context.Context) ([]statsapi.PodStats, error) { 290 return nil, nil 291 } 292 func (*fakeKubelet) ImageFsStats(_ context.Context) (*statsapi.FsStats, *statsapi.FsStats, error) { 293 return nil, nil, nil 294 } 295 func (*fakeKubelet) RlimitStats() (*statsapi.RlimitStats, error) { return nil, nil } 296 func (*fakeKubelet) GetCgroupStats(cgroupName string, updateStats bool) (*statsapi.ContainerStats, *statsapi.NetworkStats, error) { 297 return nil, nil, nil 298 } 299 func (*fakeKubelet) GetCgroupCPUAndMemoryStats(cgroupName string, updateStats bool) (*statsapi.ContainerStats, error) { 300 return nil, nil 301 } 302 303 type fakeAuth struct { 304 authenticateFunc func(*http.Request) (*authenticator.Response, bool, error) 305 attributesFunc func(user.Info, *http.Request) authorizer.Attributes 306 authorizeFunc func(authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) 307 } 308 309 func (f *fakeAuth) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) { 310 return f.authenticateFunc(req) 311 } 312 func (f *fakeAuth) GetRequestAttributes(u user.Info, req *http.Request) authorizer.Attributes { 313 return f.attributesFunc(u, req) 314 } 315 func (f *fakeAuth) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { 316 return f.authorizeFunc(a) 317 } 318 319 type serverTestFramework struct { 320 serverUnderTest *Server 321 fakeKubelet *fakeKubelet 322 fakeAuth *fakeAuth 323 testHTTPServer *httptest.Server 324 } 325 326 func newServerTest() *serverTestFramework { 327 return newServerTestWithDebug(true, nil) 328 } 329 330 func newServerTestWithDebug(enableDebugging bool, streamingServer streaming.Server) *serverTestFramework { 331 kubeCfg := &kubeletconfiginternal.KubeletConfiguration{ 332 EnableDebuggingHandlers: enableDebugging, 333 EnableSystemLogHandler: enableDebugging, 334 EnableProfilingHandler: enableDebugging, 335 EnableDebugFlagsHandler: enableDebugging, 336 } 337 return newServerTestWithDebuggingHandlers(kubeCfg, streamingServer) 338 } 339 340 func newServerTestWithDebuggingHandlers(kubeCfg *kubeletconfiginternal.KubeletConfiguration, streamingServer streaming.Server) *serverTestFramework { 341 342 fw := &serverTestFramework{} 343 fw.fakeKubelet = &fakeKubelet{ 344 hostnameFunc: func() string { 345 return "127.0.0.1" 346 }, 347 podByNameFunc: func(namespace, name string) (*v1.Pod, bool) { 348 return &v1.Pod{ 349 ObjectMeta: metav1.ObjectMeta{ 350 Namespace: namespace, 351 Name: name, 352 UID: testUID, 353 }, 354 }, true 355 }, 356 plegHealth: true, 357 streamingRuntime: streamingServer, 358 } 359 fw.fakeAuth = &fakeAuth{ 360 authenticateFunc: func(req *http.Request) (*authenticator.Response, bool, error) { 361 return &authenticator.Response{User: &user.DefaultInfo{Name: "test"}}, true, nil 362 }, 363 attributesFunc: func(u user.Info, req *http.Request) authorizer.Attributes { 364 return &authorizer.AttributesRecord{User: u} 365 }, 366 authorizeFunc: func(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) { 367 return authorizer.DecisionAllow, "", nil 368 }, 369 } 370 server := NewServer( 371 fw.fakeKubelet, 372 stats.NewResourceAnalyzer(fw.fakeKubelet, time.Minute, &record.FakeRecorder{}), 373 fw.fakeAuth, 374 oteltrace.NewNoopTracerProvider(), 375 kubeCfg, 376 ) 377 fw.serverUnderTest = &server 378 fw.testHTTPServer = httptest.NewServer(fw.serverUnderTest) 379 return fw 380 } 381 382 // A helper function to return the correct pod name. 383 func getPodName(name, namespace string) string { 384 if namespace == "" { 385 namespace = metav1.NamespaceDefault 386 } 387 return name + "_" + namespace 388 } 389 390 func TestServeLogs(t *testing.T) { 391 fw := newServerTest() 392 defer fw.testHTTPServer.Close() 393 394 content := string(`<pre><a href="kubelet.log">kubelet.log</a><a href="google.log">google.log</a></pre>`) 395 396 fw.fakeKubelet.logFunc = func(w http.ResponseWriter, req *http.Request) { 397 w.WriteHeader(http.StatusOK) 398 w.Header().Add("Content-Type", "text/html") 399 w.Write([]byte(content)) 400 } 401 402 resp, err := http.Get(fw.testHTTPServer.URL + "/logs/") 403 if err != nil { 404 t.Fatalf("Got error GETing: %v", err) 405 } 406 defer resp.Body.Close() 407 408 body, err := httputil.DumpResponse(resp, true) 409 if err != nil { 410 // copying the response body did not work 411 t.Errorf("Cannot copy resp: %#v", err) 412 } 413 result := string(body) 414 if !strings.Contains(result, "kubelet.log") || !strings.Contains(result, "google.log") { 415 t.Errorf("Received wrong data: %s", result) 416 } 417 } 418 419 func TestServeRunInContainer(t *testing.T) { 420 fw := newServerTest() 421 defer fw.testHTTPServer.Close() 422 output := "foo bar" 423 podNamespace := "other" 424 podName := "foo" 425 expectedPodName := getPodName(podName, podNamespace) 426 expectedContainerName := "baz" 427 expectedCommand := "ls -a" 428 fw.fakeKubelet.runFunc = func(podFullName string, uid types.UID, containerName string, cmd []string) ([]byte, error) { 429 if podFullName != expectedPodName { 430 t.Errorf("expected %s, got %s", expectedPodName, podFullName) 431 } 432 if containerName != expectedContainerName { 433 t.Errorf("expected %s, got %s", expectedContainerName, containerName) 434 } 435 if strings.Join(cmd, " ") != expectedCommand { 436 t.Errorf("expected: %s, got %v", expectedCommand, cmd) 437 } 438 439 return []byte(output), nil 440 } 441 442 resp, err := http.Post(fw.testHTTPServer.URL+"/run/"+podNamespace+"/"+podName+"/"+expectedContainerName+"?cmd=ls%20-a", "", nil) 443 444 if err != nil { 445 t.Fatalf("Got error POSTing: %v", err) 446 } 447 defer resp.Body.Close() 448 449 body, err := io.ReadAll(resp.Body) 450 if err != nil { 451 // copying the response body did not work 452 t.Errorf("Cannot copy resp: %#v", err) 453 } 454 result := string(body) 455 if result != output { 456 t.Errorf("expected %s, got %s", output, result) 457 } 458 } 459 460 func TestServeRunInContainerWithUID(t *testing.T) { 461 fw := newServerTest() 462 defer fw.testHTTPServer.Close() 463 output := "foo bar" 464 podNamespace := "other" 465 podName := "foo" 466 expectedPodName := getPodName(podName, podNamespace) 467 expectedContainerName := "baz" 468 expectedCommand := "ls -a" 469 fw.fakeKubelet.runFunc = func(podFullName string, uid types.UID, containerName string, cmd []string) ([]byte, error) { 470 if podFullName != expectedPodName { 471 t.Errorf("expected %s, got %s", expectedPodName, podFullName) 472 } 473 if string(uid) != testUID { 474 t.Errorf("expected %s, got %s", testUID, uid) 475 } 476 if containerName != expectedContainerName { 477 t.Errorf("expected %s, got %s", expectedContainerName, containerName) 478 } 479 if strings.Join(cmd, " ") != expectedCommand { 480 t.Errorf("expected: %s, got %v", expectedCommand, cmd) 481 } 482 483 return []byte(output), nil 484 } 485 486 resp, err := http.Post(fw.testHTTPServer.URL+"/run/"+podNamespace+"/"+podName+"/"+testUID+"/"+expectedContainerName+"?cmd=ls%20-a", "", nil) 487 if err != nil { 488 t.Fatalf("Got error POSTing: %v", err) 489 } 490 defer resp.Body.Close() 491 492 body, err := io.ReadAll(resp.Body) 493 if err != nil { 494 // copying the response body did not work 495 t.Errorf("Cannot copy resp: %#v", err) 496 } 497 result := string(body) 498 if result != output { 499 t.Errorf("expected %s, got %s", output, result) 500 } 501 } 502 503 func TestHealthCheck(t *testing.T) { 504 fw := newServerTest() 505 defer fw.testHTTPServer.Close() 506 fw.fakeKubelet.hostnameFunc = func() string { 507 return "127.0.0.1" 508 } 509 510 // Test with correct hostname, Docker version 511 assertHealthIsOk(t, fw.testHTTPServer.URL+"/healthz") 512 513 // Test with incorrect hostname 514 fw.fakeKubelet.hostnameFunc = func() string { 515 return "fake" 516 } 517 assertHealthIsOk(t, fw.testHTTPServer.URL+"/healthz") 518 } 519 520 func assertHealthFails(t *testing.T, httpURL string, expectedErrorCode int) { 521 resp, err := http.Get(httpURL) 522 if err != nil { 523 t.Fatalf("Got error GETing: %v", err) 524 } 525 defer resp.Body.Close() 526 if resp.StatusCode != expectedErrorCode { 527 t.Errorf("expected status code %d, got %d", expectedErrorCode, resp.StatusCode) 528 } 529 } 530 531 // Ensure all registered handlers & services have an associated testcase. 532 func TestAuthzCoverage(t *testing.T) { 533 fw := newServerTest() 534 defer fw.testHTTPServer.Close() 535 536 // method:path -> has coverage 537 expectedCases := map[string]bool{} 538 539 // Test all the non-web-service handlers 540 for _, path := range fw.serverUnderTest.restfulCont.RegisteredHandlePaths() { 541 expectedCases["GET:"+path] = false 542 expectedCases["POST:"+path] = false 543 } 544 545 // Test all the generated web-service paths 546 for _, ws := range fw.serverUnderTest.restfulCont.RegisteredWebServices() { 547 for _, r := range ws.Routes() { 548 expectedCases[r.Method+":"+r.Path] = false 549 } 550 } 551 552 // This is a sanity check that the Handle->HandleWithFilter() delegation is working 553 // Ideally, these would move to registered web services and this list would get shorter 554 expectedPaths := []string{"/healthz", "/metrics", "/metrics/cadvisor"} 555 for _, expectedPath := range expectedPaths { 556 if _, expected := expectedCases["GET:"+expectedPath]; !expected { 557 t.Errorf("Expected registered handle path %s was missing", expectedPath) 558 } 559 } 560 561 for _, tc := range AuthzTestCases() { 562 expectedCases[tc.Method+":"+tc.Path] = true 563 } 564 565 for tc, found := range expectedCases { 566 if !found { 567 t.Errorf("Missing authz test case for %s", tc) 568 } 569 } 570 } 571 572 func TestAuthFilters(t *testing.T) { 573 // Enable features.ContainerCheckpoint during test 574 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ContainerCheckpoint, true)() 575 576 fw := newServerTest() 577 defer fw.testHTTPServer.Close() 578 579 attributesGetter := NewNodeAuthorizerAttributesGetter(authzTestNodeName) 580 581 for _, tc := range AuthzTestCases() { 582 t.Run(tc.Method+":"+tc.Path, func(t *testing.T) { 583 var ( 584 expectedUser = AuthzTestUser() 585 586 calledAuthenticate = false 587 calledAuthorize = false 588 calledAttributes = false 589 ) 590 591 fw.fakeAuth.authenticateFunc = func(req *http.Request) (*authenticator.Response, bool, error) { 592 calledAuthenticate = true 593 return &authenticator.Response{User: expectedUser}, true, nil 594 } 595 fw.fakeAuth.attributesFunc = func(u user.Info, req *http.Request) authorizer.Attributes { 596 calledAttributes = true 597 require.Equal(t, expectedUser, u) 598 return attributesGetter.GetRequestAttributes(u, req) 599 } 600 fw.fakeAuth.authorizeFunc = func(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) { 601 calledAuthorize = true 602 tc.AssertAttributes(t, a) 603 return authorizer.DecisionNoOpinion, "", nil 604 } 605 606 req, err := http.NewRequest(tc.Method, fw.testHTTPServer.URL+tc.Path, nil) 607 require.NoError(t, err) 608 609 resp, err := http.DefaultClient.Do(req) 610 require.NoError(t, err) 611 defer resp.Body.Close() 612 613 assert.Equal(t, http.StatusForbidden, resp.StatusCode) 614 assert.True(t, calledAuthenticate, "Authenticate was not called") 615 assert.True(t, calledAttributes, "Attributes were not called") 616 assert.True(t, calledAuthorize, "Authorize was not called") 617 }) 618 } 619 } 620 621 func TestAuthenticationError(t *testing.T) { 622 var ( 623 expectedUser = &user.DefaultInfo{Name: "test"} 624 expectedAttributes = &authorizer.AttributesRecord{User: expectedUser} 625 626 calledAuthenticate = false 627 calledAuthorize = false 628 calledAttributes = false 629 ) 630 631 fw := newServerTest() 632 defer fw.testHTTPServer.Close() 633 fw.fakeAuth.authenticateFunc = func(req *http.Request) (*authenticator.Response, bool, error) { 634 calledAuthenticate = true 635 return &authenticator.Response{User: expectedUser}, true, nil 636 } 637 fw.fakeAuth.attributesFunc = func(u user.Info, req *http.Request) authorizer.Attributes { 638 calledAttributes = true 639 return expectedAttributes 640 } 641 fw.fakeAuth.authorizeFunc = func(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) { 642 calledAuthorize = true 643 return authorizer.DecisionNoOpinion, "", errors.New("Failed") 644 } 645 646 assertHealthFails(t, fw.testHTTPServer.URL+"/healthz", http.StatusInternalServerError) 647 648 if !calledAuthenticate { 649 t.Fatalf("Authenticate was not called") 650 } 651 if !calledAttributes { 652 t.Fatalf("Attributes was not called") 653 } 654 if !calledAuthorize { 655 t.Fatalf("Authorize was not called") 656 } 657 } 658 659 func TestAuthenticationFailure(t *testing.T) { 660 var ( 661 expectedUser = &user.DefaultInfo{Name: "test"} 662 expectedAttributes = &authorizer.AttributesRecord{User: expectedUser} 663 664 calledAuthenticate = false 665 calledAuthorize = false 666 calledAttributes = false 667 ) 668 669 fw := newServerTest() 670 defer fw.testHTTPServer.Close() 671 fw.fakeAuth.authenticateFunc = func(req *http.Request) (*authenticator.Response, bool, error) { 672 calledAuthenticate = true 673 return nil, false, nil 674 } 675 fw.fakeAuth.attributesFunc = func(u user.Info, req *http.Request) authorizer.Attributes { 676 calledAttributes = true 677 return expectedAttributes 678 } 679 fw.fakeAuth.authorizeFunc = func(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) { 680 calledAuthorize = true 681 return authorizer.DecisionNoOpinion, "", nil 682 } 683 684 assertHealthFails(t, fw.testHTTPServer.URL+"/healthz", http.StatusUnauthorized) 685 686 if !calledAuthenticate { 687 t.Fatalf("Authenticate was not called") 688 } 689 if calledAttributes { 690 t.Fatalf("Attributes was called unexpectedly") 691 } 692 if calledAuthorize { 693 t.Fatalf("Authorize was called unexpectedly") 694 } 695 } 696 697 func TestAuthorizationSuccess(t *testing.T) { 698 var ( 699 expectedUser = &user.DefaultInfo{Name: "test"} 700 expectedAttributes = &authorizer.AttributesRecord{User: expectedUser} 701 702 calledAuthenticate = false 703 calledAuthorize = false 704 calledAttributes = false 705 ) 706 707 fw := newServerTest() 708 defer fw.testHTTPServer.Close() 709 fw.fakeAuth.authenticateFunc = func(req *http.Request) (*authenticator.Response, bool, error) { 710 calledAuthenticate = true 711 return &authenticator.Response{User: expectedUser}, true, nil 712 } 713 fw.fakeAuth.attributesFunc = func(u user.Info, req *http.Request) authorizer.Attributes { 714 calledAttributes = true 715 return expectedAttributes 716 } 717 fw.fakeAuth.authorizeFunc = func(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) { 718 calledAuthorize = true 719 return authorizer.DecisionAllow, "", nil 720 } 721 722 assertHealthIsOk(t, fw.testHTTPServer.URL+"/healthz") 723 724 if !calledAuthenticate { 725 t.Fatalf("Authenticate was not called") 726 } 727 if !calledAttributes { 728 t.Fatalf("Attributes were not called") 729 } 730 if !calledAuthorize { 731 t.Fatalf("Authorize was not called") 732 } 733 } 734 735 func TestSyncLoopCheck(t *testing.T) { 736 fw := newServerTest() 737 defer fw.testHTTPServer.Close() 738 fw.fakeKubelet.hostnameFunc = func() string { 739 return "127.0.0.1" 740 } 741 742 fw.fakeKubelet.resyncInterval = time.Minute 743 fw.fakeKubelet.loopEntryTime = time.Now() 744 745 // Test with correct hostname, Docker version 746 assertHealthIsOk(t, fw.testHTTPServer.URL+"/healthz") 747 748 fw.fakeKubelet.loopEntryTime = time.Now().Add(time.Minute * -10) 749 assertHealthFails(t, fw.testHTTPServer.URL+"/healthz", http.StatusInternalServerError) 750 } 751 752 // returns http response status code from the HTTP GET 753 func assertHealthIsOk(t *testing.T, httpURL string) { 754 resp, err := http.Get(httpURL) 755 if err != nil { 756 t.Fatalf("Got error GETing: %v", err) 757 } 758 defer resp.Body.Close() 759 if resp.StatusCode != http.StatusOK { 760 t.Errorf("expected status code %d, got %d", http.StatusOK, resp.StatusCode) 761 } 762 body, readErr := io.ReadAll(resp.Body) 763 if readErr != nil { 764 // copying the response body did not work 765 t.Fatalf("Cannot copy resp: %#v", readErr) 766 } 767 result := string(body) 768 if !strings.Contains(result, "ok") { 769 t.Errorf("expected body contains ok, got %s", result) 770 } 771 } 772 773 func setPodByNameFunc(fw *serverTestFramework, namespace, pod, container string) { 774 fw.fakeKubelet.podByNameFunc = func(namespace, name string) (*v1.Pod, bool) { 775 return &v1.Pod{ 776 ObjectMeta: metav1.ObjectMeta{ 777 Namespace: namespace, 778 Name: pod, 779 }, 780 Spec: v1.PodSpec{ 781 Containers: []v1.Container{ 782 { 783 Name: container, 784 }, 785 }, 786 }, 787 }, true 788 } 789 } 790 791 func setGetContainerLogsFunc(fw *serverTestFramework, t *testing.T, expectedPodName, expectedContainerName string, expectedLogOptions *v1.PodLogOptions, output string) { 792 fw.fakeKubelet.containerLogsFunc = func(_ context.Context, podFullName, containerName string, logOptions *v1.PodLogOptions, stdout, stderr io.Writer) error { 793 if podFullName != expectedPodName { 794 t.Errorf("expected %s, got %s", expectedPodName, podFullName) 795 } 796 if containerName != expectedContainerName { 797 t.Errorf("expected %s, got %s", expectedContainerName, containerName) 798 } 799 if !reflect.DeepEqual(expectedLogOptions, logOptions) { 800 t.Errorf("expected %#v, got %#v", expectedLogOptions, logOptions) 801 } 802 803 io.WriteString(stdout, output) 804 return nil 805 } 806 } 807 808 func TestContainerLogs(t *testing.T) { 809 fw := newServerTest() 810 defer fw.testHTTPServer.Close() 811 812 tests := map[string]struct { 813 query string 814 podLogOption *v1.PodLogOptions 815 }{ 816 "without tail": {"", &v1.PodLogOptions{}}, 817 "with tail": {"?tailLines=5", &v1.PodLogOptions{TailLines: pointer.Int64(5)}}, 818 "with legacy tail": {"?tail=5", &v1.PodLogOptions{TailLines: pointer.Int64(5)}}, 819 "with tail all": {"?tail=all", &v1.PodLogOptions{}}, 820 "with follow": {"?follow=1", &v1.PodLogOptions{Follow: true}}, 821 } 822 823 for desc, test := range tests { 824 t.Run(desc, func(t *testing.T) { 825 output := "foo bar" 826 podNamespace := "other" 827 podName := "foo" 828 expectedPodName := getPodName(podName, podNamespace) 829 expectedContainerName := "baz" 830 setPodByNameFunc(fw, podNamespace, podName, expectedContainerName) 831 setGetContainerLogsFunc(fw, t, expectedPodName, expectedContainerName, test.podLogOption, output) 832 resp, err := http.Get(fw.testHTTPServer.URL + "/containerLogs/" + podNamespace + "/" + podName + "/" + expectedContainerName + test.query) 833 if err != nil { 834 t.Errorf("Got error GETing: %v", err) 835 } 836 defer resp.Body.Close() 837 838 body, err := io.ReadAll(resp.Body) 839 if err != nil { 840 t.Errorf("Error reading container logs: %v", err) 841 } 842 result := string(body) 843 if result != output { 844 t.Errorf("Expected: '%v', got: '%v'", output, result) 845 } 846 }) 847 } 848 } 849 850 func TestContainerLogsWithInvalidTail(t *testing.T) { 851 fw := newServerTest() 852 defer fw.testHTTPServer.Close() 853 output := "foo bar" 854 podNamespace := "other" 855 podName := "foo" 856 expectedPodName := getPodName(podName, podNamespace) 857 expectedContainerName := "baz" 858 setPodByNameFunc(fw, podNamespace, podName, expectedContainerName) 859 setGetContainerLogsFunc(fw, t, expectedPodName, expectedContainerName, &v1.PodLogOptions{}, output) 860 resp, err := http.Get(fw.testHTTPServer.URL + "/containerLogs/" + podNamespace + "/" + podName + "/" + expectedContainerName + "?tail=-1") 861 if err != nil { 862 t.Errorf("Got error GETing: %v", err) 863 } 864 defer resp.Body.Close() 865 if resp.StatusCode != http.StatusUnprocessableEntity { 866 t.Errorf("Unexpected non-error reading container logs: %#v", resp) 867 } 868 } 869 870 func TestCheckpointContainer(t *testing.T) { 871 // Enable features.ContainerCheckpoint during test 872 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ContainerCheckpoint, true)() 873 874 fw := newServerTest() 875 defer fw.testHTTPServer.Close() 876 podNamespace := "other" 877 podName := "foo" 878 expectedContainerName := "baz" 879 // GetPodByName() should always fail 880 fw.fakeKubelet.podByNameFunc = func(namespace, name string) (*v1.Pod, bool) { 881 return nil, false 882 } 883 t.Run("wrong pod namespace", func(t *testing.T) { 884 resp, err := http.Post(fw.testHTTPServer.URL+"/checkpoint/"+podNamespace+"/"+podName+"/"+expectedContainerName, "", nil) 885 if err != nil { 886 t.Errorf("Got error POSTing: %v", err) 887 } 888 defer resp.Body.Close() 889 if resp.StatusCode != http.StatusNotFound { 890 t.Errorf("Unexpected non-error checkpointing container: %#v", resp) 891 } 892 }) 893 // let GetPodByName() return a result, but our container "wrongContainerName" is not part of the Pod 894 setPodByNameFunc(fw, podNamespace, podName, expectedContainerName) 895 t.Run("wrong container name", func(t *testing.T) { 896 resp, err := http.Post(fw.testHTTPServer.URL+"/checkpoint/"+podNamespace+"/"+podName+"/wrongContainerName", "", nil) 897 if err != nil { 898 t.Errorf("Got error POSTing: %v", err) 899 } 900 defer resp.Body.Close() 901 if resp.StatusCode != http.StatusNotFound { 902 t.Errorf("Unexpected non-error checkpointing container: %#v", resp) 903 } 904 }) 905 // Now the checkpointing of the container fails 906 fw.fakeKubelet.podByNameFunc = func(namespace, name string) (*v1.Pod, bool) { 907 return &v1.Pod{ 908 ObjectMeta: metav1.ObjectMeta{ 909 Namespace: podNamespace, 910 Name: podName, 911 }, 912 Spec: v1.PodSpec{ 913 Containers: []v1.Container{ 914 { 915 Name: "checkpointingFailure", 916 }, 917 }, 918 }, 919 }, true 920 } 921 t.Run("checkpointing fails", func(t *testing.T) { 922 resp, err := http.Post(fw.testHTTPServer.URL+"/checkpoint/"+podNamespace+"/"+podName+"/checkpointingFailure", "", nil) 923 if err != nil { 924 t.Errorf("Got error POSTing: %v", err) 925 } 926 defer resp.Body.Close() 927 assert.Equal(t, resp.StatusCode, 500) 928 body, _ := io.ReadAll(resp.Body) 929 assert.Equal(t, string(body), "checkpointing of other/foo/checkpointingFailure failed (Returning error for test)") 930 }) 931 // Now test a successful checkpoint succeeds 932 setPodByNameFunc(fw, podNamespace, podName, expectedContainerName) 933 t.Run("checkpointing succeeds", func(t *testing.T) { 934 resp, err := http.Post(fw.testHTTPServer.URL+"/checkpoint/"+podNamespace+"/"+podName+"/"+expectedContainerName, "", nil) 935 if err != nil { 936 t.Errorf("Got error POSTing: %v", err) 937 } 938 assert.Equal(t, resp.StatusCode, 200) 939 }) 940 } 941 942 func makeReq(t *testing.T, method, url, clientProtocol string) *http.Request { 943 req, err := http.NewRequest(method, url, nil) 944 if err != nil { 945 t.Fatalf("error creating request: %v", err) 946 } 947 req.Header.Set("Content-Type", "") 948 req.Header.Add("X-Stream-Protocol-Version", clientProtocol) 949 return req 950 } 951 952 func TestServeExecInContainerIdleTimeout(t *testing.T) { 953 ss, err := newTestStreamingServer(100 * time.Millisecond) 954 require.NoError(t, err) 955 defer ss.testHTTPServer.Close() 956 fw := newServerTestWithDebug(true, ss) 957 defer fw.testHTTPServer.Close() 958 959 podNamespace := "other" 960 podName := "foo" 961 expectedContainerName := "baz" 962 963 url := fw.testHTTPServer.URL + "/exec/" + podNamespace + "/" + podName + "/" + expectedContainerName + "?c=ls&c=-a&" + api.ExecStdinParam + "=1" 964 965 upgradeRoundTripper, err := spdy.NewRoundTripper(&tls.Config{}) 966 if err != nil { 967 t.Fatalf("Error creating SpdyRoundTripper: %v", err) 968 } 969 c := &http.Client{Transport: upgradeRoundTripper} 970 971 resp, err := c.Do(makeReq(t, "POST", url, "v4.channel.k8s.io")) 972 if err != nil { 973 t.Fatalf("Got error POSTing: %v", err) 974 } 975 defer resp.Body.Close() 976 977 upgradeRoundTripper.Dialer = &net.Dialer{ 978 Deadline: time.Now().Add(60 * time.Second), 979 Timeout: 60 * time.Second, 980 } 981 conn, err := upgradeRoundTripper.NewConnection(resp) 982 if err != nil { 983 t.Fatalf("Unexpected error creating streaming connection: %s", err) 984 } 985 if conn == nil { 986 t.Fatal("Unexpected nil connection") 987 } 988 989 <-conn.CloseChan() 990 } 991 992 func testExecAttach(t *testing.T, verb string) { 993 tests := map[string]struct { 994 stdin bool 995 stdout bool 996 stderr bool 997 tty bool 998 responseStatusCode int 999 uid bool 1000 }{ 1001 "no input or output": {responseStatusCode: http.StatusBadRequest}, 1002 "stdin": {stdin: true, responseStatusCode: http.StatusSwitchingProtocols}, 1003 "stdout": {stdout: true, responseStatusCode: http.StatusSwitchingProtocols}, 1004 "stderr": {stderr: true, responseStatusCode: http.StatusSwitchingProtocols}, 1005 "stdout and stderr": {stdout: true, stderr: true, responseStatusCode: http.StatusSwitchingProtocols}, 1006 "stdin stdout and stderr": {stdin: true, stdout: true, stderr: true, responseStatusCode: http.StatusSwitchingProtocols}, 1007 "stdin stdout stderr with uid": {stdin: true, stdout: true, stderr: true, responseStatusCode: http.StatusSwitchingProtocols, uid: true}, 1008 } 1009 1010 for desc := range tests { 1011 test := tests[desc] 1012 t.Run(desc, func(t *testing.T) { 1013 ss, err := newTestStreamingServer(0) 1014 require.NoError(t, err) 1015 defer ss.testHTTPServer.Close() 1016 fw := newServerTestWithDebug(true, ss) 1017 defer fw.testHTTPServer.Close() 1018 fmt.Println(desc) 1019 1020 podNamespace := "other" 1021 podName := "foo" 1022 expectedPodName := getPodName(podName, podNamespace) 1023 expectedContainerName := "baz" 1024 expectedCommand := "ls -a" 1025 expectedStdin := "stdin" 1026 expectedStdout := "stdout" 1027 expectedStderr := "stderr" 1028 done := make(chan struct{}) 1029 clientStdoutReadDone := make(chan struct{}) 1030 clientStderrReadDone := make(chan struct{}) 1031 execInvoked := false 1032 attachInvoked := false 1033 1034 checkStream := func(podFullName string, uid types.UID, containerName string, streamOpts remotecommandserver.Options) { 1035 assert.Equal(t, expectedPodName, podFullName, "podFullName") 1036 if test.uid { 1037 assert.Equal(t, testUID, string(uid), "uid") 1038 } 1039 assert.Equal(t, expectedContainerName, containerName, "containerName") 1040 assert.Equal(t, test.stdin, streamOpts.Stdin, "stdin") 1041 assert.Equal(t, test.stdout, streamOpts.Stdout, "stdout") 1042 assert.Equal(t, test.tty, streamOpts.TTY, "tty") 1043 assert.Equal(t, !test.tty && test.stderr, streamOpts.Stderr, "stderr") 1044 } 1045 1046 fw.fakeKubelet.getExecCheck = func(podFullName string, uid types.UID, containerName string, cmd []string, streamOpts remotecommandserver.Options) { 1047 execInvoked = true 1048 assert.Equal(t, expectedCommand, strings.Join(cmd, " "), "cmd") 1049 checkStream(podFullName, uid, containerName, streamOpts) 1050 } 1051 1052 fw.fakeKubelet.getAttachCheck = func(podFullName string, uid types.UID, containerName string, streamOpts remotecommandserver.Options) { 1053 attachInvoked = true 1054 checkStream(podFullName, uid, containerName, streamOpts) 1055 } 1056 1057 testStream := func(containerID string, in io.Reader, out, stderr io.WriteCloser, tty bool, done chan struct{}) error { 1058 close(done) 1059 assert.Equal(t, testContainerID, containerID, "containerID") 1060 assert.Equal(t, test.tty, tty, "tty") 1061 require.Equal(t, test.stdin, in != nil, "in") 1062 require.Equal(t, test.stdout, out != nil, "out") 1063 require.Equal(t, !test.tty && test.stderr, stderr != nil, "err") 1064 1065 if test.stdin { 1066 b := make([]byte, 10) 1067 n, err := in.Read(b) 1068 assert.NoError(t, err, "reading from stdin") 1069 assert.Equal(t, expectedStdin, string(b[0:n]), "content from stdin") 1070 } 1071 1072 if test.stdout { 1073 _, err := out.Write([]byte(expectedStdout)) 1074 assert.NoError(t, err, "writing to stdout") 1075 out.Close() 1076 <-clientStdoutReadDone 1077 } 1078 1079 if !test.tty && test.stderr { 1080 _, err := stderr.Write([]byte(expectedStderr)) 1081 assert.NoError(t, err, "writing to stderr") 1082 stderr.Close() 1083 <-clientStderrReadDone 1084 } 1085 return nil 1086 } 1087 1088 ss.fakeRuntime.execFunc = func(containerID string, cmd []string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error { 1089 assert.Equal(t, expectedCommand, strings.Join(cmd, " "), "cmd") 1090 return testStream(containerID, stdin, stdout, stderr, tty, done) 1091 } 1092 1093 ss.fakeRuntime.attachFunc = func(containerID string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error { 1094 return testStream(containerID, stdin, stdout, stderr, tty, done) 1095 } 1096 1097 var url string 1098 if test.uid { 1099 url = fw.testHTTPServer.URL + "/" + verb + "/" + podNamespace + "/" + podName + "/" + testUID + "/" + expectedContainerName + "?ignore=1" 1100 } else { 1101 url = fw.testHTTPServer.URL + "/" + verb + "/" + podNamespace + "/" + podName + "/" + expectedContainerName + "?ignore=1" 1102 } 1103 if verb == "exec" { 1104 url += "&command=ls&command=-a" 1105 } 1106 if test.stdin { 1107 url += "&" + api.ExecStdinParam + "=1" 1108 } 1109 if test.stdout { 1110 url += "&" + api.ExecStdoutParam + "=1" 1111 } 1112 if test.stderr && !test.tty { 1113 url += "&" + api.ExecStderrParam + "=1" 1114 } 1115 if test.tty { 1116 url += "&" + api.ExecTTYParam + "=1" 1117 } 1118 1119 var ( 1120 resp *http.Response 1121 upgradeRoundTripper httpstream.UpgradeRoundTripper 1122 c *http.Client 1123 ) 1124 upgradeRoundTripper, err = spdy.NewRoundTripper(&tls.Config{}) 1125 if err != nil { 1126 t.Fatalf("Error creating SpdyRoundTripper: %v", err) 1127 } 1128 c = &http.Client{Transport: upgradeRoundTripper} 1129 1130 resp, err = c.Do(makeReq(t, "POST", url, "v4.channel.k8s.io")) 1131 require.NoError(t, err, "POSTing") 1132 defer resp.Body.Close() 1133 1134 _, err = io.ReadAll(resp.Body) 1135 assert.NoError(t, err, "reading response body") 1136 1137 require.Equal(t, test.responseStatusCode, resp.StatusCode, "response status") 1138 if test.responseStatusCode != http.StatusSwitchingProtocols { 1139 return 1140 } 1141 1142 conn, err := upgradeRoundTripper.NewConnection(resp) 1143 require.NoError(t, err, "creating streaming connection") 1144 defer conn.Close() 1145 1146 h := http.Header{} 1147 h.Set(api.StreamType, api.StreamTypeError) 1148 _, err = conn.CreateStream(h) 1149 require.NoError(t, err, "creating error stream") 1150 1151 if test.stdin { 1152 h.Set(api.StreamType, api.StreamTypeStdin) 1153 stream, err := conn.CreateStream(h) 1154 require.NoError(t, err, "creating stdin stream") 1155 _, err = stream.Write([]byte(expectedStdin)) 1156 require.NoError(t, err, "writing to stdin stream") 1157 } 1158 1159 var stdoutStream httpstream.Stream 1160 if test.stdout { 1161 h.Set(api.StreamType, api.StreamTypeStdout) 1162 stdoutStream, err = conn.CreateStream(h) 1163 require.NoError(t, err, "creating stdout stream") 1164 } 1165 1166 var stderrStream httpstream.Stream 1167 if test.stderr && !test.tty { 1168 h.Set(api.StreamType, api.StreamTypeStderr) 1169 stderrStream, err = conn.CreateStream(h) 1170 require.NoError(t, err, "creating stderr stream") 1171 } 1172 1173 if test.stdout { 1174 output := make([]byte, 10) 1175 n, err := stdoutStream.Read(output) 1176 close(clientStdoutReadDone) 1177 assert.NoError(t, err, "reading from stdout stream") 1178 assert.Equal(t, expectedStdout, string(output[0:n]), "stdout") 1179 } 1180 1181 if test.stderr && !test.tty { 1182 output := make([]byte, 10) 1183 n, err := stderrStream.Read(output) 1184 close(clientStderrReadDone) 1185 assert.NoError(t, err, "reading from stderr stream") 1186 assert.Equal(t, expectedStderr, string(output[0:n]), "stderr") 1187 } 1188 1189 // wait for the server to finish before checking if the attach/exec funcs were invoked 1190 <-done 1191 1192 if verb == "exec" { 1193 assert.True(t, execInvoked, "exec should be invoked") 1194 assert.False(t, attachInvoked, "attach should not be invoked") 1195 } else { 1196 assert.True(t, attachInvoked, "attach should be invoked") 1197 assert.False(t, execInvoked, "exec should not be invoked") 1198 } 1199 }) 1200 } 1201 } 1202 1203 func TestServeExecInContainer(t *testing.T) { 1204 testExecAttach(t, "exec") 1205 } 1206 1207 func TestServeAttachContainer(t *testing.T) { 1208 testExecAttach(t, "attach") 1209 } 1210 1211 func TestServePortForwardIdleTimeout(t *testing.T) { 1212 ss, err := newTestStreamingServer(100 * time.Millisecond) 1213 require.NoError(t, err) 1214 defer ss.testHTTPServer.Close() 1215 fw := newServerTestWithDebug(true, ss) 1216 defer fw.testHTTPServer.Close() 1217 1218 podNamespace := "other" 1219 podName := "foo" 1220 1221 url := fw.testHTTPServer.URL + "/portForward/" + podNamespace + "/" + podName 1222 1223 upgradeRoundTripper, err := spdy.NewRoundTripper(&tls.Config{}) 1224 if err != nil { 1225 t.Fatalf("Error creating SpdyRoundTripper: %v", err) 1226 } 1227 c := &http.Client{Transport: upgradeRoundTripper} 1228 1229 req := makeReq(t, "POST", url, "portforward.k8s.io") 1230 resp, err := c.Do(req) 1231 if err != nil { 1232 t.Fatalf("Got error POSTing: %v", err) 1233 } 1234 defer resp.Body.Close() 1235 1236 conn, err := upgradeRoundTripper.NewConnection(resp) 1237 if err != nil { 1238 t.Fatalf("Unexpected error creating streaming connection: %s", err) 1239 } 1240 if conn == nil { 1241 t.Fatal("Unexpected nil connection") 1242 } 1243 defer conn.Close() 1244 1245 <-conn.CloseChan() 1246 } 1247 1248 func TestServePortForward(t *testing.T) { 1249 tests := map[string]struct { 1250 port string 1251 uid bool 1252 clientData string 1253 containerData string 1254 shouldError bool 1255 }{ 1256 "no port": {port: "", shouldError: true}, 1257 "none number port": {port: "abc", shouldError: true}, 1258 "negative port": {port: "-1", shouldError: true}, 1259 "too large port": {port: "65536", shouldError: true}, 1260 "0 port": {port: "0", shouldError: true}, 1261 "min port": {port: "1", shouldError: false}, 1262 "normal port": {port: "8000", shouldError: false}, 1263 "normal port with data forward": {port: "8000", clientData: "client data", containerData: "container data", shouldError: false}, 1264 "max port": {port: "65535", shouldError: false}, 1265 "normal port with uid": {port: "8000", uid: true, shouldError: false}, 1266 } 1267 1268 podNamespace := "other" 1269 podName := "foo" 1270 1271 for desc := range tests { 1272 test := tests[desc] 1273 t.Run(desc, func(t *testing.T) { 1274 ss, err := newTestStreamingServer(0) 1275 require.NoError(t, err) 1276 defer ss.testHTTPServer.Close() 1277 fw := newServerTestWithDebug(true, ss) 1278 defer fw.testHTTPServer.Close() 1279 1280 portForwardFuncDone := make(chan struct{}) 1281 1282 fw.fakeKubelet.getPortForwardCheck = func(name, namespace string, uid types.UID, opts portforward.V4Options) { 1283 assert.Equal(t, podName, name, "pod name") 1284 assert.Equal(t, podNamespace, namespace, "pod namespace") 1285 if test.uid { 1286 assert.Equal(t, testUID, string(uid), "uid") 1287 } 1288 } 1289 1290 ss.fakeRuntime.portForwardFunc = func(podSandboxID string, port int32, stream io.ReadWriteCloser) error { 1291 defer close(portForwardFuncDone) 1292 assert.Equal(t, testPodSandboxID, podSandboxID, "pod sandbox id") 1293 // The port should be valid if it reaches here. 1294 testPort, err := strconv.ParseInt(test.port, 10, 32) 1295 require.NoError(t, err, "parse port") 1296 assert.Equal(t, int32(testPort), port, "port") 1297 1298 if test.clientData != "" { 1299 fromClient := make([]byte, 32) 1300 n, err := stream.Read(fromClient) 1301 assert.NoError(t, err, "reading client data") 1302 assert.Equal(t, test.clientData, string(fromClient[0:n]), "client data") 1303 } 1304 1305 if test.containerData != "" { 1306 _, err := stream.Write([]byte(test.containerData)) 1307 assert.NoError(t, err, "writing container data") 1308 } 1309 1310 return nil 1311 } 1312 1313 var url string 1314 if test.uid { 1315 url = fmt.Sprintf("%s/portForward/%s/%s/%s", fw.testHTTPServer.URL, podNamespace, podName, testUID) 1316 } else { 1317 url = fmt.Sprintf("%s/portForward/%s/%s", fw.testHTTPServer.URL, podNamespace, podName) 1318 } 1319 1320 var ( 1321 upgradeRoundTripper httpstream.UpgradeRoundTripper 1322 c *http.Client 1323 ) 1324 1325 upgradeRoundTripper, err = spdy.NewRoundTripper(&tls.Config{}) 1326 if err != nil { 1327 t.Fatalf("Error creating SpdyRoundTripper: %v", err) 1328 } 1329 c = &http.Client{Transport: upgradeRoundTripper} 1330 1331 req := makeReq(t, "POST", url, "portforward.k8s.io") 1332 resp, err := c.Do(req) 1333 require.NoError(t, err, "POSTing") 1334 defer resp.Body.Close() 1335 1336 assert.Equal(t, http.StatusSwitchingProtocols, resp.StatusCode, "status code") 1337 1338 conn, err := upgradeRoundTripper.NewConnection(resp) 1339 require.NoError(t, err, "creating streaming connection") 1340 defer conn.Close() 1341 1342 headers := http.Header{} 1343 headers.Set("streamType", "error") 1344 headers.Set("port", test.port) 1345 _, err = conn.CreateStream(headers) 1346 assert.Equal(t, test.shouldError, err != nil, "expect error") 1347 1348 if test.shouldError { 1349 return 1350 } 1351 1352 headers.Set("streamType", "data") 1353 headers.Set("port", test.port) 1354 dataStream, err := conn.CreateStream(headers) 1355 require.NoError(t, err, "create stream") 1356 1357 if test.clientData != "" { 1358 _, err := dataStream.Write([]byte(test.clientData)) 1359 assert.NoError(t, err, "writing client data") 1360 } 1361 1362 if test.containerData != "" { 1363 fromContainer := make([]byte, 32) 1364 n, err := dataStream.Read(fromContainer) 1365 assert.NoError(t, err, "reading container data") 1366 assert.Equal(t, test.containerData, string(fromContainer[0:n]), "container data") 1367 } 1368 1369 <-portForwardFuncDone 1370 }) 1371 } 1372 } 1373 1374 func TestMetricBuckets(t *testing.T) { 1375 tests := map[string]struct { 1376 url string 1377 bucket string 1378 }{ 1379 "healthz endpoint": {url: "/healthz", bucket: "healthz"}, 1380 "attach": {url: "/attach/podNamespace/podID/containerName", bucket: "attach"}, 1381 "attach with uid": {url: "/attach/podNamespace/podID/uid/containerName", bucket: "attach"}, 1382 "configz": {url: "/configz", bucket: "configz"}, 1383 "containerLogs": {url: "/containerLogs/podNamespace/podID/containerName", bucket: "containerLogs"}, 1384 "debug v flags": {url: "/debug/flags/v", bucket: "debug"}, 1385 "pprof with sub": {url: "/debug/pprof/subpath", bucket: "debug"}, 1386 "exec": {url: "/exec/podNamespace/podID/containerName", bucket: "exec"}, 1387 "exec with uid": {url: "/exec/podNamespace/podID/uid/containerName", bucket: "exec"}, 1388 "healthz": {url: "/healthz/", bucket: "healthz"}, 1389 "healthz log sub": {url: "/healthz/log", bucket: "healthz"}, 1390 "healthz ping": {url: "/healthz/ping", bucket: "healthz"}, 1391 "healthz sync loop": {url: "/healthz/syncloop", bucket: "healthz"}, 1392 "logs": {url: "/logs/", bucket: "logs"}, 1393 "logs with path": {url: "/logs/logpath", bucket: "logs"}, 1394 "metrics": {url: "/metrics", bucket: "metrics"}, 1395 "metrics cadvisor sub": {url: "/metrics/cadvisor", bucket: "metrics/cadvisor"}, 1396 "metrics probes sub": {url: "/metrics/probes", bucket: "metrics/probes"}, 1397 "metrics resource sub": {url: "/metrics/resource", bucket: "metrics/resource"}, 1398 "pods": {url: "/pods/", bucket: "pods"}, 1399 "portForward": {url: "/portForward/podNamespace/podID", bucket: "portForward"}, 1400 "portForward with uid": {url: "/portForward/podNamespace/podID/uid", bucket: "portForward"}, 1401 "run": {url: "/run/podNamespace/podID/containerName", bucket: "run"}, 1402 "run with uid": {url: "/run/podNamespace/podID/uid/containerName", bucket: "run"}, 1403 "runningpods": {url: "/runningpods/", bucket: "runningpods"}, 1404 "stats": {url: "/stats/", bucket: "stats"}, 1405 "stats summary sub": {url: "/stats/summary", bucket: "stats"}, 1406 "invalid path": {url: "/junk", bucket: "other"}, 1407 "invalid path starting with good": {url: "/healthzjunk", bucket: "other"}, 1408 } 1409 fw := newServerTest() 1410 defer fw.testHTTPServer.Close() 1411 1412 for _, test := range tests { 1413 path := test.url 1414 bucket := test.bucket 1415 require.Equal(t, fw.serverUnderTest.getMetricBucket(path), bucket) 1416 } 1417 } 1418 1419 func TestMetricMethodBuckets(t *testing.T) { 1420 tests := map[string]struct { 1421 method string 1422 bucket string 1423 }{ 1424 "normal GET": {method: "GET", bucket: "GET"}, 1425 "normal POST": {method: "POST", bucket: "POST"}, 1426 "invalid method": {method: "WEIRD", bucket: "other"}, 1427 } 1428 1429 fw := newServerTest() 1430 defer fw.testHTTPServer.Close() 1431 1432 for _, test := range tests { 1433 method := test.method 1434 bucket := test.bucket 1435 require.Equal(t, fw.serverUnderTest.getMetricMethodBucket(method), bucket) 1436 } 1437 } 1438 1439 func TestDebuggingDisabledHandlers(t *testing.T) { 1440 // for backward compatibility even if enablesystemLogHandler or enableProfilingHandler is set but not 1441 // enableDebuggingHandler then /logs, /pprof and /flags shouldn't be served. 1442 kubeCfg := &kubeletconfiginternal.KubeletConfiguration{ 1443 EnableDebuggingHandlers: false, 1444 EnableSystemLogHandler: true, 1445 EnableDebugFlagsHandler: true, 1446 EnableProfilingHandler: true, 1447 } 1448 fw := newServerTestWithDebuggingHandlers(kubeCfg, nil) 1449 defer fw.testHTTPServer.Close() 1450 1451 paths := []string{ 1452 "/run", "/exec", "/attach", "/portForward", "/containerLogs", "/runningpods", 1453 "/run/", "/exec/", "/attach/", "/portForward/", "/containerLogs/", "/runningpods/", 1454 "/run/xxx", "/exec/xxx", "/attach/xxx", "/debug/pprof/profile", "/logs/kubelet.log", 1455 } 1456 1457 for _, p := range paths { 1458 verifyEndpointResponse(t, fw, p, "Debug endpoints are disabled.\n") 1459 } 1460 } 1461 1462 func TestDisablingLogAndProfilingHandler(t *testing.T) { 1463 kubeCfg := &kubeletconfiginternal.KubeletConfiguration{ 1464 EnableDebuggingHandlers: true, 1465 } 1466 fw := newServerTestWithDebuggingHandlers(kubeCfg, nil) 1467 defer fw.testHTTPServer.Close() 1468 1469 // verify debug endpoints are disabled 1470 verifyEndpointResponse(t, fw, "/logs/kubelet.log", "logs endpoint is disabled.\n") 1471 verifyEndpointResponse(t, fw, "/debug/pprof/profile?seconds=2", "profiling endpoint is disabled.\n") 1472 verifyEndpointResponse(t, fw, "/debug/flags/v", "flags endpoint is disabled.\n") 1473 } 1474 1475 func TestFailedParseParamsSummaryHandler(t *testing.T) { 1476 fw := newServerTest() 1477 defer fw.testHTTPServer.Close() 1478 1479 resp, err := http.Post(fw.testHTTPServer.URL+"/stats/summary", "invalid/content/type", nil) 1480 assert.NoError(t, err) 1481 defer resp.Body.Close() 1482 v, err := io.ReadAll(resp.Body) 1483 assert.NoError(t, err) 1484 assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) 1485 assert.Contains(t, string(v), "parse form failed") 1486 } 1487 1488 func verifyEndpointResponse(t *testing.T, fw *serverTestFramework, path string, expectedResponse string) { 1489 resp, err := http.Get(fw.testHTTPServer.URL + path) 1490 require.NoError(t, err) 1491 assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode) 1492 body, err := io.ReadAll(resp.Body) 1493 require.NoError(t, err) 1494 assert.Equal(t, expectedResponse, string(body)) 1495 1496 resp, err = http.Post(fw.testHTTPServer.URL+path, "", nil) 1497 require.NoError(t, err) 1498 assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode) 1499 body, err = io.ReadAll(resp.Body) 1500 require.NoError(t, err) 1501 assert.Equal(t, expectedResponse, string(body)) 1502 } 1503 1504 func TestTrimURLPath(t *testing.T) { 1505 tests := []struct { 1506 path, expected string 1507 }{ 1508 {"", ""}, 1509 {"//", ""}, 1510 {"/pods", "pods"}, 1511 {"pods", "pods"}, 1512 {"pods/", "pods"}, 1513 {"good/", "good"}, 1514 {"pods/probes", "pods"}, 1515 {"metrics", "metrics"}, 1516 {"metrics/resource", "metrics/resource"}, 1517 {"metrics/hello", "metrics/hello"}, 1518 } 1519 1520 for _, test := range tests { 1521 assert.Equal(t, test.expected, getURLRootPath(test.path), fmt.Sprintf("path is: %s", test.path)) 1522 } 1523 }