k8s.io/kubernetes@v1.29.3/pkg/kubelet/kuberuntime/helpers_linux_test.go (about) 1 /* 2 Copyright 2016 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package kuberuntime 18 19 import ( 20 "path/filepath" 21 "testing" 22 23 "github.com/stretchr/testify/assert" 24 "github.com/stretchr/testify/require" 25 26 v1 "k8s.io/api/core/v1" 27 utilfeature "k8s.io/apiserver/pkg/util/feature" 28 featuregatetesting "k8s.io/component-base/featuregate/testing" 29 runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1" 30 "k8s.io/kubernetes/pkg/features" 31 "k8s.io/kubernetes/pkg/kubelet/cm" 32 ) 33 34 func seccompLocalhostRef(profileName string) string { 35 return filepath.Join(fakeSeccompProfileRoot, profileName) 36 } 37 38 func TestMilliCPUToQuota(t *testing.T) { 39 for _, testCase := range []struct { 40 msg string 41 input int64 42 expected int64 43 period uint64 44 }{ 45 { 46 msg: "all-zero", 47 input: int64(0), 48 expected: int64(0), 49 period: uint64(0), 50 }, 51 { 52 msg: "5 input default quota and period", 53 input: int64(5), 54 expected: int64(1000), 55 period: uint64(100000), 56 }, 57 { 58 msg: "9 input default quota and period", 59 input: int64(9), 60 expected: int64(1000), 61 period: uint64(100000), 62 }, 63 { 64 msg: "10 input default quota and period", 65 input: int64(10), 66 expected: int64(1000), 67 period: uint64(100000), 68 }, 69 { 70 msg: "200 input 20k quota and default period", 71 input: int64(200), 72 expected: int64(20000), 73 period: uint64(100000), 74 }, 75 { 76 msg: "500 input 50k quota and default period", 77 input: int64(500), 78 expected: int64(50000), 79 period: uint64(100000), 80 }, 81 { 82 msg: "1k input 100k quota and default period", 83 input: int64(1000), 84 expected: int64(100000), 85 period: uint64(100000), 86 }, 87 { 88 msg: "1500 input 150k quota and default period", 89 input: int64(1500), 90 expected: int64(150000), 91 period: uint64(100000), 92 }} { 93 t.Run(testCase.msg, func(t *testing.T) { 94 quota := milliCPUToQuota(testCase.input, int64(testCase.period)) 95 if quota != testCase.expected { 96 t.Errorf("Input %v and %v, expected quota %v, but got quota %v", testCase.input, testCase.period, testCase.expected, quota) 97 } 98 }) 99 } 100 } 101 102 func TestMilliCPUToQuotaWithCustomCPUCFSQuotaPeriod(t *testing.T) { 103 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CPUCFSQuotaPeriod, true)() 104 105 for _, testCase := range []struct { 106 msg string 107 input int64 108 expected int64 109 period uint64 110 }{ 111 { 112 msg: "all-zero", 113 input: int64(0), 114 expected: int64(0), 115 period: uint64(0), 116 }, 117 { 118 msg: "5 input default quota and period", 119 input: int64(5), 120 expected: minQuotaPeriod, 121 period: uint64(100000), 122 }, 123 { 124 msg: "9 input default quota and period", 125 input: int64(9), 126 expected: minQuotaPeriod, 127 period: uint64(100000), 128 }, 129 { 130 msg: "10 input default quota and period", 131 input: int64(10), 132 expected: minQuotaPeriod, 133 period: uint64(100000), 134 }, 135 { 136 msg: "200 input 20k quota and default period", 137 input: int64(200), 138 expected: int64(20000), 139 period: uint64(100000), 140 }, 141 { 142 msg: "500 input 50k quota and default period", 143 input: int64(500), 144 expected: int64(50000), 145 period: uint64(100000), 146 }, 147 { 148 msg: "1k input 100k quota and default period", 149 input: int64(1000), 150 expected: int64(100000), 151 period: uint64(100000), 152 }, 153 { 154 msg: "1500 input 150k quota and default period", 155 input: int64(1500), 156 expected: int64(150000), 157 period: uint64(100000), 158 }, 159 { 160 msg: "5 input 10k period and default quota expected", 161 input: int64(5), 162 period: uint64(10000), 163 expected: minQuotaPeriod, 164 }, 165 { 166 msg: "5 input 5k period and default quota expected", 167 input: int64(5), 168 period: uint64(5000), 169 expected: minQuotaPeriod, 170 }, 171 { 172 msg: "9 input 10k period and default quota expected", 173 input: int64(9), 174 period: uint64(10000), 175 expected: minQuotaPeriod, 176 }, 177 { 178 msg: "10 input 200k period and 2000 quota expected", 179 input: int64(10), 180 period: uint64(200000), 181 expected: int64(2000), 182 }, 183 { 184 msg: "200 input 200k period and 40k quota", 185 input: int64(200), 186 period: uint64(200000), 187 expected: int64(40000), 188 }, 189 { 190 msg: "500 input 20k period and 20k expected quota", 191 input: int64(500), 192 period: uint64(20000), 193 expected: int64(10000), 194 }, 195 { 196 msg: "1000 input 10k period and 10k expected quota", 197 input: int64(1000), 198 period: uint64(10000), 199 expected: int64(10000), 200 }, 201 { 202 msg: "1500 input 5000 period and 7500 expected quota", 203 input: int64(1500), 204 period: uint64(5000), 205 expected: int64(7500), 206 }} { 207 t.Run(testCase.msg, func(t *testing.T) { 208 quota := milliCPUToQuota(testCase.input, int64(testCase.period)) 209 if quota != testCase.expected { 210 t.Errorf("Input %v and %v, expected quota %v, but got quota %v", testCase.input, testCase.period, testCase.expected, quota) 211 } 212 }) 213 } 214 } 215 216 func TestGetSeccompProfile(t *testing.T) { 217 _, _, m, err := createTestRuntimeManager() 218 require.NoError(t, err) 219 220 unconfinedProfile := &runtimeapi.SecurityProfile{ 221 ProfileType: runtimeapi.SecurityProfile_Unconfined, 222 } 223 224 runtimeDefaultProfile := &runtimeapi.SecurityProfile{ 225 ProfileType: runtimeapi.SecurityProfile_RuntimeDefault, 226 } 227 228 tests := []struct { 229 description string 230 annotation map[string]string 231 podSc *v1.PodSecurityContext 232 containerSc *v1.SecurityContext 233 containerName string 234 expectedProfile *runtimeapi.SecurityProfile 235 expectedError string 236 }{ 237 { 238 description: "no seccomp should return unconfined", 239 expectedProfile: unconfinedProfile, 240 }, 241 { 242 description: "pod seccomp profile set to unconfined returns unconfined", 243 podSc: &v1.PodSecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeUnconfined}}, 244 expectedProfile: unconfinedProfile, 245 }, 246 { 247 description: "container seccomp profile set to unconfined returns unconfined", 248 containerSc: &v1.SecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeUnconfined}}, 249 expectedProfile: unconfinedProfile, 250 }, 251 { 252 description: "pod seccomp profile set to SeccompProfileTypeRuntimeDefault returns runtime/default", 253 podSc: &v1.PodSecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeRuntimeDefault}}, 254 expectedProfile: runtimeDefaultProfile, 255 }, 256 { 257 description: "container seccomp profile set to SeccompProfileTypeRuntimeDefault returns runtime/default", 258 containerSc: &v1.SecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeRuntimeDefault}}, 259 expectedProfile: runtimeDefaultProfile, 260 }, 261 { 262 description: "pod seccomp profile set to SeccompProfileTypeLocalhost returns 'localhost/' + LocalhostProfile", 263 podSc: &v1.PodSecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeLocalhost, LocalhostProfile: getLocal("filename")}}, 264 expectedProfile: &runtimeapi.SecurityProfile{ 265 ProfileType: runtimeapi.SecurityProfile_Localhost, 266 LocalhostRef: seccompLocalhostRef("filename"), 267 }, 268 }, 269 { 270 description: "pod seccomp profile set to SeccompProfileTypeLocalhost with empty LocalhostProfile returns error", 271 podSc: &v1.PodSecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeLocalhost}}, 272 expectedError: "localhostProfile must be set if seccompProfile type is Localhost.", 273 }, 274 { 275 description: "container seccomp profile set to SeccompProfileTypeLocalhost with empty LocalhostProfile returns error", 276 containerSc: &v1.SecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeLocalhost}}, 277 expectedError: "localhostProfile must be set if seccompProfile type is Localhost.", 278 }, 279 { 280 description: "container seccomp profile set to SeccompProfileTypeLocalhost returns 'localhost/' + LocalhostProfile", 281 containerSc: &v1.SecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeLocalhost, LocalhostProfile: getLocal("filename2")}}, 282 expectedProfile: &runtimeapi.SecurityProfile{ 283 ProfileType: runtimeapi.SecurityProfile_Localhost, 284 LocalhostRef: seccompLocalhostRef("filename2"), 285 }, 286 }, 287 { 288 description: "prioritise container field over pod field", 289 podSc: &v1.PodSecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeUnconfined}}, 290 containerSc: &v1.SecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeRuntimeDefault}}, 291 expectedProfile: runtimeDefaultProfile, 292 }, 293 { 294 description: "prioritise container field over pod field", 295 podSc: &v1.PodSecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeLocalhost, LocalhostProfile: getLocal("field-pod-profile.json")}}, 296 containerSc: &v1.SecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeLocalhost, LocalhostProfile: getLocal("field-cont-profile.json")}}, 297 containerName: "container1", 298 expectedProfile: &runtimeapi.SecurityProfile{ 299 ProfileType: runtimeapi.SecurityProfile_Localhost, 300 LocalhostRef: seccompLocalhostRef("field-cont-profile.json"), 301 }, 302 }, 303 } 304 305 for i, test := range tests { 306 seccompProfile, err := m.getSeccompProfile(test.annotation, test.containerName, test.podSc, test.containerSc, false) 307 if test.expectedError != "" { 308 assert.EqualError(t, err, test.expectedError, "TestCase[%d]: %s", i, test.description) 309 } else { 310 assert.NoError(t, err, "TestCase[%d]: %s", i, test.description) 311 assert.Equal(t, test.expectedProfile, seccompProfile, "TestCase[%d]: %s", i, test.description) 312 } 313 } 314 } 315 316 func TestGetSeccompProfileDefaultSeccomp(t *testing.T) { 317 _, _, m, err := createTestRuntimeManager() 318 require.NoError(t, err) 319 320 unconfinedProfile := &runtimeapi.SecurityProfile{ 321 ProfileType: runtimeapi.SecurityProfile_Unconfined, 322 } 323 324 runtimeDefaultProfile := &runtimeapi.SecurityProfile{ 325 ProfileType: runtimeapi.SecurityProfile_RuntimeDefault, 326 } 327 328 tests := []struct { 329 description string 330 annotation map[string]string 331 podSc *v1.PodSecurityContext 332 containerSc *v1.SecurityContext 333 containerName string 334 expectedProfile *runtimeapi.SecurityProfile 335 expectedError string 336 }{ 337 { 338 description: "no seccomp should return RuntimeDefault", 339 expectedProfile: runtimeDefaultProfile, 340 }, 341 { 342 description: "pod seccomp profile set to unconfined returns unconfined", 343 podSc: &v1.PodSecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeUnconfined}}, 344 expectedProfile: unconfinedProfile, 345 }, 346 { 347 description: "container seccomp profile set to unconfined returns unconfined", 348 containerSc: &v1.SecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeUnconfined}}, 349 expectedProfile: unconfinedProfile, 350 }, 351 { 352 description: "pod seccomp profile set to SeccompProfileTypeRuntimeDefault returns runtime/default", 353 podSc: &v1.PodSecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeRuntimeDefault}}, 354 expectedProfile: runtimeDefaultProfile, 355 }, 356 { 357 description: "container seccomp profile set to SeccompProfileTypeRuntimeDefault returns runtime/default", 358 containerSc: &v1.SecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeRuntimeDefault}}, 359 expectedProfile: runtimeDefaultProfile, 360 }, 361 { 362 description: "pod seccomp profile set to SeccompProfileTypeLocalhost returns 'localhost/' + LocalhostProfile", 363 podSc: &v1.PodSecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeLocalhost, LocalhostProfile: getLocal("filename")}}, 364 expectedProfile: &runtimeapi.SecurityProfile{ 365 ProfileType: runtimeapi.SecurityProfile_Localhost, 366 LocalhostRef: seccompLocalhostRef("filename"), 367 }, 368 }, 369 { 370 description: "pod seccomp profile set to SeccompProfileTypeLocalhost with empty LocalhostProfile returns error", 371 podSc: &v1.PodSecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeLocalhost}}, 372 expectedError: "localhostProfile must be set if seccompProfile type is Localhost.", 373 }, 374 { 375 description: "container seccomp profile set to SeccompProfileTypeLocalhost with empty LocalhostProfile returns error", 376 containerSc: &v1.SecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeLocalhost}}, 377 expectedError: "localhostProfile must be set if seccompProfile type is Localhost.", 378 }, 379 { 380 description: "container seccomp profile set to SeccompProfileTypeLocalhost returns 'localhost/' + LocalhostProfile", 381 containerSc: &v1.SecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeLocalhost, LocalhostProfile: getLocal("filename2")}}, 382 expectedProfile: &runtimeapi.SecurityProfile{ 383 ProfileType: runtimeapi.SecurityProfile_Localhost, 384 LocalhostRef: seccompLocalhostRef("filename2"), 385 }, 386 }, 387 { 388 description: "prioritise container field over pod field", 389 podSc: &v1.PodSecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeUnconfined}}, 390 containerSc: &v1.SecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeRuntimeDefault}}, 391 expectedProfile: runtimeDefaultProfile, 392 }, 393 { 394 description: "prioritise container field over pod field", 395 podSc: &v1.PodSecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeLocalhost, LocalhostProfile: getLocal("field-pod-profile.json")}}, 396 containerSc: &v1.SecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeLocalhost, LocalhostProfile: getLocal("field-cont-profile.json")}}, 397 containerName: "container1", 398 expectedProfile: &runtimeapi.SecurityProfile{ 399 ProfileType: runtimeapi.SecurityProfile_Localhost, 400 LocalhostRef: seccompLocalhostRef("field-cont-profile.json"), 401 }, 402 }, 403 } 404 405 for i, test := range tests { 406 seccompProfile, err := m.getSeccompProfile(test.annotation, test.containerName, test.podSc, test.containerSc, true) 407 if test.expectedError != "" { 408 assert.EqualError(t, err, test.expectedError, "TestCase[%d]: %s", i, test.description) 409 } else { 410 assert.NoError(t, err, "TestCase[%d]: %s", i, test.description) 411 assert.Equal(t, test.expectedProfile, seccompProfile, "TestCase[%d]: %s", i, test.description) 412 } 413 } 414 } 415 416 func getLocal(v string) *string { 417 return &v 418 } 419 420 func TestSharesToMilliCPU(t *testing.T) { 421 knownMilliCPUToShares := map[int64]int64{ 422 0: 2, 423 1: 2, 424 2: 2, 425 3: 3, 426 4: 4, 427 32: 32, 428 64: 65, 429 100: 102, 430 250: 256, 431 500: 512, 432 1000: 1024, 433 1500: 1536, 434 2000: 2048, 435 } 436 437 t.Run("sharesToMilliCPUTest", func(t *testing.T) { 438 var testMilliCPU int64 439 for testMilliCPU = 0; testMilliCPU <= 2000; testMilliCPU++ { 440 shares := int64(cm.MilliCPUToShares(testMilliCPU)) 441 if expectedShares, found := knownMilliCPUToShares[testMilliCPU]; found { 442 if shares != expectedShares { 443 t.Errorf("Test milliCPIToShares: Input milliCPU %v, expected shares %v, but got %v", testMilliCPU, expectedShares, shares) 444 } 445 } 446 expectedMilliCPU := testMilliCPU 447 if testMilliCPU < 2 { 448 expectedMilliCPU = 2 449 } 450 milliCPU := sharesToMilliCPU(shares) 451 if milliCPU != expectedMilliCPU { 452 t.Errorf("Test sharesToMilliCPU: Input shares %v, expected milliCPU %v, but got %v", shares, expectedMilliCPU, milliCPU) 453 } 454 } 455 }) 456 } 457 458 func TestQuotaToMilliCPU(t *testing.T) { 459 for _, tc := range []struct { 460 name string 461 quota int64 462 period int64 463 expected int64 464 }{ 465 { 466 name: "50m", 467 quota: int64(5000), 468 period: int64(100000), 469 expected: int64(50), 470 }, 471 { 472 name: "750m", 473 quota: int64(75000), 474 period: int64(100000), 475 expected: int64(750), 476 }, 477 { 478 name: "1000m", 479 quota: int64(100000), 480 period: int64(100000), 481 expected: int64(1000), 482 }, 483 { 484 name: "1500m", 485 quota: int64(150000), 486 period: int64(100000), 487 expected: int64(1500), 488 }} { 489 t.Run(tc.name, func(t *testing.T) { 490 milliCPU := quotaToMilliCPU(tc.quota, tc.period) 491 if milliCPU != tc.expected { 492 t.Errorf("Test %s: Input quota %v and period %v, expected milliCPU %v, but got %v", tc.name, tc.quota, tc.period, tc.expected, milliCPU) 493 } 494 }) 495 } 496 }