k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/kubelet/userns/userns_manager_test.go (about)

     1  /*
     2  Copyright 2022 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 userns
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"os"
    23  	goruntime "runtime"
    24  	"testing"
    25  
    26  	"github.com/stretchr/testify/assert"
    27  	"github.com/stretchr/testify/require"
    28  	v1 "k8s.io/api/core/v1"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/types"
    31  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    32  	featuregatetesting "k8s.io/component-base/featuregate/testing"
    33  	runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
    34  	pkgfeatures "k8s.io/kubernetes/pkg/features"
    35  	kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
    36  )
    37  
    38  const (
    39  	// skip the first block
    40  	minimumMappingUID = userNsLength
    41  	// allocate enough space for 2000 user namespaces
    42  	mappingLen  = userNsLength * 2000
    43  	testMaxPods = 110
    44  )
    45  
    46  type testUserNsPodsManager struct {
    47  	podDir         string
    48  	podList        []types.UID
    49  	userns         bool
    50  	maxPods        int
    51  	mappingFirstID uint32
    52  	mappingLen     uint32
    53  }
    54  
    55  func (m *testUserNsPodsManager) GetPodDir(podUID types.UID) string {
    56  	if m.podDir == "" {
    57  		return "/tmp/non-existant-dir.This-is-not-used-in-tests"
    58  	}
    59  	return m.podDir
    60  }
    61  
    62  func (m *testUserNsPodsManager) ListPodsFromDisk() ([]types.UID, error) {
    63  	if len(m.podList) == 0 {
    64  		return nil, nil
    65  	}
    66  	return m.podList, nil
    67  }
    68  
    69  func (m *testUserNsPodsManager) HandlerSupportsUserNamespaces(runtimeHandler string) (bool, error) {
    70  	if runtimeHandler == "error" {
    71  		return false, errors.New("unknown runtime")
    72  	}
    73  	return m.userns, nil
    74  }
    75  
    76  func (m *testUserNsPodsManager) GetKubeletMappings() (uint32, uint32, error) {
    77  	if m.mappingFirstID != 0 {
    78  		return m.mappingFirstID, m.mappingLen, nil
    79  	}
    80  	return minimumMappingUID, mappingLen, nil
    81  }
    82  
    83  func (m *testUserNsPodsManager) GetMaxPods() int {
    84  	if m.maxPods != 0 {
    85  		return m.maxPods
    86  	}
    87  
    88  	return testMaxPods
    89  }
    90  
    91  func TestUserNsManagerAllocate(t *testing.T) {
    92  	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, pkgfeatures.UserNamespacesSupport, true)
    93  
    94  	testUserNsPodsManager := &testUserNsPodsManager{}
    95  	m, err := MakeUserNsManager(testUserNsPodsManager)
    96  	require.NoError(t, err)
    97  
    98  	allocated, length, err := m.allocateOne("one")
    99  	assert.NoError(t, err)
   100  	assert.Equal(t, userNsLength, int(length), "m.isSet(%d).length=%v", allocated, length)
   101  	assert.Equal(t, true, m.isSet(allocated), "m.isSet(%d)", allocated)
   102  
   103  	allocated2, length2, err := m.allocateOne("two")
   104  	assert.NoError(t, err)
   105  	assert.NotEqual(t, allocated, allocated2, "allocated != allocated2")
   106  	assert.Equal(t, length, length2, "length == length2")
   107  
   108  	// verify that re-adding the same pod with the same settings won't fail
   109  	err = m.record("two", allocated2, length2)
   110  	assert.NoError(t, err)
   111  	// but it fails if anyting is different
   112  	err = m.record("two", allocated2+1, length2)
   113  	assert.Error(t, err)
   114  
   115  	m.Release("one")
   116  	m.Release("two")
   117  	assert.Equal(t, false, m.isSet(allocated), "m.isSet(%d)", allocated)
   118  	assert.Equal(t, false, m.isSet(allocated2), "m.nsSet(%d)", allocated2)
   119  
   120  	var allocs []uint32
   121  	for i := 0; i < 1000; i++ {
   122  		allocated, length, err = m.allocateOne(types.UID(fmt.Sprintf("%d", i)))
   123  		assert.Equal(t, userNsLength, int(length), "length is not the expected. iter: %v", i)
   124  		assert.NoError(t, err)
   125  		assert.True(t, allocated >= minimumMappingUID)
   126  		// The last ID of the userns range (allocated+userNsLength) should be within bounds.
   127  		assert.True(t, allocated <= minimumMappingUID+mappingLen-userNsLength)
   128  		allocs = append(allocs, allocated)
   129  	}
   130  	for i, v := range allocs {
   131  		assert.Equal(t, true, m.isSet(v), "m.isSet(%d) should be true", v)
   132  		m.Release(types.UID(fmt.Sprintf("%d", i)))
   133  		assert.Equal(t, false, m.isSet(v), "m.isSet(%d) should be false", v)
   134  
   135  		err = m.record(types.UID(fmt.Sprintf("%d", i)), v, userNsLength)
   136  		assert.NoError(t, err)
   137  		m.Release(types.UID(fmt.Sprintf("%d", i)))
   138  		assert.Equal(t, false, m.isSet(v), "m.isSet(%d) should be false", v)
   139  	}
   140  }
   141  
   142  func TestMakeUserNsManager(t *testing.T) {
   143  	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, pkgfeatures.UserNamespacesSupport, true)
   144  
   145  	cases := []struct {
   146  		name           string
   147  		mappingFirstID uint32
   148  		mappingLen     uint32
   149  		maxPods        int
   150  		success        bool
   151  	}{
   152  		{
   153  			name:    "default",
   154  			success: true,
   155  		},
   156  		{
   157  			name:           "firstID not multiple",
   158  			mappingFirstID: 65536 + 1,
   159  		},
   160  		{
   161  			name:           "firstID is less than 65535",
   162  			mappingFirstID: 1,
   163  		},
   164  		{
   165  			name:           "mappingLen not multiple",
   166  			mappingFirstID: 65536,
   167  			mappingLen:     65536 + 1,
   168  		},
   169  		{
   170  			name:           "range can't fit maxPods",
   171  			mappingFirstID: 65536,
   172  			mappingLen:     65536,
   173  			maxPods:        2,
   174  		},
   175  	}
   176  
   177  	for _, tc := range cases {
   178  		t.Run(tc.name, func(t *testing.T) {
   179  			testUserNsPodsManager := &testUserNsPodsManager{
   180  				podDir:         t.TempDir(),
   181  				mappingFirstID: tc.mappingFirstID,
   182  				mappingLen:     tc.mappingLen,
   183  				maxPods:        tc.maxPods,
   184  			}
   185  			_, err := MakeUserNsManager(testUserNsPodsManager)
   186  
   187  			if tc.success {
   188  				assert.NoError(t, err)
   189  			} else {
   190  				assert.Error(t, err)
   191  			}
   192  		})
   193  	}
   194  }
   195  
   196  func TestUserNsManagerParseUserNsFile(t *testing.T) {
   197  	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, pkgfeatures.UserNamespacesSupport, true)
   198  
   199  	cases := []struct {
   200  		name    string
   201  		file    string
   202  		success bool
   203  	}{
   204  		{
   205  			name: "basic",
   206  			file: `{
   207  	                        "uidMappings":[ { "hostId":131072, "containerId":0, "length":65536 } ],
   208  	                        "gidMappings":[ { "hostId":131072, "containerId":0, "length":65536 } ]
   209                                 }`,
   210  			success: true,
   211  		},
   212  		{
   213  			name: "invalid length",
   214  			file: `{
   215  	                        "uidMappings":[ { "hostId":131072, "containerId":0, "length":0 } ],
   216  	                        "gidMappings":[ { "hostId":131072, "containerId":0, "length":0 } ]
   217                                 }`,
   218  			success: false,
   219  		},
   220  		{
   221  			name: "wrong offset",
   222  			file: `{
   223  	                        "uidMappings":[ {"hostId":131072, "containerId":0, "length":65536 } ],
   224  	                        "gidMappings":[ {"hostId":1, "containerId":0, "length":65536 } ]
   225                                 }`,
   226  			success: false,
   227  		},
   228  		{
   229  			name: "two GID mappings",
   230  			file: `{
   231  	                        "uidMappings":[ { "hostId":131072, "containerId":0, "length":userNsLength } ],
   232  	                        "gidMappings":[ { "hostId":131072, "containerId":0, "length":userNsLength }, { "hostId":196608, "containerId":0, "length":65536 } ]
   233                                 }`,
   234  			success: false,
   235  		},
   236  		{
   237  			name: "two UID mappings",
   238  			file: `{
   239  	                        "uidMappings":[ { "hostId":131072, "containerId":0, "length":65536 }, { "hostId":196608, "containerId":0, "length":65536 } ],
   240  	                        "gidMappings":[ { "hostId":131072, "containerId":0, "length":65536 } ]
   241                                 }`,
   242  			success: false,
   243  		},
   244  		{
   245  			name: "no root UID",
   246  			file: `{
   247  	                        "uidMappings":[ { "hostId":131072, "containerId":1, "length":65536 } ],
   248  	                        "gidMappings":[ { "hostId":131072, "containerId":0, "length":65536 } ]
   249                                 }`,
   250  			success: false,
   251  		},
   252  		{
   253  			name: "no root GID",
   254  			file: `{
   255  	                        "uidMappings":[ { "hostId":131072, "containerId":0, "length":65536 } ],
   256  	                        "gidMappings":[ { "hostId":131072, "containerId":1, "length":65536 } ]
   257                                 }`,
   258  			success: false,
   259  		},
   260  	}
   261  
   262  	testUserNsPodsManager := &testUserNsPodsManager{}
   263  	m, err := MakeUserNsManager(testUserNsPodsManager)
   264  	assert.NoError(t, err)
   265  
   266  	for _, tc := range cases {
   267  		t.Run(tc.name, func(t *testing.T) {
   268  			// We don't validate the result. It was parsed with the json parser, we trust that.
   269  			_, err = m.parseUserNsFileAndRecord(types.UID(tc.name), []byte(tc.file))
   270  			if (tc.success && err == nil) || (!tc.success && err != nil) {
   271  				return
   272  			}
   273  
   274  			t.Errorf("expected success: %v but got error: %v", tc.success, err)
   275  		})
   276  	}
   277  }
   278  
   279  func TestGetOrCreateUserNamespaceMappings(t *testing.T) {
   280  	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, pkgfeatures.UserNamespacesSupport, true)
   281  
   282  	trueVal := true
   283  	falseVal := false
   284  
   285  	cases := []struct {
   286  		name           string
   287  		pod            *v1.Pod
   288  		expMode        runtimeapi.NamespaceMode
   289  		runtimeUserns  bool
   290  		runtimeHandler string
   291  		success        bool
   292  		skipOnWindows  bool
   293  	}{
   294  		{
   295  			name:    "no user namespace",
   296  			pod:     &v1.Pod{},
   297  			expMode: runtimeapi.NamespaceMode_NODE,
   298  			success: true,
   299  		},
   300  		{
   301  			name:    "nil pod",
   302  			pod:     nil,
   303  			expMode: runtimeapi.NamespaceMode_NODE,
   304  			success: true,
   305  		},
   306  		{
   307  			name: "opt-in to host user namespace",
   308  			pod: &v1.Pod{
   309  				Spec: v1.PodSpec{
   310  					HostUsers: &trueVal,
   311  				},
   312  			},
   313  			expMode: runtimeapi.NamespaceMode_NODE,
   314  			success: true,
   315  		},
   316  		{
   317  			name: "user namespace",
   318  			pod: &v1.Pod{
   319  				Spec: v1.PodSpec{
   320  					HostUsers: &falseVal,
   321  				},
   322  			},
   323  			expMode:       runtimeapi.NamespaceMode_POD,
   324  			runtimeUserns: true,
   325  			success:       true,
   326  			skipOnWindows: true,
   327  		},
   328  		{
   329  			name: "user namespace, but no runtime support",
   330  			pod: &v1.Pod{
   331  				Spec: v1.PodSpec{
   332  					HostUsers: &falseVal,
   333  				},
   334  			},
   335  			runtimeUserns: false,
   336  		},
   337  		{
   338  			name: "user namespace, but runtime returns error",
   339  			pod: &v1.Pod{
   340  				Spec: v1.PodSpec{
   341  					HostUsers: &falseVal,
   342  				},
   343  			},
   344  			// This handler name makes the fake runtime return an error.
   345  			runtimeHandler: "error",
   346  		},
   347  	}
   348  
   349  	for _, tc := range cases {
   350  		t.Run(tc.name, func(t *testing.T) {
   351  			if tc.skipOnWindows && goruntime.GOOS == "windows" {
   352  				// TODO: remove skip once the failing test has been fixed.
   353  				t.Skip("Skip failing test on Windows.")
   354  			}
   355  			// These tests will create the userns file, so use an existing podDir.
   356  			testUserNsPodsManager := &testUserNsPodsManager{
   357  				podDir: t.TempDir(),
   358  				userns: tc.runtimeUserns,
   359  			}
   360  			m, err := MakeUserNsManager(testUserNsPodsManager)
   361  			assert.NoError(t, err)
   362  
   363  			userns, err := m.GetOrCreateUserNamespaceMappings(tc.pod, tc.runtimeHandler)
   364  			if (tc.success && err != nil) || (!tc.success && err == nil) {
   365  				t.Errorf("expected success: %v but got error: %v", tc.success, err)
   366  			}
   367  
   368  			if userns.GetMode() != tc.expMode {
   369  				t.Errorf("expected mode: %v but got: %v", tc.expMode, userns.GetMode())
   370  			}
   371  		})
   372  	}
   373  }
   374  
   375  func TestCleanupOrphanedPodUsernsAllocations(t *testing.T) {
   376  	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, pkgfeatures.UserNamespacesSupport, true)
   377  
   378  	cases := []struct {
   379  		name                 string
   380  		runningPods          []*kubecontainer.Pod
   381  		pods                 []*v1.Pod
   382  		listPods             []types.UID /* pods to list */
   383  		podSetBeforeCleanup  []types.UID /* pods to record before cleanup */
   384  		podSetAfterCleanup   []types.UID /* pods set expected after cleanup */
   385  		podUnsetAfterCleanup []types.UID /* pods set expected after cleanup */
   386  	}{
   387  		{
   388  			name:     "no stale pods",
   389  			listPods: []types.UID{"pod-1", "pod-2"},
   390  		},
   391  		{
   392  			name:                 "no stale pods set",
   393  			podSetBeforeCleanup:  []types.UID{"pod-1", "pod-2"},
   394  			listPods:             []types.UID{"pod-1", "pod-2"},
   395  			podUnsetAfterCleanup: []types.UID{"pod-1", "pod-2"},
   396  		},
   397  		{
   398  			name:                 "one running pod",
   399  			listPods:             []types.UID{"pod-1", "pod-2"},
   400  			podSetBeforeCleanup:  []types.UID{"pod-1", "pod-2"},
   401  			runningPods:          []*kubecontainer.Pod{{ID: "pod-1"}},
   402  			podSetAfterCleanup:   []types.UID{"pod-1"},
   403  			podUnsetAfterCleanup: []types.UID{"pod-2"},
   404  		},
   405  		{
   406  			name:                 "pod set before cleanup but not listed ==> unset",
   407  			podSetBeforeCleanup:  []types.UID{"pod-1", "pod-2"},
   408  			runningPods:          []*kubecontainer.Pod{{ID: "pod-1"}},
   409  			podUnsetAfterCleanup: []types.UID{"pod-1", "pod-2"},
   410  		},
   411  		{
   412  			name:                 "one pod",
   413  			listPods:             []types.UID{"pod-1", "pod-2"},
   414  			podSetBeforeCleanup:  []types.UID{"pod-1", "pod-2"},
   415  			pods:                 []*v1.Pod{{ObjectMeta: metav1.ObjectMeta{UID: "pod-1"}}},
   416  			podSetAfterCleanup:   []types.UID{"pod-1"},
   417  			podUnsetAfterCleanup: []types.UID{"pod-2"},
   418  		},
   419  		{
   420  			name:                 "no listed pods ==> all unset",
   421  			podSetBeforeCleanup:  []types.UID{"pod-1", "pod-2"},
   422  			podUnsetAfterCleanup: []types.UID{"pod-1", "pod-2"},
   423  		},
   424  	}
   425  
   426  	for _, tc := range cases {
   427  		t.Run(tc.name, func(t *testing.T) {
   428  			testUserNsPodsManager := &testUserNsPodsManager{
   429  				podDir:  t.TempDir(),
   430  				podList: tc.listPods,
   431  			}
   432  			m, err := MakeUserNsManager(testUserNsPodsManager)
   433  			require.NoError(t, err)
   434  
   435  			// Record the userns range as used
   436  			for i, pod := range tc.podSetBeforeCleanup {
   437  				err := m.record(pod, uint32((i+1)*65536), 65536)
   438  				require.NoError(t, err)
   439  			}
   440  
   441  			err = m.CleanupOrphanedPodUsernsAllocations(tc.pods, tc.runningPods)
   442  			require.NoError(t, err)
   443  
   444  			for _, pod := range tc.podSetAfterCleanup {
   445  				ok := m.podAllocated(pod)
   446  				assert.True(t, ok, "pod %q should be allocated", pod)
   447  			}
   448  
   449  			for _, pod := range tc.podUnsetAfterCleanup {
   450  				ok := m.podAllocated(pod)
   451  				assert.False(t, ok, "pod %q should not be allocated", pod)
   452  			}
   453  		})
   454  	}
   455  }
   456  
   457  type failingUserNsPodsManager struct {
   458  	testUserNsPodsManager
   459  }
   460  
   461  func (m *failingUserNsPodsManager) ListPodsFromDisk() ([]types.UID, error) {
   462  	return nil, os.ErrPermission
   463  }
   464  
   465  func TestMakeUserNsManagerFailsListPod(t *testing.T) {
   466  	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, pkgfeatures.UserNamespacesSupport, true)
   467  
   468  	testUserNsPodsManager := &failingUserNsPodsManager{}
   469  	_, err := MakeUserNsManager(testUserNsPodsManager)
   470  	assert.Error(t, err)
   471  	assert.ErrorContains(t, err, "read pods from disk")
   472  }
   473  
   474  func TestRecordBounds(t *testing.T) {
   475  	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, pkgfeatures.UserNamespacesSupport, true)
   476  
   477  	// Allow exactly for 1 pod
   478  	testUserNsPodsManager := &testUserNsPodsManager{
   479  		mappingFirstID: 65536,
   480  		mappingLen:     65536,
   481  		maxPods:        1,
   482  	}
   483  	m, err := MakeUserNsManager(testUserNsPodsManager)
   484  	require.NoError(t, err)
   485  
   486  	// The first pod allocation should succeed.
   487  	err = m.record(types.UID(fmt.Sprintf("%d", 0)), 65536, 65536)
   488  	require.NoError(t, err)
   489  
   490  	// The next allocation should fail, as there is no space left.
   491  	err = m.record(types.UID(fmt.Sprintf("%d", 2)), uint32(2*65536), 65536)
   492  	assert.Error(t, err)
   493  	assert.ErrorContains(t, err, "out of range")
   494  }