diff --git a/cmd/containerd-shim-runhcs-v1/service_internal_taskshim_test.go b/cmd/containerd-shim-runhcs-v1/service_internal_taskshim_test.go index 036f43f31b..6aa6bd8e2e 100644 --- a/cmd/containerd-shim-runhcs-v1/service_internal_taskshim_test.go +++ b/cmd/containerd-shim-runhcs-v1/service_internal_taskshim_test.go @@ -6,12 +6,15 @@ import ( "context" "fmt" "math/rand" + "os" + "path/filepath" "strconv" "testing" "time" "github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/options" "github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/stats" + "github.com/Microsoft/hcsshim/pkg/ctrdtaskapi" task "github.com/containerd/containerd/api/runtime/task/v2" "github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/protobuf" @@ -536,6 +539,67 @@ func Test_TaskShim_updateInternal_Success(t *testing.T) { } } +// Tests if a requested mount is valid for windows containers. +// Currently only host volumes/directories are supported to be mounted +// on a running windows container. +func Test_TaskShimWindowsMount_updateInternal_Success(t *testing.T) { + s, t1, _ := setupTaskServiceWithFakes(t) + t1.isWCOW = true + + hostRWSharedDirectory := t.TempDir() + fRW, _ := os.OpenFile(filepath.Join(hostRWSharedDirectory, "readwrite"), os.O_RDWR|os.O_CREATE, 0755) + fRW.Close() + + resources := &ctrdtaskapi.ContainerMount{ + HostPath: hostRWSharedDirectory, + ContainerPath: hostRWSharedDirectory, + ReadOnly: true, + Type: "", + } + any, err := typeurl.MarshalAny(resources) + if err != nil { + t.Fatal(err) + } + + resp, err := s.updateInternal(context.TODO(), &task.UpdateTaskRequest{ID: t1.ID(), Resources: protobuf.FromAny(any)}) + if err != nil { + t.Fatalf("should not have failed update mount with error, got: %v", err) + } + if resp == nil { + t.Fatalf("should have returned an empty resp") + } +} + +func Test_TaskShimWindowsMount_updateInternal_Error(t *testing.T) { + s, t1, _ := setupTaskServiceWithFakes(t) + t1.isWCOW = true + + hostRWSharedDirectory := t.TempDir() + tmpVhdPath := filepath.Join(hostRWSharedDirectory, "test-vhd.vhdx") + + fRW, _ := os.OpenFile(filepath.Join(tmpVhdPath, "readwrite"), os.O_RDWR|os.O_CREATE, 0755) + fRW.Close() + + resources := &ctrdtaskapi.ContainerMount{ + HostPath: tmpVhdPath, + ContainerPath: tmpVhdPath, + ReadOnly: true, + Type: "virtual-disk", + } + any, err := typeurl.MarshalAny(resources) + if err != nil { + t.Fatal(err) + } + + resp, err := s.updateInternal(context.TODO(), &task.UpdateTaskRequest{ID: t1.ID(), Resources: protobuf.FromAny(any)}) + if err == nil { + t.Fatalf("should have failed update mount with error") + } + if resp != nil { + t.Fatalf("should have returned a nil resp, got: %v", resp) + } +} + func Test_TaskShim_updateInternal_Error(t *testing.T) { s, t1, _ := setupTaskServiceWithFakes(t) diff --git a/cmd/containerd-shim-runhcs-v1/task.go b/cmd/containerd-shim-runhcs-v1/task.go index 14c14b186d..ce525a71f0 100644 --- a/cmd/containerd-shim-runhcs-v1/task.go +++ b/cmd/containerd-shim-runhcs-v1/task.go @@ -110,6 +110,7 @@ func verifyTaskUpdateResourcesType(data interface{}) error { case *specs.WindowsResources: case *specs.LinuxResources: case *ctrdtaskapi.PolicyFragment: + case *ctrdtaskapi.ContainerMount: default: return errNotSupportedResourcesRequest } diff --git a/cmd/containerd-shim-runhcs-v1/task_hcs.go b/cmd/containerd-shim-runhcs-v1/task_hcs.go index 77d2aee7ca..b46487e356 100644 --- a/cmd/containerd-shim-runhcs-v1/task_hcs.go +++ b/cmd/containerd-shim-runhcs-v1/task_hcs.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "sync" "time" @@ -22,10 +23,12 @@ import ( "go.opencensus.io/trace" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/Microsoft/go-winio/pkg/fs" runhcsopts "github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/options" "github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/stats" "github.com/Microsoft/hcsshim/internal/cmd" "github.com/Microsoft/hcsshim/internal/cow" + "github.com/Microsoft/hcsshim/internal/guestpath" "github.com/Microsoft/hcsshim/internal/hcs" "github.com/Microsoft/hcsshim/internal/hcs/resourcepaths" "github.com/Microsoft/hcsshim/internal/hcs/schema1" @@ -44,6 +47,7 @@ import ( "github.com/Microsoft/hcsshim/internal/uvm" "github.com/Microsoft/hcsshim/osversion" "github.com/Microsoft/hcsshim/pkg/annotations" + "github.com/Microsoft/hcsshim/pkg/ctrdtaskapi" ) func newHcsStandaloneTask(ctx context.Context, events publisher, req *task.CreateTaskRequest, s *specs.Spec) (shimTask, error) { @@ -862,7 +866,15 @@ func (ht *hcsTask) Update(ctx context.Context, req *task.UpdateTaskRequest) erro func (ht *hcsTask) updateTaskContainerResources(ctx context.Context, data interface{}, annotations map[string]string) error { if ht.isWCOW { - return ht.updateWCOWResources(ctx, data, annotations) + switch resources := data.(type) { + case *specs.WindowsResources: + return ht.updateWCOWResources(ctx, resources, annotations) + case *ctrdtaskapi.ContainerMount: + // Adding mount to a running container is currently only supported for windows containers + return ht.updateWCOWContainerMount(ctx, resources, annotations) + default: + return errNotSupportedResourcesRequest + } } return ht.updateLCOWResources(ctx, data, annotations) @@ -898,11 +910,7 @@ func isValidWindowsCPUResources(c *specs.WindowsCPUResources) bool { (c.Maximum != nil && (c.Count == nil && c.Shares == nil)) } -func (ht *hcsTask) updateWCOWResources(ctx context.Context, data interface{}, annotations map[string]string) error { - resources, ok := data.(*specs.WindowsResources) - if !ok { - return errors.New("must have resources be type *WindowsResources when updating a wcow container") - } +func (ht *hcsTask) updateWCOWResources(ctx context.Context, resources *specs.WindowsResources, annotations map[string]string) error { if resources.Memory != nil && resources.Memory.Limit != nil { newMemorySizeInMB := *resources.Memory.Limit / memory.MiB memoryLimit := hcsoci.NormalizeMemorySize(ctx, ht.id, newMemorySizeInMB) @@ -961,3 +969,111 @@ func (ht *hcsTask) ProcessorInfo(ctx context.Context) (*processorInfo, error) { count: ht.host.ProcessorCount(), }, nil } + +// Add mount as vSMB share to the UVM at the given destination path +func (ht *hcsTask) addMountToUVM(ctx context.Context, src string, dst string, isRO bool) (string, error) { + options := ht.host.DefaultVSMBOptions(isRO) + vsmbShare, err := ht.host.AddVSMB(ctx, src, options) + if err != nil { + return "", errors.Wrapf(err, "failed to add mount as vSMB share to UVM") + } + + defer func() { + if err != nil { + _ = vsmbShare.Release(ctx) + } + }() + + sharePath, err := ht.host.GetVSMBUvmPath(ctx, src, isRO) + if err != nil { + return "", errors.Wrapf(err, "failed to get vsmb path") + } + // add mount to list of resources to be released on container cleanup + ht.cr.Add(vsmbShare) + return sharePath, nil +} + +func (ht *hcsTask) requestAddContainerMount(ctx context.Context, resourcePath string, settings interface{}) error { + modification := &hcsschema.ModifySettingRequest{ + ResourcePath: resourcePath, + RequestType: guestrequest.RequestTypeAdd, + Settings: settings, + } + return ht.c.Modify(ctx, modification) +} + +func isMountTypeSupported(hostPath, mountType string) bool { + // currently we only support mounting of host volumes/directories + switch mountType { + case "bind": + case "physical-disk": + case "virtual-disk": + case "extensible-virtual-disk": + return false + default: + // Ensure that host path is not sandbox://, hugepages:// + if strings.HasPrefix(hostPath, guestpath.SandboxMountPrefix) || + strings.HasPrefix(hostPath, guestpath.HugePagesMountPrefix) { + return false + } else { + // mountType == "" means the mount is treated as a normal directory + // mount and this is supported + return mountType == "" + } + } + return false +} + +func (ht *hcsTask) updateWCOWContainerMount(ctx context.Context, resources *ctrdtaskapi.ContainerMount, annotations map[string]string) error { + // Hcsschema v2 should be supported + if osversion.Build() < osversion.RS5 { + // OSVerions < RS5 only support hcsshema v1 + return fmt.Errorf("hcsschema v1 unsupported") + } + + if resources.HostPath == "" || resources.ContainerPath == "" { + return fmt.Errorf("invalid OCI spec - a mount must have both host and container path set") + } + + // Check for valid mount type + if !isMountTypeSupported(resources.HostPath, resources.Type) { + return fmt.Errorf("invalid mount type %v. Currently only host volumes/directories can be mounted to running containers", resources.Type) + } + + if ht.host == nil { + // HCS has a bug where it does not correctly resolve file (not dir) paths + // if the path includes a symlink. Therefore, we resolve the path here before + // passing it in. The issue does not occur with VSMB, so don't need to worry + // about the isolated case. + hostPath, err := fs.ResolvePath(resources.HostPath) + if err != nil { + return errors.Wrapf(err, "failed to resolve path for hostPath %s", resources.HostPath) + } + + // process isolated windows container + settings := hcsschema.MappedDirectory{ + HostPath: hostPath, + ContainerPath: resources.ContainerPath, + ReadOnly: resources.ReadOnly, + } + if err := ht.requestAddContainerMount(ctx, resourcepaths.SiloMappedDirectoryResourcePath, settings); err != nil { + return errors.Wrapf(err, "failed to add mount to process isolated container") + } + } else { + // if it is a mount request for a running hyperV WCOW container, we should first mount volume to the + // UVM as a VSMB share and then mount to the running container using the src path as seen by the UVM + guestPath, err := ht.addMountToUVM(ctx, resources.HostPath, resources.ContainerPath, resources.ReadOnly) + if err != nil { + return err + } + settings := hcsschema.MappedDirectory{ + HostPath: guestPath, + ContainerPath: resources.ContainerPath, + ReadOnly: resources.ReadOnly, + } + if err := ht.requestAddContainerMount(ctx, resourcepaths.SiloMappedDirectoryResourcePath, settings); err != nil { + return errors.Wrapf(err, "failed to add mount to hyperV container") + } + } + return nil +} diff --git a/cmd/containerd-shim-runhcs-v1/task_test.go b/cmd/containerd-shim-runhcs-v1/task_test.go index 1d285424ad..66ab66734c 100644 --- a/cmd/containerd-shim-runhcs-v1/task_test.go +++ b/cmd/containerd-shim-runhcs-v1/task_test.go @@ -9,6 +9,7 @@ import ( "github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/options" "github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/stats" "github.com/Microsoft/hcsshim/internal/shimdiag" + "github.com/Microsoft/hcsshim/pkg/ctrdtaskapi" v1 "github.com/containerd/cgroups/v3/cgroup1/stats" task "github.com/containerd/containerd/api/runtime/task/v2" "github.com/containerd/containerd/errdefs" @@ -107,7 +108,24 @@ func (tst *testShimTask) Update(ctx context.Context, req *task.UpdateTaskRequest if err != nil { return errors.Wrapf(err, "failed to unmarshal resources for container %s update request", req.ID) } - return verifyTaskUpdateResourcesType(data) + if err := verifyTaskUpdateResourcesType(data); err != nil { + return err + } + + if tst.isWCOW { + switch request := data.(type) { + case *ctrdtaskapi.ContainerMount: + // Adding mount to a running container is currently only supported for windows containers + if isMountTypeSupported(request.HostPath, request.Type) { + return nil + } else { + return errNotSupportedResourcesRequest + } + default: + return nil + } + } + return nil } func (tst *testShimTask) Share(ctx context.Context, req *shimdiag.ShareRequest) error { diff --git a/pkg/ctrdtaskapi/update.go b/pkg/ctrdtaskapi/update.go index 766edaff94..f49e204f25 100644 --- a/pkg/ctrdtaskapi/update.go +++ b/pkg/ctrdtaskapi/update.go @@ -6,6 +6,7 @@ import ( func init() { typeurl.Register(&PolicyFragment{}, "github.com/Microsoft/hcsshim/pkg/ctrdtaskapi", "PolicyFragment") + typeurl.Register(&ContainerMount{}, "github.com/Microsoft/hcsshim/pkg/ctrdtaskapi", "ContainerMount") } type PolicyFragment struct { @@ -15,3 +16,10 @@ type PolicyFragment struct { // fragment and any additional information required for validation. Fragment string `json:"fragment,omitempty"` } + +type ContainerMount struct { + HostPath string + ContainerPath string + ReadOnly bool + Type string +}