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