1/* 2 Copyright The containerd 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 17package server 18 19import ( 20 "net" 21 "testing" 22 23 cni "github.com/containerd/go-cni" 24 "github.com/containerd/typeurl" 25 imagespec "github.com/opencontainers/image-spec/specs-go/v1" 26 runtimespec "github.com/opencontainers/runtime-spec/specs-go" 27 "github.com/stretchr/testify/assert" 28 runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" 29 30 "github.com/containerd/containerd/pkg/cri/annotations" 31 criconfig "github.com/containerd/containerd/pkg/cri/config" 32 sandboxstore "github.com/containerd/containerd/pkg/cri/store/sandbox" 33) 34 35func TestSandboxContainerSpec(t *testing.T) { 36 testID := "test-id" 37 nsPath := "test-cni" 38 for desc, test := range map[string]struct { 39 configChange func(*runtime.PodSandboxConfig) 40 podAnnotations []string 41 imageConfigChange func(*imagespec.ImageConfig) 42 specCheck func(*testing.T, *runtimespec.Spec) 43 expectErr bool 44 }{ 45 "should return error when entrypoint and cmd are empty": { 46 imageConfigChange: func(c *imagespec.ImageConfig) { 47 c.Entrypoint = nil 48 c.Cmd = nil 49 }, 50 expectErr: true, 51 }, 52 "a passthrough annotation should be passed as an OCI annotation": { 53 podAnnotations: []string{"c"}, 54 specCheck: func(t *testing.T, spec *runtimespec.Spec) { 55 assert.Equal(t, spec.Annotations["c"], "d") 56 }, 57 }, 58 "a non-passthrough annotation should not be passed as an OCI annotation": { 59 configChange: func(c *runtime.PodSandboxConfig) { 60 c.Annotations["d"] = "e" 61 }, 62 podAnnotations: []string{"c"}, 63 specCheck: func(t *testing.T, spec *runtimespec.Spec) { 64 assert.Equal(t, spec.Annotations["c"], "d") 65 _, ok := spec.Annotations["d"] 66 assert.False(t, ok) 67 }, 68 }, 69 "passthrough annotations should support wildcard match": { 70 configChange: func(c *runtime.PodSandboxConfig) { 71 c.Annotations["t.f"] = "j" 72 c.Annotations["z.g"] = "o" 73 c.Annotations["z"] = "o" 74 c.Annotations["y.ca"] = "b" 75 c.Annotations["y"] = "b" 76 }, 77 podAnnotations: []string{"t*", "z.*", "y.c*"}, 78 specCheck: func(t *testing.T, spec *runtimespec.Spec) { 79 assert.Equal(t, spec.Annotations["t.f"], "j") 80 assert.Equal(t, spec.Annotations["z.g"], "o") 81 assert.Equal(t, spec.Annotations["y.ca"], "b") 82 _, ok := spec.Annotations["y"] 83 assert.False(t, ok) 84 _, ok = spec.Annotations["z"] 85 assert.False(t, ok) 86 }, 87 }, 88 } { 89 t.Logf("TestCase %q", desc) 90 c := newTestCRIService() 91 config, imageConfig, specCheck := getRunPodSandboxTestData() 92 if test.configChange != nil { 93 test.configChange(config) 94 } 95 96 if test.imageConfigChange != nil { 97 test.imageConfigChange(imageConfig) 98 } 99 spec, err := c.sandboxContainerSpec(testID, config, imageConfig, nsPath, 100 test.podAnnotations) 101 if test.expectErr { 102 assert.Error(t, err) 103 assert.Nil(t, spec) 104 continue 105 } 106 assert.NoError(t, err) 107 assert.NotNil(t, spec) 108 specCheck(t, testID, spec) 109 if test.specCheck != nil { 110 test.specCheck(t, spec) 111 } 112 } 113} 114 115func TestTypeurlMarshalUnmarshalSandboxMeta(t *testing.T) { 116 for desc, test := range map[string]struct { 117 configChange func(*runtime.PodSandboxConfig) 118 }{ 119 "should marshal original config": {}, 120 "should marshal Linux": { 121 configChange: func(c *runtime.PodSandboxConfig) { 122 if c.Linux == nil { 123 c.Linux = &runtime.LinuxPodSandboxConfig{} 124 } 125 c.Linux.SecurityContext = &runtime.LinuxSandboxSecurityContext{ 126 NamespaceOptions: &runtime.NamespaceOption{ 127 Network: runtime.NamespaceMode_NODE, 128 Pid: runtime.NamespaceMode_NODE, 129 Ipc: runtime.NamespaceMode_NODE, 130 }, 131 SupplementalGroups: []int64{1111, 2222}, 132 } 133 }, 134 }, 135 } { 136 t.Logf("TestCase %q", desc) 137 meta := &sandboxstore.Metadata{ 138 ID: "1", 139 Name: "sandbox_1", 140 NetNSPath: "/home/cloud", 141 } 142 meta.Config, _, _ = getRunPodSandboxTestData() 143 if test.configChange != nil { 144 test.configChange(meta.Config) 145 } 146 147 any, err := typeurl.MarshalAny(meta) 148 assert.NoError(t, err) 149 data, err := typeurl.UnmarshalAny(any) 150 assert.NoError(t, err) 151 assert.IsType(t, &sandboxstore.Metadata{}, data) 152 curMeta, ok := data.(*sandboxstore.Metadata) 153 assert.True(t, ok) 154 assert.Equal(t, meta, curMeta) 155 } 156} 157 158func TestToCNIPortMappings(t *testing.T) { 159 for desc, test := range map[string]struct { 160 criPortMappings []*runtime.PortMapping 161 cniPortMappings []cni.PortMapping 162 }{ 163 "empty CRI port mapping should map to empty CNI port mapping": {}, 164 "CRI port mapping should be converted to CNI port mapping properly": { 165 criPortMappings: []*runtime.PortMapping{ 166 { 167 Protocol: runtime.Protocol_UDP, 168 ContainerPort: 1234, 169 HostPort: 5678, 170 HostIp: "123.124.125.126", 171 }, 172 { 173 Protocol: runtime.Protocol_TCP, 174 ContainerPort: 4321, 175 HostPort: 8765, 176 HostIp: "126.125.124.123", 177 }, 178 { 179 Protocol: runtime.Protocol_SCTP, 180 ContainerPort: 1234, 181 HostPort: 5678, 182 HostIp: "123.124.125.126", 183 }, 184 }, 185 cniPortMappings: []cni.PortMapping{ 186 { 187 HostPort: 5678, 188 ContainerPort: 1234, 189 Protocol: "udp", 190 HostIP: "123.124.125.126", 191 }, 192 { 193 HostPort: 8765, 194 ContainerPort: 4321, 195 Protocol: "tcp", 196 HostIP: "126.125.124.123", 197 }, 198 { 199 HostPort: 5678, 200 ContainerPort: 1234, 201 Protocol: "sctp", 202 HostIP: "123.124.125.126", 203 }, 204 }, 205 }, 206 "CRI port mapping without host port should be skipped": { 207 criPortMappings: []*runtime.PortMapping{ 208 { 209 Protocol: runtime.Protocol_UDP, 210 ContainerPort: 1234, 211 HostIp: "123.124.125.126", 212 }, 213 { 214 Protocol: runtime.Protocol_TCP, 215 ContainerPort: 4321, 216 HostPort: 8765, 217 HostIp: "126.125.124.123", 218 }, 219 }, 220 cniPortMappings: []cni.PortMapping{ 221 { 222 HostPort: 8765, 223 ContainerPort: 4321, 224 Protocol: "tcp", 225 HostIP: "126.125.124.123", 226 }, 227 }, 228 }, 229 "CRI port mapping with unsupported protocol should be skipped": { 230 criPortMappings: []*runtime.PortMapping{ 231 { 232 Protocol: runtime.Protocol_TCP, 233 ContainerPort: 4321, 234 HostPort: 8765, 235 HostIp: "126.125.124.123", 236 }, 237 }, 238 cniPortMappings: []cni.PortMapping{ 239 { 240 HostPort: 8765, 241 ContainerPort: 4321, 242 Protocol: "tcp", 243 HostIP: "126.125.124.123", 244 }, 245 }, 246 }, 247 } { 248 t.Logf("TestCase %q", desc) 249 assert.Equal(t, test.cniPortMappings, toCNIPortMappings(test.criPortMappings)) 250 } 251} 252 253func TestSelectPodIP(t *testing.T) { 254 for desc, test := range map[string]struct { 255 ips []string 256 expectedIP string 257 expectedAdditionalIPs []string 258 }{ 259 "ipv4 should be picked even if ipv6 comes first": { 260 ips: []string{"2001:db8:85a3::8a2e:370:7334", "192.168.17.43"}, 261 expectedIP: "192.168.17.43", 262 expectedAdditionalIPs: []string{"2001:db8:85a3::8a2e:370:7334"}, 263 }, 264 "ipv4 should be picked when there is only ipv4": { 265 ips: []string{"192.168.17.43"}, 266 expectedIP: "192.168.17.43", 267 expectedAdditionalIPs: nil, 268 }, 269 "ipv6 should be picked when there is no ipv4": { 270 ips: []string{"2001:db8:85a3::8a2e:370:7334"}, 271 expectedIP: "2001:db8:85a3::8a2e:370:7334", 272 expectedAdditionalIPs: nil, 273 }, 274 "the first ipv4 should be picked when there are multiple ipv4": { // unlikely to happen 275 ips: []string{"2001:db8:85a3::8a2e:370:7334", "192.168.17.43", "2001:db8:85a3::8a2e:370:7335", "192.168.17.45"}, 276 expectedIP: "192.168.17.43", 277 expectedAdditionalIPs: []string{"2001:db8:85a3::8a2e:370:7334", "2001:db8:85a3::8a2e:370:7335", "192.168.17.45"}, 278 }, 279 } { 280 t.Logf("TestCase %q", desc) 281 var ipConfigs []*cni.IPConfig 282 for _, ip := range test.ips { 283 ipConfigs = append(ipConfigs, &cni.IPConfig{ 284 IP: net.ParseIP(ip), 285 }) 286 } 287 ip, additionalIPs := selectPodIPs(ipConfigs) 288 assert.Equal(t, test.expectedIP, ip) 289 assert.Equal(t, test.expectedAdditionalIPs, additionalIPs) 290 } 291} 292 293func TestHostAccessingSandbox(t *testing.T) { 294 privilegedContext := &runtime.PodSandboxConfig{ 295 Linux: &runtime.LinuxPodSandboxConfig{ 296 SecurityContext: &runtime.LinuxSandboxSecurityContext{ 297 Privileged: true, 298 }, 299 }, 300 } 301 nonPrivilegedContext := &runtime.PodSandboxConfig{ 302 Linux: &runtime.LinuxPodSandboxConfig{ 303 SecurityContext: &runtime.LinuxSandboxSecurityContext{ 304 Privileged: false, 305 }, 306 }, 307 } 308 hostNamespace := &runtime.PodSandboxConfig{ 309 Linux: &runtime.LinuxPodSandboxConfig{ 310 SecurityContext: &runtime.LinuxSandboxSecurityContext{ 311 Privileged: false, 312 NamespaceOptions: &runtime.NamespaceOption{ 313 Network: runtime.NamespaceMode_NODE, 314 Pid: runtime.NamespaceMode_NODE, 315 Ipc: runtime.NamespaceMode_NODE, 316 }, 317 }, 318 }, 319 } 320 tests := []struct { 321 name string 322 config *runtime.PodSandboxConfig 323 want bool 324 }{ 325 {"Security Context is nil", nil, false}, 326 {"Security Context is privileged", privilegedContext, false}, 327 {"Security Context is not privileged", nonPrivilegedContext, false}, 328 {"Security Context namespace host access", hostNamespace, true}, 329 } 330 for _, tt := range tests { 331 t.Run(tt.name, func(t *testing.T) { 332 if got := hostAccessingSandbox(tt.config); got != tt.want { 333 t.Errorf("hostAccessingSandbox() = %v, want %v", got, tt.want) 334 } 335 }) 336 } 337} 338 339func TestGetSandboxRuntime(t *testing.T) { 340 untrustedWorkloadRuntime := criconfig.Runtime{ 341 Type: "io.containerd.runtime.v1.linux", 342 Engine: "untrusted-workload-runtime", 343 Root: "", 344 } 345 346 defaultRuntime := criconfig.Runtime{ 347 Type: "io.containerd.runtime.v1.linux", 348 Engine: "default-runtime", 349 Root: "", 350 } 351 352 fooRuntime := criconfig.Runtime{ 353 Type: "io.containerd.runtime.v1.linux", 354 Engine: "foo-bar", 355 Root: "", 356 } 357 358 for desc, test := range map[string]struct { 359 sandboxConfig *runtime.PodSandboxConfig 360 runtimeHandler string 361 runtimes map[string]criconfig.Runtime 362 expectErr bool 363 expectedRuntime criconfig.Runtime 364 }{ 365 "should return error if untrusted workload requires host access": { 366 sandboxConfig: &runtime.PodSandboxConfig{ 367 Linux: &runtime.LinuxPodSandboxConfig{ 368 SecurityContext: &runtime.LinuxSandboxSecurityContext{ 369 Privileged: false, 370 NamespaceOptions: &runtime.NamespaceOption{ 371 Network: runtime.NamespaceMode_NODE, 372 Pid: runtime.NamespaceMode_NODE, 373 Ipc: runtime.NamespaceMode_NODE, 374 }, 375 }, 376 }, 377 Annotations: map[string]string{ 378 annotations.UntrustedWorkload: "true", 379 }, 380 }, 381 runtimes: map[string]criconfig.Runtime{ 382 criconfig.RuntimeDefault: defaultRuntime, 383 criconfig.RuntimeUntrusted: untrustedWorkloadRuntime, 384 }, 385 expectErr: true, 386 }, 387 "should use untrusted workload runtime for untrusted workload": { 388 sandboxConfig: &runtime.PodSandboxConfig{ 389 Annotations: map[string]string{ 390 annotations.UntrustedWorkload: "true", 391 }, 392 }, 393 runtimes: map[string]criconfig.Runtime{ 394 criconfig.RuntimeDefault: defaultRuntime, 395 criconfig.RuntimeUntrusted: untrustedWorkloadRuntime, 396 }, 397 expectedRuntime: untrustedWorkloadRuntime, 398 }, 399 "should use default runtime for regular workload": { 400 sandboxConfig: &runtime.PodSandboxConfig{}, 401 runtimes: map[string]criconfig.Runtime{ 402 criconfig.RuntimeDefault: defaultRuntime, 403 }, 404 expectedRuntime: defaultRuntime, 405 }, 406 "should use default runtime for trusted workload": { 407 sandboxConfig: &runtime.PodSandboxConfig{ 408 Annotations: map[string]string{ 409 annotations.UntrustedWorkload: "false", 410 }, 411 }, 412 runtimes: map[string]criconfig.Runtime{ 413 criconfig.RuntimeDefault: defaultRuntime, 414 criconfig.RuntimeUntrusted: untrustedWorkloadRuntime, 415 }, 416 expectedRuntime: defaultRuntime, 417 }, 418 "should return error if untrusted workload runtime is required but not configured": { 419 sandboxConfig: &runtime.PodSandboxConfig{ 420 Annotations: map[string]string{ 421 annotations.UntrustedWorkload: "true", 422 }, 423 }, 424 runtimes: map[string]criconfig.Runtime{ 425 criconfig.RuntimeDefault: defaultRuntime, 426 }, 427 expectErr: true, 428 }, 429 "should use 'untrusted' runtime for untrusted workload": { 430 sandboxConfig: &runtime.PodSandboxConfig{ 431 Annotations: map[string]string{ 432 annotations.UntrustedWorkload: "true", 433 }, 434 }, 435 runtimes: map[string]criconfig.Runtime{ 436 criconfig.RuntimeDefault: defaultRuntime, 437 criconfig.RuntimeUntrusted: untrustedWorkloadRuntime, 438 }, 439 expectedRuntime: untrustedWorkloadRuntime, 440 }, 441 "should use 'untrusted' runtime for untrusted workload & handler": { 442 sandboxConfig: &runtime.PodSandboxConfig{ 443 Annotations: map[string]string{ 444 annotations.UntrustedWorkload: "true", 445 }, 446 }, 447 runtimeHandler: "untrusted", 448 runtimes: map[string]criconfig.Runtime{ 449 criconfig.RuntimeDefault: defaultRuntime, 450 criconfig.RuntimeUntrusted: untrustedWorkloadRuntime, 451 }, 452 expectedRuntime: untrustedWorkloadRuntime, 453 }, 454 "should return an error if untrusted annotation with conflicting handler": { 455 sandboxConfig: &runtime.PodSandboxConfig{ 456 Annotations: map[string]string{ 457 annotations.UntrustedWorkload: "true", 458 }, 459 }, 460 runtimeHandler: "foo", 461 runtimes: map[string]criconfig.Runtime{ 462 criconfig.RuntimeDefault: defaultRuntime, 463 criconfig.RuntimeUntrusted: untrustedWorkloadRuntime, 464 "foo": fooRuntime, 465 }, 466 expectErr: true, 467 }, 468 "should use correct runtime for a runtime handler": { 469 sandboxConfig: &runtime.PodSandboxConfig{}, 470 runtimeHandler: "foo", 471 runtimes: map[string]criconfig.Runtime{ 472 criconfig.RuntimeDefault: defaultRuntime, 473 criconfig.RuntimeUntrusted: untrustedWorkloadRuntime, 474 "foo": fooRuntime, 475 }, 476 expectedRuntime: fooRuntime, 477 }, 478 "should return error if runtime handler is required but not configured": { 479 sandboxConfig: &runtime.PodSandboxConfig{}, 480 runtimeHandler: "bar", 481 runtimes: map[string]criconfig.Runtime{ 482 criconfig.RuntimeDefault: defaultRuntime, 483 "foo": fooRuntime, 484 }, 485 expectErr: true, 486 }, 487 } { 488 t.Run(desc, func(t *testing.T) { 489 cri := newTestCRIService() 490 cri.config = criconfig.Config{ 491 PluginConfig: criconfig.DefaultConfig(), 492 } 493 cri.config.ContainerdConfig.DefaultRuntimeName = criconfig.RuntimeDefault 494 cri.config.ContainerdConfig.Runtimes = test.runtimes 495 r, err := cri.getSandboxRuntime(test.sandboxConfig, test.runtimeHandler) 496 assert.Equal(t, test.expectErr, err != nil) 497 assert.Equal(t, test.expectedRuntime, r) 498 }) 499 } 500} 501