 bfde58e3cd
			
		
	
	bfde58e3cd
	
	
	
		
			
			Currently when handling 'container_path' elements in container mounts we simply call
filepath.Clean on those paths. However, filepath.Clean adds an extra '.' if the path is a
simple drive letter ('E:' or 'Z:' etc.). These type of paths cause failures (with incorrect
parameter error) when creating containers via hcsshim. This commit checks for such paths
and doesn't call filepath.Clean on them.
It also adds a new check to error out if the destination path is a C drive and moves the
dst path checks out of the named pipe condition.
Signed-off-by: Amit Barve <ambarve@microsoft.com>
		
	
		
			
				
	
	
		
			277 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			277 lines
		
	
	
		
			8.6 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"
 | |
| 	"os"
 | |
| 	"path/filepath"
 | |
| 	"sort"
 | |
| 	"strings"
 | |
| 
 | |
| 	"github.com/containerd/containerd/containers"
 | |
| 	"github.com/containerd/containerd/oci"
 | |
| 	runtimespec "github.com/opencontainers/runtime-spec/specs-go"
 | |
| 	runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
 | |
| 
 | |
| 	osinterface "github.com/containerd/containerd/pkg/os"
 | |
| )
 | |
| 
 | |
| // WithWindowsNetworkNamespace sets windows network namespace for container.
 | |
| // TODO(windows): Move this into container/containerd.
 | |
| func WithWindowsNetworkNamespace(path string) oci.SpecOpts {
 | |
| 	return func(ctx context.Context, client oci.Client, c *containers.Container, s *runtimespec.Spec) error {
 | |
| 		if s.Windows == nil {
 | |
| 			s.Windows = &runtimespec.Windows{}
 | |
| 		}
 | |
| 		if s.Windows.Network == nil {
 | |
| 			s.Windows.Network = &runtimespec.WindowsNetwork{}
 | |
| 		}
 | |
| 		s.Windows.Network.NetworkNamespace = path
 | |
| 		return nil
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // namedPipePath returns true if the given path is to a named pipe.
 | |
| func namedPipePath(p string) bool {
 | |
| 	return strings.HasPrefix(p, `\\.\pipe\`)
 | |
| }
 | |
| 
 | |
| // cleanMount returns a cleaned version of the mount path. The input is returned
 | |
| // as-is if it is a named pipe path.
 | |
| func cleanMount(p string) string {
 | |
| 	if namedPipePath(p) {
 | |
| 		return p
 | |
| 	}
 | |
| 	return filepath.Clean(p)
 | |
| }
 | |
| 
 | |
| func parseMount(osi osinterface.OS, mount *runtime.Mount) (*runtimespec.Mount, error) {
 | |
| 	var (
 | |
| 		dst = mount.GetContainerPath()
 | |
| 		src = mount.GetHostPath()
 | |
| 	)
 | |
| 	// In the case of a named pipe mount on Windows, don't stat the file
 | |
| 	// or do other operations that open it, as that could interfere with
 | |
| 	// the listening process. filepath.Clean also breaks named pipe
 | |
| 	// paths, so don't use it.
 | |
| 	if !namedPipePath(src) {
 | |
| 		if _, err := osi.Stat(src); err != nil {
 | |
| 			// Create the host path if it doesn't exist. This will align
 | |
| 			// the behavior with the Linux implementation, but it doesn't
 | |
| 			// align with Docker's behavior on Windows.
 | |
| 			if !os.IsNotExist(err) {
 | |
| 				return nil, fmt.Errorf("failed to stat %q: %w", src, err)
 | |
| 			}
 | |
| 			if err := osi.MkdirAll(src, 0755); err != nil {
 | |
| 				return nil, fmt.Errorf("failed to mkdir %q: %w", src, err)
 | |
| 			}
 | |
| 		}
 | |
| 		var err error
 | |
| 		src, err = osi.ResolveSymbolicLink(src)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("failed to resolve symlink %q: %w", src, err)
 | |
| 		}
 | |
| 		// hcsshim requires clean path, especially '/' -> '\'. Additionally,
 | |
| 		// for the destination, absolute paths should have the C: prefix.
 | |
| 		src = filepath.Clean(src)
 | |
| 
 | |
| 		// filepath.Clean adds a '.' at the end if the path is a
 | |
| 		// drive (like Z:, E: etc.). Keeping this '.' in the path
 | |
| 		// causes incorrect parameter error when starting the
 | |
| 		// container on windows.  Remove it here.
 | |
| 		if !(len(dst) == 2 && dst[1] == ':') {
 | |
| 			dst = filepath.Clean(dst)
 | |
| 			if dst[0] == '\\' {
 | |
| 				dst = "C:" + dst
 | |
| 			}
 | |
| 		} else if dst[0] == 'c' || dst[0] == 'C' {
 | |
| 			return nil, fmt.Errorf("destination path can not be C drive")
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	var options []string
 | |
| 	// NOTE(random-liu): we don't change all mounts to `ro` when root filesystem
 | |
| 	// is readonly. This is different from docker's behavior, but make more sense.
 | |
| 	if mount.GetReadonly() {
 | |
| 		options = append(options, "ro")
 | |
| 	} else {
 | |
| 		options = append(options, "rw")
 | |
| 	}
 | |
| 	return &runtimespec.Mount{Source: src, Destination: dst, Options: options}, nil
 | |
| }
 | |
| 
 | |
| // WithWindowsMounts sorts and adds runtime and CRI mounts to the spec for
 | |
| // windows container.
 | |
| func WithWindowsMounts(osi osinterface.OS, config *runtime.ContainerConfig, extra []*runtime.Mount) oci.SpecOpts {
 | |
| 	return func(ctx context.Context, client oci.Client, _ *containers.Container, s *runtimespec.Spec) error {
 | |
| 		// mergeMounts merge CRI mounts with extra mounts. If a mount destination
 | |
| 		// is mounted by both a CRI mount and an extra mount, the CRI mount will
 | |
| 		// be kept.
 | |
| 		var (
 | |
| 			criMounts = config.GetMounts()
 | |
| 			mounts    = append([]*runtime.Mount{}, criMounts...)
 | |
| 		)
 | |
| 		// Copy all mounts from extra mounts, except for mounts overridden by CRI.
 | |
| 		for _, e := range extra {
 | |
| 			found := false
 | |
| 			for _, c := range criMounts {
 | |
| 				if cleanMount(e.ContainerPath) == cleanMount(c.ContainerPath) {
 | |
| 					found = true
 | |
| 					break
 | |
| 				}
 | |
| 			}
 | |
| 			if !found {
 | |
| 				mounts = append(mounts, e)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// Sort mounts in number of parts. This ensures that high level mounts don't
 | |
| 		// shadow other mounts.
 | |
| 		sort.Sort(orderedMounts(mounts))
 | |
| 
 | |
| 		// Copy all mounts from default mounts, except for
 | |
| 		// mounts overridden by supplied mount;
 | |
| 		mountSet := make(map[string]struct{})
 | |
| 		for _, m := range mounts {
 | |
| 			mountSet[cleanMount(m.ContainerPath)] = struct{}{}
 | |
| 		}
 | |
| 
 | |
| 		defaultMounts := s.Mounts
 | |
| 		s.Mounts = nil
 | |
| 
 | |
| 		for _, m := range defaultMounts {
 | |
| 			dst := cleanMount(m.Destination)
 | |
| 			if _, ok := mountSet[dst]; ok {
 | |
| 				// filter out mount overridden by a supplied mount
 | |
| 				continue
 | |
| 			}
 | |
| 			s.Mounts = append(s.Mounts, m)
 | |
| 		}
 | |
| 
 | |
| 		for _, mount := range mounts {
 | |
| 			parsedMount, err := parseMount(osi, mount)
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 			s.Mounts = append(s.Mounts, *parsedMount)
 | |
| 		}
 | |
| 		return nil
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // WithWindowsResources sets the provided resource restrictions for windows.
 | |
| func WithWindowsResources(resources *runtime.WindowsContainerResources) oci.SpecOpts {
 | |
| 	return func(ctx context.Context, client oci.Client, c *containers.Container, s *runtimespec.Spec) error {
 | |
| 		if resources == nil {
 | |
| 			return nil
 | |
| 		}
 | |
| 		if s.Windows == nil {
 | |
| 			s.Windows = &runtimespec.Windows{}
 | |
| 		}
 | |
| 		if s.Windows.Resources == nil {
 | |
| 			s.Windows.Resources = &runtimespec.WindowsResources{}
 | |
| 		}
 | |
| 		if s.Windows.Resources.Memory == nil {
 | |
| 			s.Windows.Resources.Memory = &runtimespec.WindowsMemoryResources{}
 | |
| 		}
 | |
| 
 | |
| 		var (
 | |
| 			count  = uint64(resources.GetCpuCount())
 | |
| 			shares = uint16(resources.GetCpuShares())
 | |
| 			max    = uint16(resources.GetCpuMaximum())
 | |
| 			limit  = uint64(resources.GetMemoryLimitInBytes())
 | |
| 		)
 | |
| 		if s.Windows.Resources.CPU == nil && (count != 0 || shares != 0 || max != 0) {
 | |
| 			s.Windows.Resources.CPU = &runtimespec.WindowsCPUResources{}
 | |
| 		}
 | |
| 		if count != 0 {
 | |
| 			s.Windows.Resources.CPU.Count = &count
 | |
| 		}
 | |
| 		if shares != 0 {
 | |
| 			s.Windows.Resources.CPU.Shares = &shares
 | |
| 		}
 | |
| 		if max != 0 {
 | |
| 			s.Windows.Resources.CPU.Maximum = &max
 | |
| 		}
 | |
| 		if limit != 0 {
 | |
| 			s.Windows.Resources.Memory.Limit = &limit
 | |
| 		}
 | |
| 		return nil
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // WithWindowsDefaultSandboxShares sets the default sandbox CPU shares
 | |
| func WithWindowsDefaultSandboxShares(ctx context.Context, client oci.Client, c *containers.Container, s *runtimespec.Spec) error {
 | |
| 	if s.Windows == nil {
 | |
| 		s.Windows = &runtimespec.Windows{}
 | |
| 	}
 | |
| 	if s.Windows.Resources == nil {
 | |
| 		s.Windows.Resources = &runtimespec.WindowsResources{}
 | |
| 	}
 | |
| 	if s.Windows.Resources.CPU == nil {
 | |
| 		s.Windows.Resources.CPU = &runtimespec.WindowsCPUResources{}
 | |
| 	}
 | |
| 	i := uint16(DefaultSandboxCPUshares)
 | |
| 	s.Windows.Resources.CPU.Shares = &i
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // WithWindowsCredentialSpec assigns `credentialSpec` to the
 | |
| // `runtime.Spec.Windows.CredentialSpec` field.
 | |
| func WithWindowsCredentialSpec(credentialSpec string) oci.SpecOpts {
 | |
| 	return func(ctx context.Context, client oci.Client, c *containers.Container, s *runtimespec.Spec) error {
 | |
| 		if s.Windows == nil {
 | |
| 			s.Windows = &runtimespec.Windows{}
 | |
| 		}
 | |
| 		s.Windows.CredentialSpec = credentialSpec
 | |
| 		return nil
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // WithDevices sets the provided devices onto the container spec
 | |
| func WithDevices(config *runtime.ContainerConfig) oci.SpecOpts {
 | |
| 	return func(ctx context.Context, client oci.Client, c *containers.Container, s *runtimespec.Spec) (err error) {
 | |
| 		for _, device := range config.GetDevices() {
 | |
| 			if device.ContainerPath != "" {
 | |
| 				return fmt.Errorf("unexpected ContainerPath %s, must be empty", device.ContainerPath)
 | |
| 			}
 | |
| 
 | |
| 			if device.Permissions != "" {
 | |
| 				return fmt.Errorf("unexpected Permissions %s, must be empty", device.Permissions)
 | |
| 			}
 | |
| 
 | |
| 			hostPath := device.HostPath
 | |
| 			if strings.HasPrefix(hostPath, "class/") {
 | |
| 				hostPath = strings.Replace(hostPath, "class/", "class://", 1)
 | |
| 			}
 | |
| 
 | |
| 			splitParts := strings.SplitN(hostPath, "://", 2)
 | |
| 			if len(splitParts) != 2 {
 | |
| 				return fmt.Errorf("unrecognised HostPath format %v, must match IDType://ID", device.HostPath)
 | |
| 			}
 | |
| 
 | |
| 			o := oci.WithWindowsDevice(splitParts[0], splitParts[1])
 | |
| 			if err := o(ctx, client, c, s); err != nil {
 | |
| 				return fmt.Errorf("failed adding device with HostPath %v: %w", device.HostPath, err)
 | |
| 			}
 | |
| 		}
 | |
| 		return nil
 | |
| 	}
 | |
| }
 |