kubernetes/pkg/kubelet/server/server_test.go

1990 lines
66 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 server
import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"net/http/httputil"
"net/url"
"reflect"
"strconv"
"strings"
"testing"
"time"
cadvisorapi "github.com/google/cadvisor/info/v1"
cadvisorapiv2 "github.com/google/cadvisor/info/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/utils/ptr"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/apimachinery/pkg/util/httpstream/spdy"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/tools/remotecommand"
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
statsapi "k8s.io/kubelet/pkg/apis/stats/v1alpha1"
api "k8s.io/kubernetes/pkg/apis/core"
// Do some initialization to decode the query parameters correctly.
"k8s.io/apiserver/pkg/server/healthz"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
zpagesfeatures "k8s.io/component-base/zpages/features"
"k8s.io/component-base/zpages/flagz"
"k8s.io/kubelet/pkg/cri/streaming"
"k8s.io/kubelet/pkg/cri/streaming/portforward"
remotecommandserver "k8s.io/kubelet/pkg/cri/streaming/remotecommand"
_ "k8s.io/kubernetes/pkg/apis/core/install"
"k8s.io/kubernetes/pkg/features"
kubeletconfiginternal "k8s.io/kubernetes/pkg/kubelet/apis/config"
"k8s.io/kubernetes/pkg/kubelet/cm"
"k8s.io/kubernetes/pkg/kubelet/server/stats"
"k8s.io/kubernetes/pkg/volume"
)
const (
testUID = "9b01b80f-8fb4-11e4-95ab-4200af06647"
testContainerID = "container789"
testPodSandboxID = "pod0987"
)
type fakeKubelet struct {
podByNameFunc func(namespace, name string) (*v1.Pod, bool)
machineInfoFunc func() (*cadvisorapi.MachineInfo, error)
podsFunc func() []*v1.Pod
runningPodsFunc func(ctx context.Context) ([]*v1.Pod, error)
logFunc func(w http.ResponseWriter, req *http.Request)
runFunc func(podFullName string, uid types.UID, containerName string, cmd []string) ([]byte, error)
getExecCheck func(string, types.UID, string, []string, remotecommandserver.Options)
getAttachCheck func(string, types.UID, string, remotecommandserver.Options)
getPortForwardCheck func(string, string, types.UID, portforward.V4Options)
containerLogsFunc func(ctx context.Context, podFullName, containerName string, logOptions *v1.PodLogOptions, stdout, stderr io.Writer) error
hostnameFunc func() string
resyncInterval time.Duration
loopEntryTime time.Time
plegHealth bool
streamingRuntime streaming.Server
}
func (fk *fakeKubelet) LatestLoopEntryTime() time.Time {
return fk.loopEntryTime
}
func (fk *fakeKubelet) GetPodByName(namespace, name string) (*v1.Pod, bool) {
return fk.podByNameFunc(namespace, name)
}
func (fk *fakeKubelet) GetRequestedContainersInfo(containerName string, options cadvisorapiv2.RequestOptions) (map[string]*cadvisorapi.ContainerInfo, error) {
return map[string]*cadvisorapi.ContainerInfo{}, nil
}
func (fk *fakeKubelet) GetCachedMachineInfo() (*cadvisorapi.MachineInfo, error) {
return fk.machineInfoFunc()
}
func (*fakeKubelet) GetVersionInfo() (*cadvisorapi.VersionInfo, error) {
return &cadvisorapi.VersionInfo{}, nil
}
func (fk *fakeKubelet) GetPods() []*v1.Pod {
return fk.podsFunc()
}
func (fk *fakeKubelet) GetRunningPods(ctx context.Context) ([]*v1.Pod, error) {
return fk.runningPodsFunc(ctx)
}
func (fk *fakeKubelet) ServeLogs(w http.ResponseWriter, req *http.Request) {
fk.logFunc(w, req)
}
func (fk *fakeKubelet) GetKubeletContainerLogs(ctx context.Context, podFullName, containerName string, logOptions *v1.PodLogOptions, stdout, stderr io.Writer) error {
return fk.containerLogsFunc(ctx, podFullName, containerName, logOptions, stdout, stderr)
}
func (fk *fakeKubelet) GetHostname() string {
return fk.hostnameFunc()
}
func (fk *fakeKubelet) RunInContainer(_ context.Context, podFullName string, uid types.UID, containerName string, cmd []string) ([]byte, error) {
return fk.runFunc(podFullName, uid, containerName, cmd)
}
func (fk *fakeKubelet) CheckpointContainer(_ context.Context, podUID types.UID, podFullName, containerName string, options *runtimeapi.CheckpointContainerRequest) error {
if containerName == "checkpointingFailure" {
return fmt.Errorf("Returning error for test")
}
return nil
}
func (fk *fakeKubelet) ListMetricDescriptors(ctx context.Context) ([]*runtimeapi.MetricDescriptor, error) {
return nil, nil
}
func (fk *fakeKubelet) ListPodSandboxMetrics(ctx context.Context) ([]*runtimeapi.PodSandboxMetrics, error) {
return nil, nil
}
func (fk *fakeKubelet) SyncLoopHealthCheck(req *http.Request) error {
duration := fk.resyncInterval * 2
minDuration := time.Minute * 5
if duration < minDuration {
duration = minDuration
}
enterLoopTime := fk.LatestLoopEntryTime()
if !enterLoopTime.IsZero() && time.Now().After(enterLoopTime.Add(duration)) {
return fmt.Errorf("sync Loop took longer than expected")
}
return nil
}
type fakeRuntime struct {
execFunc func(string, []string, io.Reader, io.WriteCloser, io.WriteCloser, bool, <-chan remotecommand.TerminalSize) error
attachFunc func(string, io.Reader, io.WriteCloser, io.WriteCloser, bool, <-chan remotecommand.TerminalSize) error
portForwardFunc func(string, int32, io.ReadWriteCloser) error
}
func (f *fakeRuntime) Exec(_ context.Context, containerID string, cmd []string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error {
return f.execFunc(containerID, cmd, stdin, stdout, stderr, tty, resize)
}
func (f *fakeRuntime) Attach(_ context.Context, containerID string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error {
return f.attachFunc(containerID, stdin, stdout, stderr, tty, resize)
}
func (f *fakeRuntime) PortForward(_ context.Context, podSandboxID string, port int32, stream io.ReadWriteCloser) error {
return f.portForwardFunc(podSandboxID, port, stream)
}
type testStreamingServer struct {
streaming.Server
fakeRuntime *fakeRuntime
testHTTPServer *httptest.Server
}
func newTestStreamingServer(streamIdleTimeout time.Duration) (s *testStreamingServer, err error) {
s = &testStreamingServer{}
s.testHTTPServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
s.ServeHTTP(w, r)
}))
defer func() {
if err != nil {
s.testHTTPServer.Close()
}
}()
testURL, err := url.Parse(s.testHTTPServer.URL)
if err != nil {
return nil, err
}
s.fakeRuntime = &fakeRuntime{}
config := streaming.DefaultConfig
config.BaseURL = testURL
if streamIdleTimeout != 0 {
config.StreamIdleTimeout = streamIdleTimeout
}
s.Server, err = streaming.NewServer(config, s.fakeRuntime)
if err != nil {
return nil, err
}
return s, nil
}
func (fk *fakeKubelet) GetExec(_ context.Context, podFullName string, podUID types.UID, containerName string, cmd []string, streamOpts remotecommandserver.Options) (*url.URL, error) {
if fk.getExecCheck != nil {
fk.getExecCheck(podFullName, podUID, containerName, cmd, streamOpts)
}
// Always use testContainerID
resp, err := fk.streamingRuntime.GetExec(&runtimeapi.ExecRequest{
ContainerId: testContainerID,
Cmd: cmd,
Tty: streamOpts.TTY,
Stdin: streamOpts.Stdin,
Stdout: streamOpts.Stdout,
Stderr: streamOpts.Stderr,
})
if err != nil {
return nil, err
}
return url.Parse(resp.GetUrl())
}
func (fk *fakeKubelet) GetAttach(_ context.Context, podFullName string, podUID types.UID, containerName string, streamOpts remotecommandserver.Options) (*url.URL, error) {
if fk.getAttachCheck != nil {
fk.getAttachCheck(podFullName, podUID, containerName, streamOpts)
}
// Always use testContainerID
resp, err := fk.streamingRuntime.GetAttach(&runtimeapi.AttachRequest{
ContainerId: testContainerID,
Tty: streamOpts.TTY,
Stdin: streamOpts.Stdin,
Stdout: streamOpts.Stdout,
Stderr: streamOpts.Stderr,
})
if err != nil {
return nil, err
}
return url.Parse(resp.GetUrl())
}
func (fk *fakeKubelet) GetPortForward(ctx context.Context, podName, podNamespace string, podUID types.UID, portForwardOpts portforward.V4Options) (*url.URL, error) {
if fk.getPortForwardCheck != nil {
fk.getPortForwardCheck(podName, podNamespace, podUID, portForwardOpts)
}
// Always use testPodSandboxID
resp, err := fk.streamingRuntime.GetPortForward(&runtimeapi.PortForwardRequest{
PodSandboxId: testPodSandboxID,
Port: portForwardOpts.Ports,
})
if err != nil {
return nil, err
}
return url.Parse(resp.GetUrl())
}
func (fk *fakeKubelet) GetPortForward(ctx context.Context, podName, podNamespace string, podUID types.UID, portForwardOpts portforward.V4Options) (*url.URL, error) {
if fk.getPortForwardCheck != nil {
fk.getPortForwardCheck(podName, podNamespace, podUID, portForwardOpts)
}
// Always use testPodSandboxID
resp, err := fk.streamingRuntime.GetPortForward(&runtimeapi.PortForwardRequest{
PodSandboxId: testPodSandboxID,
Port: portForwardOpts.Ports,
})
if err != nil {
return nil, err
}
return url.Parse(resp.GetUrl())
}
// Unused functions
func (*fakeKubelet) GetNode() (*v1.Node, error) { return nil, nil }
func (*fakeKubelet) GetNodeConfig() cm.NodeConfig { return cm.NodeConfig{} }
func (*fakeKubelet) GetPodCgroupRoot() string { return "" }
func (*fakeKubelet) GetPodByCgroupfs(cgroupfs string) (*v1.Pod, bool) { return nil, false }
func (fk *fakeKubelet) ListVolumesForPod(podUID types.UID) (map[string]volume.Volume, bool) {
return map[string]volume.Volume{}, true
}
func (*fakeKubelet) ListBlockVolumesForPod(podUID types.UID) (map[string]volume.BlockVolume, bool) {
return map[string]volume.BlockVolume{}, true
}
func (*fakeKubelet) RootFsStats() (*statsapi.FsStats, error) { return nil, nil }
func (*fakeKubelet) ListPodStats(_ context.Context) ([]statsapi.PodStats, error) { return nil, nil }
func (*fakeKubelet) ListPodStatsAndUpdateCPUNanoCoreUsage(_ context.Context) ([]statsapi.PodStats, error) {
return nil, nil
}
func (*fakeKubelet) ListPodCPUAndMemoryStats(_ context.Context) ([]statsapi.PodStats, error) {
return nil, nil
}
func (*fakeKubelet) ImageFsStats(_ context.Context) (*statsapi.FsStats, *statsapi.FsStats, error) {
return nil, nil, nil
}
func (*fakeKubelet) RlimitStats() (*statsapi.RlimitStats, error) { return nil, nil }
func (*fakeKubelet) GetCgroupStats(cgroupName string, updateStats bool) (*statsapi.ContainerStats, *statsapi.NetworkStats, error) {
return nil, nil, nil
}
func (*fakeKubelet) GetCgroupCPUAndMemoryStats(cgroupName string, updateStats bool) (*statsapi.ContainerStats, error) {
return nil, nil
}
type fakeAuth struct {
authenticateFunc func(*http.Request) (*authenticator.Response, bool, error)
attributesFunc func(user.Info, *http.Request) []authorizer.Attributes
authorizeFunc func(authorizer.Attributes) (authorized authorizer.Decision, reason string, err error)
}
func (f *fakeAuth) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
return f.authenticateFunc(req)
}
func (f *fakeAuth) GetRequestAttributes(u user.Info, req *http.Request) []authorizer.Attributes {
return f.attributesFunc(u, req)
}
func (f *fakeAuth) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
return f.authorizeFunc(a)
}
type serverTestFramework struct {
serverUnderTest *Server
fakeKubelet *fakeKubelet
fakeAuth *fakeAuth
testHTTPServer *httptest.Server
}
func newServerTest() *serverTestFramework {
return newServerTestWithDebug(true, nil)
}
func newServerTestWithDebug(enableDebugging bool, streamingServer streaming.Server) *serverTestFramework {
kubeCfg := &kubeletconfiginternal.KubeletConfiguration{
EnableDebuggingHandlers: enableDebugging,
EnableSystemLogHandler: enableDebugging,
EnableProfilingHandler: enableDebugging,
EnableDebugFlagsHandler: enableDebugging,
}
return newServerTestWithDebuggingHandlers(kubeCfg, streamingServer)
}
func newServerTestWithDebuggingHandlers(kubeCfg *kubeletconfiginternal.KubeletConfiguration, streamingServer streaming.Server) *serverTestFramework {
fw := &serverTestFramework{}
fw.fakeKubelet = &fakeKubelet{
hostnameFunc: func() string {
return "127.0.0.1"
},
podByNameFunc: func(namespace, name string) (*v1.Pod, bool) {
return &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: name,
UID: testUID,
},
}, true
},
plegHealth: true,
streamingRuntime: streamingServer,
}
fw.fakeAuth = &fakeAuth{
authenticateFunc: func(req *http.Request) (*authenticator.Response, bool, error) {
return &authenticator.Response{User: &user.DefaultInfo{Name: "test"}}, true, nil
},
attributesFunc: func(u user.Info, req *http.Request) []authorizer.Attributes {
return []authorizer.Attributes{&authorizer.AttributesRecord{User: u}}
},
authorizeFunc: func(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
return authorizer.DecisionAllow, "", nil
},
}
server := NewServer(
fw.fakeKubelet,
stats.NewResourceAnalyzer(fw.fakeKubelet, time.Minute, &record.FakeRecorder{}),
[]healthz.HealthChecker{},
flagz.NamedFlagSetsReader{},
fw.fakeAuth,
kubeCfg,
)
fw.serverUnderTest = &server
fw.testHTTPServer = httptest.NewServer(fw.serverUnderTest)
return fw
}
// A helper function to return the correct pod name.
func getPodName(name, namespace string) string {
if namespace == "" {
namespace = metav1.NamespaceDefault
}
return name + "_" + namespace
}
func TestServeLogs(t *testing.T) {
fw := newServerTest()
defer fw.testHTTPServer.Close()
content := string(`<pre><a href="kubelet.log">kubelet.log</a><a href="google.log">google.log</a></pre>`)
fw.fakeKubelet.logFunc = func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusOK)
w.Header().Add("Content-Type", "text/html")
w.Write([]byte(content))
}
resp, err := http.Get(fw.testHTTPServer.URL + "/logs/")
if err != nil {
t.Fatalf("Got error GETing: %v", err)
}
defer resp.Body.Close()
body, err := httputil.DumpResponse(resp, true)
if err != nil {
// copying the response body did not work
t.Errorf("Cannot copy resp: %#v", err)
}
result := string(body)
if !strings.Contains(result, "kubelet.log") || !strings.Contains(result, "google.log") {
t.Errorf("Received wrong data: %s", result)
}
}
func TestServeRunInContainer(t *testing.T) {
fw := newServerTest()
defer fw.testHTTPServer.Close()
output := "foo bar"
podNamespace := "other"
podName := "foo"
expectedPodName := getPodName(podName, podNamespace)
expectedContainerName := "baz"
expectedCommand := "ls -a"
fw.fakeKubelet.runFunc = func(podFullName string, uid types.UID, containerName string, cmd []string) ([]byte, error) {
if podFullName != expectedPodName {
t.Errorf("expected %s, got %s", expectedPodName, podFullName)
}
if containerName != expectedContainerName {
t.Errorf("expected %s, got %s", expectedContainerName, containerName)
}
if strings.Join(cmd, " ") != expectedCommand {
t.Errorf("expected: %s, got %v", expectedCommand, cmd)
}
return []byte(output), nil
}
resp, err := http.Post(fw.testHTTPServer.URL+"/run/"+podNamespace+"/"+podName+"/"+expectedContainerName+"?cmd=ls%20-a", "", nil)
if err != nil {
t.Fatalf("Got error POSTing: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
// copying the response body did not work
t.Errorf("Cannot copy resp: %#v", err)
}
result := string(body)
if result != output {
t.Errorf("expected %s, got %s", output, result)
}
}
func TestServeRunInContainerWithUID(t *testing.T) {
fw := newServerTest()
defer fw.testHTTPServer.Close()
output := "foo bar"
podNamespace := "other"
podName := "foo"
expectedPodName := getPodName(podName, podNamespace)
expectedContainerName := "baz"
expectedCommand := "ls -a"
fw.fakeKubelet.runFunc = func(podFullName string, uid types.UID, containerName string, cmd []string) ([]byte, error) {
if podFullName != expectedPodName {
t.Errorf("expected %s, got %s", expectedPodName, podFullName)
}
if string(uid) != testUID {
t.Errorf("expected %s, got %s", testUID, uid)
}
if containerName != expectedContainerName {
t.Errorf("expected %s, got %s", expectedContainerName, containerName)
}
if strings.Join(cmd, " ") != expectedCommand {
t.Errorf("expected: %s, got %v", expectedCommand, cmd)
}
return []byte(output), nil
}
resp, err := http.Post(fw.testHTTPServer.URL+"/run/"+podNamespace+"/"+podName+"/"+testUID+"/"+expectedContainerName+"?cmd=ls%20-a", "", nil)
if err != nil {
t.Fatalf("Got error POSTing: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
// copying the response body did not work
t.Errorf("Cannot copy resp: %#v", err)
}
result := string(body)
if result != output {
t.Errorf("expected %s, got %s", output, result)
}
}
func TestHealthCheck(t *testing.T) {
fw := newServerTest()
defer fw.testHTTPServer.Close()
fw.fakeKubelet.hostnameFunc = func() string {
return "127.0.0.1"
}
// Test with correct hostname, Docker version
assertHealthIsOk(t, fw.testHTTPServer.URL+"/healthz")
// Test with incorrect hostname
fw.fakeKubelet.hostnameFunc = func() string {
return "fake"
}
assertHealthIsOk(t, fw.testHTTPServer.URL+"/healthz")
}
func assertHealthFails(t *testing.T, httpURL string, expectedErrorCode int) {
resp, err := http.Get(httpURL)
if err != nil {
t.Fatalf("Got error GETing: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != expectedErrorCode {
t.Errorf("expected status code %d, got %d", expectedErrorCode, resp.StatusCode)
}
}
// Ensure all registered handlers & services have an associated testcase.
func TestAuthzCoverage(t *testing.T) {
fw := newServerTest()
defer fw.testHTTPServer.Close()
for _, fineGrained := range []bool{false, true} {
t.Run(fmt.Sprintf("fineGrained=%v", fineGrained), func(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KubeletFineGrainedAuthz, fineGrained)
// method:path -> has coverage
expectedCases := map[string]bool{}
// Test all the non-web-service handlers
for _, path := range fw.serverUnderTest.restfulCont.RegisteredHandlePaths() {
expectedCases["GET:"+path] = false
expectedCases["POST:"+path] = false
}
// Test all the generated web-service paths
for _, ws := range fw.serverUnderTest.restfulCont.RegisteredWebServices() {
for _, r := range ws.Routes() {
expectedCases[r.Method+":"+r.Path] = false
}
}
// This is a sanity check that the Handle->HandleWithFilter() delegation is working
// Ideally, these would move to registered web services and this list would get shorter
expectedPaths := []string{"/healthz", "/metrics", "/metrics/cadvisor"}
for _, expectedPath := range expectedPaths {
if _, expected := expectedCases["GET:"+expectedPath]; !expected {
t.Errorf("Expected registered handle path %s was missing", expectedPath)
}
}
for _, tc := range AuthzTestCases(fineGrained) {
expectedCases[tc.Method+":"+tc.Path] = true
}
for tc, found := range expectedCases {
if !found {
t.Errorf("Missing authz test case for %s", tc)
}
}
})
}
}
func TestInstallAuthNotRequiredHandlers(t *testing.T) {
fw := newServerTestWithDebug(false, nil)
defer fw.testHTTPServer.Close()
// No new handlers should be added to this list.
allowedAuthNotRequiredHandlers := sets.NewString(
"/healthz",
"/healthz/log",
"/healthz/ping",
"/healthz/syncloop",
"/metrics",
"/metrics/slis",
"/metrics/cadvisor",
"/metrics/probes",
"/metrics/resource",
"/pods/",
"/stats/",
"/stats/summary",
)
// These handlers are explicitly disabled.
debuggingDisabledHandlers := sets.NewString(
"/run/",
"/exec/",
"/attach/",
"/portForward/",
"/containerLogs/",
"/runningpods/",
"/debug/pprof/",
"/logs/",
)
allowedAuthNotRequiredHandlers.Insert(debuggingDisabledHandlers.UnsortedList()...)
// Test all the non-web-service handlers
for _, path := range fw.serverUnderTest.restfulCont.RegisteredHandlePaths() {
if !allowedAuthNotRequiredHandlers.Has(path) {
t.Errorf("New handler %q must require auth", path)
}
}
// Test all the generated web-service paths
for _, ws := range fw.serverUnderTest.restfulCont.RegisteredWebServices() {
for _, r := range ws.Routes() {
if !allowedAuthNotRequiredHandlers.Has(r.Path) {
t.Errorf("New handler %q must require auth", r.Path)
}
}
}
// Ensure the disabled handlers are in fact disabled.
for path := range debuggingDisabledHandlers {
for _, method := range []string{"GET", "POST", "PUT", "PATCH", "DELETE"} {
t.Run(method+":"+path, func(t *testing.T) {
req, err := http.NewRequest(method, fw.testHTTPServer.URL+path, nil)
require.NoError(t, err)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close() //nolint:errcheck
assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode)
})
}
}
}
func TestAuthFilters(t *testing.T) {
// Enable features.ContainerCheckpoint during test
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ContainerCheckpoint, true)
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, zpagesfeatures.ComponentStatusz, true)
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, zpagesfeatures.ComponentFlagz, true)
fw := newServerTest()
defer fw.testHTTPServer.Close()
attributesGetter := NewNodeAuthorizerAttributesGetter(authzTestNodeName)
for _, fineGraned := range []bool{false, true} {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KubeletFineGrainedAuthz, fineGraned)
for _, tc := range AuthzTestCases(fineGraned) {
t.Run(fmt.Sprintf("method=%v:path=%v:fineGrained=%v", tc.Method, tc.Method, fineGraned), func(t *testing.T) {
var (
expectedUser = AuthzTestUser()
calledAuthenticate = false
calledAuthorize = false
calledAttributes = false
)
fw.fakeAuth.authenticateFunc = func(req *http.Request) (*authenticator.Response, bool, error) {
calledAuthenticate = true
return &authenticator.Response{User: expectedUser}, true, nil
}
fw.fakeAuth.attributesFunc = func(u user.Info, req *http.Request) []authorizer.Attributes {
calledAttributes = true
require.Equal(t, expectedUser, u)
attrs := attributesGetter.GetRequestAttributes(u, req)
tc.AssertAttributes(t, attrs)
return attrs
}
fw.fakeAuth.authorizeFunc = func(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
calledAuthorize = true
return authorizer.DecisionNoOpinion, "", nil
}
req, err := http.NewRequest(tc.Method, fw.testHTTPServer.URL+tc.Path, nil)
require.NoError(t, err)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close() //nolint:errcheck
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
assert.True(t, calledAuthenticate, "Authenticate was not called")
assert.True(t, calledAttributes, "Attributes were not called")
assert.True(t, calledAuthorize, "Authorize was not called")
})
}
}
}
func TestAuthenticationError(t *testing.T) {
var (
expectedUser = &user.DefaultInfo{Name: "test"}
expectedAttributes = []authorizer.Attributes{&authorizer.AttributesRecord{User: expectedUser}}
calledAuthenticate = false
calledAuthorize = false
calledAttributes = false
)
fw := newServerTest()
defer fw.testHTTPServer.Close()
fw.fakeAuth.authenticateFunc = func(req *http.Request) (*authenticator.Response, bool, error) {
calledAuthenticate = true
return &authenticator.Response{User: expectedUser}, true, nil
}
fw.fakeAuth.attributesFunc = func(u user.Info, req *http.Request) []authorizer.Attributes {
calledAttributes = true
return expectedAttributes
}
fw.fakeAuth.authorizeFunc = func(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
calledAuthorize = true
return authorizer.DecisionNoOpinion, "", errors.New("Failed")
}
assertHealthFails(t, fw.testHTTPServer.URL+"/healthz", http.StatusInternalServerError)
if !calledAuthenticate {
t.Fatalf("Authenticate was not called")
}
if !calledAttributes {
t.Fatalf("Attributes was not called")
}
if !calledAuthorize {
t.Fatalf("Authorize was not called")
}
}
func TestAuthenticationFailure(t *testing.T) {
var (
expectedUser = &user.DefaultInfo{Name: "test"}
expectedAttributes = []authorizer.Attributes{&authorizer.AttributesRecord{User: expectedUser}}
calledAuthenticate = false
calledAuthorize = false
calledAttributes = false
)
fw := newServerTest()
defer fw.testHTTPServer.Close()
fw.fakeAuth.authenticateFunc = func(req *http.Request) (*authenticator.Response, bool, error) {
calledAuthenticate = true
return nil, false, nil
}
fw.fakeAuth.attributesFunc = func(u user.Info, req *http.Request) []authorizer.Attributes {
calledAttributes = true
return expectedAttributes
}
fw.fakeAuth.authorizeFunc = func(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
calledAuthorize = true
return authorizer.DecisionNoOpinion, "", nil
}
assertHealthFails(t, fw.testHTTPServer.URL+"/healthz", http.StatusUnauthorized)
if !calledAuthenticate {
t.Fatalf("Authenticate was not called")
}
if calledAttributes {
t.Fatalf("Attributes was called unexpectedly")
}
if calledAuthorize {
t.Fatalf("Authorize was called unexpectedly")
}
}
func TestAuthorizationSuccess(t *testing.T) {
var (
expectedUser = &user.DefaultInfo{Name: "test"}
expectedAttributes = []authorizer.Attributes{&authorizer.AttributesRecord{User: expectedUser}}
calledAuthenticate = false
calledAuthorize = false
calledAttributes = false
)
fw := newServerTest()
defer fw.testHTTPServer.Close()
fw.fakeAuth.authenticateFunc = func(req *http.Request) (*authenticator.Response, bool, error) {
calledAuthenticate = true
return &authenticator.Response{User: expectedUser}, true, nil
}
fw.fakeAuth.attributesFunc = func(u user.Info, req *http.Request) []authorizer.Attributes {
calledAttributes = true
return expectedAttributes
}
fw.fakeAuth.authorizeFunc = func(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
calledAuthorize = true
return authorizer.DecisionAllow, "", nil
}
assertHealthIsOk(t, fw.testHTTPServer.URL+"/healthz")
if !calledAuthenticate {
t.Fatalf("Authenticate was not called")
}
if !calledAttributes {
t.Fatalf("Attributes were not called")
}
if !calledAuthorize {
t.Fatalf("Authorize was not called")
}
}
func TestSyncLoopCheck(t *testing.T) {
fw := newServerTest()
defer fw.testHTTPServer.Close()
fw.fakeKubelet.hostnameFunc = func() string {
return "127.0.0.1"
}
fw.fakeKubelet.resyncInterval = time.Minute
fw.fakeKubelet.loopEntryTime = time.Now()
// Test with correct hostname, Docker version
assertHealthIsOk(t, fw.testHTTPServer.URL+"/healthz")
fw.fakeKubelet.loopEntryTime = time.Now().Add(time.Minute * -10)
assertHealthFails(t, fw.testHTTPServer.URL+"/healthz", http.StatusInternalServerError)
}
// returns http response status code from the HTTP GET
func assertHealthIsOk(t *testing.T, httpURL string) {
resp, err := http.Get(httpURL)
if err != nil {
t.Fatalf("Got error GETing: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("expected status code %d, got %d", http.StatusOK, resp.StatusCode)
}
body, readErr := io.ReadAll(resp.Body)
if readErr != nil {
// copying the response body did not work
t.Fatalf("Cannot copy resp: %#v", readErr)
}
result := string(body)
if !strings.Contains(result, "ok") {
t.Errorf("expected body contains ok, got %s", result)
}
}
func setPodByNameFunc(fw *serverTestFramework, namespace, pod, container string) {
fw.fakeKubelet.podByNameFunc = func(namespace, name string) (*v1.Pod, bool) {
return &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: pod,
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: container,
},
},
},
}, true
}
}
func setGetContainerLogsFunc(fw *serverTestFramework, t *testing.T, expectedPodName, expectedContainerName string, expectedLogOptions *v1.PodLogOptions, output string) {
fw.fakeKubelet.containerLogsFunc = func(_ context.Context, podFullName, containerName string, logOptions *v1.PodLogOptions, stdout, stderr io.Writer) error {
if podFullName != expectedPodName {
t.Errorf("expected %s, got %s", expectedPodName, podFullName)
}
if containerName != expectedContainerName {
t.Errorf("expected %s, got %s", expectedContainerName, containerName)
}
if !reflect.DeepEqual(expectedLogOptions, logOptions) {
t.Errorf("expected %#v, got %#v", expectedLogOptions, logOptions)
}
io.WriteString(stdout, output)
return nil
}
}
func TestContainerLogs(t *testing.T) {
fw := newServerTest()
defer fw.testHTTPServer.Close()
tests := map[string]struct {
query string
podLogOption *v1.PodLogOptions
}{
"without tail": {"", &v1.PodLogOptions{}},
"with tail": {"?tailLines=5", &v1.PodLogOptions{TailLines: ptr.To[int64](5)}},
"with legacy tail": {"?tail=5", &v1.PodLogOptions{TailLines: ptr.To[int64](5)}},
"with tail all": {"?tail=all", &v1.PodLogOptions{}},
"with follow": {"?follow=1", &v1.PodLogOptions{Follow: true}},
}
for desc, test := range tests {
// To make sure the original behavior doesn't change no matter the feature PodLogsQuerySplitStreams is enabled or not.
for _, enablePodLogsQuerySplitStreams := range []bool{true, false} {
t.Run(fmt.Sprintf("%s (enablePodLogsQuerySplitStreams=%v)", desc, enablePodLogsQuerySplitStreams), func(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodLogsQuerySplitStreams, enablePodLogsQuerySplitStreams)
expectedLogOptions := test.podLogOption.DeepCopy()
if enablePodLogsQuerySplitStreams && expectedLogOptions.Stream == nil {
// The HTTP handler will internally set the default stream value.
expectedLogOptions.Stream = ptr.To(v1.LogStreamAll)
}
output := "foo bar"
podNamespace := "other"
podName := "foo"
expectedPodName := getPodName(podName, podNamespace)
expectedContainerName := "baz"
setPodByNameFunc(fw, podNamespace, podName, expectedContainerName)
setGetContainerLogsFunc(fw, t, expectedPodName, expectedContainerName, expectedLogOptions, output)
resp, err := http.Get(fw.testHTTPServer.URL + "/containerLogs/" + podNamespace + "/" + podName + "/" + expectedContainerName + test.query)
if err != nil {
t.Errorf("Got error GETing: %v", err)
}
defer func() {
_ = resp.Body.Close()
}()
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Errorf("Error reading container logs: %v", err)
}
result := string(body)
if result != output {
t.Errorf("Expected: '%v', got: '%v'", output, result)
}
})
}
}
}
func TestContainerLogsWithInvalidTail(t *testing.T) {
fw := newServerTest()
defer fw.testHTTPServer.Close()
output := "foo bar"
podNamespace := "other"
podName := "foo"
expectedPodName := getPodName(podName, podNamespace)
expectedContainerName := "baz"
setPodByNameFunc(fw, podNamespace, podName, expectedContainerName)
setGetContainerLogsFunc(fw, t, expectedPodName, expectedContainerName, &v1.PodLogOptions{}, output)
resp, err := http.Get(fw.testHTTPServer.URL + "/containerLogs/" + podNamespace + "/" + podName + "/" + expectedContainerName + "?tail=-1")
if err != nil {
t.Errorf("Got error GETing: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusUnprocessableEntity {
t.Errorf("Unexpected non-error reading container logs: %#v", resp)
}
}
func TestContainerLogsWithSeparateStream(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodLogsQuerySplitStreams, true)
type logEntry struct {
stream string
msg string
}
fw := newServerTest()
defer fw.testHTTPServer.Close()
var (
streamStdout = v1.LogStreamStdout
streamStderr = v1.LogStreamStderr
streamAll = v1.LogStreamAll
)
testCases := []struct {
name string
query string
logs []logEntry
expectedOutput string
expectedLogOptions *v1.PodLogOptions
}{
{
// Defaulters don't work if the query is empty.
// See also https://github.com/kubernetes/kubernetes/issues/128589
name: "empty query should return all logs",
logs: []logEntry{
{stream: v1.LogStreamStdout, msg: "foo\n"},
{stream: v1.LogStreamStderr, msg: "bar\n"},
},
query: "",
expectedLogOptions: &v1.PodLogOptions{
Stream: &streamAll,
},
expectedOutput: "foo\nbar\n",
},
{
name: "missing stream param should return all logs",
logs: []logEntry{
{stream: v1.LogStreamStdout, msg: "foo\n"},
{stream: v1.LogStreamStderr, msg: "bar\n"},
},
query: "?limitBytes=100",
expectedLogOptions: &v1.PodLogOptions{
Stream: &streamAll,
LimitBytes: ptr.To[int64](100),
},
expectedOutput: "foo\nbar\n",
},
{
name: "only stdout logs",
logs: []logEntry{
{stream: v1.LogStreamStdout, msg: "out1\n"},
{stream: v1.LogStreamStderr, msg: "err1\n"},
{stream: v1.LogStreamStdout, msg: "out2\n"},
},
query: "?stream=Stdout",
expectedLogOptions: &v1.PodLogOptions{
Stream: &streamStdout,
},
expectedOutput: "out1\nout2\n",
},
{
name: "only stderr logs",
logs: []logEntry{
{stream: v1.LogStreamStderr, msg: "err1\n"},
{stream: v1.LogStreamStderr, msg: "err2\n"},
{stream: v1.LogStreamStdout, msg: "out1\n"},
},
query: "?stream=Stderr",
expectedLogOptions: &v1.PodLogOptions{
Stream: &streamStderr,
},
expectedOutput: "err1\nerr2\n",
},
{
name: "return all logs",
logs: []logEntry{
{stream: v1.LogStreamStdout, msg: "out1\n"},
{stream: v1.LogStreamStderr, msg: "err1\n"},
{stream: v1.LogStreamStdout, msg: "out2\n"},
},
query: "?stream=All",
expectedLogOptions: &v1.PodLogOptions{
Stream: &streamAll,
},
expectedOutput: "out1\nerr1\nout2\n",
},
{
name: "stdout logs with legacy tail",
logs: []logEntry{
{stream: v1.LogStreamStdout, msg: "out1\n"},
{stream: v1.LogStreamStderr, msg: "err1\n"},
{stream: v1.LogStreamStdout, msg: "out2\n"},
},
query: "?stream=All&tail=1",
expectedLogOptions: &v1.PodLogOptions{
Stream: &streamAll,
TailLines: ptr.To[int64](1),
},
expectedOutput: "out2\n",
},
{
name: "return the last 2 lines of logs",
logs: []logEntry{
{stream: v1.LogStreamStdout, msg: "out1\n"},
{stream: v1.LogStreamStderr, msg: "err1\n"},
{stream: v1.LogStreamStdout, msg: "out2\n"},
},
query: "?stream=All&tailLines=2",
expectedLogOptions: &v1.PodLogOptions{
Stream: &streamAll,
TailLines: ptr.To[int64](2),
},
expectedOutput: "err1\nout2\n",
},
{
name: "return the first 6 bytes of the stdout log stream",
logs: []logEntry{
{stream: v1.LogStreamStderr, msg: "err1\n"},
{stream: v1.LogStreamStdout, msg: "out1\n"},
{stream: v1.LogStreamStderr, msg: "err2\n"},
{stream: v1.LogStreamStdout, msg: "out2\n"},
},
query: "?stream=Stdout&limitBytes=6",
expectedLogOptions: &v1.PodLogOptions{
Stream: &streamStdout,
LimitBytes: ptr.To[int64](6),
},
expectedOutput: "out1\no",
},
{
name: "invalid stream",
logs: []logEntry{
{stream: v1.LogStreamStderr, msg: "err1\n"},
{stream: v1.LogStreamStdout, msg: "out1\n"},
},
query: "?stream=invalid",
expectedLogOptions: nil,
expectedOutput: `{"message": "Invalid request."}`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
podNamespace := "other"
podName := "foo"
expectedContainerName := "baz"
setPodByNameFunc(fw, podNamespace, podName, expectedContainerName)
fw.fakeKubelet.containerLogsFunc = func(_ context.Context, podFullName, containerName string, logOptions *v1.PodLogOptions, stdout, stderr io.Writer) error {
if !reflect.DeepEqual(tc.expectedLogOptions, logOptions) {
t.Errorf("expected %#v, got %#v", tc.expectedLogOptions, logOptions)
}
var dst io.Writer
tailLines := len(tc.logs)
if logOptions.TailLines != nil {
tailLines = int(*logOptions.TailLines)
}
remain := 0
if logOptions.LimitBytes != nil {
remain = int(*logOptions.LimitBytes)
} else {
for _, log := range tc.logs {
remain += len(log.msg)
}
}
logs := tc.logs[len(tc.logs)-tailLines:]
for _, log := range logs {
switch log.stream {
case v1.LogStreamStdout:
dst = stdout
case v1.LogStreamStderr:
dst = stderr
}
// Skip if the stream is not requested
if dst == nil {
continue
}
line := log.msg
if len(line) > remain {
line = line[:remain]
}
_, _ = io.WriteString(dst, line)
remain -= len(line)
if remain <= 0 {
return nil
}
}
return nil
}
resp, err := http.Get(fw.testHTTPServer.URL + "/containerLogs/" + podNamespace + "/" + podName + "/" + expectedContainerName + tc.query)
if err != nil {
t.Errorf("Got error GETing: %v", err)
}
defer func() {
_ = resp.Body.Close()
}()
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Errorf("Error reading container logs: %v", err)
}
result := string(body)
if result != tc.expectedOutput {
t.Errorf("Expected: %q, got: %q", tc.expectedOutput, result)
}
})
}
}
func TestCheckpointContainer(t *testing.T) {
podNamespace := "other"
podName := "foo"
expectedContainerName := "baz"
setupTest := func(featureGate bool) *serverTestFramework {
// Enable features.ContainerCheckpoint during test
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ContainerCheckpoint, featureGate)
fw := newServerTest()
// GetPodByName() should always fail
fw.fakeKubelet.podByNameFunc = func(namespace, name string) (*v1.Pod, bool) {
return nil, false
}
return fw
}
fw := setupTest(true)
defer fw.testHTTPServer.Close()
t.Run("wrong pod namespace", func(t *testing.T) {
resp, err := http.Post(fw.testHTTPServer.URL+"/checkpoint/"+podNamespace+"/"+podName+"/"+expectedContainerName, "", nil)
if err != nil {
t.Errorf("Got error POSTing: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNotFound {
t.Errorf("Unexpected non-error checkpointing container: %#v", resp)
}
})
// let GetPodByName() return a result, but our container "wrongContainerName" is not part of the Pod
setPodByNameFunc(fw, podNamespace, podName, expectedContainerName)
t.Run("wrong container name", func(t *testing.T) {
resp, err := http.Post(fw.testHTTPServer.URL+"/checkpoint/"+podNamespace+"/"+podName+"/wrongContainerName", "", nil)
if err != nil {
t.Errorf("Got error POSTing: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNotFound {
t.Errorf("Unexpected non-error checkpointing container: %#v", resp)
}
})
// Now the checkpointing of the container fails
fw.fakeKubelet.podByNameFunc = func(namespace, name string) (*v1.Pod, bool) {
return &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: podNamespace,
Name: podName,
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "checkpointingFailure",
},
},
},
}, true
}
t.Run("checkpointing fails", func(t *testing.T) {
resp, err := http.Post(fw.testHTTPServer.URL+"/checkpoint/"+podNamespace+"/"+podName+"/checkpointingFailure", "", nil)
if err != nil {
t.Errorf("Got error POSTing: %v", err)
}
defer resp.Body.Close()
assert.Equal(t, 500, resp.StatusCode)
body, _ := io.ReadAll(resp.Body)
assert.Equal(t, "checkpointing of other/foo/checkpointingFailure failed (Returning error for test)", string(body))
})
// Now test a successful checkpoint succeeds
setPodByNameFunc(fw, podNamespace, podName, expectedContainerName)
t.Run("checkpointing succeeds", func(t *testing.T) {
resp, err := http.Post(fw.testHTTPServer.URL+"/checkpoint/"+podNamespace+"/"+podName+"/"+expectedContainerName, "", nil)
if err != nil {
t.Errorf("Got error POSTing: %v", err)
}
assert.Equal(t, 200, resp.StatusCode)
})
// Now test for 404 if checkpointing support is explicitly disabled.
fw.testHTTPServer.Close()
fw = setupTest(false)
defer fw.testHTTPServer.Close()
setPodByNameFunc(fw, podNamespace, podName, expectedContainerName)
t.Run("checkpointing fails because disabled", func(t *testing.T) {
resp, err := http.Post(fw.testHTTPServer.URL+"/checkpoint/"+podNamespace+"/"+podName+"/"+expectedContainerName, "", nil)
if err != nil {
t.Errorf("Got error POSTing: %v", err)
}
assert.Equal(t, 404, resp.StatusCode)
})
}
func makeReq(t *testing.T, method, url, clientProtocol string) *http.Request {
req, err := http.NewRequest(method, url, nil)
if err != nil {
t.Fatalf("error creating request: %v", err)
}
req.Header.Set("Content-Type", "")
req.Header.Add("X-Stream-Protocol-Version", clientProtocol)
return req
}
func TestServeExecInContainerIdleTimeout(t *testing.T) {
ss, err := newTestStreamingServer(100 * time.Millisecond)
require.NoError(t, err)
defer ss.testHTTPServer.Close()
fw := newServerTestWithDebug(true, ss)
defer fw.testHTTPServer.Close()
podNamespace := "other"
podName := "foo"
expectedContainerName := "baz"
url := fw.testHTTPServer.URL + "/exec/" + podNamespace + "/" + podName + "/" + expectedContainerName + "?c=ls&c=-a&" + api.ExecStdinParam + "=1"
upgradeRoundTripper, err := spdy.NewRoundTripper(&tls.Config{})
if err != nil {
t.Fatalf("Error creating SpdyRoundTripper: %v", err)
}
c := &http.Client{Transport: upgradeRoundTripper}
resp, err := c.Do(makeReq(t, "POST", url, "v4.channel.k8s.io"))
if err != nil {
t.Fatalf("Got error POSTing: %v", err)
}
defer resp.Body.Close()
upgradeRoundTripper.Dialer = &net.Dialer{
Deadline: time.Now().Add(60 * time.Second),
Timeout: 60 * time.Second,
}
conn, err := upgradeRoundTripper.NewConnection(resp)
if err != nil {
t.Fatalf("Unexpected error creating streaming connection: %s", err)
}
if conn == nil {
t.Fatal("Unexpected nil connection")
}
<-conn.CloseChan()
}
func testExecAttach(t *testing.T, verb string) {
tests := map[string]struct {
stdin bool
stdout bool
stderr bool
tty bool
responseStatusCode int
uid bool
}{
"no input or output": {responseStatusCode: http.StatusBadRequest},
"stdin": {stdin: true, responseStatusCode: http.StatusSwitchingProtocols},
"stdout": {stdout: true, responseStatusCode: http.StatusSwitchingProtocols},
"stderr": {stderr: true, responseStatusCode: http.StatusSwitchingProtocols},
"stdout and stderr": {stdout: true, stderr: true, responseStatusCode: http.StatusSwitchingProtocols},
"stdin stdout and stderr": {stdin: true, stdout: true, stderr: true, responseStatusCode: http.StatusSwitchingProtocols},
"stdin stdout stderr with uid": {stdin: true, stdout: true, stderr: true, responseStatusCode: http.StatusSwitchingProtocols, uid: true},
}
for desc := range tests {
test := tests[desc]
t.Run(desc, func(t *testing.T) {
ss, err := newTestStreamingServer(0)
require.NoError(t, err)
defer ss.testHTTPServer.Close()
fw := newServerTestWithDebug(true, ss)
defer fw.testHTTPServer.Close()
fmt.Println(desc)
podNamespace := "other"
podName := "foo"
expectedPodName := getPodName(podName, podNamespace)
expectedContainerName := "baz"
expectedCommand := "ls -a"
expectedStdin := "stdin"
expectedStdout := "stdout"
expectedStderr := "stderr"
done := make(chan struct{})
clientStdoutReadDone := make(chan struct{})
clientStderrReadDone := make(chan struct{})
execInvoked := false
attachInvoked := false
checkStream := func(podFullName string, uid types.UID, containerName string, streamOpts remotecommandserver.Options) {
assert.Equal(t, expectedPodName, podFullName, "podFullName")
if test.uid {
assert.Equal(t, testUID, string(uid), "uid")
}
assert.Equal(t, expectedContainerName, containerName, "containerName")
assert.Equal(t, test.stdin, streamOpts.Stdin, "stdin")
assert.Equal(t, test.stdout, streamOpts.Stdout, "stdout")
assert.Equal(t, test.tty, streamOpts.TTY, "tty")
assert.Equal(t, !test.tty && test.stderr, streamOpts.Stderr, "stderr")
}
fw.fakeKubelet.getExecCheck = func(podFullName string, uid types.UID, containerName string, cmd []string, streamOpts remotecommandserver.Options) {
execInvoked = true
assert.Equal(t, expectedCommand, strings.Join(cmd, " "), "cmd")
checkStream(podFullName, uid, containerName, streamOpts)
}
fw.fakeKubelet.getAttachCheck = func(podFullName string, uid types.UID, containerName string, streamOpts remotecommandserver.Options) {
attachInvoked = true
checkStream(podFullName, uid, containerName, streamOpts)
}
testStream := func(containerID string, in io.Reader, out, stderr io.WriteCloser, tty bool, done chan struct{}) error {
close(done)
assert.Equal(t, testContainerID, containerID, "containerID")
assert.Equal(t, test.tty, tty, "tty")
require.Equal(t, test.stdin, in != nil, "in")
require.Equal(t, test.stdout, out != nil, "out")
require.Equal(t, !test.tty && test.stderr, stderr != nil, "err")
if test.stdin {
b := make([]byte, 10)
n, err := in.Read(b)
assert.NoError(t, err, "reading from stdin")
assert.Equal(t, expectedStdin, string(b[0:n]), "content from stdin")
}
if test.stdout {
_, err := out.Write([]byte(expectedStdout))
assert.NoError(t, err, "writing to stdout")
out.Close()
<-clientStdoutReadDone
}
if !test.tty && test.stderr {
_, err := stderr.Write([]byte(expectedStderr))
assert.NoError(t, err, "writing to stderr")
stderr.Close()
<-clientStderrReadDone
}
return nil
}
ss.fakeRuntime.execFunc = func(containerID string, cmd []string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error {
assert.Equal(t, expectedCommand, strings.Join(cmd, " "), "cmd")
return testStream(containerID, stdin, stdout, stderr, tty, done)
}
ss.fakeRuntime.attachFunc = func(containerID string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error {
return testStream(containerID, stdin, stdout, stderr, tty, done)
}
var url string
if test.uid {
url = fw.testHTTPServer.URL + "/" + verb + "/" + podNamespace + "/" + podName + "/" + testUID + "/" + expectedContainerName + "?ignore=1"
} else {
url = fw.testHTTPServer.URL + "/" + verb + "/" + podNamespace + "/" + podName + "/" + expectedContainerName + "?ignore=1"
}
if verb == "exec" {
url += "&command=ls&command=-a"
}
if test.stdin {
url += "&" + api.ExecStdinParam + "=1"
}
if test.stdout {
url += "&" + api.ExecStdoutParam + "=1"
}
if test.stderr && !test.tty {
url += "&" + api.ExecStderrParam + "=1"
}
if test.tty {
url += "&" + api.ExecTTYParam + "=1"
}
var (
resp *http.Response
upgradeRoundTripper httpstream.UpgradeRoundTripper
c *http.Client
)
upgradeRoundTripper, err = spdy.NewRoundTripper(&tls.Config{})
if err != nil {
t.Fatalf("Error creating SpdyRoundTripper: %v", err)
}
c = &http.Client{Transport: upgradeRoundTripper}
resp, err = c.Do(makeReq(t, "POST", url, "v4.channel.k8s.io"))
require.NoError(t, err, "POSTing")
defer resp.Body.Close()
_, err = io.ReadAll(resp.Body)
assert.NoError(t, err, "reading response body")
require.Equal(t, test.responseStatusCode, resp.StatusCode, "response status")
if test.responseStatusCode != http.StatusSwitchingProtocols {
return
}
conn, err := upgradeRoundTripper.NewConnection(resp)
require.NoError(t, err, "creating streaming connection")
defer conn.Close()
h := http.Header{}
h.Set(api.StreamType, api.StreamTypeError)
_, err = conn.CreateStream(h)
require.NoError(t, err, "creating error stream")
if test.stdin {
h.Set(api.StreamType, api.StreamTypeStdin)
stream, err := conn.CreateStream(h)
require.NoError(t, err, "creating stdin stream")
_, err = stream.Write([]byte(expectedStdin))
require.NoError(t, err, "writing to stdin stream")
}
var stdoutStream httpstream.Stream
if test.stdout {
h.Set(api.StreamType, api.StreamTypeStdout)
stdoutStream, err = conn.CreateStream(h)
require.NoError(t, err, "creating stdout stream")
}
var stderrStream httpstream.Stream
if test.stderr && !test.tty {
h.Set(api.StreamType, api.StreamTypeStderr)
stderrStream, err = conn.CreateStream(h)
require.NoError(t, err, "creating stderr stream")
}
if test.stdout {
output := make([]byte, 10)
n, err := stdoutStream.Read(output)
close(clientStdoutReadDone)
assert.NoError(t, err, "reading from stdout stream")
assert.Equal(t, expectedStdout, string(output[0:n]), "stdout")
}
if test.stderr && !test.tty {
output := make([]byte, 10)
n, err := stderrStream.Read(output)
close(clientStderrReadDone)
assert.NoError(t, err, "reading from stderr stream")
assert.Equal(t, expectedStderr, string(output[0:n]), "stderr")
}
// wait for the server to finish before checking if the attach/exec funcs were invoked
<-done
if verb == "exec" {
assert.True(t, execInvoked, "exec should be invoked")
assert.False(t, attachInvoked, "attach should not be invoked")
} else {
assert.True(t, attachInvoked, "attach should be invoked")
assert.False(t, execInvoked, "exec should not be invoked")
}
})
}
}
func TestServeExecInContainer(t *testing.T) {
testExecAttach(t, "exec")
}
func TestServeAttachContainer(t *testing.T) {
testExecAttach(t, "attach")
}
func TestServePortForwardIdleTimeout(t *testing.T) {
ss, err := newTestStreamingServer(100 * time.Millisecond)
require.NoError(t, err)
defer ss.testHTTPServer.Close()
fw := newServerTestWithDebug(true, ss)
defer fw.testHTTPServer.Close()
podNamespace := "other"
podName := "foo"
url := fw.testHTTPServer.URL + "/portForward/" + podNamespace + "/" + podName
upgradeRoundTripper, err := spdy.NewRoundTripper(&tls.Config{})
if err != nil {
t.Fatalf("Error creating SpdyRoundTripper: %v", err)
}
c := &http.Client{Transport: upgradeRoundTripper}
req := makeReq(t, "POST", url, "portforward.k8s.io")
resp, err := c.Do(req)
if err != nil {
t.Fatalf("Got error POSTing: %v", err)
}
defer resp.Body.Close()
conn, err := upgradeRoundTripper.NewConnection(resp)
if err != nil {
t.Fatalf("Unexpected error creating streaming connection: %s", err)
}
if conn == nil {
t.Fatal("Unexpected nil connection")
}
defer conn.Close()
<-conn.CloseChan()
}
func TestServePortForward(t *testing.T) {
tests := map[string]struct {
port string
uid bool
clientData string
containerData string
shouldError bool
}{
"no port": {port: "", shouldError: true},
"none number port": {port: "abc", shouldError: true},
"negative port": {port: "-1", shouldError: true},
"too large port": {port: "65536", shouldError: true},
"0 port": {port: "0", shouldError: true},
"min port": {port: "1", shouldError: false},
"normal port": {port: "8000", shouldError: false},
"normal port with data forward": {port: "8000", clientData: "client data", containerData: "container data", shouldError: false},
"max port": {port: "65535", shouldError: false},
"normal port with uid": {port: "8000", uid: true, shouldError: false},
}
podNamespace := "other"
podName := "foo"
for desc := range tests {
test := tests[desc]
t.Run(desc, func(t *testing.T) {
ss, err := newTestStreamingServer(0)
require.NoError(t, err)
defer ss.testHTTPServer.Close()
fw := newServerTestWithDebug(true, ss)
defer fw.testHTTPServer.Close()
portForwardFuncDone := make(chan struct{})
fw.fakeKubelet.getPortForwardCheck = func(name, namespace string, uid types.UID, opts portforward.V4Options) {
assert.Equal(t, podName, name, "pod name")
assert.Equal(t, podNamespace, namespace, "pod namespace")
if test.uid {
assert.Equal(t, testUID, string(uid), "uid")
}
}
ss.fakeRuntime.portForwardFunc = func(podSandboxID string, port int32, stream io.ReadWriteCloser) error {
defer close(portForwardFuncDone)
assert.Equal(t, testPodSandboxID, podSandboxID, "pod sandbox id")
// The port should be valid if it reaches here.
testPort, err := strconv.ParseInt(test.port, 10, 32)
require.NoError(t, err, "parse port")
assert.Equal(t, int32(testPort), port, "port")
if test.clientData != "" {
fromClient := make([]byte, 32)
n, err := stream.Read(fromClient)
assert.NoError(t, err, "reading client data")
assert.Equal(t, test.clientData, string(fromClient[0:n]), "client data")
}
if test.containerData != "" {
_, err := stream.Write([]byte(test.containerData))
assert.NoError(t, err, "writing container data")
}
return nil
}
var url string
if test.uid {
url = fmt.Sprintf("%s/portForward/%s/%s/%s", fw.testHTTPServer.URL, podNamespace, podName, testUID)
} else {
url = fmt.Sprintf("%s/portForward/%s/%s", fw.testHTTPServer.URL, podNamespace, podName)
}
var (
upgradeRoundTripper httpstream.UpgradeRoundTripper
c *http.Client
)
upgradeRoundTripper, err = spdy.NewRoundTripper(&tls.Config{})
if err != nil {
t.Fatalf("Error creating SpdyRoundTripper: %v", err)
}
c = &http.Client{Transport: upgradeRoundTripper}
req := makeReq(t, "POST", url, "portforward.k8s.io")
resp, err := c.Do(req)
require.NoError(t, err, "POSTing")
defer resp.Body.Close()
assert.Equal(t, http.StatusSwitchingProtocols, resp.StatusCode, "status code")
conn, err := upgradeRoundTripper.NewConnection(resp)
require.NoError(t, err, "creating streaming connection")
defer conn.Close()
headers := http.Header{}
headers.Set("streamType", "error")
headers.Set("port", test.port)
_, err = conn.CreateStream(headers)
assert.Equal(t, test.shouldError, err != nil, "expect error")
if test.shouldError {
return
}
headers.Set("streamType", "data")
headers.Set("port", test.port)
dataStream, err := conn.CreateStream(headers)
require.NoError(t, err, "create stream")
if test.clientData != "" {
_, err := dataStream.Write([]byte(test.clientData))
assert.NoError(t, err, "writing client data")
}
if test.containerData != "" {
fromContainer := make([]byte, 32)
n, err := dataStream.Read(fromContainer)
assert.NoError(t, err, "reading container data")
assert.Equal(t, test.containerData, string(fromContainer[0:n]), "container data")
}
<-portForwardFuncDone
})
}
}
func TestMetricBuckets(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, zpagesfeatures.ComponentStatusz, true)
tests := map[string]struct {
url string
bucket string
}{
"healthz endpoint": {url: "/healthz", bucket: "healthz"},
"attach": {url: "/attach/podNamespace/podID/containerName", bucket: "attach"},
"attach with uid": {url: "/attach/podNamespace/podID/uid/containerName", bucket: "attach"},
"configz": {url: "/configz", bucket: "configz"},
"containerLogs": {url: "/containerLogs/podNamespace/podID/containerName", bucket: "containerLogs"},
"debug v flags": {url: "/debug/flags/v", bucket: "debug"},
"pprof with sub": {url: "/debug/pprof/subpath", bucket: "debug"},
"exec": {url: "/exec/podNamespace/podID/containerName", bucket: "exec"},
"exec with uid": {url: "/exec/podNamespace/podID/uid/containerName", bucket: "exec"},
"healthz": {url: "/healthz/", bucket: "healthz"},
"healthz log sub": {url: "/healthz/log", bucket: "healthz"},
"healthz ping": {url: "/healthz/ping", bucket: "healthz"},
"healthz sync loop": {url: "/healthz/syncloop", bucket: "healthz"},
"logs": {url: "/logs/", bucket: "logs"},
"logs with path": {url: "/logs/logpath", bucket: "logs"},
"metrics": {url: "/metrics", bucket: "metrics"},
"metrics cadvisor sub": {url: "/metrics/cadvisor", bucket: "metrics/cadvisor"},
"metrics probes sub": {url: "/metrics/probes", bucket: "metrics/probes"},
"metrics resource sub": {url: "/metrics/resource", bucket: "metrics/resource"},
"pods": {url: "/pods/", bucket: "pods"},
"portForward": {url: "/portForward/podNamespace/podID", bucket: "portForward"},
"portForward with uid": {url: "/portForward/podNamespace/podID/uid", bucket: "portForward"},
"run": {url: "/run/podNamespace/podID/containerName", bucket: "run"},
"run with uid": {url: "/run/podNamespace/podID/uid/containerName", bucket: "run"},
"runningpods": {url: "/runningpods/", bucket: "runningpods"},
"stats": {url: "/stats/", bucket: "stats"},
"stats summary sub": {url: "/stats/summary", bucket: "stats"},
"statusz": {url: "/statusz", bucket: "statusz"},
"/flagz": {url: "/flagz", bucket: "flagz"},
"invalid path": {url: "/junk", bucket: "other"},
"invalid path starting with good": {url: "/healthzjunk", bucket: "other"},
}
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, zpagesfeatures.ComponentFlagz, true)
fw := newServerTest()
defer fw.testHTTPServer.Close()
for _, test := range tests {
path := test.url
bucket := test.bucket
require.Equal(t, fw.serverUnderTest.getMetricBucket(path), bucket)
}
}
func TestMetricMethodBuckets(t *testing.T) {
tests := map[string]struct {
method string
bucket string
}{
"normal GET": {method: "GET", bucket: "GET"},
"normal POST": {method: "POST", bucket: "POST"},
"invalid method": {method: "WEIRD", bucket: "other"},
}
fw := newServerTest()
defer fw.testHTTPServer.Close()
for _, test := range tests {
method := test.method
bucket := test.bucket
require.Equal(t, fw.serverUnderTest.getMetricMethodBucket(method), bucket)
}
}
func TestDebuggingDisabledHandlers(t *testing.T) {
// for backward compatibility even if enablesystemLogHandler or enableProfilingHandler is set but not
// enableDebuggingHandler then /logs, /pprof and /flags shouldn't be served.
kubeCfg := &kubeletconfiginternal.KubeletConfiguration{
EnableDebuggingHandlers: false,
EnableSystemLogHandler: true,
EnableDebugFlagsHandler: true,
EnableProfilingHandler: true,
}
fw := newServerTestWithDebuggingHandlers(kubeCfg, nil)
defer fw.testHTTPServer.Close()
paths := []string{
"/run", "/exec", "/attach", "/portForward", "/containerLogs", "/runningpods",
"/run/", "/exec/", "/attach/", "/portForward/", "/containerLogs/", "/runningpods/",
"/run/xxx", "/exec/xxx", "/attach/xxx", "/debug/pprof/profile", "/logs/kubelet.log",
}
for _, p := range paths {
verifyEndpointResponse(t, fw, p, "Debug endpoints are disabled.\n")
}
}
func TestDisablingLogAndProfilingHandler(t *testing.T) {
kubeCfg := &kubeletconfiginternal.KubeletConfiguration{
EnableDebuggingHandlers: true,
}
fw := newServerTestWithDebuggingHandlers(kubeCfg, nil)
defer fw.testHTTPServer.Close()
// verify debug endpoints are disabled
verifyEndpointResponse(t, fw, "/logs/kubelet.log", "logs endpoint is disabled.\n")
verifyEndpointResponse(t, fw, "/debug/pprof/profile?seconds=2", "profiling endpoint is disabled.\n")
verifyEndpointResponse(t, fw, "/debug/flags/v", "flags endpoint is disabled.\n")
}
func TestFailedParseParamsSummaryHandler(t *testing.T) {
fw := newServerTest()
defer fw.testHTTPServer.Close()
resp, err := http.Post(fw.testHTTPServer.URL+"/stats/summary", "invalid/content/type", nil)
assert.NoError(t, err)
defer resp.Body.Close()
v, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
assert.Contains(t, string(v), "parse form failed")
}
func verifyEndpointResponse(t *testing.T, fw *serverTestFramework, path string, expectedResponse string) {
resp, err := http.Get(fw.testHTTPServer.URL + path)
require.NoError(t, err)
assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, expectedResponse, string(body))
resp, err = http.Post(fw.testHTTPServer.URL+path, "", nil)
require.NoError(t, err)
assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode)
body, err = io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, expectedResponse, string(body))
}
func TestTrimURLPath(t *testing.T) {
tests := []struct {
path, expected string
}{
{"", ""},
{"//", ""},
{"/pods", "pods"},
{"pods", "pods"},
{"pods/", "pods"},
{"good/", "good"},
{"pods/probes", "pods"},
{"metrics", "metrics"},
{"metrics/resource", "metrics/resource"},
{"metrics/hello", "metrics/hello"},
}
for _, test := range tests {
assert.Equalf(t, test.expected, getURLRootPath(test.path), "path is: %s", test.path)
}
}
func TestFineGrainedAuthz(t *testing.T) {
// Enable features.ContainerCheckpoint during test
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KubeletFineGrainedAuthz, true)
fw := newServerTest()
defer fw.testHTTPServer.Close()
attributesGetter := NewNodeAuthorizerAttributesGetter(authzTestNodeName)
testCases := []struct {
name string
path string
expectedSubResources []string
authorizer func(authorizer.Attributes) (authorized authorizer.Decision, reason string, err error)
wantStatusCode int
wantCalledAuthorizeCount int
}{
{
name: "both subresources rejected",
path: "/configz",
expectedSubResources: []string{"configz", "proxy"},
authorizer: func(authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
return authorizer.DecisionNoOpinion, "", nil
},
wantStatusCode: 403,
wantCalledAuthorizeCount: 2,
},
{
name: "fine grained rejected, proxy accepted",
path: "/configz",
expectedSubResources: []string{"configz", "proxy"},
authorizer: func(a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
switch a.GetSubresource() {
case "configz":
return authorizer.DecisionNoOpinion, "", nil
case "proxy":
return authorizer.DecisionAllow, "", nil
default:
return authorizer.DecisionNoOpinion, "", fmt.Errorf("unexpected subresource %v", a.GetSubresource())
}
},
wantStatusCode: 200,
wantCalledAuthorizeCount: 2,
},
{
name: "fine grained accepted",
path: "/configz",
expectedSubResources: []string{"configz", "proxy"},
authorizer: func(a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
switch a.GetSubresource() {
case "configz":
return authorizer.DecisionAllow, "", nil
case "proxy":
return authorizer.DecisionNoOpinion, "", fmt.Errorf("did not expect code to reach here")
default:
return authorizer.DecisionNoOpinion, "", fmt.Errorf("unexpected subresource %v", a.GetSubresource())
}
},
wantStatusCode: 200,
wantCalledAuthorizeCount: 1,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
calledAuthenticate := false
calledAuthorizeCount := 0
calledAttributes := false
fw.fakeAuth.authenticateFunc = func(req *http.Request) (*authenticator.Response, bool, error) {
calledAuthenticate = true
return &authenticator.Response{User: AuthzTestUser()}, true, nil
}
fw.fakeAuth.attributesFunc = func(u user.Info, req *http.Request) []authorizer.Attributes {
calledAttributes = true
attrs := attributesGetter.GetRequestAttributes(u, req)
var gotSubresources []string
for _, attr := range attrs {
gotSubresources = append(gotSubresources, attr.GetSubresource())
}
require.Equal(t, tc.expectedSubResources, gotSubresources)
return attrs
}
fw.fakeAuth.authorizeFunc = func(a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
calledAuthorizeCount += 1
return tc.authorizer(a)
}
req, err := http.NewRequest("GET", fw.testHTTPServer.URL+tc.path, nil)
require.NoError(t, err)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, tc.wantStatusCode, resp.StatusCode)
assert.True(t, calledAuthenticate, "Authenticate was not called")
assert.True(t, calledAttributes, "Attributes were not called")
assert.Equal(t, tc.wantCalledAuthorizeCount, calledAuthorizeCount)
})
}
}
func TestNewServerRegistersMetricsSLIsEndpointTwice(t *testing.T) {
host := &fakeKubelet{
hostnameFunc: func() string {
return "127.0.0.1"
},
}
resourceAnalyzer := stats.NewResourceAnalyzer(nil, time.Minute, &record.FakeRecorder{})
server1 := NewServer(host, resourceAnalyzer, []healthz.HealthChecker{}, flagz.NamedFlagSetsReader{}, nil, nil)
server2 := NewServer(host, resourceAnalyzer, []healthz.HealthChecker{}, flagz.NamedFlagSetsReader{}, nil, nil)
// Check if both servers registered the /metrics/slis endpoint
assert.Contains(t, server1.restfulCont.RegisteredHandlePaths(), "/metrics/slis", "First server should register /metrics/slis")
assert.Contains(t, server2.restfulCont.RegisteredHandlePaths(), "/metrics/slis", "Second server should register /metrics/slis")
}