Skip to content

Commit d4641e1

Browse files
author
Kazuyoshi Kato
authored
Merge pull request containerd#6618 from TBBle/handle-device-host_path-on-windows
Handle CRI Device.HostPath on Windows
2 parents dc745fc + 2a42599 commit d4641e1

File tree

11 files changed

+454
-5
lines changed

11 files changed

+454
-5
lines changed

cmd/ctr/commands/commands.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,6 @@ var (
165165
Name: "memory-limit",
166166
Usage: "memory limit (in bytes) for the container",
167167
},
168-
cli.StringSliceFlag{
169-
Name: "device",
170-
Usage: "file path to a device to add to the container; or a path to a directory tree of devices to add to the container",
171-
},
172168
cli.StringSliceFlag{
173169
Name: "cap-add",
174170
Usage: "add Linux capabilities (Set capabilities with 'CAP_' prefix)",

cmd/ctr/commands/commands_unix.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,8 @@ func init() {
4040
}, cli.StringFlag{
4141
Name: "rootfs-propagation",
4242
Usage: "set the propagation of the container rootfs",
43+
}, cli.StringSliceFlag{
44+
Name: "device",
45+
Usage: "file path to a device to add to the container; or a path to a directory tree of devices to add to the container",
4346
})
4447
}

cmd/ctr/commands/commands_windows.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,8 @@ func init() {
2424
ContainerFlags = append(ContainerFlags, cli.Uint64Flag{
2525
Name: "cpu-count",
2626
Usage: "number of CPUs available to the container",
27+
}, cli.StringSliceFlag{
28+
Name: "device",
29+
Usage: "identifier of a device to add to the container (e.g. class://5B45201D-F2F2-4F3B-85BB-30FF1F953599)",
2730
})
2831
}

cmd/ctr/commands/run/run_windows.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package run
1919
import (
2020
gocontext "context"
2121
"errors"
22+
"strings"
2223

2324
"github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/options"
2425
"github.com/containerd/console"
@@ -142,6 +143,16 @@ func NewContainer(ctx gocontext.Context, client *containerd.Client, context *cli
142143
if ccount != 0 {
143144
opts = append(opts, oci.WithWindowsCPUCount(ccount))
144145
}
146+
for _, dev := range context.StringSlice("device") {
147+
parts := strings.Split(dev, "://")
148+
if len(parts) != 2 {
149+
return nil, errors.New("devices must be in the format IDType://ID")
150+
}
151+
if parts[0] == "" {
152+
return nil, errors.New("devices must have a non-empty IDType")
153+
}
154+
opts = append(opts, oci.WithWindowsDevice(parts[0], parts[1]))
155+
}
145156
}
146157

147158
runtime := context.String("runtime")

integration/main_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,15 @@ func WithSupplementalGroups(gids []int64) ContainerOpts { //nolint:unused
297297
}
298298
}
299299

300+
// WithDevice adds a device mount.
301+
func WithDevice(containerPath, hostPath, permissions string) ContainerOpts { //nolint:unused
302+
return func(c *runtime.ContainerConfig) {
303+
c.Devices = append(c.Devices, &runtime.Device{
304+
ContainerPath: containerPath, HostPath: hostPath, Permissions: permissions,
305+
})
306+
}
307+
}
308+
300309
// ContainerConfig creates a container config given a name and image name
301310
// and additional container config options
302311
func ContainerConfig(name, image string, opts ...ContainerOpts) *runtime.ContainerConfig {

integration/windows_device_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
//go:build windows
2+
// +build windows
3+
4+
/*
5+
Copyright The containerd Authors.
6+
7+
Licensed under the Apache License, Version 2.0 (the "License");
8+
you may not use this file except in compliance with the License.
9+
You may obtain a copy of the License at
10+
11+
http://www.apache.org/licenses/LICENSE-2.0
12+
13+
Unless required by applicable law or agreed to in writing, software
14+
distributed under the License is distributed on an "AS IS" BASIS,
15+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
See the License for the specific language governing permissions and
17+
limitations under the License.
18+
*/
19+
20+
package integration
21+
22+
import (
23+
"fmt"
24+
"os"
25+
"path/filepath"
26+
"testing"
27+
"time"
28+
29+
"github.com/stretchr/testify/assert"
30+
"github.com/stretchr/testify/require"
31+
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
32+
)
33+
34+
func TestWindowsDevice(t *testing.T) {
35+
testPodLogDir := t.TempDir()
36+
37+
t.Log("Create a sandbox with log directory")
38+
sb, sbConfig := PodSandboxConfigWithCleanup(t, "sandbox", "windows-device",
39+
WithPodLogDirectory(testPodLogDir),
40+
)
41+
42+
var (
43+
// TODO: An image with the device-dumper
44+
testImage = GetImage(BusyBox)
45+
containerName = "test-container"
46+
)
47+
48+
const (
49+
// Mount this device class to expose host-GPU-accelerated DirectX inside the container
50+
// https://techcommunity.microsoft.com/t5/containers/bringing-gpu-acceleration-to-windows-containers/ba-p/393939
51+
GUID_DEVINTERFACE_DISPLAY_ADAPTER = "5B45201D-F2F2-4F3B-85BB-30FF1F953599" //nolint:revive
52+
)
53+
54+
EnsureImageExists(t, testImage)
55+
56+
t.Log("Create a container to run the device test")
57+
cnConfig := ContainerConfig(
58+
containerName,
59+
testImage,
60+
// Per C:\windows\System32\containers\devices.def, enabling GUID_DEVINTERFACE_DISPLAY_ADAPTER
61+
// will mount the host driver store into the container at this location.
62+
WithCommand("sh", "-c", "ls -d /Windows/System32/HostDriverStore/* | grep /Windows/System32/HostDriverStore/FileRepository"),
63+
WithLogPath(containerName),
64+
WithDevice("", "class/"+GUID_DEVINTERFACE_DISPLAY_ADAPTER, ""),
65+
)
66+
cn, err := runtimeService.CreateContainer(sb, cnConfig, sbConfig)
67+
require.NoError(t, err)
68+
69+
t.Log("Start the container")
70+
require.NoError(t, runtimeService.StartContainer(cn))
71+
72+
t.Log("Wait for container to finish running")
73+
require.NoError(t, Eventually(func() (bool, error) {
74+
s, err := runtimeService.ContainerStatus(cn)
75+
if err != nil {
76+
return false, err
77+
}
78+
if s.GetState() == runtime.ContainerState_CONTAINER_EXITED {
79+
return true, nil
80+
}
81+
return false, nil
82+
}, time.Second, 30*time.Second))
83+
84+
t.Log("Check container log")
85+
content, err := os.ReadFile(filepath.Join(testPodLogDir, containerName))
86+
assert.NoError(t, err)
87+
checkContainerLog(t, string(content), []string{
88+
fmt.Sprintf("%s %s %s", runtime.Stdout, runtime.LogTagFull, "/Windows/System32/HostDriverStore/FileRepository"),
89+
})
90+
}

oci/spec_opts.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1367,3 +1367,17 @@ func tryReadonlyMounts(mounts []mount.Mount) []mount.Mount {
13671367
}
13681368
return mounts
13691369
}
1370+
1371+
// WithWindowsDevice adds a device exposed to a Windows (WCOW or LCOW) Container
1372+
func WithWindowsDevice(idType, id string) SpecOpts {
1373+
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
1374+
if idType == "" {
1375+
return errors.New("missing idType")
1376+
}
1377+
if s.Windows == nil {
1378+
s.Windows = &specs.Windows{}
1379+
}
1380+
s.Windows.Devices = append(s.Windows.Devices, specs.WindowsDevice{IDType: idType, ID: id})
1381+
return nil
1382+
}
1383+
}

oci/spec_opts_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ import (
3030
"strings"
3131
"testing"
3232

33+
"github.com/stretchr/testify/assert"
34+
"github.com/stretchr/testify/require"
35+
3336
"github.com/containerd/containerd/content"
3437
"github.com/opencontainers/go-digest"
3538
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
@@ -704,3 +707,75 @@ func TestWithoutMounts(t *testing.T) {
704707
t.Fatalf("expected %+v, got %+v", expected, s.Mounts)
705708
}
706709
}
710+
711+
func TestWithWindowsDevice(t *testing.T) {
712+
testcases := []struct {
713+
name string
714+
idType string
715+
id string
716+
717+
expectError bool
718+
expectedWindowsDevices []specs.WindowsDevice
719+
}{
720+
{
721+
name: "empty_idType_and_id",
722+
idType: "",
723+
id: "",
724+
expectError: true,
725+
},
726+
{
727+
name: "empty_idType",
728+
idType: "",
729+
id: "5B45201D-F2F2-4F3B-85BB-30FF1F953599",
730+
expectError: true,
731+
},
732+
{
733+
name: "empty_id",
734+
idType: "class",
735+
id: "",
736+
737+
expectError: false,
738+
expectedWindowsDevices: []specs.WindowsDevice{{ID: "", IDType: "class"}},
739+
},
740+
{
741+
name: "idType_and_id",
742+
idType: "class",
743+
id: "5B45201D-F2F2-4F3B-85BB-30FF1F953599",
744+
745+
expectError: false,
746+
expectedWindowsDevices: []specs.WindowsDevice{{ID: "5B45201D-F2F2-4F3B-85BB-30FF1F953599", IDType: "class"}},
747+
},
748+
}
749+
750+
for _, tc := range testcases {
751+
t.Run(tc.name, func(t *testing.T) {
752+
spec := Spec{
753+
Version: specs.Version,
754+
Root: &specs.Root{},
755+
Windows: &specs.Windows{},
756+
}
757+
758+
opts := []SpecOpts{
759+
WithWindowsDevice(tc.idType, tc.id),
760+
}
761+
762+
for _, opt := range opts {
763+
if err := opt(nil, nil, nil, &spec); err != nil {
764+
if tc.expectError {
765+
assert.Error(t, err)
766+
} else {
767+
require.NoError(t, err)
768+
}
769+
}
770+
}
771+
772+
if len(tc.expectedWindowsDevices) != 0 {
773+
require.NotNil(t, spec.Windows)
774+
require.NotNil(t, spec.Windows.Devices)
775+
assert.ElementsMatch(t, spec.Windows.Devices, tc.expectedWindowsDevices)
776+
} else if spec.Windows != nil && spec.Windows.Devices != nil {
777+
assert.ElementsMatch(t, spec.Windows.Devices, tc.expectedWindowsDevices)
778+
}
779+
})
780+
}
781+
}

pkg/cri/opts/spec_windows.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,34 @@ func WithWindowsCredentialSpec(credentialSpec string) oci.SpecOpts {
230230
return nil
231231
}
232232
}
233+
234+
// WithDevices sets the provided devices onto the container spec
235+
func WithDevices(config *runtime.ContainerConfig) oci.SpecOpts {
236+
return func(ctx context.Context, client oci.Client, c *containers.Container, s *runtimespec.Spec) (err error) {
237+
for _, device := range config.GetDevices() {
238+
if device.ContainerPath != "" {
239+
return fmt.Errorf("unexpected ContainerPath %s, must be empty", device.ContainerPath)
240+
}
241+
242+
if device.Permissions != "" {
243+
return fmt.Errorf("unexpected Permissions %s, must be empty", device.Permissions)
244+
}
245+
246+
hostPath := device.HostPath
247+
if strings.HasPrefix(hostPath, "class/") {
248+
hostPath = strings.Replace(hostPath, "class/", "class://", 1)
249+
}
250+
251+
splitParts := strings.SplitN(hostPath, "://", 2)
252+
if len(splitParts) != 2 {
253+
return fmt.Errorf("unrecognised HostPath format %v, must match IDType://ID", device.HostPath)
254+
}
255+
256+
o := oci.WithWindowsDevice(splitParts[0], splitParts[1])
257+
if err := o(ctx, client, c, s); err != nil {
258+
return fmt.Errorf("failed adding device with HostPath %v: %w", device.HostPath, err)
259+
}
260+
}
261+
return nil
262+
}
263+
}

0 commit comments

Comments
 (0)