diff --git a/oci/spec.go b/oci/spec.go index a1c98ddcb..bee3b44d6 100644 --- a/oci/spec.go +++ b/oci/spec.go @@ -21,11 +21,11 @@ import ( "path/filepath" "runtime" - "github.com/containerd/containerd/namespaces" - "github.com/containerd/containerd/platforms" + "github.com/opencontainers/runtime-spec/specs-go" "github.com/containerd/containerd/containers" - specs "github.com/opencontainers/runtime-spec/specs-go" + "github.com/containerd/containerd/namespaces" + "github.com/containerd/containerd/platforms" ) const ( @@ -66,15 +66,19 @@ func generateDefaultSpecWithPlatform(ctx context.Context, platform, id string, s return err } - if plat.OS == "windows" { + switch plat.OS { + case "windows": err = populateDefaultWindowsSpec(ctx, s, id) - } else { + case "darwin": + err = populateDefaultDarwinSpec(s) + default: err = populateDefaultUnixSpec(ctx, s, id) if err == nil && runtime.GOOS == "windows" { // To run LCOW we have a Linux and Windows section. Add an empty one now. s.Windows = &specs.Windows{} } } + return err } @@ -207,3 +211,12 @@ func populateDefaultWindowsSpec(ctx context.Context, s *Spec, id string) error { } return nil } + +func populateDefaultDarwinSpec(s *Spec) error { + *s = Spec{ + Version: specs.Version, + Root: &specs.Root{}, + Process: &specs.Process{Cwd: "/"}, + } + return nil +} diff --git a/oci/spec_opts_test.go b/oci/spec_opts_test.go index a70225d5e..e3a31e08d 100644 --- a/oci/spec_opts_test.go +++ b/oci/spec_opts_test.go @@ -30,17 +30,16 @@ import ( "strings" "testing" + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/opencontainers/runtime-spec/specs-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/containerd/containerd/containers" "github.com/containerd/containerd/content" "github.com/containerd/containerd/errdefs" - "github.com/opencontainers/go-digest" - ocispec "github.com/opencontainers/image-spec/specs-go/v1" - - "github.com/containerd/containerd/containers" "github.com/containerd/containerd/namespaces" - "github.com/opencontainers/runtime-spec/specs-go" ) type blob []byte @@ -302,13 +301,20 @@ func TestWithDefaultSpec(t *testing.T) { t.Fatal(err) } - var expected Spec - var err error - if runtime.GOOS == "windows" { + var ( + expected Spec + err error + ) + + switch runtime.GOOS { + case "windows": err = populateDefaultWindowsSpec(ctx, &expected, c.ID) - } else { + case "darwin": + err = populateDefaultDarwinSpec(&expected) + default: err = populateDefaultUnixSpec(ctx, &expected, c.ID) } + if err != nil { t.Fatal(err) } diff --git a/oci/spec_test.go b/oci/spec_test.go index 7078ed9a3..ef85391f8 100644 --- a/oci/spec_test.go +++ b/oci/spec_test.go @@ -21,10 +21,11 @@ import ( "runtime" "testing" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/containerd/containerd/containers" "github.com/containerd/containerd/namespaces" "github.com/containerd/containerd/pkg/testutil" - specs "github.com/opencontainers/runtime-spec/specs-go" ) func TestGenerateSpec(t *testing.T) { @@ -39,7 +40,7 @@ func TestGenerateSpec(t *testing.T) { t.Fatal("GenerateSpec() returns a nil spec") } - if runtime.GOOS != "windows" { + if runtime.GOOS == "linux" { // check for matching caps defaults := defaultUnixCaps() for _, cl := range [][]string{ @@ -61,9 +62,9 @@ func TestGenerateSpec(t *testing.T) { t.Errorf("ns at %d does not match set %q != %q", i, defaultNS[i], ns) } } - } else { + } else if runtime.GOOS == "windows" { if s.Windows == nil { - t.Fatal("Windows section of spec not filled in on Windows platform") + t.Fatal("Windows section of spec not filled in for Windows spec") } } @@ -122,7 +123,7 @@ func TestSpecWithTTY(t *testing.T) { if !s.Process.Terminal { t.Error("terminal net set WithTTY()") } - if runtime.GOOS != "windows" { + if runtime.GOOS == "linux" { v := s.Process.Env[len(s.Process.Env)-1] if v != "TERM=xterm" { t.Errorf("xterm not set in env for TTY") diff --git a/pkg/cri/opts/spec_darwin_opts.go b/pkg/cri/opts/spec_darwin_opts.go new file mode 100644 index 000000000..ec48b6fc4 --- /dev/null +++ b/pkg/cri/opts/spec_darwin_opts.go @@ -0,0 +1,119 @@ +/* + 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" + + runtimespec "github.com/opencontainers/runtime-spec/specs-go" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1" + + "github.com/containerd/containerd/containers" + "github.com/containerd/containerd/oci" + osinterface "github.com/containerd/containerd/pkg/os" +) + +// WithDarwinMounts adds mounts from CRI's container config + extra mounts. +func WithDarwinMounts(osi osinterface.OS, config *runtime.ContainerConfig, extra []*runtime.Mount) oci.SpecOpts { + return func(ctx context.Context, client oci.Client, container *containers.Container, s *oci.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[filepath.Clean(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 { + var ( + dst = mount.GetContainerPath() + src = mount.GetHostPath() + ) + + // Create the host path if it doesn't exist. + if _, err := osi.Stat(src); err != nil { + if !os.IsNotExist(err) { + return fmt.Errorf("failed to stat %q: %w", src, err) + } + if err := osi.MkdirAll(src, 0755); err != nil { + return fmt.Errorf("failed to mkdir %q: %w", src, err) + } + } + + src, err := osi.ResolveSymbolicLink(src) + if err != nil { + return fmt.Errorf("failed to resolve symlink %q: %w", src, err) + } + + var options []string + if mount.GetReadonly() { + options = append(options, "ro") + } else { + options = append(options, "rw") + } + + s.Mounts = append(s.Mounts, runtimespec.Mount{ + Source: src, + Destination: dst, + Type: "bind", + Options: options, + }) + } + return nil + } +} diff --git a/pkg/cri/sbserver/container_create.go b/pkg/cri/sbserver/container_create.go index 813560c51..1c7077f54 100644 --- a/pkg/cri/sbserver/container_create.go +++ b/pkg/cri/sbserver/container_create.go @@ -353,7 +353,7 @@ func (c *criService) volumeMounts(containerRootDir string, criMounts []*runtime. } // runtimeSpec returns a default runtime spec used in cri-containerd. -func (c *criService) runtimeSpec(id string, baseSpecFile string, opts ...oci.SpecOpts) (*runtimespec.Spec, error) { +func (c *criService) runtimeSpec(id string, platform platforms.Platform, baseSpecFile string, opts ...oci.SpecOpts) (*runtimespec.Spec, error) { // GenerateSpec needs namespace. ctx := ctrdutil.NamespacedContext() container := &containers.Container{ID: id} @@ -379,7 +379,7 @@ func (c *criService) runtimeSpec(id string, baseSpecFile string, opts ...oci.Spe return &spec, nil } - spec, err := oci.GenerateSpec(ctx, nil, container, opts...) + spec, err := oci.GenerateSpecWithPlatform(ctx, nil, platforms.Format(platform), container, opts...) if err != nil { return nil, fmt.Errorf("failed to generate spec: %w", err) } @@ -421,6 +421,10 @@ func (c *criService) buildContainerSpec( ociRuntime config.Runtime, ) (_ *runtimespec.Spec, retErr error) { var ( + specOpts []oci.SpecOpts + err error + + // Platform helpers isLinux = platform.OS == "linux" isWindows = platform.OS == "windows" isDarwin = platform.OS == "darwin" @@ -428,7 +432,7 @@ func (c *criService) buildContainerSpec( switch { case isLinux: - return c.buildLinuxSpec( + specOpts, err = c.buildLinuxSpec( id, sandboxID, sandboxPid, @@ -442,7 +446,7 @@ func (c *criService) buildContainerSpec( ociRuntime, ) case isWindows: - return c.buildWindowsSpec( + specOpts, err = c.buildWindowsSpec( id, sandboxID, sandboxPid, @@ -456,7 +460,7 @@ func (c *criService) buildContainerSpec( ociRuntime, ) case isDarwin: - return c.buildDarwinSpec( + specOpts, err = c.buildDarwinSpec( id, sandboxID, containerName, @@ -470,6 +474,12 @@ func (c *criService) buildContainerSpec( default: return nil, fmt.Errorf("unsupported spec platform: %s", platform.OS) } + + if err != nil { + return nil, fmt.Errorf("failed to generate spec opts: %w", err) + } + + return c.runtimeSpec(id, platform, ociRuntime.BaseRuntimeSpec, specOpts...) } func (c *criService) buildLinuxSpec( @@ -484,7 +494,7 @@ func (c *criService) buildLinuxSpec( imageConfig *imagespec.ImageConfig, extraMounts []*runtime.Mount, ociRuntime config.Runtime, -) (_ *runtimespec.Spec, retErr error) { +) (_ []oci.SpecOpts, retErr error) { specOpts := []oci.SpecOpts{ oci.WithoutRunMount, } @@ -704,7 +714,7 @@ func (c *criService) buildLinuxSpec( specOpts = append(specOpts, oci.WithLinuxNamespace(runtimespec.LinuxNamespace{Type: runtimespec.CgroupNamespace})) } - return c.runtimeSpec(id, ociRuntime.BaseRuntimeSpec, specOpts...) + return specOpts, nil } func (c *criService) buildWindowsSpec( @@ -719,7 +729,7 @@ func (c *criService) buildWindowsSpec( imageConfig *imagespec.ImageConfig, extraMounts []*runtime.Mount, ociRuntime config.Runtime, -) (_ *runtimespec.Spec, retErr error) { +) (_ []oci.SpecOpts, retErr error) { specOpts := []oci.SpecOpts{ customopts.WithProcessArgs(config, imageConfig), } @@ -807,7 +817,7 @@ func (c *criService) buildWindowsSpec( customopts.WithAnnotation(annotations.WindowsHostProcess, strconv.FormatBool(sandboxHpc)), ) - return c.runtimeSpec(id, ociRuntime.BaseRuntimeSpec, specOpts...) + return specOpts, nil } func (c *criService) buildDarwinSpec( @@ -820,7 +830,7 @@ func (c *criService) buildDarwinSpec( imageConfig *imagespec.ImageConfig, extraMounts []*runtime.Mount, ociRuntime config.Runtime, -) (_ *runtimespec.Spec, retErr error) { +) (_ []oci.SpecOpts, retErr error) { specOpts := []oci.SpecOpts{ customopts.WithProcessArgs(config, imageConfig), } @@ -843,6 +853,8 @@ func (c *criService) buildDarwinSpec( } specOpts = append(specOpts, oci.WithEnv(env)) + specOpts = append(specOpts, customopts.WithDarwinMounts(c.os, config, extraMounts)) + for pKey, pValue := range getPassthroughAnnotations(sandboxConfig.Annotations, ociRuntime.PodAnnotations) { specOpts = append(specOpts, customopts.WithAnnotation(pKey, pValue)) @@ -863,5 +875,5 @@ func (c *criService) buildDarwinSpec( customopts.WithAnnotation(annotations.ImageName, imageName), ) - return c.runtimeSpec(id, ociRuntime.BaseRuntimeSpec, specOpts...) + return specOpts, nil } diff --git a/pkg/cri/sbserver/container_create_other_test.go b/pkg/cri/sbserver/container_create_other_test.go index 5d0a4e0a0..cc639305d 100644 --- a/pkg/cri/sbserver/container_create_other_test.go +++ b/pkg/cri/sbserver/container_create_other_test.go @@ -21,11 +21,12 @@ package sbserver import ( "testing" - "github.com/containerd/containerd/pkg/cri/annotations" imagespec "github.com/opencontainers/image-spec/specs-go/v1" runtimespec "github.com/opencontainers/runtime-spec/specs-go" "github.com/stretchr/testify/assert" runtime "k8s.io/cri-api/pkg/apis/runtime/v1" + + "github.com/containerd/containerd/pkg/cri/annotations" ) // checkMount is defined by all tests but not used here @@ -52,6 +53,19 @@ func getCreateContainerTestData() (*runtime.ContainerConfig, *runtime.PodSandbox }, Labels: map[string]string{"a": "b"}, Annotations: map[string]string{"ca-c": "ca-d"}, + Mounts: []*runtime.Mount{ + // everything default + { + ContainerPath: "container-path-1", + HostPath: "host-path-1", + }, + // readOnly + { + ContainerPath: "container-path-2", + HostPath: "host-path-2", + Readonly: true, + }, + }, } sandboxConfig := &runtime.PodSandboxConfig{ Metadata: &runtime.PodSandboxMetadata{ @@ -69,12 +83,15 @@ func getCreateContainerTestData() (*runtime.ContainerConfig, *runtime.PodSandbox WorkingDir: "/workspace", } specCheck := func(t *testing.T, id string, sandboxID string, sandboxPid uint32, spec *runtimespec.Spec) { - assert.Equal(t, relativeRootfsPath, spec.Root.Path) assert.Equal(t, []string{"test", "command", "test", "args"}, spec.Process.Args) assert.Equal(t, "test-cwd", spec.Process.Cwd) assert.Contains(t, spec.Process.Env, "k1=v1", "k2=v2", "k3=v3=v3bis", "ik4=iv4=iv4bis=boop") assert.Contains(t, spec.Process.Env, "ik1=iv1", "ik2=iv2", "ik3=iv3=iv3bis", "k4=v4=v4bis=foop") + t.Logf("Check bind mount") + checkMount(t, spec.Mounts, "host-path-1", "container-path-1", "bind", []string{"rw"}, nil) + checkMount(t, spec.Mounts, "host-path-2", "container-path-2", "bind", []string{"ro"}, nil) + t.Logf("Check PodSandbox annotations") assert.Contains(t, spec.Annotations, annotations.SandboxID) assert.EqualValues(t, spec.Annotations[annotations.SandboxID], sandboxID) diff --git a/pkg/cri/sbserver/container_create_test.go b/pkg/cri/sbserver/container_create_test.go index d04edc99d..744deef13 100644 --- a/pkg/cri/sbserver/container_create_test.go +++ b/pkg/cri/sbserver/container_create_test.go @@ -418,6 +418,7 @@ func TestBaseRuntimeSpec(t *testing.T) { out, err := c.runtimeSpec( "id1", + platforms.DefaultSpec(), "/etc/containerd/cri-base.json", oci.WithHostname("new-host"), oci.WithDomainname("new-domain"),