250 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			250 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| /*
 | |
|    Copyright The containerd Authors.
 | |
| 
 | |
|    Licensed under the Apache License, Version 2.0 (the "License");
 | |
|    you may not use this file except in compliance with the License.
 | |
|    You may obtain a copy of the License at
 | |
| 
 | |
|        http://www.apache.org/licenses/LICENSE-2.0
 | |
| 
 | |
|    Unless required by applicable law or agreed to in writing, software
 | |
|    distributed under the License is distributed on an "AS IS" BASIS,
 | |
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
|    See the License for the specific language governing permissions and
 | |
|    limitations under the License.
 | |
| */
 | |
| 
 | |
| package opts
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"strings"
 | |
| 	"testing"
 | |
| 
 | |
| 	"github.com/opencontainers/runtime-spec/specs-go"
 | |
| 	"github.com/stretchr/testify/assert"
 | |
| 	"github.com/stretchr/testify/require"
 | |
| 	runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
 | |
| 
 | |
| 	"github.com/containerd/containerd/containers"
 | |
| 	"github.com/containerd/containerd/namespaces"
 | |
| 	"github.com/containerd/containerd/oci"
 | |
| 	osinterface "github.com/containerd/containerd/pkg/os"
 | |
| )
 | |
| 
 | |
| func TestWithDevices(t *testing.T) {
 | |
| 	testcases := []struct {
 | |
| 		name    string
 | |
| 		devices []*runtime.Device
 | |
| 		isLCOW  bool
 | |
| 
 | |
| 		expectError            bool
 | |
| 		expectedWindowsDevices []specs.WindowsDevice
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "empty",
 | |
| 
 | |
| 			expectError: false,
 | |
| 		},
 | |
| 		// The only supported field is HostPath
 | |
| 		{
 | |
| 			name:    "empty fields",
 | |
| 			devices: []*runtime.Device{{}},
 | |
| 
 | |
| 			expectError: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name:    "containerPath",
 | |
| 			devices: []*runtime.Device{{ContainerPath: "something"}},
 | |
| 
 | |
| 			expectError: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name:    "permissions",
 | |
| 			devices: []*runtime.Device{{Permissions: "something"}},
 | |
| 
 | |
| 			expectError: true,
 | |
| 		},
 | |
| 		// Produced by https://github.com/aarnaud/k8s-directx-device-plugin/blob/0f3db32622daa577c85621941682bee6f9080954/cmd/k8s-device-plugin/main.go
 | |
| 		// This is also the syntax dockershim and cri-dockerd support (or rather, pass through to docker, which parses this syntax)
 | |
| 		{
 | |
| 			name:    "hostPath_docker_style",
 | |
| 			devices: []*runtime.Device{{HostPath: "class/5B45201D-F2F2-4F3B-85BB-30FF1F953599"}},
 | |
| 
 | |
| 			expectError:            false,
 | |
| 			expectedWindowsDevices: []specs.WindowsDevice{{ID: "5B45201D-F2F2-4F3B-85BB-30FF1F953599", IDType: "class"}},
 | |
| 		},
 | |
| 		// Docker _only_ accepts `class` ID Type, so anything else should fail.
 | |
| 		// See https://github.com/moby/moby/blob/v20.10.13/daemon/oci_windows.go#L283-L294
 | |
| 		{
 | |
| 			name:    "hostPath_docker_style_non-class_idtype",
 | |
| 			devices: []*runtime.Device{{HostPath: "vpci-location-path/5B45201D-F2F2-4F3B-85BB-30FF1F953599"}},
 | |
| 
 | |
| 			expectError: true,
 | |
| 		},
 | |
| 		// A bunch of examples from https://github.com/microsoft/hcsshim/blob/v0.9.2/test/cri-containerd/container_virtual_device_test.go
 | |
| 		{
 | |
| 			name: "hostPath_hcsshim_lcow_gpu",
 | |
| 			// Not actually a GPU PCIP instance, but my personal machine doesn't have any PCIP devices, so I found one on the 'net.
 | |
| 			devices: []*runtime.Device{{HostPath: `gpu://PCIP\VEN_8086&DEV_43A2&SUBSYS_72708086&REV_00\3&11583659&0&F5`}},
 | |
| 			isLCOW:  true,
 | |
| 
 | |
| 			expectError:            false,
 | |
| 			expectedWindowsDevices: []specs.WindowsDevice{{ID: `PCIP\VEN_8086&DEV_43A2&SUBSYS_72708086&REV_00\3&11583659&0&F5`, IDType: "gpu"}},
 | |
| 		},
 | |
| 		{
 | |
| 			name:    "hostPath_hcsshim_wcow_location_path",
 | |
| 			devices: []*runtime.Device{{HostPath: "vpci-location-path://PCIROOT(0)#PCI(0100)#PCI(0000)#PCI(0000)#PCI(0001)"}},
 | |
| 
 | |
| 			expectError:            false,
 | |
| 			expectedWindowsDevices: []specs.WindowsDevice{{ID: "PCIROOT(0)#PCI(0100)#PCI(0000)#PCI(0000)#PCI(0001)", IDType: "vpci-location-path"}},
 | |
| 		},
 | |
| 		{
 | |
| 			name:    "hostPath_hcsshim_wcow_class_guid",
 | |
| 			devices: []*runtime.Device{{HostPath: "class://5B45201D-F2F2-4F3B-85BB-30FF1F953599"}},
 | |
| 
 | |
| 			expectError:            false,
 | |
| 			expectedWindowsDevices: []specs.WindowsDevice{{ID: "5B45201D-F2F2-4F3B-85BB-30FF1F953599", IDType: "class"}},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "hostPath_hcsshim_wcow_gpu_hyper-v",
 | |
| 			// Not actually a GPU PCIP instance, but my personal machine doesn't have any PCIP devices, so I found one on the 'net.
 | |
| 			devices: []*runtime.Device{{HostPath: `vpci://PCIP\VEN_8086&DEV_43A2&SUBSYS_72708086&REV_00\3&11583659&0&F5`}},
 | |
| 
 | |
| 			expectError:            false,
 | |
| 			expectedWindowsDevices: []specs.WindowsDevice{{ID: `PCIP\VEN_8086&DEV_43A2&SUBSYS_72708086&REV_00\3&11583659&0&F5`, IDType: "vpci"}},
 | |
| 		},
 | |
| 		// Example from https://github.com/microsoft/hcsshim/blob/v0.9.2/test/cri-containerd/container_test.go
 | |
| 		// According to https://github.com/jterry75/cri/blob/f8e83e63cc027d0e9c0c984f9db3cba58d3672d4/pkg/server/container_create_windows.go#L625-L649
 | |
| 		// this is intended to generate LinuxDevice entries that the GCS shim in a LCOW container host will remap
 | |
| 		// into device mounts from its own in-UVM kernel.
 | |
| 		// From discussion on https://github.com/containerd/containerd/pull/6618, we reject this syntax for now.
 | |
| 		{
 | |
| 			name:    "hostPath_hcsshim_lcow_sandbox_device",
 | |
| 			devices: []*runtime.Device{{HostPath: "/dev/fuse"}},
 | |
| 			isLCOW:  true,
 | |
| 
 | |
| 			expectError: true,
 | |
| 		},
 | |
| 		// Some edge cases suggested by the above real-world examples
 | |
| 		{
 | |
| 			name:    "hostPath_no_slash",
 | |
| 			devices: []*runtime.Device{{HostPath: "no_slash"}},
 | |
| 
 | |
| 			expectError: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name:    "hostPath_but_no_type",
 | |
| 			devices: []*runtime.Device{{HostPath: "://5B45201D-F2F2-4F3B-85BB-30FF1F953599"}},
 | |
| 
 | |
| 			expectError: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name:    "hostPath_but_no_id",
 | |
| 			devices: []*runtime.Device{{HostPath: "gpu://"}},
 | |
| 
 | |
| 			expectError:            false,
 | |
| 			expectedWindowsDevices: []specs.WindowsDevice{{ID: "", IDType: "gpu"}},
 | |
| 		},
 | |
| 		{
 | |
| 			name:    "hostPath_dockerstyle_with_slashes_in_id",
 | |
| 			devices: []*runtime.Device{{HostPath: "class/slashed/id"}},
 | |
| 
 | |
| 			expectError:            false,
 | |
| 			expectedWindowsDevices: []specs.WindowsDevice{{ID: "slashed/id", IDType: "class"}},
 | |
| 		},
 | |
| 		{
 | |
| 			name:    "hostPath_docker_style_non-class_idtypewith_slashes_in_id",
 | |
| 			devices: []*runtime.Device{{HostPath: "vpci-location-path/slashed/id"}},
 | |
| 
 | |
| 			expectError: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "hostPath_hcsshim_wcow_location_path_twice",
 | |
| 			devices: []*runtime.Device{
 | |
| 				{HostPath: "vpci-location-path://PCIROOT(0)#PCI(0100)#PCI(0000)#PCI(0000)#PCI(0001)"},
 | |
| 				{HostPath: "vpci-location-path://PCIROOT(0)#PCI(0100)#PCI(0000)#PCI(0000)#PCI(0002)"}},
 | |
| 
 | |
| 			expectError: false,
 | |
| 			expectedWindowsDevices: []specs.WindowsDevice{
 | |
| 				{ID: "PCIROOT(0)#PCI(0100)#PCI(0000)#PCI(0000)#PCI(0001)", IDType: "vpci-location-path"},
 | |
| 				{ID: "PCIROOT(0)#PCI(0100)#PCI(0000)#PCI(0000)#PCI(0002)", IDType: "vpci-location-path"},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tc := range testcases {
 | |
| 		t.Run(tc.name, func(t *testing.T) {
 | |
| 			var (
 | |
| 				ctx = namespaces.WithNamespace(context.Background(), "testing")
 | |
| 				c   = &containers.Container{ID: t.Name()}
 | |
| 			)
 | |
| 
 | |
| 			config := runtime.ContainerConfig{}
 | |
| 			config.Devices = tc.devices
 | |
| 
 | |
| 			specOpts := []oci.SpecOpts{WithWindowsDevices(&config)}
 | |
| 
 | |
| 			platform := "windows"
 | |
| 			if tc.isLCOW {
 | |
| 				platform = "linux"
 | |
| 			}
 | |
| 
 | |
| 			spec, err := oci.GenerateSpecWithPlatform(ctx, nil, platform, c, specOpts...)
 | |
| 			if tc.expectError {
 | |
| 				assert.Error(t, err)
 | |
| 			} else {
 | |
| 				require.NoError(t, err)
 | |
| 			}
 | |
| 
 | |
| 			// Ensure we got the right LCOWness in the spec
 | |
| 			if tc.isLCOW {
 | |
| 				assert.NotNil(t, spec.Linux)
 | |
| 			} else {
 | |
| 				assert.Nil(t, spec.Linux)
 | |
| 			}
 | |
| 
 | |
| 			if len(tc.expectedWindowsDevices) != 0 {
 | |
| 				require.NotNil(t, spec.Windows)
 | |
| 				require.NotNil(t, spec.Windows.Devices)
 | |
| 				assert.Equal(t, spec.Windows.Devices, tc.expectedWindowsDevices)
 | |
| 			} else if spec.Windows != nil && spec.Windows.Devices != nil {
 | |
| 				assert.Empty(t, spec.Windows.Devices)
 | |
| 			}
 | |
| 
 | |
| 			if spec.Linux != nil && spec.Linux.Devices != nil {
 | |
| 				assert.Empty(t, spec.Linux.Devices)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestDriveMounts(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		mnt                   *runtime.Mount
 | |
| 		expectedContainerPath string
 | |
| 		expectedError         error
 | |
| 	}{
 | |
| 		{&runtime.Mount{HostPath: `C:\`, ContainerPath: `D:\foo`}, `D:\foo`, nil},
 | |
| 		{&runtime.Mount{HostPath: `C:\`, ContainerPath: `D:\`}, `D:\`, nil},
 | |
| 		{&runtime.Mount{HostPath: `C:\`, ContainerPath: `D:`}, `D:`, nil},
 | |
| 		{&runtime.Mount{HostPath: `\\.\pipe\a_fake_pipe_name_that_shouldnt_exist`, ContainerPath: `\\.\pipe\foo`}, `\\.\pipe\foo`, nil},
 | |
| 		// If `C:\` is passed as container path it should continue and forward that to HCS and fail
 | |
| 		// to align with docker's behavior.
 | |
| 		{&runtime.Mount{HostPath: `C:\`, ContainerPath: `C:\`}, `C:\`, nil},
 | |
| 
 | |
| 		// If `C:` is passed we can detect and fail immediately.
 | |
| 		{&runtime.Mount{HostPath: `C:\`, ContainerPath: `C:`}, ``, fmt.Errorf("destination path can not be C drive")},
 | |
| 	}
 | |
| 	var realOS osinterface.RealOS
 | |
| 	for _, test := range tests {
 | |
| 		parsedMount, err := parseMount(realOS, test.mnt)
 | |
| 		if err != nil && !strings.EqualFold(err.Error(), test.expectedError.Error()) {
 | |
| 			t.Fatalf("expected err: %s, got %s instead", test.expectedError, err)
 | |
| 		} else if err == nil && test.expectedContainerPath != parsedMount.Destination {
 | |
| 			t.Fatalf("expected container path: %s, got %s instead", test.expectedContainerPath, parsedMount.Destination)
 | |
| 		}
 | |
| 	}
 | |
| }
 | 
