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 "context" 21 "path/filepath" 22 goruntime "runtime" 23 "testing" 24 25 "github.com/containerd/containerd/oci" 26 imagespec "github.com/opencontainers/image-spec/specs-go/v1" 27 runtimespec "github.com/opencontainers/runtime-spec/specs-go" 28 "github.com/stretchr/testify/assert" 29 "github.com/stretchr/testify/require" 30 runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" 31 32 "github.com/containerd/containerd/pkg/cri/config" 33 "github.com/containerd/containerd/pkg/cri/constants" 34 "github.com/containerd/containerd/pkg/cri/opts" 35) 36 37func checkMount(t *testing.T, mounts []runtimespec.Mount, src, dest, typ string, 38 contains, notcontains []string) { 39 found := false 40 for _, m := range mounts { 41 if m.Source == src && m.Destination == dest { 42 assert.Equal(t, m.Type, typ) 43 for _, c := range contains { 44 assert.Contains(t, m.Options, c) 45 } 46 for _, n := range notcontains { 47 assert.NotContains(t, m.Options, n) 48 } 49 found = true 50 break 51 } 52 } 53 assert.True(t, found, "mount from %q to %q not found", src, dest) 54} 55 56const testImageName = "container-image-name" 57 58func TestGeneralContainerSpec(t *testing.T) { 59 testID := "test-id" 60 testPid := uint32(1234) 61 containerConfig, sandboxConfig, imageConfig, specCheck := getCreateContainerTestData() 62 ociRuntime := config.Runtime{} 63 c := newTestCRIService() 64 testSandboxID := "sandbox-id" 65 testContainerName := "container-name" 66 spec, err := c.containerSpec(testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime) 67 require.NoError(t, err) 68 specCheck(t, testID, testSandboxID, testPid, spec) 69} 70 71func TestPodAnnotationPassthroughContainerSpec(t *testing.T) { 72 if goruntime.GOOS == "darwin" { 73 t.Skip("not implemented on Darwin") 74 } 75 76 testID := "test-id" 77 testSandboxID := "sandbox-id" 78 testContainerName := "container-name" 79 testPid := uint32(1234) 80 81 for desc, test := range map[string]struct { 82 podAnnotations []string 83 configChange func(*runtime.PodSandboxConfig) 84 specCheck func(*testing.T, *runtimespec.Spec) 85 }{ 86 "a passthrough annotation should be passed as an OCI annotation": { 87 podAnnotations: []string{"c"}, 88 specCheck: func(t *testing.T, spec *runtimespec.Spec) { 89 assert.Equal(t, spec.Annotations["c"], "d") 90 }, 91 }, 92 "a non-passthrough annotation should not be passed as an OCI annotation": { 93 configChange: func(c *runtime.PodSandboxConfig) { 94 c.Annotations["d"] = "e" 95 }, 96 podAnnotations: []string{"c"}, 97 specCheck: func(t *testing.T, spec *runtimespec.Spec) { 98 assert.Equal(t, spec.Annotations["c"], "d") 99 _, ok := spec.Annotations["d"] 100 assert.False(t, ok) 101 }, 102 }, 103 "passthrough annotations should support wildcard match": { 104 configChange: func(c *runtime.PodSandboxConfig) { 105 c.Annotations["t.f"] = "j" 106 c.Annotations["z.g"] = "o" 107 c.Annotations["z"] = "o" 108 c.Annotations["y.ca"] = "b" 109 c.Annotations["y"] = "b" 110 }, 111 podAnnotations: []string{"t*", "z.*", "y.c*"}, 112 specCheck: func(t *testing.T, spec *runtimespec.Spec) { 113 t.Logf("%+v", spec.Annotations) 114 assert.Equal(t, spec.Annotations["t.f"], "j") 115 assert.Equal(t, spec.Annotations["z.g"], "o") 116 assert.Equal(t, spec.Annotations["y.ca"], "b") 117 _, ok := spec.Annotations["y"] 118 assert.False(t, ok) 119 _, ok = spec.Annotations["z"] 120 assert.False(t, ok) 121 }, 122 }, 123 } { 124 t.Run(desc, func(t *testing.T) { 125 c := newTestCRIService() 126 containerConfig, sandboxConfig, imageConfig, specCheck := getCreateContainerTestData() 127 if test.configChange != nil { 128 test.configChange(sandboxConfig) 129 } 130 131 ociRuntime := config.Runtime{ 132 PodAnnotations: test.podAnnotations, 133 } 134 spec, err := c.containerSpec(testID, testSandboxID, testPid, "", testContainerName, testImageName, 135 containerConfig, sandboxConfig, imageConfig, nil, ociRuntime) 136 assert.NoError(t, err) 137 assert.NotNil(t, spec) 138 specCheck(t, testID, testSandboxID, testPid, spec) 139 if test.specCheck != nil { 140 test.specCheck(t, spec) 141 } 142 }) 143 } 144} 145 146func TestContainerSpecCommand(t *testing.T) { 147 for desc, test := range map[string]struct { 148 criEntrypoint []string 149 criArgs []string 150 imageEntrypoint []string 151 imageArgs []string 152 expected []string 153 expectErr bool 154 }{ 155 "should use cri entrypoint if it's specified": { 156 criEntrypoint: []string{"a", "b"}, 157 imageEntrypoint: []string{"c", "d"}, 158 imageArgs: []string{"e", "f"}, 159 expected: []string{"a", "b"}, 160 }, 161 "should use cri entrypoint if it's specified even if it's empty": { 162 criEntrypoint: []string{}, 163 criArgs: []string{"a", "b"}, 164 imageEntrypoint: []string{"c", "d"}, 165 imageArgs: []string{"e", "f"}, 166 expected: []string{"a", "b"}, 167 }, 168 "should use cri entrypoint and args if they are specified": { 169 criEntrypoint: []string{"a", "b"}, 170 criArgs: []string{"c", "d"}, 171 imageEntrypoint: []string{"e", "f"}, 172 imageArgs: []string{"g", "h"}, 173 expected: []string{"a", "b", "c", "d"}, 174 }, 175 "should use image entrypoint if cri entrypoint is not specified": { 176 criArgs: []string{"a", "b"}, 177 imageEntrypoint: []string{"c", "d"}, 178 imageArgs: []string{"e", "f"}, 179 expected: []string{"c", "d", "a", "b"}, 180 }, 181 "should use image args if both cri entrypoint and args are not specified": { 182 imageEntrypoint: []string{"c", "d"}, 183 imageArgs: []string{"e", "f"}, 184 expected: []string{"c", "d", "e", "f"}, 185 }, 186 "should return error if both entrypoint and args are empty": { 187 expectErr: true, 188 }, 189 } { 190 191 config, _, imageConfig, _ := getCreateContainerTestData() 192 config.Command = test.criEntrypoint 193 config.Args = test.criArgs 194 imageConfig.Entrypoint = test.imageEntrypoint 195 imageConfig.Cmd = test.imageArgs 196 197 var spec runtimespec.Spec 198 err := opts.WithProcessArgs(config, imageConfig)(context.Background(), nil, nil, &spec) 199 if test.expectErr { 200 assert.Error(t, err) 201 continue 202 } 203 assert.NoError(t, err) 204 assert.Equal(t, test.expected, spec.Process.Args, desc) 205 } 206} 207 208func TestVolumeMounts(t *testing.T) { 209 testContainerRootDir := "test-container-root" 210 for desc, test := range map[string]struct { 211 criMounts []*runtime.Mount 212 imageVolumes map[string]struct{} 213 expectedMountDest []string 214 }{ 215 "should setup rw mount for image volumes": { 216 imageVolumes: map[string]struct{}{ 217 "/test-volume-1": {}, 218 "/test-volume-2": {}, 219 }, 220 expectedMountDest: []string{ 221 "/test-volume-1", 222 "/test-volume-2", 223 }, 224 }, 225 "should skip image volumes if already mounted by CRI": { 226 criMounts: []*runtime.Mount{ 227 { 228 ContainerPath: "/test-volume-1", 229 HostPath: "/test-hostpath-1", 230 }, 231 }, 232 imageVolumes: map[string]struct{}{ 233 "/test-volume-1": {}, 234 "/test-volume-2": {}, 235 }, 236 expectedMountDest: []string{ 237 "/test-volume-2", 238 }, 239 }, 240 "should compare and return cleanpath": { 241 criMounts: []*runtime.Mount{ 242 { 243 ContainerPath: "/test-volume-1", 244 HostPath: "/test-hostpath-1", 245 }, 246 }, 247 imageVolumes: map[string]struct{}{ 248 "/test-volume-1/": {}, 249 "/test-volume-2/": {}, 250 }, 251 expectedMountDest: []string{ 252 "/test-volume-2/", 253 }, 254 }, 255 } { 256 t.Logf("TestCase %q", desc) 257 config := &imagespec.ImageConfig{ 258 Volumes: test.imageVolumes, 259 } 260 c := newTestCRIService() 261 got := c.volumeMounts(testContainerRootDir, test.criMounts, config) 262 assert.Len(t, got, len(test.expectedMountDest)) 263 for _, dest := range test.expectedMountDest { 264 found := false 265 for _, m := range got { 266 if m.ContainerPath == dest { 267 found = true 268 assert.Equal(t, 269 filepath.Dir(m.HostPath), 270 filepath.Join(testContainerRootDir, "volumes")) 271 break 272 } 273 } 274 assert.True(t, found) 275 } 276 } 277} 278 279func TestContainerAnnotationPassthroughContainerSpec(t *testing.T) { 280 if goruntime.GOOS == "darwin" { 281 t.Skip("not implemented on Darwin") 282 } 283 284 testID := "test-id" 285 testSandboxID := "sandbox-id" 286 testContainerName := "container-name" 287 testPid := uint32(1234) 288 289 for desc, test := range map[string]struct { 290 podAnnotations []string 291 containerAnnotations []string 292 podConfigChange func(*runtime.PodSandboxConfig) 293 configChange func(*runtime.ContainerConfig) 294 specCheck func(*testing.T, *runtimespec.Spec) 295 }{ 296 "passthrough annotations from pod and container should be passed as an OCI annotation": { 297 podConfigChange: func(p *runtime.PodSandboxConfig) { 298 p.Annotations["pod.annotation.1"] = "1" 299 p.Annotations["pod.annotation.2"] = "2" 300 p.Annotations["pod.annotation.3"] = "3" 301 }, 302 configChange: func(c *runtime.ContainerConfig) { 303 c.Annotations["container.annotation.1"] = "1" 304 c.Annotations["container.annotation.2"] = "2" 305 c.Annotations["container.annotation.3"] = "3" 306 }, 307 podAnnotations: []string{"pod.annotation.1"}, 308 containerAnnotations: []string{"container.annotation.1"}, 309 specCheck: func(t *testing.T, spec *runtimespec.Spec) { 310 assert.Equal(t, "1", spec.Annotations["container.annotation.1"]) 311 _, ok := spec.Annotations["container.annotation.2"] 312 assert.False(t, ok) 313 _, ok = spec.Annotations["container.annotation.3"] 314 assert.False(t, ok) 315 assert.Equal(t, "1", spec.Annotations["pod.annotation.1"]) 316 _, ok = spec.Annotations["pod.annotation.2"] 317 assert.False(t, ok) 318 _, ok = spec.Annotations["pod.annotation.3"] 319 assert.False(t, ok) 320 }, 321 }, 322 "passthrough annotations from pod and container should support wildcard": { 323 podConfigChange: func(p *runtime.PodSandboxConfig) { 324 p.Annotations["pod.annotation.1"] = "1" 325 p.Annotations["pod.annotation.2"] = "2" 326 p.Annotations["pod.annotation.3"] = "3" 327 }, 328 configChange: func(c *runtime.ContainerConfig) { 329 c.Annotations["container.annotation.1"] = "1" 330 c.Annotations["container.annotation.2"] = "2" 331 c.Annotations["container.annotation.3"] = "3" 332 }, 333 podAnnotations: []string{"pod.annotation.*"}, 334 containerAnnotations: []string{"container.annotation.*"}, 335 specCheck: func(t *testing.T, spec *runtimespec.Spec) { 336 assert.Equal(t, "1", spec.Annotations["container.annotation.1"]) 337 assert.Equal(t, "2", spec.Annotations["container.annotation.2"]) 338 assert.Equal(t, "3", spec.Annotations["container.annotation.3"]) 339 assert.Equal(t, "1", spec.Annotations["pod.annotation.1"]) 340 assert.Equal(t, "2", spec.Annotations["pod.annotation.2"]) 341 assert.Equal(t, "3", spec.Annotations["pod.annotation.3"]) 342 }, 343 }, 344 "annotations should not pass through if no passthrough annotations are configured": { 345 podConfigChange: func(p *runtime.PodSandboxConfig) { 346 p.Annotations["pod.annotation.1"] = "1" 347 p.Annotations["pod.annotation.2"] = "2" 348 p.Annotations["pod.annotation.3"] = "3" 349 }, 350 configChange: func(c *runtime.ContainerConfig) { 351 c.Annotations["container.annotation.1"] = "1" 352 c.Annotations["container.annotation.2"] = "2" 353 c.Annotations["container.annotation.3"] = "3" 354 }, 355 podAnnotations: []string{}, 356 containerAnnotations: []string{}, 357 specCheck: func(t *testing.T, spec *runtimespec.Spec) { 358 _, ok := spec.Annotations["container.annotation.1"] 359 assert.False(t, ok) 360 _, ok = spec.Annotations["container.annotation.2"] 361 assert.False(t, ok) 362 _, ok = spec.Annotations["container.annotation.3"] 363 assert.False(t, ok) 364 _, ok = spec.Annotations["pod.annotation.1"] 365 assert.False(t, ok) 366 _, ok = spec.Annotations["pod.annotation.2"] 367 assert.False(t, ok) 368 _, ok = spec.Annotations["pod.annotation.3"] 369 assert.False(t, ok) 370 }, 371 }, 372 } { 373 t.Run(desc, func(t *testing.T) { 374 c := newTestCRIService() 375 containerConfig, sandboxConfig, imageConfig, specCheck := getCreateContainerTestData() 376 if test.configChange != nil { 377 test.configChange(containerConfig) 378 } 379 if test.podConfigChange != nil { 380 test.podConfigChange(sandboxConfig) 381 } 382 ociRuntime := config.Runtime{ 383 PodAnnotations: test.podAnnotations, 384 ContainerAnnotations: test.containerAnnotations, 385 } 386 spec, err := c.containerSpec(testID, testSandboxID, testPid, "", testContainerName, testImageName, 387 containerConfig, sandboxConfig, imageConfig, nil, ociRuntime) 388 assert.NoError(t, err) 389 assert.NotNil(t, spec) 390 specCheck(t, testID, testSandboxID, testPid, spec) 391 if test.specCheck != nil { 392 test.specCheck(t, spec) 393 } 394 }) 395 } 396} 397 398func TestBaseRuntimeSpec(t *testing.T) { 399 c := newTestCRIService() 400 c.baseOCISpecs = map[string]*oci.Spec{ 401 "/etc/containerd/cri-base.json": { 402 Version: "1.0.2", 403 Hostname: "old", 404 }, 405 } 406 407 out, err := c.runtimeSpec("id1", "/etc/containerd/cri-base.json", oci.WithHostname("new")) 408 assert.NoError(t, err) 409 410 assert.Equal(t, "1.0.2", out.Version) 411 assert.Equal(t, "new", out.Hostname) 412 413 // Make sure original base spec not changed 414 assert.NotEqual(t, out, c.baseOCISpecs["/etc/containerd/cri-base.json"]) 415 assert.Equal(t, c.baseOCISpecs["/etc/containerd/cri-base.json"].Hostname, "old") 416 417 assert.Equal(t, filepath.Join("/", constants.K8sContainerdNamespace, "id1"), out.Linux.CgroupsPath) 418} 419