Move oci to pkg/oci

Signed-off-by: Derek McGowan <derek@mcg.dev>
This commit is contained in:
Derek McGowan
2024-01-17 09:55:48 -08:00
parent fa8cae99d1
commit e59f64792b
90 changed files with 70 additions and 70 deletions

View File

@@ -17,8 +17,8 @@
package annotations
import (
"github.com/containerd/containerd/v2/oci"
customopts "github.com/containerd/containerd/v2/pkg/cri/opts"
"github.com/containerd/containerd/v2/pkg/oci"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
)

View File

@@ -27,7 +27,7 @@ import (
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
"github.com/containerd/containerd/v2/core/containers"
"github.com/containerd/containerd/v2/oci"
"github.com/containerd/containerd/v2/pkg/oci"
osinterface "github.com/containerd/containerd/v2/pkg/os"
)

View File

@@ -32,7 +32,7 @@ import (
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
"github.com/containerd/containerd/v2/core/containers"
"github.com/containerd/containerd/v2/oci"
"github.com/containerd/containerd/v2/pkg/oci"
"github.com/containerd/log"
)

View File

@@ -33,7 +33,7 @@ import (
"github.com/containerd/containerd/v2/core/containers"
"github.com/containerd/containerd/v2/core/mount"
"github.com/containerd/containerd/v2/oci"
"github.com/containerd/containerd/v2/pkg/oci"
osinterface "github.com/containerd/containerd/v2/pkg/os"
"github.com/containerd/log"
)

View File

@@ -22,7 +22,7 @@ import (
"context"
"github.com/containerd/containerd/v2/core/containers"
"github.com/containerd/containerd/v2/oci"
"github.com/containerd/containerd/v2/pkg/oci"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
)

View File

@@ -22,8 +22,8 @@ import (
"context"
"github.com/containerd/containerd/v2/core/containers"
"github.com/containerd/containerd/v2/oci"
"github.com/containerd/containerd/v2/pkg/errdefs"
"github.com/containerd/containerd/v2/pkg/oci"
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
runtimespec "github.com/opencontainers/runtime-spec/specs-go"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"

View File

@@ -30,8 +30,8 @@ import (
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
"github.com/containerd/containerd/v2/core/containers"
"github.com/containerd/containerd/v2/oci"
"github.com/containerd/containerd/v2/pkg/cri/util"
"github.com/containerd/containerd/v2/pkg/oci"
)
// DefaultSandboxCPUshares is default cpu shares for sandbox container.

View File

@@ -24,7 +24,7 @@ import (
"strings"
"github.com/containerd/containerd/v2/core/containers"
"github.com/containerd/containerd/v2/oci"
"github.com/containerd/containerd/v2/pkg/oci"
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
runtimespec "github.com/opencontainers/runtime-spec/specs-go"
"golang.org/x/sys/windows"

View File

@@ -25,7 +25,7 @@ import (
"strings"
"github.com/containerd/containerd/v2/core/containers"
"github.com/containerd/containerd/v2/oci"
"github.com/containerd/containerd/v2/pkg/oci"
osinterface "github.com/containerd/containerd/v2/pkg/os"
runtimespec "github.com/opencontainers/runtime-spec/specs-go"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"

View File

@@ -28,8 +28,8 @@ import (
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
"github.com/containerd/containerd/v2/core/containers"
"github.com/containerd/containerd/v2/oci"
"github.com/containerd/containerd/v2/pkg/namespaces"
"github.com/containerd/containerd/v2/pkg/oci"
osinterface "github.com/containerd/containerd/v2/pkg/os"
)

View File

@@ -31,9 +31,9 @@ import (
"k8s.io/klog/v2"
srvconfig "github.com/containerd/containerd/v2/cmd/containerd/server/config"
"github.com/containerd/containerd/v2/oci"
criconfig "github.com/containerd/containerd/v2/pkg/cri/config"
"github.com/containerd/containerd/v2/pkg/cri/constants"
"github.com/containerd/containerd/v2/pkg/oci"
"github.com/containerd/containerd/v2/platforms"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/containerd/v2/plugins/services/warning"

View File

@@ -24,8 +24,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/containerd/containerd/v2/oci"
criconfig "github.com/containerd/containerd/v2/pkg/cri/config"
"github.com/containerd/containerd/v2/pkg/oci"
)
func TestLoadBaseOCISpec(t *testing.T) {

View File

@@ -36,7 +36,6 @@ import (
containerd "github.com/containerd/containerd/v2/client"
"github.com/containerd/containerd/v2/core/containers"
"github.com/containerd/containerd/v2/oci"
"github.com/containerd/containerd/v2/pkg/blockio"
"github.com/containerd/containerd/v2/pkg/cri/annotations"
criconfig "github.com/containerd/containerd/v2/pkg/cri/config"
@@ -45,6 +44,7 @@ import (
customopts "github.com/containerd/containerd/v2/pkg/cri/opts"
containerstore "github.com/containerd/containerd/v2/pkg/cri/store/container"
"github.com/containerd/containerd/v2/pkg/cri/util"
"github.com/containerd/containerd/v2/pkg/oci"
"github.com/containerd/containerd/v2/platforms"
)

View File

@@ -31,7 +31,7 @@ import (
"github.com/containerd/containerd/v2/contrib/apparmor"
"github.com/containerd/containerd/v2/contrib/seccomp"
"github.com/containerd/containerd/v2/core/snapshots"
"github.com/containerd/containerd/v2/oci"
"github.com/containerd/containerd/v2/pkg/oci"
customopts "github.com/containerd/containerd/v2/pkg/cri/opts"
)

View File

@@ -30,7 +30,7 @@ import (
"github.com/containerd/containerd/v2/contrib/seccomp"
"github.com/containerd/containerd/v2/core/containers"
"github.com/containerd/containerd/v2/core/mount"
"github.com/containerd/containerd/v2/oci"
"github.com/containerd/containerd/v2/pkg/oci"
"github.com/containerd/containerd/v2/platforms"
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
runtimespec "github.com/opencontainers/runtime-spec/specs-go"

View File

@@ -23,7 +23,7 @@ import (
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
"github.com/containerd/containerd/v2/core/snapshots"
"github.com/containerd/containerd/v2/oci"
"github.com/containerd/containerd/v2/pkg/oci"
)
func (c *criService) containerSpecOpts(config *runtime.ContainerConfig, imageConfig *imagespec.ImageConfig) ([]oci.SpecOpts, error) {

View File

@@ -33,10 +33,10 @@ import (
"github.com/stretchr/testify/require"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
"github.com/containerd/containerd/v2/oci"
"github.com/containerd/containerd/v2/pkg/cri/config"
"github.com/containerd/containerd/v2/pkg/cri/constants"
"github.com/containerd/containerd/v2/pkg/cri/opts"
"github.com/containerd/containerd/v2/pkg/oci"
)
var currentPlatform = platforms.DefaultSpec()

View File

@@ -23,7 +23,7 @@ import (
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
"github.com/containerd/containerd/v2/core/snapshots"
"github.com/containerd/containerd/v2/oci"
"github.com/containerd/containerd/v2/pkg/oci"
)
// No extra spec options needed for windows.

View File

@@ -25,9 +25,9 @@ import (
"time"
containerd "github.com/containerd/containerd/v2/client"
"github.com/containerd/containerd/v2/oci"
containerdio "github.com/containerd/containerd/v2/pkg/cio"
"github.com/containerd/containerd/v2/pkg/errdefs"
"github.com/containerd/containerd/v2/pkg/oci"
"github.com/containerd/log"
"k8s.io/client-go/tools/remotecommand"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"

View File

@@ -27,10 +27,10 @@ import (
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
"github.com/containerd/containerd/v2/core/containers"
"github.com/containerd/containerd/v2/oci"
criconfig "github.com/containerd/containerd/v2/pkg/cri/config"
crilabels "github.com/containerd/containerd/v2/pkg/cri/labels"
containerstore "github.com/containerd/containerd/v2/pkg/cri/store/container"
"github.com/containerd/containerd/v2/pkg/oci"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/containerd/v2/protobuf/types"
runcoptions "github.com/containerd/containerd/v2/runtime/v2/runc/options"

View File

@@ -25,7 +25,7 @@ import (
"strings"
"github.com/containerd/containerd/v2/contrib/seccomp"
"github.com/containerd/containerd/v2/oci"
"github.com/containerd/containerd/v2/pkg/oci"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
)

View File

@@ -29,7 +29,6 @@ import (
eventtypes "github.com/containerd/containerd/v2/api/events"
containerd "github.com/containerd/containerd/v2/client"
"github.com/containerd/containerd/v2/core/sandbox"
"github.com/containerd/containerd/v2/oci"
criconfig "github.com/containerd/containerd/v2/pkg/cri/config"
"github.com/containerd/containerd/v2/pkg/cri/constants"
"github.com/containerd/containerd/v2/pkg/cri/server/base"
@@ -37,6 +36,7 @@ import (
imagestore "github.com/containerd/containerd/v2/pkg/cri/store/image"
ctrdutil "github.com/containerd/containerd/v2/pkg/cri/util"
"github.com/containerd/containerd/v2/pkg/errdefs"
"github.com/containerd/containerd/v2/pkg/oci"
osinterface "github.com/containerd/containerd/v2/pkg/os"
"github.com/containerd/containerd/v2/platforms"
"github.com/containerd/containerd/v2/plugins"

View File

@@ -31,12 +31,12 @@ import (
containerd "github.com/containerd/containerd/v2/client"
"github.com/containerd/containerd/v2/core/containers"
"github.com/containerd/containerd/v2/oci"
crilabels "github.com/containerd/containerd/v2/pkg/cri/labels"
imagestore "github.com/containerd/containerd/v2/pkg/cri/store/image"
sandboxstore "github.com/containerd/containerd/v2/pkg/cri/store/sandbox"
ctrdutil "github.com/containerd/containerd/v2/pkg/cri/util"
clabels "github.com/containerd/containerd/v2/pkg/labels"
"github.com/containerd/containerd/v2/pkg/oci"
)
const (

View File

@@ -22,8 +22,8 @@ import (
"strings"
"testing"
"github.com/containerd/containerd/v2/oci"
crilabels "github.com/containerd/containerd/v2/pkg/cri/labels"
"github.com/containerd/containerd/v2/pkg/oci"
docker "github.com/distribution/reference"
imagedigest "github.com/opencontainers/go-digest"
runtimespec "github.com/opencontainers/runtime-spec/specs-go"

View File

@@ -22,7 +22,7 @@ import (
"strconv"
"strings"
"github.com/containerd/containerd/v2/oci"
"github.com/containerd/containerd/v2/pkg/oci"
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
runtimespec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/opencontainers/selinux/go-selinux"

View File

@@ -20,8 +20,8 @@ package podsandbox
import (
"github.com/containerd/containerd/v2/core/snapshots"
"github.com/containerd/containerd/v2/oci"
"github.com/containerd/containerd/v2/pkg/cri/annotations"
"github.com/containerd/containerd/v2/pkg/oci"
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
runtimespec "github.com/opencontainers/runtime-spec/specs-go"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"

View File

@@ -20,7 +20,7 @@ import (
"fmt"
"strconv"
"github.com/containerd/containerd/v2/oci"
"github.com/containerd/containerd/v2/pkg/oci"
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
runtimespec "github.com/opencontainers/runtime-spec/specs-go"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"

View File

@@ -32,7 +32,6 @@ import (
containerd "github.com/containerd/containerd/v2/client"
"github.com/containerd/containerd/v2/core/sandbox"
"github.com/containerd/containerd/v2/oci"
criconfig "github.com/containerd/containerd/v2/pkg/cri/config"
"github.com/containerd/containerd/v2/pkg/cri/nri"
"github.com/containerd/containerd/v2/pkg/cri/server/podsandbox"
@@ -42,6 +41,7 @@ import (
sandboxstore "github.com/containerd/containerd/v2/pkg/cri/store/sandbox"
snapshotstore "github.com/containerd/containerd/v2/pkg/cri/store/snapshot"
ctrdutil "github.com/containerd/containerd/v2/pkg/cri/util"
"github.com/containerd/containerd/v2/pkg/oci"
osinterface "github.com/containerd/containerd/v2/pkg/os"
"github.com/containerd/containerd/v2/pkg/registrar"
)

38
pkg/oci/client.go Normal file
View File

@@ -0,0 +1,38 @@
/*
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 oci
import (
"context"
"github.com/containerd/containerd/v2/core/content"
"github.com/containerd/containerd/v2/core/snapshots"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
// Client interface used by SpecOpt
type Client interface {
SnapshotService(snapshotterName string) snapshots.Snapshotter
}
// Image interface used by some SpecOpt to query image configuration
type Image interface {
// Config descriptor for the image.
Config(ctx context.Context) (ocispec.Descriptor, error)
// ContentStore provides a content store which contains image blob data
ContentStore() content.Store
}

73
pkg/oci/mounts.go Normal file
View File

@@ -0,0 +1,73 @@
//go:build !freebsd
/*
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 oci
import (
specs "github.com/opencontainers/runtime-spec/specs-go"
)
func defaultMounts() []specs.Mount {
return []specs.Mount{
{
Destination: "/proc",
Type: "proc",
Source: "proc",
Options: []string{"nosuid", "noexec", "nodev"},
},
{
Destination: "/dev",
Type: "tmpfs",
Source: "tmpfs",
Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"},
},
{
Destination: "/dev/pts",
Type: "devpts",
Source: "devpts",
Options: []string{"nosuid", "noexec", "newinstance", "ptmxmode=0666", "mode=0620", "gid=5"},
},
{
Destination: "/dev/shm",
Type: "tmpfs",
Source: "shm",
Options: []string{"nosuid", "noexec", "nodev", "mode=1777", "size=65536k"},
},
{
Destination: "/dev/mqueue",
Type: "mqueue",
Source: "mqueue",
Options: []string{"nosuid", "noexec", "nodev"},
},
{
Destination: "/sys",
Type: "sysfs",
Source: "sysfs",
Options: []string{"nosuid", "noexec", "nodev", "ro"},
},
{
Destination: "/run",
Type: "tmpfs",
Source: "tmpfs",
Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"},
},
}
}
// appendOSMounts is only used on FreeBSD, and a no-op on other platforms.
func appendOSMounts(_ *Spec, _ string) {}

65
pkg/oci/mounts_freebsd.go Normal file
View File

@@ -0,0 +1,65 @@
/*
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 oci
import (
specs "github.com/opencontainers/runtime-spec/specs-go"
)
func defaultMounts() []specs.Mount {
return []specs.Mount{
{
Destination: "/dev",
Type: "devfs",
Source: "devfs",
Options: []string{"ruleset=4"},
},
{
Destination: "/dev/fd",
Type: "fdescfs",
Source: "fdescfs",
},
}
}
// appendOSMounts modifies the mount spec to mount emulated Linux filesystems on FreeBSD,
// as per: https://wiki.freebsd.org/LinuxJails
func appendOSMounts(s *Spec, os string) {
// No-op for FreeBSD containers
if os != "linux" {
return
}
/* The nosuid noexec options are for consistency with Linux mounts: on FreeBSD it is
by default impossible to execute anything from these filesystems.
*/
var mounts = []specs.Mount{
{
Destination: "/proc",
Type: "linprocfs",
Source: "linprocfs",
Options: []string{"nosuid", "noexec"},
},
{
Destination: "/sys",
Type: "linsysfs",
Source: "linsysfs",
Options: []string{"nosuid", "noexec", "nodev"},
},
}
s.Mounts = append(mounts, s.Mounts...)
}

266
pkg/oci/spec.go Normal file
View File

@@ -0,0 +1,266 @@
/*
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 oci
import (
"context"
"encoding/json"
"os"
"path/filepath"
"runtime"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/containerd/containerd/v2/api/types"
"github.com/containerd/containerd/v2/core/containers"
"github.com/containerd/containerd/v2/pkg/namespaces"
"github.com/containerd/containerd/v2/platforms"
)
const (
rwm = "rwm"
defaultRootfsPath = "rootfs"
)
var (
defaultUnixEnv = []string{
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
}
)
// Spec is a type alias to the OCI runtime spec to allow third part SpecOpts
// to be created without the "issues" with go vendoring and package imports
type Spec = specs.Spec
const ConfigFilename = "config.json"
// ReadSpec deserializes JSON into an OCI runtime Spec from a given path.
func ReadSpec(path string) (*Spec, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
var s Spec
if err := json.NewDecoder(f).Decode(&s); err != nil {
return nil, err
}
return &s, nil
}
// GenerateSpec will generate a default spec from the provided image
// for use as a containerd container
func GenerateSpec(ctx context.Context, client Client, c *containers.Container, opts ...SpecOpts) (*Spec, error) {
return GenerateSpecWithPlatform(ctx, client, platforms.DefaultString(), c, opts...)
}
// GenerateSpecWithPlatform will generate a default spec from the provided image
// for use as a containerd container in the platform requested.
func GenerateSpecWithPlatform(ctx context.Context, client Client, platform string, c *containers.Container, opts ...SpecOpts) (*Spec, error) {
var s Spec
if err := generateDefaultSpecWithPlatform(ctx, platform, c.ID, &s); err != nil {
return nil, err
}
return &s, ApplyOpts(ctx, client, c, &s, opts...)
}
func generateDefaultSpecWithPlatform(ctx context.Context, platform, id string, s *Spec) error {
plat, err := platforms.Parse(platform)
if err != nil {
return err
}
switch plat.OS {
case "windows":
err = populateDefaultWindowsSpec(ctx, s, id)
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
}
// ApplyOpts applies the options to the given spec, injecting data from the
// context, client and container instance.
func ApplyOpts(ctx context.Context, client Client, c *containers.Container, s *Spec, opts ...SpecOpts) error {
for _, o := range opts {
if err := o(ctx, client, c, s); err != nil {
return err
}
}
return nil
}
func defaultUnixCaps() []string {
return []string{
"CAP_CHOWN",
"CAP_DAC_OVERRIDE",
"CAP_FSETID",
"CAP_FOWNER",
"CAP_MKNOD",
"CAP_NET_RAW",
"CAP_SETGID",
"CAP_SETUID",
"CAP_SETFCAP",
"CAP_SETPCAP",
"CAP_NET_BIND_SERVICE",
"CAP_SYS_CHROOT",
"CAP_KILL",
"CAP_AUDIT_WRITE",
}
}
func defaultUnixNamespaces() []specs.LinuxNamespace {
return []specs.LinuxNamespace{
{
Type: specs.PIDNamespace,
},
{
Type: specs.IPCNamespace,
},
{
Type: specs.UTSNamespace,
},
{
Type: specs.MountNamespace,
},
{
Type: specs.NetworkNamespace,
},
}
}
func populateDefaultUnixSpec(ctx context.Context, s *Spec, id string) error {
ns, err := namespaces.NamespaceRequired(ctx)
if err != nil {
return err
}
*s = Spec{
Version: specs.Version,
Root: &specs.Root{
Path: defaultRootfsPath,
},
Process: &specs.Process{
Cwd: "/",
NoNewPrivileges: true,
User: specs.User{
UID: 0,
GID: 0,
},
Capabilities: &specs.LinuxCapabilities{
Bounding: defaultUnixCaps(),
Permitted: defaultUnixCaps(),
Effective: defaultUnixCaps(),
},
Rlimits: []specs.POSIXRlimit{
{
Type: "RLIMIT_NOFILE",
Hard: uint64(1024),
Soft: uint64(1024),
},
},
},
Linux: &specs.Linux{
MaskedPaths: []string{
"/proc/acpi",
"/proc/asound",
"/proc/kcore",
"/proc/keys",
"/proc/latency_stats",
"/proc/timer_list",
"/proc/timer_stats",
"/proc/sched_debug",
"/sys/firmware",
"/sys/devices/virtual/powercap",
"/proc/scsi",
},
ReadonlyPaths: []string{
"/proc/bus",
"/proc/fs",
"/proc/irq",
"/proc/sys",
"/proc/sysrq-trigger",
},
CgroupsPath: filepath.Join("/", ns, id),
Resources: &specs.LinuxResources{
Devices: []specs.LinuxDeviceCgroup{
{
Allow: false,
Access: rwm,
},
},
},
Namespaces: defaultUnixNamespaces(),
},
}
s.Mounts = defaultMounts()
return nil
}
func populateDefaultWindowsSpec(ctx context.Context, s *Spec, id string) error {
*s = Spec{
Version: specs.Version,
Root: &specs.Root{},
Process: &specs.Process{
Cwd: `C:\`,
},
Windows: &specs.Windows{},
}
return nil
}
func populateDefaultDarwinSpec(s *Spec) error {
*s = Spec{
Version: specs.Version,
Root: &specs.Root{},
Process: &specs.Process{Cwd: "/"},
}
return nil
}
// DescriptorFromProto converts containerds protobuf [types.Descriptor]
// to the OCI image specs [ocispec.Descriptor].
func DescriptorFromProto(d *types.Descriptor) ocispec.Descriptor {
return ocispec.Descriptor{
MediaType: d.MediaType,
Digest: digest.Digest(d.Digest),
Size: d.Size,
Annotations: d.Annotations,
}
}
// DescriptorToProto converts the OCI image specs [ocispec.Descriptor]
// to containerds protobuf [types.Descriptor].
func DescriptorToProto(d ocispec.Descriptor) *types.Descriptor {
return &types.Descriptor{
MediaType: d.MediaType,
Digest: d.Digest.String(),
Size: d.Size,
Annotations: d.Annotations,
}
}

1674
pkg/oci/spec_opts.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,81 @@
/*
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 oci
import (
"context"
"github.com/containerd/containerd/v2/core/containers"
"github.com/containerd/containerd/v2/pkg/cap"
specs "github.com/opencontainers/runtime-spec/specs-go"
)
// WithHostDevices adds all the hosts device nodes to the container's spec
func WithHostDevices(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
setLinux(s)
devs, err := HostDevices()
if err != nil {
return err
}
s.Linux.Devices = append(s.Linux.Devices, devs...)
return nil
}
// WithDevices recursively adds devices from the passed in path and associated cgroup rules for that device.
// If devicePath is a dir it traverses the dir to add all devices in that dir.
// If devicePath is not a dir, it attempts to add the single device.
// If containerPath is not set then the device path is used for the container path.
func WithDevices(devicePath, containerPath, permissions string) SpecOpts {
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
devs, err := getDevices(devicePath, containerPath)
if err != nil {
return err
}
for i := range devs {
s.Linux.Devices = append(s.Linux.Devices, devs[i])
s.Linux.Resources.Devices = append(s.Linux.Resources.Devices, specs.LinuxDeviceCgroup{
Allow: true,
Type: devs[i].Type,
Major: &devs[i].Major,
Minor: &devs[i].Minor,
Access: permissions,
})
}
return nil
}
}
// WithAllCurrentCapabilities propagates the effective capabilities of the caller process to the container process.
// The capability set may differ from WithAllKnownCapabilities when running in a container.
var WithAllCurrentCapabilities = func(ctx context.Context, client Client, c *containers.Container, s *Spec) error {
caps, err := cap.Current()
if err != nil {
return err
}
return WithCapabilities(caps)(ctx, client, c, s)
}
// WithAllKnownCapabilities sets all the known linux capabilities for the container process
var WithAllKnownCapabilities = func(ctx context.Context, client Client, c *containers.Container, s *Spec) error {
caps := cap.Known()
return WithCapabilities(caps)(ctx, client, c, s)
}
func escapeAndCombineArgs(args []string) string {
panic("not supported")
}

View File

@@ -0,0 +1,765 @@
/*
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 oci
import (
"context"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/containerd/containerd/v2/core/containers"
"github.com/containerd/containerd/v2/pkg/cap"
"github.com/containerd/containerd/v2/pkg/testutil"
"github.com/containerd/continuity/fs/fstest"
specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/sys/unix"
)
//nolint:gosec
func TestWithUserID(t *testing.T) {
t.Parallel()
expectedPasswd := `root:x:0:0:root:/root:/bin/ash
guest:x:405:100:guest:/dev/null:/sbin/nologin
`
td := t.TempDir()
apply := fstest.Apply(
fstest.CreateDir("/etc", 0777),
fstest.CreateFile("/etc/passwd", []byte(expectedPasswd), 0777),
)
if err := apply.Apply(td); err != nil {
t.Fatalf("failed to apply: %v", err)
}
c := containers.Container{ID: t.Name()}
testCases := []struct {
userID uint32
expectedUID uint32
expectedGID uint32
}{
{
userID: 0,
expectedUID: 0,
expectedGID: 0,
},
{
userID: 405,
expectedUID: 405,
expectedGID: 100,
},
{
userID: 1000,
expectedUID: 1000,
expectedGID: 0,
},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(fmt.Sprintf("user %d", testCase.userID), func(t *testing.T) {
t.Parallel()
s := Spec{
Version: specs.Version,
Root: &specs.Root{
Path: td,
},
Linux: &specs.Linux{},
}
err := WithUserID(testCase.userID)(context.Background(), nil, &c, &s)
assert.NoError(t, err)
assert.Equal(t, testCase.expectedUID, s.Process.User.UID)
assert.Equal(t, testCase.expectedGID, s.Process.User.GID)
})
}
}
//nolint:gosec
func TestWithUsername(t *testing.T) {
t.Parallel()
expectedPasswd := `root:x:0:0:root:/root:/bin/ash
guest:x:405:100:guest:/dev/null:/sbin/nologin
`
td := t.TempDir()
apply := fstest.Apply(
fstest.CreateDir("/etc", 0777),
fstest.CreateFile("/etc/passwd", []byte(expectedPasswd), 0777),
)
if err := apply.Apply(td); err != nil {
t.Fatalf("failed to apply: %v", err)
}
c := containers.Container{ID: t.Name()}
testCases := []struct {
user string
expectedUID uint32
expectedGID uint32
err string
}{
{
user: "root",
expectedUID: 0,
expectedGID: 0,
},
{
user: "guest",
expectedUID: 405,
expectedGID: 100,
},
{
user: "1000",
err: "no users found",
},
{
user: "unknown",
err: "no users found",
},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.user, func(t *testing.T) {
t.Parallel()
s := Spec{
Version: specs.Version,
Root: &specs.Root{
Path: td,
},
Linux: &specs.Linux{},
}
err := WithUsername(testCase.user)(context.Background(), nil, &c, &s)
if err != nil {
assert.EqualError(t, err, testCase.err)
}
assert.Equal(t, testCase.expectedUID, s.Process.User.UID)
assert.Equal(t, testCase.expectedGID, s.Process.User.GID)
})
}
}
//nolint:gosec
func TestWithAdditionalGIDs(t *testing.T) {
t.Parallel()
expectedPasswd := `root:x:0:0:root:/root:/bin/ash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
`
expectedGroup := `root:x:0:root
bin:x:1:root,bin,daemon
daemon:x:2:root,bin,daemon
sys:x:3:root,bin,adm
`
td := t.TempDir()
apply := fstest.Apply(
fstest.CreateDir("/etc", 0777),
fstest.CreateFile("/etc/passwd", []byte(expectedPasswd), 0777),
fstest.CreateFile("/etc/group", []byte(expectedGroup), 0777),
)
if err := apply.Apply(td); err != nil {
t.Fatalf("failed to apply: %v", err)
}
c := containers.Container{ID: t.Name()}
testCases := []struct {
user string
expected []uint32
}{
{
user: "root",
expected: []uint32{0, 1, 2, 3},
},
{
user: "1000",
expected: []uint32{0},
},
{
user: "bin",
expected: []uint32{0, 2, 3},
},
{
user: "bin:root",
expected: []uint32{0},
},
{
user: "daemon",
expected: []uint32{0, 1},
},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.user, func(t *testing.T) {
t.Parallel()
s := Spec{
Version: specs.Version,
Root: &specs.Root{
Path: td,
},
}
err := WithAdditionalGIDs(testCase.user)(context.Background(), nil, &c, &s)
assert.NoError(t, err)
assert.Equal(t, testCase.expected, s.Process.User.AdditionalGids)
})
}
}
// withAllKnownCaps sets all known capabilities.
// This function differs from the exported function
// by also setting inheritable capabilities.
func withAllKnownCaps(s *specs.Spec) error {
caps := cap.Known()
if err := WithCapabilities(caps)(context.Background(), nil, nil, s); err != nil {
return err
}
s.Process.Capabilities.Inheritable = caps
return nil
}
func TestSetCaps(t *testing.T) {
t.Parallel()
var s specs.Spec
// Add base set of capabilities
if err := WithCapabilities([]string{"CAP_CHOWN"})(context.Background(), nil, nil, &s); err != nil {
t.Fatal(err)
}
for i, cl := range [][]string{
s.Process.Capabilities.Bounding,
s.Process.Capabilities.Effective,
s.Process.Capabilities.Permitted,
} {
if !capsContain(cl, "CAP_CHOWN") {
t.Errorf("cap list %d does not contain added cap", i)
}
if len(cl) != 1 {
t.Errorf("cap list %d does not have only 1 cap", i)
}
}
if len(s.Process.Capabilities.Inheritable) != 0 {
t.Errorf("inheritable cap list is not empty")
}
// Add all caps then overwrite with single cap
if err := withAllKnownCaps(&s); err != nil {
t.Fatal(err)
}
if err := WithCapabilities([]string{"CAP_CHOWN"})(context.Background(), nil, nil, &s); err != nil {
t.Fatal(err)
}
for i, cl := range [][]string{
s.Process.Capabilities.Bounding,
s.Process.Capabilities.Effective,
s.Process.Capabilities.Permitted,
s.Process.Capabilities.Inheritable,
} {
if !capsContain(cl, "CAP_CHOWN") {
t.Errorf("cap list %d does not contain added cap", i)
}
if len(cl) != 1 {
t.Errorf("cap list %d does not have only 1 cap", i)
}
}
// Add all caps, drop single cap, then overwrite with single cap
if err := withAllKnownCaps(&s); err != nil {
t.Fatal(err)
}
if err := WithDroppedCapabilities([]string{"CAP_CHOWN"})(context.Background(), nil, nil, &s); err != nil {
t.Fatal(err)
}
if err := WithCapabilities([]string{"CAP_CHOWN"})(context.Background(), nil, nil, &s); err != nil {
t.Fatal(err)
}
for i, cl := range [][]string{
s.Process.Capabilities.Bounding,
s.Process.Capabilities.Effective,
s.Process.Capabilities.Permitted,
} {
if !capsContain(cl, "CAP_CHOWN") {
t.Errorf("cap list %d does not contain added cap", i)
}
if len(cl) != 1 {
t.Errorf("cap list %d does not have only 1 cap", i)
}
}
if len(s.Process.Capabilities.Inheritable) != 0 {
t.Errorf("inheritable cap list is not empty")
}
}
func TestAddCaps(t *testing.T) {
t.Parallel()
var s specs.Spec
if err := WithAddedCapabilities([]string{"CAP_CHOWN"})(context.Background(), nil, nil, &s); err != nil {
t.Fatal(err)
}
for i, cl := range [][]string{
s.Process.Capabilities.Bounding,
s.Process.Capabilities.Effective,
s.Process.Capabilities.Permitted,
} {
if !capsContain(cl, "CAP_CHOWN") {
t.Errorf("cap list %d does not contain added cap", i)
}
}
if len(s.Process.Capabilities.Inheritable) != 0 {
t.Errorf("inheritable cap list is not empty")
}
}
func TestDropCaps(t *testing.T) {
t.Parallel()
var s specs.Spec
if err := withAllKnownCaps(&s); err != nil {
t.Fatal(err)
}
if err := WithDroppedCapabilities([]string{"CAP_CHOWN"})(context.Background(), nil, nil, &s); err != nil {
t.Fatal(err)
}
for i, cl := range [][]string{
s.Process.Capabilities.Bounding,
s.Process.Capabilities.Effective,
s.Process.Capabilities.Permitted,
s.Process.Capabilities.Inheritable,
} {
if capsContain(cl, "CAP_CHOWN") {
t.Errorf("cap list %d contains dropped cap", i)
}
}
// Add all capabilities back and drop a different cap.
if err := withAllKnownCaps(&s); err != nil {
t.Fatal(err)
}
if err := WithDroppedCapabilities([]string{"CAP_FOWNER"})(context.Background(), nil, nil, &s); err != nil {
t.Fatal(err)
}
for i, cl := range [][]string{
s.Process.Capabilities.Bounding,
s.Process.Capabilities.Effective,
s.Process.Capabilities.Permitted,
s.Process.Capabilities.Inheritable,
} {
if capsContain(cl, "CAP_FOWNER") {
t.Errorf("cap list %d contains dropped cap", i)
}
if !capsContain(cl, "CAP_CHOWN") {
t.Errorf("cap list %d doesn't contain non-dropped cap", i)
}
}
// Drop all duplicated caps.
if err := WithCapabilities([]string{"CAP_CHOWN", "CAP_CHOWN"})(context.Background(), nil, nil, &s); err != nil {
t.Fatal(err)
}
if err := WithDroppedCapabilities([]string{"CAP_CHOWN"})(context.Background(), nil, nil, &s); err != nil {
t.Fatal(err)
}
for i, cl := range [][]string{
s.Process.Capabilities.Bounding,
s.Process.Capabilities.Effective,
s.Process.Capabilities.Permitted,
s.Process.Capabilities.Inheritable,
} {
if len(cl) != 0 {
t.Errorf("cap list %d is not empty", i)
}
}
// Add all capabilities back and drop all
if err := withAllKnownCaps(&s); err != nil {
t.Fatal(err)
}
if err := WithCapabilities(nil)(context.Background(), nil, nil, &s); err != nil {
t.Fatal(err)
}
for i, cl := range [][]string{
s.Process.Capabilities.Bounding,
s.Process.Capabilities.Effective,
s.Process.Capabilities.Permitted,
s.Process.Capabilities.Inheritable,
} {
if len(cl) != 0 {
t.Errorf("cap list %d is not empty", i)
}
}
}
func TestGetDevices(t *testing.T) {
testutil.RequiresRoot(t)
dir, err := os.MkdirTemp("/dev", t.Name())
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
zero := filepath.Join(dir, "zero")
if err := os.WriteFile(zero, nil, 0600); err != nil {
t.Fatal(err)
}
if err := unix.Mount("/dev/zero", zero, "", unix.MS_BIND, ""); err != nil {
t.Fatal(err)
}
defer unix.Unmount(filepath.Join(dir, "zero"), unix.MNT_DETACH)
t.Run("single device", func(t *testing.T) {
t.Run("no container path", func(t *testing.T) {
devices, err := getDevices(dir, "")
if err != nil {
t.Fatal(err)
}
if len(devices) != 1 {
t.Fatalf("expected one device %v", devices)
}
if devices[0].Path != zero {
t.Fatalf("got unexpected device path %s", devices[0].Path)
}
})
t.Run("with container path", func(t *testing.T) {
newPath := "/dev/testNew"
devices, err := getDevices(dir, newPath)
if err != nil {
t.Fatal(err)
}
if len(devices) != 1 {
t.Fatalf("expected one device %v", devices)
}
if devices[0].Path != filepath.Join(newPath, "zero") {
t.Fatalf("got unexpected device path %s", devices[0].Path)
}
})
})
t.Run("two devices", func(t *testing.T) {
nullDev := filepath.Join(dir, "null")
if err := os.WriteFile(nullDev, nil, 0600); err != nil {
t.Fatal(err)
}
if err := unix.Mount("/dev/null", nullDev, "", unix.MS_BIND, ""); err != nil {
t.Fatal(err)
}
defer unix.Unmount(filepath.Join(dir, "null"), unix.MNT_DETACH)
devices, err := getDevices(dir, "")
if err != nil {
t.Fatal(err)
}
if len(devices) != 2 {
t.Fatalf("expected two devices %v", devices)
}
if devices[0].Path == devices[1].Path {
t.Fatalf("got same path for the two devices %s", devices[0].Path)
}
if devices[0].Path != zero && devices[0].Path != nullDev {
t.Fatalf("got unexpected device path %s", devices[0].Path)
}
if devices[1].Path != zero && devices[1].Path != nullDev {
t.Fatalf("got unexpected device path %s", devices[1].Path)
}
if devices[0].Major == devices[1].Major && devices[0].Minor == devices[1].Minor {
t.Fatalf("got sema mojor and minor on two devices %s %s", devices[0].Path, devices[1].Path)
}
})
t.Run("With symlink in dir", func(t *testing.T) {
if err := os.Symlink("/dev/zero", filepath.Join(dir, "zerosym")); err != nil {
t.Fatal(err)
}
devices, err := getDevices(dir, "")
if err != nil {
t.Fatal(err)
}
if len(devices) != 1 {
t.Fatalf("expected one device %v", devices)
}
if devices[0].Path != filepath.Join(dir, "zero") {
t.Fatalf("got unexpected device path, expected %q, got %q", filepath.Join(dir, "zero"), devices[0].Path)
}
})
t.Run("No devices", func(t *testing.T) {
dir := dir + "2"
if err := os.MkdirAll(dir, 0755); err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
t.Run("empty dir", func(T *testing.T) {
devices, err := getDevices(dir, "")
if err != nil {
t.Fatal(err)
}
if len(devices) != 0 {
t.Fatalf("expected no devices, got %+v", devices)
}
})
t.Run("symlink to device in dir", func(t *testing.T) {
if err := os.Symlink("/dev/zero", filepath.Join(dir, "zerosym")); err != nil {
t.Fatal(err)
}
defer os.Remove(filepath.Join(dir, "zerosym"))
devices, err := getDevices(dir, "")
if err != nil {
t.Fatal(err)
}
if len(devices) != 0 {
t.Fatalf("expected no devices, got %+v", devices)
}
})
t.Run("regular file in dir", func(t *testing.T) {
if err := os.WriteFile(filepath.Join(dir, "somefile"), []byte("hello"), 0600); err != nil {
t.Fatal(err)
}
defer os.Remove(filepath.Join(dir, "somefile"))
devices, err := getDevices(dir, "")
if err != nil {
t.Fatal(err)
}
if len(devices) != 0 {
t.Fatalf("expected no devices, got %+v", devices)
}
})
})
}
func TestWithAppendAdditionalGroups(t *testing.T) {
t.Parallel()
expectedContent := `root:x:0:root
bin:x:1:root,bin,daemon
daemon:x:2:root,bin,daemon
`
td := t.TempDir()
apply := fstest.Apply(
fstest.CreateDir("/etc", 0777),
fstest.CreateFile("/etc/group", []byte(expectedContent), 0777),
)
if err := apply.Apply(td); err != nil {
t.Fatalf("failed to apply: %v", err)
}
c := containers.Container{ID: t.Name()}
testCases := []struct {
name string
additionalGIDs []uint32
groups []string
expected []uint32
err string
}{
{
name: "no additional gids",
groups: []string{},
expected: []uint32{0},
},
{
name: "no additional gids, append root gid",
groups: []string{"root"},
expected: []uint32{0},
},
{
name: "no additional gids, append bin and daemon gids",
groups: []string{"bin", "daemon"},
expected: []uint32{0, 1, 2},
},
{
name: "has root additional gids, append bin and daemon gids",
additionalGIDs: []uint32{0},
groups: []string{"bin", "daemon"},
expected: []uint32{0, 1, 2},
},
{
name: "append group id",
groups: []string{"999"},
expected: []uint32{0, 999},
},
{
name: "unknown group",
groups: []string{"unknown"},
err: "unable to find group unknown",
expected: []uint32{0},
},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
s := Spec{
Version: specs.Version,
Root: &specs.Root{
Path: td,
},
Process: &specs.Process{
User: specs.User{
AdditionalGids: testCase.additionalGIDs,
},
},
}
err := WithAppendAdditionalGroups(testCase.groups...)(context.Background(), nil, &c, &s)
if err != nil {
assert.EqualError(t, err, testCase.err)
}
assert.Equal(t, testCase.expected, s.Process.User.AdditionalGids)
})
}
}
func TestWithAppendAdditionalGroupsNoEtcGroup(t *testing.T) {
t.Parallel()
td := t.TempDir()
apply := fstest.Apply()
if err := apply.Apply(td); err != nil {
t.Fatalf("failed to apply: %v", err)
}
c := containers.Container{ID: t.Name()}
testCases := []struct {
name string
additionalGIDs []uint32
groups []string
expected []uint32
err string
}{
{
name: "no additional gids",
groups: []string{},
expected: []uint32{0},
},
{
name: "no additional gids, append root group",
groups: []string{"root"},
err: fmt.Sprintf("unable to find group root: open %s: no such file or directory", filepath.Join(td, "etc", "group")),
expected: []uint32{0},
},
{
name: "append group id",
groups: []string{"999"},
expected: []uint32{0, 999},
},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
s := Spec{
Version: specs.Version,
Root: &specs.Root{
Path: td,
},
Process: &specs.Process{
User: specs.User{
AdditionalGids: testCase.additionalGIDs,
},
},
}
err := WithAppendAdditionalGroups(testCase.groups...)(context.Background(), nil, &c, &s)
if err != nil {
assert.EqualError(t, err, testCase.err)
}
assert.Equal(t, testCase.expected, s.Process.User.AdditionalGids)
})
}
}
func TestWithLinuxDeviceFollowSymlinks(t *testing.T) {
// Create symlink to /dev/zero for the symlink test case
zero := "/dev/zero"
_, err := os.Stat(zero)
require.NoError(t, err, "Host does not have /dev/zero")
dir := t.TempDir()
symZero := filepath.Join(dir, "zero")
err = os.Symlink(zero, symZero)
require.NoError(t, err, "unexpected error creating symlink")
testcases := []struct {
name string
path string
followSymlinks bool
expectError bool
expectedLinuxDevices []specs.LinuxDevice
}{
{
name: "regularDeviceresolvesPath",
path: zero,
expectError: false,
expectedLinuxDevices: []specs.LinuxDevice{{
Path: zero,
Type: "c",
}},
},
{
name: "symlinkedDeviceResolvesPath",
path: symZero,
expectError: false,
expectedLinuxDevices: []specs.LinuxDevice{{
Path: zero,
Type: "c",
}},
},
{
name: "symlinkedDeviceResolvesFakePath_error",
path: "/fake/path",
expectError: true,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
spec := Spec{
Version: specs.Version,
Root: &specs.Root{},
Linux: &specs.Linux{},
}
opts := []SpecOpts{
WithLinuxDeviceFollowSymlinks(tc.path, ""),
}
for _, opt := range opts {
if err := opt(nil, nil, nil, &spec); err != nil {
if tc.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
}
}
if len(tc.expectedLinuxDevices) != 0 {
require.NotNil(t, spec.Linux)
require.Len(t, spec.Linux.Devices, 1)
assert.Equal(t, spec.Linux.Devices[0].Path, tc.expectedLinuxDevices[0].Path)
assert.Equal(t, spec.Linux.Devices[0].Type, tc.expectedLinuxDevices[0].Type)
}
})
}
}

View File

@@ -0,0 +1,36 @@
//go:build !linux
/*
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 oci
import (
"context"
"github.com/containerd/containerd/v2/core/containers"
)
// WithAllCurrentCapabilities propagates the effective capabilities of the caller process to the container process.
// The capability set may differ from WithAllKnownCapabilities when running in a container.
var WithAllCurrentCapabilities = func(ctx context.Context, client Client, c *containers.Container, s *Spec) error {
return WithCapabilities(nil)(ctx, client, c, s)
}
// WithAllKnownCapabilities sets all the known linux capabilities for the container process
var WithAllKnownCapabilities = func(ctx context.Context, client Client, c *containers.Container, s *Spec) error {
return WithCapabilities(nil)(ctx, client, c, s)
}

View File

@@ -0,0 +1,32 @@
//go:build !windows
/*
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 oci
import (
"context"
"github.com/containerd/containerd/v2/core/containers"
)
// WithDefaultPathEnv sets the $PATH environment variable to the
// default PATH defined in this package.
func WithDefaultPathEnv(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
s.Process.Env = replaceOrAppendEnvValues(s.Process.Env, defaultUnixEnv)
return nil
}

View File

@@ -0,0 +1,43 @@
//go:build !windows
/*
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 oci
import (
"context"
"testing"
"github.com/containerd/containerd/v2/pkg/namespaces"
specs "github.com/opencontainers/runtime-spec/specs-go"
)
func TestWithDefaultPathEnv(t *testing.T) {
t.Parallel()
s := Spec{}
s.Process = &specs.Process{
Env: []string{},
}
var (
defaultUnixEnv = "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
ctx = namespaces.WithNamespace(context.Background(), "test")
)
WithDefaultPathEnv(ctx, nil, nil, &s)
if !Contains(s.Process.Env, defaultUnixEnv) {
t.Fatal("default Unix Env not found")
}
}

773
pkg/oci/spec_opts_test.go Normal file
View File

@@ -0,0 +1,773 @@
/*
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 oci
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"os"
"path/filepath"
"reflect"
"runtime"
"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/v2/core/containers"
"github.com/containerd/containerd/v2/core/content"
"github.com/containerd/containerd/v2/pkg/errdefs"
"github.com/containerd/containerd/v2/pkg/namespaces"
)
type blob []byte
func (b blob) ReadAt(p []byte, off int64) (int, error) {
if off >= int64(len(b)) {
return 0, io.EOF
}
return copy(p, b[off:]), nil
}
func (b blob) Close() error {
return nil
}
func (b blob) Size() int64 {
return int64(len(b))
}
type fakeImage struct {
config ocispec.Descriptor
blobs map[string]blob
}
func newFakeImage(config ocispec.Image) (Image, error) {
configBlob, err := json.Marshal(config)
if err != nil {
return nil, err
}
configDescriptor := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageConfig,
Digest: digest.NewDigestFromBytes(digest.SHA256, configBlob),
}
return fakeImage{
config: configDescriptor,
blobs: map[string]blob{
configDescriptor.Digest.String(): configBlob,
},
}, nil
}
func (i fakeImage) Config(ctx context.Context) (ocispec.Descriptor, error) {
return i.config, nil
}
func (i fakeImage) ContentStore() content.Store {
return i
}
func (i fakeImage) ReaderAt(ctx context.Context, dec ocispec.Descriptor) (content.ReaderAt, error) {
blob, found := i.blobs[dec.Digest.String()]
if !found {
return nil, errdefs.ErrNotFound
}
return blob, nil
}
func (i fakeImage) Info(ctx context.Context, dgst digest.Digest) (content.Info, error) {
return content.Info{}, errdefs.ErrNotImplemented
}
func (i fakeImage) Update(ctx context.Context, info content.Info, fieldpaths ...string) (content.Info, error) {
return content.Info{}, errdefs.ErrNotImplemented
}
func (i fakeImage) Walk(ctx context.Context, fn content.WalkFunc, filters ...string) error {
return errdefs.ErrNotImplemented
}
func (i fakeImage) Delete(ctx context.Context, dgst digest.Digest) error {
return errdefs.ErrNotImplemented
}
func (i fakeImage) Status(ctx context.Context, ref string) (content.Status, error) {
return content.Status{}, errdefs.ErrNotImplemented
}
func (i fakeImage) ListStatuses(ctx context.Context, filters ...string) ([]content.Status, error) {
return nil, errdefs.ErrNotImplemented
}
func (i fakeImage) Abort(ctx context.Context, ref string) error {
return errdefs.ErrNotImplemented
}
func (i fakeImage) Writer(ctx context.Context, opts ...content.WriterOpt) (content.Writer, error) {
return nil, errdefs.ErrNotImplemented
}
func TestReplaceOrAppendEnvValues(t *testing.T) {
t.Parallel()
defaults := []string{
"o=ups", "p=$e", "x=foo", "y=boo", "z", "t=",
}
overrides := []string{
"x=bar", "y", "a=42", "o=", "e", "s=",
}
expected := []string{
"o=", "p=$e", "x=bar", "z", "t=", "a=42", "s=",
}
defaultsOrig := make([]string, len(defaults))
copy(defaultsOrig, defaults)
overridesOrig := make([]string, len(overrides))
copy(overridesOrig, overrides)
results := replaceOrAppendEnvValues(defaults, overrides)
if err := assertEqualsStringArrays(defaults, defaultsOrig); err != nil {
t.Fatal(err)
}
if err := assertEqualsStringArrays(overrides, overridesOrig); err != nil {
t.Fatal(err)
}
if err := assertEqualsStringArrays(results, expected); err != nil {
t.Fatal(err)
}
}
func TestWithDefaultSpecForPlatform(t *testing.T) {
t.Parallel()
var (
s Spec
c = containers.Container{ID: "TestWithDefaultSpecForPlatform"}
ctx = namespaces.WithNamespace(context.Background(), "test")
)
platforms := []string{"linux/amd64", "windows/amd64"}
for _, p := range platforms {
if err := ApplyOpts(ctx, nil, &c, &s, WithDefaultSpecForPlatform(p)); err != nil {
t.Fatal(err)
}
}
}
func Contains(a []string, x string) bool {
for _, n := range a {
if x == n {
return true
}
}
return false
}
func TestWithProcessCwd(t *testing.T) {
t.Parallel()
s := Spec{}
opts := []SpecOpts{
WithProcessCwd("testCwd"),
}
var expectedCwd = "testCwd"
for _, opt := range opts {
if err := opt(nil, nil, nil, &s); err != nil {
t.Fatal(err)
}
}
if s.Process.Cwd != expectedCwd {
t.Fatal("Process has a wrong current working directory")
}
}
func TestWithEnv(t *testing.T) {
t.Parallel()
s := Spec{}
s.Process = &specs.Process{
Env: []string{"DEFAULT=test"},
}
WithEnv([]string{"env=1"})(context.Background(), nil, nil, &s)
if len(s.Process.Env) != 2 {
t.Fatal("didn't append")
}
WithEnv([]string{"env2=1"})(context.Background(), nil, nil, &s)
if len(s.Process.Env) != 3 {
t.Fatal("didn't append")
}
WithEnv([]string{"env2=2"})(context.Background(), nil, nil, &s)
if s.Process.Env[2] != "env2=2" {
t.Fatal("couldn't update")
}
WithEnv([]string{"env2"})(context.Background(), nil, nil, &s)
if len(s.Process.Env) != 2 {
t.Fatal("couldn't unset")
}
}
func TestWithMounts(t *testing.T) {
t.Parallel()
s := Spec{
Mounts: []specs.Mount{
{
Source: "default-source",
Destination: "default-dest",
},
},
}
WithMounts([]specs.Mount{
{
Source: "new-source",
Destination: "new-dest",
},
})(nil, nil, nil, &s)
if len(s.Mounts) != 2 {
t.Fatal("didn't append")
}
if s.Mounts[1].Source != "new-source" {
t.Fatal("invalid mount")
}
if s.Mounts[1].Destination != "new-dest" {
t.Fatal("invalid mount")
}
}
func TestWithDefaultSpec(t *testing.T) {
t.Parallel()
var (
s Spec
c = containers.Container{ID: "TestWithDefaultSpec"}
ctx = namespaces.WithNamespace(context.Background(), "test")
)
if err := ApplyOpts(ctx, nil, &c, &s, WithDefaultSpec()); err != nil {
t.Fatal(err)
}
var (
expected Spec
err error
)
switch runtime.GOOS {
case "windows":
err = populateDefaultWindowsSpec(ctx, &expected, c.ID)
case "darwin":
err = populateDefaultDarwinSpec(&expected)
default:
err = populateDefaultUnixSpec(ctx, &expected, c.ID)
}
if err != nil {
t.Fatal(err)
}
if reflect.DeepEqual(s, Spec{}) {
t.Fatalf("spec should not be empty")
}
if !reflect.DeepEqual(&s, &expected) {
t.Fatalf("spec from option differs from default: \n%#v != \n%#v", &s, expected)
}
}
func TestWithSpecFromFile(t *testing.T) {
t.Parallel()
var (
s Spec
c = containers.Container{ID: "TestWithDefaultSpec"}
ctx = namespaces.WithNamespace(context.Background(), "test")
)
fp, err := os.CreateTemp("", "testwithdefaultspec.json")
if err != nil {
t.Fatal(err)
}
defer func() {
if err := os.Remove(fp.Name()); err != nil {
log.Printf("failed to remove tempfile %v: %v", fp.Name(), err)
}
}()
defer fp.Close()
expected, err := GenerateSpec(ctx, nil, &c)
if err != nil {
t.Fatal(err)
}
p, err := json.Marshal(expected)
if err != nil {
t.Fatal(err)
}
if _, err := fp.Write(p); err != nil {
t.Fatal(err)
}
if err := ApplyOpts(ctx, nil, &c, &s, WithSpecFromFile(fp.Name())); err != nil {
t.Fatal(err)
}
if reflect.DeepEqual(s, Spec{}) {
t.Fatalf("spec should not be empty")
}
if !reflect.DeepEqual(&s, expected) {
t.Fatalf("spec from option differs from default: \n%#v != \n%#v", &s, expected)
}
}
func TestWithMemoryLimit(t *testing.T) {
var (
ctx = namespaces.WithNamespace(context.Background(), "testing")
c = containers.Container{ID: t.Name()}
m = uint64(768 * 1024 * 1024)
o = WithMemoryLimit(m)
)
// Test with all three supported scenarios
platforms := []string{"", "linux/amd64", "windows/amd64"}
for _, p := range platforms {
var spec *Spec
var err error
if p == "" {
t.Log("Testing GenerateSpec default platform")
spec, err = GenerateSpec(ctx, nil, &c, o)
// Convert the platform to the default based on GOOS like
// GenerateSpec does.
switch runtime.GOOS {
case "linux":
p = "linux/amd64"
case "windows":
p = "windows/amd64"
}
} else {
t.Logf("Testing GenerateSpecWithPlatform with platform: '%s'", p)
spec, err = GenerateSpecWithPlatform(ctx, nil, p, &c, o)
}
if err != nil {
t.Fatalf("failed to generate spec with: %v", err)
}
switch p {
case "linux/amd64":
if *spec.Linux.Resources.Memory.Limit != int64(m) {
t.Fatalf("spec.Linux.Resources.Memory.Limit expected: %v, got: %v", m, *spec.Linux.Resources.Memory.Limit)
}
// If we are linux/amd64 on Windows GOOS it is LCOW
if runtime.GOOS == "windows" {
// Verify that we also set the Windows section.
if *spec.Windows.Resources.Memory.Limit != m {
t.Fatalf("for LCOW spec.Windows.Resources.Memory.Limit is also expected: %v, got: %v", m, *spec.Windows.Resources.Memory.Limit)
}
} else {
if spec.Windows != nil {
t.Fatalf("spec.Windows section should not be set for linux/amd64 spec on non-windows platform")
}
}
case "windows/amd64":
if *spec.Windows.Resources.Memory.Limit != m {
t.Fatalf("spec.Windows.Resources.Memory.Limit expected: %v, got: %v", m, *spec.Windows.Resources.Memory.Limit)
}
if spec.Linux != nil {
t.Fatalf("spec.Linux section should not be set for windows/amd64 spec ever")
}
}
}
}
func isEqualStringArrays(values, expected []string) bool {
if len(values) != len(expected) {
return false
}
for i, x := range expected {
if values[i] != x {
return false
}
}
return true
}
func assertEqualsStringArrays(values, expected []string) error {
if !isEqualStringArrays(values, expected) {
return fmt.Errorf("expected %s, but found %s", expected, values)
}
return nil
}
func TestWithTTYSize(t *testing.T) {
t.Parallel()
s := Spec{}
opts := []SpecOpts{
WithTTYSize(10, 20),
}
var (
expectedWidth = uint(10)
expectedHeight = uint(20)
)
for _, opt := range opts {
if err := opt(nil, nil, nil, &s); err != nil {
t.Fatal(err)
}
}
if s.Process.ConsoleSize.Height != expectedWidth && s.Process.ConsoleSize.Height != expectedHeight {
t.Fatal("Process Console has invalid size")
}
}
func TestWithUserNamespace(t *testing.T) {
t.Parallel()
s := Spec{}
opts := []SpecOpts{
WithUserNamespace([]specs.LinuxIDMapping{
{
ContainerID: 1,
HostID: 2,
Size: 10000,
},
}, []specs.LinuxIDMapping{
{
ContainerID: 2,
HostID: 3,
Size: 20000,
},
}),
}
for _, opt := range opts {
if err := opt(nil, nil, nil, &s); err != nil {
t.Fatal(err)
}
}
expectedUIDMapping := specs.LinuxIDMapping{
ContainerID: 1,
HostID: 2,
Size: 10000,
}
expectedGIDMapping := specs.LinuxIDMapping{
ContainerID: 2,
HostID: 3,
Size: 20000,
}
if !(len(s.Linux.UIDMappings) == 1 && s.Linux.UIDMappings[0] == expectedUIDMapping) || !(len(s.Linux.GIDMappings) == 1 && s.Linux.GIDMappings[0] == expectedGIDMapping) {
t.Fatal("WithUserNamespace Cannot set the uid/gid mappings for the task")
}
}
func TestWithImageConfigArgs(t *testing.T) {
t.Parallel()
img, err := newFakeImage(ocispec.Image{
Config: ocispec.ImageConfig{
Env: []string{"z=bar", "y=baz"},
Entrypoint: []string{"create", "--namespace=test"},
Cmd: []string{"", "--debug"},
},
})
if err != nil {
t.Fatal(err)
}
s := Spec{
Version: specs.Version,
Root: &specs.Root{},
Windows: &specs.Windows{},
}
opts := []SpecOpts{
WithEnv([]string{"x=foo", "y=boo"}),
WithProcessArgs("run", "--foo", "xyz", "--bar"),
WithImageConfigArgs(img, []string{"--boo", "bar"}),
}
expectedEnv := []string{"z=bar", "y=boo", "x=foo"}
expectedArgs := []string{"create", "--namespace=test", "--boo", "bar"}
for _, opt := range opts {
if err := opt(nil, nil, nil, &s); err != nil {
t.Fatal(err)
}
}
if err := assertEqualsStringArrays(s.Process.Env, expectedEnv); err != nil {
t.Fatal(err)
}
if err := assertEqualsStringArrays(s.Process.Args, expectedArgs); err != nil {
t.Fatal(err)
}
}
func TestDevShmSize(t *testing.T) {
t.Parallel()
ss := []Spec{
{
Mounts: []specs.Mount{
{
Destination: "/dev/shm",
Type: "tmpfs",
Source: "shm",
Options: []string{"nosuid", "noexec", "nodev", "mode=1777"},
},
},
},
{
Mounts: []specs.Mount{
{
Destination: "/test/shm",
Type: "tmpfs",
Source: "shm",
Options: []string{"nosuid", "noexec", "nodev", "mode=1777", "size=65536k"},
},
},
},
{
Mounts: []specs.Mount{
{
Destination: "/test/shm",
Type: "tmpfs",
Source: "shm",
Options: []string{"nosuid", "noexec", "nodev", "mode=1777", "size=65536k"},
},
{
Destination: "/dev/shm",
Type: "tmpfs",
Source: "shm",
Options: []string{"nosuid", "noexec", "nodev", "mode=1777", "size=65536k", "size=131072k"},
},
},
},
}
expected := "1024k"
for _, s := range ss {
s := s
if err := WithDevShmSize(1024)(nil, nil, nil, &s); err != nil {
if err != ErrNoShmMount {
t.Fatal(err)
}
if getDevShmMount(&s) == nil {
continue
}
t.Fatal("excepted nil /dev/shm mount")
}
m := getDevShmMount(&s)
if m == nil {
t.Fatal("no shm mount found")
}
size, err := getShmSize(m.Options)
if err != nil {
t.Fatal(err)
}
if size != expected {
t.Fatalf("size %s not equal %s", size, expected)
}
}
}
func getDevShmMount(s *Spec) *specs.Mount {
for _, m := range s.Mounts {
if filepath.Clean(m.Destination) == "/dev/shm" && m.Source == "shm" && m.Type == "tmpfs" {
return &m
}
}
return nil
}
func getShmSize(opts []string) (string, error) {
// linux will use the last size option
var so string
for _, o := range opts {
if strings.HasPrefix(o, "size=") {
if so != "" {
return "", errors.New("contains multiple size options")
}
so = o
}
}
if so == "" {
return "", errors.New("shm size not specified")
}
parts := strings.Split(so, "=")
if len(parts) != 2 {
return "", errors.New("invalid size format")
}
return parts[1], nil
}
func TestWithoutMounts(t *testing.T) {
t.Parallel()
var s Spec
x := func(s string) string {
if runtime.GOOS == "windows" {
return filepath.Join("C:\\", filepath.Clean(s))
}
return s
}
opts := []SpecOpts{
WithMounts([]specs.Mount{
{
Destination: x("/dst1"),
Source: x("/src1"),
},
{
Destination: x("/dst2"),
Source: x("/src2"),
},
{
Destination: x("/dst3"),
Source: x("/src3"),
},
}),
WithoutMounts(x("/dst2"), x("/dst3")),
WithMounts([]specs.Mount{
{
Destination: x("/dst4"),
Source: x("/src4"),
},
}),
}
expected := []specs.Mount{
{
Destination: x("/dst1"),
Source: x("/src1"),
},
{
Destination: x("/dst4"),
Source: x("/src4"),
},
}
for _, opt := range opts {
if err := opt(nil, nil, nil, &s); err != nil {
t.Fatal(err)
}
}
if !reflect.DeepEqual(expected, s.Mounts) {
t.Fatalf("expected %+v, got %+v", expected, s.Mounts)
}
}
func TestWithWindowsDevice(t *testing.T) {
testcases := []struct {
name string
idType string
id string
expectError bool
expectedWindowsDevices []specs.WindowsDevice
}{
{
name: "empty_idType_and_id",
idType: "",
id: "",
expectError: true,
},
{
name: "empty_idType",
idType: "",
id: "5B45201D-F2F2-4F3B-85BB-30FF1F953599",
expectError: true,
},
{
name: "empty_id",
idType: "class",
id: "",
expectError: false,
expectedWindowsDevices: []specs.WindowsDevice{{ID: "", IDType: "class"}},
},
{
name: "idType_and_id",
idType: "class",
id: "5B45201D-F2F2-4F3B-85BB-30FF1F953599",
expectError: false,
expectedWindowsDevices: []specs.WindowsDevice{{ID: "5B45201D-F2F2-4F3B-85BB-30FF1F953599", IDType: "class"}},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
spec := Spec{
Version: specs.Version,
Root: &specs.Root{},
Windows: &specs.Windows{},
}
opts := []SpecOpts{
WithWindowsDevice(tc.idType, tc.id),
}
for _, opt := range opts {
if err := opt(nil, nil, nil, &spec); err != nil {
if tc.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
}
}
}
if len(tc.expectedWindowsDevices) != 0 {
require.NotNil(t, spec.Windows)
require.NotNil(t, spec.Windows.Devices)
assert.ElementsMatch(t, spec.Windows.Devices, tc.expectedWindowsDevices)
} else if spec.Windows != nil && spec.Windows.Devices != nil {
assert.ElementsMatch(t, spec.Windows.Devices, tc.expectedWindowsDevices)
}
})
}
}

55
pkg/oci/spec_opts_unix.go Normal file
View File

@@ -0,0 +1,55 @@
//go:build !linux && !windows
/*
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 oci
import (
"context"
"github.com/containerd/containerd/v2/core/containers"
)
// WithHostDevices adds all the hosts device nodes to the container's spec
func WithHostDevices(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
setLinux(s)
devs, err := HostDevices()
if err != nil {
return err
}
s.Linux.Devices = append(s.Linux.Devices, devs...)
return nil
}
// WithDevices recursively adds devices from the passed in path.
// If devicePath is a dir it traverses the dir to add all devices in that dir.
// If devicePath is not a dir, it attempts to add the single device.
func WithDevices(devicePath, containerPath, permissions string) SpecOpts {
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
devs, err := getDevices(devicePath, containerPath)
if err != nil {
return err
}
s.Linux.Devices = append(s.Linux.Devices, devs...)
return nil
}
}
func escapeAndCombineArgs(args []string) string {
panic("not supported")
}

View File

@@ -0,0 +1,72 @@
//go:build !windows
/*
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 oci
import (
"context"
"testing"
"github.com/containerd/containerd/v2/core/containers"
"github.com/containerd/containerd/v2/pkg/namespaces"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
specs "github.com/opencontainers/runtime-spec/specs-go"
)
func TestWithImageConfigNoEnv(t *testing.T) {
t.Parallel()
var (
s Spec
c = containers.Container{ID: t.Name()}
ctx = namespaces.WithNamespace(context.Background(), "test")
)
err := populateDefaultUnixSpec(ctx, &s, c.ID)
if err != nil {
t.Fatal(err)
}
// test hack: we don't want to test the WithAdditionalGIDs portion of the image config code
s.Windows = &specs.Windows{}
img, err := newFakeImage(ocispec.Image{
Config: ocispec.ImageConfig{
Entrypoint: []string{"create", "--namespace=test"},
Cmd: []string{"", "--debug"},
},
})
if err != nil {
t.Fatal(err)
}
opts := []SpecOpts{
WithImageConfigArgs(img, []string{"--boo", "bar"}),
}
// verify that if an image has no environment that we get a default Unix path
expectedEnv := []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}
for _, opt := range opts {
if err := opt(nil, nil, nil, &s); err != nil {
t.Fatal(err)
}
}
if err := assertEqualsStringArrays(s.Process.Env, expectedEnv); err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,69 @@
/*
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 oci
import (
"context"
"errors"
"strings"
"github.com/opencontainers/runtime-spec/specs-go"
"golang.org/x/sys/windows"
"github.com/containerd/containerd/v2/core/containers"
)
func escapeAndCombineArgs(args []string) string {
escaped := make([]string, len(args))
for i, a := range args {
escaped[i] = windows.EscapeArg(a)
}
return strings.Join(escaped, " ")
}
// WithProcessCommandLine replaces the command line on the generated spec
func WithProcessCommandLine(cmdLine string) SpecOpts {
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
setProcess(s)
s.Process.Args = nil
s.Process.CommandLine = cmdLine
return nil
}
}
// WithHostDevices adds all the hosts device nodes to the container's spec
//
// Not supported on windows
func WithHostDevices(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
return nil
}
func DeviceFromPath(path string) (*specs.LinuxDevice, error) {
return nil, errors.New("device from path not supported on Windows")
}
// WithDevices does nothing on Windows.
func WithDevices(devicePath, containerPath, permissions string) SpecOpts {
return func(ctx context.Context, client Client, container *containers.Container, spec *Spec) error {
return nil
}
}
// Windows containers have default path configured at bootup
func WithDefaultPathEnv(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
return nil
}

View File

@@ -0,0 +1,550 @@
/*
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 oci
import (
"context"
"os"
"testing"
"github.com/containerd/containerd/v2/core/containers"
"github.com/containerd/containerd/v2/pkg/namespaces"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opencontainers/runtime-spec/specs-go"
)
func TestWithCPUCount(t *testing.T) {
var (
ctx = namespaces.WithNamespace(context.Background(), "testing")
c = containers.Container{ID: t.Name()}
cpu = uint64(8)
o = WithWindowsCPUCount(cpu)
)
// Test with all three supported scenarios
platforms := []string{"", "linux/amd64", "windows/amd64"}
for _, p := range platforms {
var spec *Spec
var err error
if p == "" {
t.Log("Testing GenerateSpec default platform")
spec, err = GenerateSpec(ctx, nil, &c, o)
} else {
t.Logf("Testing GenerateSpecWithPlatform with platform: '%s'", p)
spec, err = GenerateSpecWithPlatform(ctx, nil, p, &c, o)
}
if err != nil {
t.Fatalf("failed to generate spec with: %v", err)
}
if *spec.Windows.Resources.CPU.Count != cpu {
t.Fatalf("spec.Windows.Resources.CPU.Count expected: %v, got: %v", cpu, *spec.Windows.Resources.CPU.Count)
}
if spec.Linux != nil && spec.Linux.Resources != nil && spec.Linux.Resources.CPU != nil {
t.Fatalf("spec.Linux.Resources.CPU section should not be set on GOOS=windows")
}
}
}
func TestWithWindowsIgnoreFlushesDuringBoot(t *testing.T) {
var (
ctx = namespaces.WithNamespace(context.Background(), "testing")
c = containers.Container{ID: t.Name()}
o = WithWindowsIgnoreFlushesDuringBoot()
)
// Test with all supported scenarios
platforms := []string{"", "windows/amd64"}
for _, p := range platforms {
var spec *Spec
var err error
if p == "" {
t.Log("Testing GenerateSpec default platform")
spec, err = GenerateSpec(ctx, nil, &c, o)
} else {
t.Logf("Testing GenerateSpecWithPlatform with platform: '%s'", p)
spec, err = GenerateSpecWithPlatform(ctx, nil, p, &c, o)
}
if err != nil {
t.Fatalf("failed to generate spec with: %v", err)
}
if spec.Windows.IgnoreFlushesDuringBoot != true {
t.Fatalf("spec.Windows.IgnoreFlushesDuringBoot expected: true")
}
}
}
func TestWithWindowNetworksAllowUnqualifiedDNSQuery(t *testing.T) {
var (
ctx = namespaces.WithNamespace(context.Background(), "testing")
c = containers.Container{ID: t.Name()}
o = WithWindowNetworksAllowUnqualifiedDNSQuery()
)
// Test with all supported scenarios
platforms := []string{"", "windows/amd64"}
for _, p := range platforms {
var spec *Spec
var err error
if p == "" {
t.Log("Testing GenerateSpec default platform")
spec, err = GenerateSpec(ctx, nil, &c, o)
} else {
t.Logf("Testing GenerateSpecWithPlatform with platform: '%s'", p)
spec, err = GenerateSpecWithPlatform(ctx, nil, p, &c, o)
}
if err != nil {
t.Fatalf("failed to generate spec with: %v", err)
}
if spec.Windows.Network.AllowUnqualifiedDNSQuery != true {
t.Fatalf("spec.Windows.Network.AllowUnqualifiedDNSQuery expected: true")
}
}
}
// TestWithProcessArgsOverwritesWithImage verifies that when calling
// WithImageConfig followed by WithProcessArgs when `ArgsEscaped==false` that
// the process args overwrite the image args.
func TestWithProcessArgsOverwritesWithImage(t *testing.T) {
t.Parallel()
img, err := newFakeImage(ocispec.Image{
Config: ocispec.ImageConfig{
Entrypoint: []string{"powershell.exe", "-Command", "Write-Host Hello"},
Cmd: []string{"cmd.exe", "/S", "/C", "echo Hello"},
ArgsEscaped: false,
},
})
if err != nil {
t.Fatal(err)
}
s := Spec{
Version: specs.Version,
Root: &specs.Root{},
Windows: &specs.Windows{},
}
args := []string{"cmd.exe", "echo", "should be set"}
opts := []SpecOpts{
WithImageConfig(img),
WithProcessArgs(args...),
}
for _, opt := range opts {
if err := opt(nil, nil, nil, &s); err != nil {
t.Fatal(err)
}
}
if err := assertEqualsStringArrays(args, s.Process.Args); err != nil {
t.Fatal(err)
}
if s.Process.CommandLine != "" {
t.Fatalf("Expected empty CommandLine, got: '%s'", s.Process.CommandLine)
}
}
// TestWithProcessArgsOverwritesWithImageArgsEscaped verifies that when calling
// WithImageConfig followed by WithProcessArgs when `ArgsEscaped==true` that the
// process args overwrite the image args.
func TestWithProcessArgsOverwritesWithImageArgsEscaped(t *testing.T) {
t.Parallel()
img, err := newFakeImage(ocispec.Image{
Config: ocispec.ImageConfig{
Entrypoint: []string{`powershell.exe -Command "C:\My Data\MyExe.exe" -arg1 "-arg2 value2"`},
Cmd: []string{`cmd.exe /S /C "C:\test path\test.exe"`},
ArgsEscaped: true,
},
})
if err != nil {
t.Fatal(err)
}
s := Spec{
Version: specs.Version,
Root: &specs.Root{},
Windows: &specs.Windows{},
}
args := []string{"cmd.exe", "echo", "should be set"}
opts := []SpecOpts{
WithImageConfig(img),
WithProcessArgs(args...),
}
for _, opt := range opts {
if err := opt(nil, nil, nil, &s); err != nil {
t.Fatal(err)
}
}
if err := assertEqualsStringArrays(args, s.Process.Args); err != nil {
t.Fatal(err)
}
if s.Process.CommandLine != "" {
t.Fatalf("Expected empty CommandLine, got: '%s'", s.Process.CommandLine)
}
}
// TestWithImageOverwritesWithProcessArgs verifies that when calling
// WithProcessArgs followed by WithImageConfig `ArgsEscaped==false` that the
// image args overwrites process args.
func TestWithImageOverwritesWithProcessArgs(t *testing.T) {
t.Parallel()
img, err := newFakeImage(ocispec.Image{
Config: ocispec.ImageConfig{
Entrypoint: []string{"powershell.exe", "-Command"},
Cmd: []string{"Write-Host", "echo Hello"},
},
})
if err != nil {
t.Fatal(err)
}
s := Spec{
Version: specs.Version,
Root: &specs.Root{},
Windows: &specs.Windows{},
}
opts := []SpecOpts{
WithProcessArgs("cmd.exe", "echo", "should not be set"),
WithImageConfig(img),
}
for _, opt := range opts {
if err := opt(nil, nil, nil, &s); err != nil {
t.Fatal(err)
}
}
expectedArgs := []string{"powershell.exe", "-Command", "Write-Host", "echo Hello"}
if err := assertEqualsStringArrays(expectedArgs, s.Process.Args); err != nil {
t.Fatal(err)
}
if s.Process.CommandLine != "" {
t.Fatalf("Expected empty CommandLine, got: '%s'", s.Process.CommandLine)
}
}
// TestWithImageOverwritesWithProcessArgs verifies that when calling
// WithProcessArgs followed by WithImageConfig `ArgsEscaped==true` that the
// image args overwrites process args.
func TestWithImageArgsEscapedOverwritesWithProcessArgs(t *testing.T) {
t.Parallel()
img, err := newFakeImage(ocispec.Image{
Config: ocispec.ImageConfig{
Entrypoint: []string{`powershell.exe -Command "C:\My Data\MyExe.exe" -arg1 "-arg2 value2"`},
Cmd: []string{`cmd.exe /S /C "C:\test path\test.exe"`},
ArgsEscaped: true,
},
})
if err != nil {
t.Fatal(err)
}
s := Spec{
Version: specs.Version,
Root: &specs.Root{},
Windows: &specs.Windows{},
}
opts := []SpecOpts{
WithProcessArgs("cmd.exe", "echo", "should not be set"),
WithImageConfig(img),
}
expectedCommandLine := `powershell.exe -Command "C:\My Data\MyExe.exe" -arg1 "-arg2 value2" "cmd.exe /S /C \"C:\test path\test.exe\""`
for _, opt := range opts {
if err := opt(nil, nil, nil, &s); err != nil {
t.Fatal(err)
}
}
if s.Process.Args != nil {
t.Fatalf("Expected empty Process.Args, got: '%v'", s.Process.Args)
}
if expectedCommandLine != s.Process.CommandLine {
t.Fatalf("Expected CommandLine '%s', got: '%s'", expectedCommandLine, s.Process.CommandLine)
}
}
func TestWithImageConfigArgsWindows(t *testing.T) {
testcases := []struct {
name string
entrypoint []string
cmd []string
args []string
expectError bool
// When ArgsEscaped==false we always expect args and CommandLine==""
expectedArgs []string
}{
{
// This is not really a valid test case since Docker would have made
// the default cmd to be the shell. So just verify it hits the error
// case we expect.
name: "EmptyEntrypoint_EmptyCmd_EmptyArgs",
entrypoint: nil,
cmd: nil,
args: nil,
expectError: true,
},
{
name: "EmptyEntrypoint_EmptyCmd_Args",
entrypoint: nil,
cmd: nil,
args: []string{"additional", "args"},
expectedArgs: []string{"additional", "args"},
},
{
name: "EmptyEntrypoint_Cmd_EmptyArgs",
entrypoint: nil,
cmd: []string{"cmd", "args"},
args: nil,
expectedArgs: []string{"cmd", "args"},
},
{
name: "EmptyEntrypoint_Cmd_Args",
entrypoint: nil,
cmd: []string{"cmd", "args"},
args: []string{"additional", "args"},
expectedArgs: []string{"additional", "args"}, // Args overwrite Cmd
},
{
name: "Entrypoint_EmptyCmd_EmptyArgs",
entrypoint: []string{"entrypoint", "args"},
cmd: nil,
args: nil,
expectedArgs: []string{"entrypoint", "args"},
},
{
name: "Entrypoint_EmptyCmd_Args",
entrypoint: []string{"entrypoint", "args"},
cmd: nil,
args: []string{"additional", "args"},
expectedArgs: []string{"entrypoint", "args", "additional", "args"},
},
{
name: "Entrypoint_Cmd_EmptyArgs",
entrypoint: []string{"entrypoint", "args"},
cmd: []string{"cmd", "args"},
args: nil,
expectedArgs: []string{"entrypoint", "args", "cmd", "args"},
},
{
name: "Entrypoint_Cmd_Args",
entrypoint: []string{"entrypoint", "args"},
cmd: []string{"cmd", "args"},
args: []string{"additional", "args"}, // Args overwrites Cmd
expectedArgs: []string{"entrypoint", "args", "additional", "args"},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
img, err := newFakeImage(ocispec.Image{
Config: ocispec.ImageConfig{
Entrypoint: tc.entrypoint,
Cmd: tc.cmd,
},
})
if err != nil {
t.Fatal(err)
}
s := Spec{
Version: specs.Version,
Root: &specs.Root{},
Windows: &specs.Windows{},
}
opts := []SpecOpts{
WithImageConfigArgs(img, tc.args),
}
for _, opt := range opts {
if err := opt(nil, nil, nil, &s); err != nil {
if tc.expectError {
continue
}
t.Fatal(err)
}
}
if err := assertEqualsStringArrays(tc.expectedArgs, s.Process.Args); err != nil {
t.Fatal(err)
}
if s.Process.CommandLine != "" {
t.Fatalf("Expected empty CommandLine, got: '%s'", s.Process.CommandLine)
}
})
}
}
func TestWithImageConfigArgsEscapedWindows(t *testing.T) {
testcases := []struct {
name string
entrypoint []string
cmd []string
args []string
expectError bool
expectedArgs []string
expectedCommandLine string
}{
{
// This is not really a valid test case since Docker would have made
// the default cmd to be the shell. So just verify it hits the error
// case we expect.
name: "EmptyEntrypoint_EmptyCmd_EmptyArgs",
entrypoint: nil,
cmd: nil,
args: nil,
expectError: true,
expectedArgs: nil,
expectedCommandLine: "",
},
{
// This case is special for ArgsEscaped, since there is no Image
// Default Args should be passed as ProcessArgs not as Cmdline
name: "EmptyEntrypoint_EmptyCmd_Args",
entrypoint: nil,
cmd: nil,
args: []string{"additional", "-args", "hello world"},
expectedArgs: []string{"additional", "-args", "hello world"},
expectedCommandLine: "",
},
{
name: "EmptyEntrypoint_Cmd_EmptyArgs",
entrypoint: nil,
cmd: []string{`cmd -args "hello world"`},
args: nil,
expectedCommandLine: `cmd -args "hello world"`,
},
{
// This case is a second special case for ArgsEscaped, since Args
// overwrite Cmd the args are not from the image, so ArgsEscaped
// should be ignored, and passed as Args not CommandLine.
name: "EmptyEntrypoint_Cmd_Args",
entrypoint: nil,
cmd: []string{`cmd -args "hello world"`},
args: []string{"additional", "args"},
expectedArgs: []string{"additional", "args"}, // Args overwrite Cmd
expectedCommandLine: "",
},
{
name: "Entrypoint_EmptyCmd_EmptyArgs",
entrypoint: []string{`"C:\My Folder\MyProcess.exe" -arg1 "test value"`},
cmd: nil,
args: nil,
expectedCommandLine: `"C:\My Folder\MyProcess.exe" -arg1 "test value"`,
},
{
name: "Entrypoint_EmptyCmd_Args",
entrypoint: []string{`"C:\My Folder\MyProcess.exe" -arg1 "test value"`},
cmd: nil,
args: []string{"additional", "args with spaces"},
expectedCommandLine: `"C:\My Folder\MyProcess.exe" -arg1 "test value" additional "args with spaces"`,
},
{
// This case will not work in Docker today so adding the test to
// confirm we fail in the same way. Although the appending of
// Entrypoint + " " + Cmd here works, Cmd is double escaped and the
// container would not launch. This is because when Docker built
// such an image it escaped both Entrypoint and Cmd. However the
// docs say that CMD should always be appened to entrypoint if not
// overwritten so this results in an incorrect cmdline.
name: "Entrypoint_Cmd_EmptyArgs",
entrypoint: []string{`"C:\My Folder\MyProcess.exe" -arg1 "test value"`},
cmd: []string{`cmd -args "hello world"`},
args: nil,
expectedCommandLine: `"C:\My Folder\MyProcess.exe" -arg1 "test value" "cmd -args \"hello world\""`,
},
{
name: "Entrypoint_Cmd_Args",
entrypoint: []string{`"C:\My Folder\MyProcess.exe" -arg1 "test value"`},
cmd: []string{`cmd -args "hello world"`},
args: []string{"additional", "args with spaces"}, // Args overwrites Cmd
expectedCommandLine: `"C:\My Folder\MyProcess.exe" -arg1 "test value" additional "args with spaces"`,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
img, err := newFakeImage(ocispec.Image{
Config: ocispec.ImageConfig{
Entrypoint: tc.entrypoint,
Cmd: tc.cmd,
ArgsEscaped: true,
},
})
if err != nil {
t.Fatal(err)
}
s := Spec{
Version: specs.Version,
Root: &specs.Root{},
Windows: &specs.Windows{},
}
opts := []SpecOpts{
WithImageConfigArgs(img, tc.args),
}
for _, opt := range opts {
if err := opt(nil, nil, nil, &s); err != nil {
if tc.expectError {
continue
}
t.Fatal(err)
}
}
if err := assertEqualsStringArrays(tc.expectedArgs, s.Process.Args); err != nil {
t.Fatal(err)
}
if tc.expectedCommandLine != s.Process.CommandLine {
t.Fatalf("Expected CommandLine: '%s', got: '%s'", tc.expectedCommandLine, s.Process.CommandLine)
}
})
}
}
func TestWindowsDefaultPathEnv(t *testing.T) {
t.Parallel()
s := Spec{}
s.Process = &specs.Process{
Env: []string{},
}
var (
defaultUnixEnv = "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
ctx = namespaces.WithNamespace(context.Background(), "test")
)
//check that the default PATH environment is not null
if os.Getenv("PATH") == "" {
t.Fatal("PATH environment variable is not set")
}
WithDefaultPathEnv(ctx, nil, nil, &s)
//check that the path is not overwritten by the unix default path
if Contains(s.Process.Env, defaultUnixEnv) {
t.Fatal("default Windows Env overwritten by the default Unix Env")
}
}

327
pkg/oci/spec_test.go Normal file
View File

@@ -0,0 +1,327 @@
/*
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 oci
import (
"context"
"runtime"
"testing"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/containerd/containerd/v2/core/containers"
"github.com/containerd/containerd/v2/pkg/namespaces"
"github.com/containerd/containerd/v2/pkg/testutil"
)
func TestGenerateSpec(t *testing.T) {
t.Parallel()
ctx := namespaces.WithNamespace(context.Background(), "testing")
s, err := GenerateSpec(ctx, nil, &containers.Container{ID: t.Name()})
if err != nil {
t.Fatal(err)
}
if s == nil {
t.Fatal("GenerateSpec() returns a nil spec")
}
if runtime.GOOS == "linux" {
// check for matching caps
defaults := defaultUnixCaps()
for _, cl := range [][]string{
s.Process.Capabilities.Bounding,
s.Process.Capabilities.Permitted,
s.Process.Capabilities.Effective,
} {
for i := 0; i < len(defaults); i++ {
if cl[i] != defaults[i] {
t.Errorf("cap at %d does not match set %q != %q", i, defaults[i], cl[i])
}
}
}
// check default namespaces
defaultNS := defaultUnixNamespaces()
for i, ns := range s.Linux.Namespaces {
if defaultNS[i] != ns {
t.Errorf("ns at %d does not match set %q != %q", i, defaultNS[i], ns)
}
}
} else if runtime.GOOS == "windows" {
if s.Windows == nil {
t.Fatal("Windows section of spec not filled in for Windows spec")
}
}
// test that we don't have tty set
if s.Process.Terminal {
t.Error("terminal set on default process")
}
}
func TestGenerateSpecWithPlatform(t *testing.T) {
t.Parallel()
ctx := namespaces.WithNamespace(context.Background(), "testing")
platforms := []string{"windows/amd64", "linux/amd64"}
for _, p := range platforms {
t.Logf("Testing platform: %s", p)
s, err := GenerateSpecWithPlatform(ctx, nil, p, &containers.Container{ID: t.Name()})
if err != nil {
t.Fatalf("failed to generate spec: %v", err)
}
if s.Root == nil {
t.Fatal("expected non nil Root section.")
}
if s.Process == nil {
t.Fatal("expected non nil Process section.")
}
if p == "windows/amd64" {
if s.Linux != nil {
t.Fatal("expected nil Linux section")
}
if s.Windows == nil {
t.Fatal("expected non nil Windows section")
}
} else {
if s.Linux == nil {
t.Fatal("expected non nil Linux section")
}
if runtime.GOOS == "windows" && s.Windows == nil {
t.Fatal("expected non nil Windows section for LCOW")
} else if runtime.GOOS != "windows" && s.Windows != nil {
t.Fatal("expected nil Windows section")
}
}
}
}
func TestSpecWithTTY(t *testing.T) {
t.Parallel()
ctx := namespaces.WithNamespace(context.Background(), "testing")
s, err := GenerateSpec(ctx, nil, &containers.Container{ID: t.Name()}, WithTTY)
if err != nil {
t.Fatal(err)
}
if !s.Process.Terminal {
t.Error("terminal net set WithTTY()")
}
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")
}
} else {
if len(s.Process.Env) != 0 {
t.Fatal("Windows process args should be empty by default")
}
}
}
func TestWithLinuxNamespace(t *testing.T) {
t.Parallel()
ctx := namespaces.WithNamespace(context.Background(), "testing")
replacedNS := specs.LinuxNamespace{Type: specs.NetworkNamespace, Path: "/var/run/netns/test"}
var s *specs.Spec
var err error
if runtime.GOOS != "windows" {
s, err = GenerateSpec(ctx, nil, &containers.Container{ID: t.Name()}, WithLinuxNamespace(replacedNS))
} else {
s, err = GenerateSpecWithPlatform(ctx, nil, "linux/amd64", &containers.Container{ID: t.Name()}, WithLinuxNamespace(replacedNS))
}
if err != nil {
t.Fatal(err)
}
defaultNS := defaultUnixNamespaces()
found := false
for i, ns := range s.Linux.Namespaces {
if ns == replacedNS && !found {
found = true
continue
}
if defaultNS[i] != ns {
t.Errorf("ns at %d does not match set %q != %q", i, defaultNS[i], ns)
}
}
}
func TestWithCapabilities(t *testing.T) {
t.Parallel()
ctx := namespaces.WithNamespace(context.Background(), "testing")
opts := []SpecOpts{
WithCapabilities([]string{"CAP_SYS_ADMIN"}),
}
var s *specs.Spec
var err error
if runtime.GOOS != "windows" {
s, err = GenerateSpec(ctx, nil, &containers.Container{ID: t.Name()}, opts...)
} else {
s, err = GenerateSpecWithPlatform(ctx, nil, "linux/amd64", &containers.Container{ID: t.Name()}, opts...)
}
if err != nil {
t.Fatal(err)
}
if len(s.Process.Capabilities.Bounding) != 1 || s.Process.Capabilities.Bounding[0] != "CAP_SYS_ADMIN" {
t.Error("Unexpected capabilities set")
}
if len(s.Process.Capabilities.Effective) != 1 || s.Process.Capabilities.Effective[0] != "CAP_SYS_ADMIN" {
t.Error("Unexpected capabilities set")
}
if len(s.Process.Capabilities.Permitted) != 1 || s.Process.Capabilities.Permitted[0] != "CAP_SYS_ADMIN" {
t.Error("Unexpected capabilities set")
}
if len(s.Process.Capabilities.Inheritable) != 0 {
t.Errorf("Unexpected capabilities set: length is non zero (%d)", len(s.Process.Capabilities.Inheritable))
}
}
func TestWithCapabilitiesNil(t *testing.T) {
t.Parallel()
ctx := namespaces.WithNamespace(context.Background(), "testing")
s, err := GenerateSpec(ctx, nil, &containers.Container{ID: t.Name()},
WithCapabilities(nil),
)
if err != nil {
t.Fatal(err)
}
if len(s.Process.Capabilities.Bounding) != 0 {
t.Errorf("Unexpected capabilities set: length is non zero (%d)", len(s.Process.Capabilities.Bounding))
}
if len(s.Process.Capabilities.Effective) != 0 {
t.Errorf("Unexpected capabilities set: length is non zero (%d)", len(s.Process.Capabilities.Effective))
}
if len(s.Process.Capabilities.Permitted) != 0 {
t.Errorf("Unexpected capabilities set: length is non zero (%d)", len(s.Process.Capabilities.Permitted))
}
if len(s.Process.Capabilities.Inheritable) != 0 {
t.Errorf("Unexpected capabilities set: length is non zero (%d)", len(s.Process.Capabilities.Inheritable))
}
}
func TestPopulateDefaultWindowsSpec(t *testing.T) {
var (
c = containers.Container{ID: "TestWithDefaultSpec"}
ctx = namespaces.WithNamespace(context.Background(), "test")
)
var expected Spec
populateDefaultWindowsSpec(ctx, &expected, c.ID)
if expected.Windows == nil {
t.Error("Cannot populate windows Spec")
}
}
func TestPopulateDefaultUnixSpec(t *testing.T) {
var (
c = containers.Container{ID: "TestWithDefaultSpec"}
ctx = namespaces.WithNamespace(context.Background(), "test")
)
var expected Spec
populateDefaultUnixSpec(ctx, &expected, c.ID)
if expected.Linux == nil {
t.Error("Cannot populate Unix Spec")
}
}
func TestWithPrivileged(t *testing.T) {
t.Parallel()
if runtime.GOOS == "linux" {
// because WithPrivileged depends on CapEff in /proc/self/status
testutil.RequiresRoot(t)
}
ctx := namespaces.WithNamespace(context.Background(), "testing")
opts := []SpecOpts{
WithCapabilities(nil),
WithMounts([]specs.Mount{
{Type: "cgroup", Destination: "/sys/fs/cgroup", Options: []string{"ro"}},
}),
WithPrivileged,
}
var s *specs.Spec
var err error
if runtime.GOOS != "windows" {
s, err = GenerateSpec(ctx, nil, &containers.Container{ID: t.Name()}, opts...)
} else {
s, err = GenerateSpecWithPlatform(ctx, nil, "linux/amd64", &containers.Container{ID: t.Name()}, opts...)
}
if err != nil {
t.Fatal(err)
}
if runtime.GOOS != "linux" {
return
}
if len(s.Process.Capabilities.Bounding) == 0 {
t.Error("Expected capabilities to be set with privileged")
}
var foundSys, foundCgroup bool
for _, m := range s.Mounts {
switch m.Type {
case "sysfs":
foundSys = true
var found bool
for _, o := range m.Options {
switch o {
case "ro":
t.Errorf("Found unexpected read only %s mount", m.Type)
case "rw":
found = true
}
}
if !found {
t.Errorf("Did not find rw mount option for %s", m.Type)
}
case "cgroup":
foundCgroup = true
var found bool
for _, o := range m.Options {
switch o {
case "ro":
t.Errorf("Found unexpected read only %s mount", m.Type)
case "rw":
found = true
}
}
if !found {
t.Errorf("Did not find rw mount option for %s", m.Type)
}
}
}
if !foundSys {
t.Error("Did not find mount for sysfs")
}
if !foundCgroup {
t.Error("Did not find mount for cgroupfs")
}
}

180
pkg/oci/utils_unix.go Normal file
View File

@@ -0,0 +1,180 @@
//go:build !windows
/*
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 oci
import (
"errors"
"fmt"
"os"
"path/filepath"
"github.com/containerd/containerd/v2/pkg/userns"
specs "github.com/opencontainers/runtime-spec/specs-go"
"golang.org/x/sys/unix"
)
// ErrNotADevice denotes that a file is not a valid linux device.
var ErrNotADevice = errors.New("not a device node")
// Testing dependencies
var (
osReadDir = os.ReadDir
usernsRunningInUserNS = userns.RunningInUserNS
overrideDeviceFromPath func(path string) error
)
// HostDevices returns all devices that can be found under /dev directory.
func HostDevices() ([]specs.LinuxDevice, error) {
return getDevices("/dev", "")
}
func getDevices(path, containerPath string) ([]specs.LinuxDevice, error) {
stat, err := os.Stat(path)
if err != nil {
return nil, fmt.Errorf("error stating device path: %w", err)
}
if !stat.IsDir() {
dev, err := DeviceFromPath(path)
if err != nil {
return nil, err
}
if containerPath != "" {
dev.Path = containerPath
}
return []specs.LinuxDevice{*dev}, nil
}
files, err := osReadDir(path)
if err != nil {
return nil, err
}
var out []specs.LinuxDevice
for _, f := range files {
switch {
case f.IsDir():
switch f.Name() {
// ".lxc" & ".lxd-mounts" added to address https://github.com/lxc/lxd/issues/2825
// ".udev" added to address https://github.com/opencontainers/runc/issues/2093
case "pts", "shm", "fd", "mqueue", ".lxc", ".lxd-mounts", ".udev":
continue
default:
var cp string
if containerPath != "" {
cp = filepath.Join(containerPath, filepath.Base(f.Name()))
}
sub, err := getDevices(filepath.Join(path, f.Name()), cp)
if err != nil {
if errors.Is(err, os.ErrPermission) && usernsRunningInUserNS() {
// ignore the "permission denied" error if running in userns.
// This allows rootless containers to use devices that are
// accessible, ignoring devices / subdirectories that are not.
continue
}
return nil, err
}
out = append(out, sub...)
continue
}
case f.Name() == "console":
continue
default:
device, err := DeviceFromPath(filepath.Join(path, f.Name()))
if err != nil {
if err == ErrNotADevice {
continue
}
if os.IsNotExist(err) {
continue
}
if errors.Is(err, os.ErrPermission) && usernsRunningInUserNS() {
// ignore the "permission denied" error if running in userns.
// This allows rootless containers to use devices that are
// accessible, ignoring devices that are not.
continue
}
return nil, err
}
if device.Type == fifoDevice {
continue
}
if containerPath != "" {
device.Path = filepath.Join(containerPath, filepath.Base(f.Name()))
}
out = append(out, *device)
}
}
return out, nil
}
// TODO consider adding these consts to the OCI runtime-spec.
const (
wildcardDevice = "a" //nolint:nolintlint,unused,varcheck // currently unused, but should be included when upstreaming to OCI runtime-spec.
blockDevice = "b"
charDevice = "c" // or "u"
fifoDevice = "p"
)
// DeviceFromPath takes the path to a device to look up the information about a
// linux device and returns that information as a LinuxDevice struct.
func DeviceFromPath(path string) (*specs.LinuxDevice, error) {
if overrideDeviceFromPath != nil {
if err := overrideDeviceFromPath(path); err != nil {
return nil, err
}
}
var stat unix.Stat_t
if err := unix.Lstat(path, &stat); err != nil {
return nil, err
}
var (
devNumber = uint64(stat.Rdev) //nolint:nolintlint,unconvert // the type is 32bit on mips.
major = unix.Major(devNumber)
minor = unix.Minor(devNumber)
)
var (
devType string
mode = stat.Mode
)
switch mode & unix.S_IFMT {
case unix.S_IFBLK:
devType = blockDevice
case unix.S_IFCHR:
devType = charDevice
case unix.S_IFIFO:
devType = fifoDevice
default:
return nil, ErrNotADevice
}
fm := os.FileMode(mode &^ unix.S_IFMT)
return &specs.LinuxDevice{
Type: devType,
Path: path,
Major: int64(major),
Minor: int64(minor),
FileMode: &fm,
UID: &stat.Uid,
GID: &stat.Gid,
}, nil
}

View File

@@ -0,0 +1,54 @@
//go:build !go1.17 && !windows && !darwin
/*
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 oci
import "io/fs"
// The following code is adapted from go1.17.1/src/io/fs/readdir.go
// to compensate for the lack of fs.FileInfoToDirEntry in Go 1.16.
// dirInfo is a DirEntry based on a FileInfo.
type dirInfo struct {
fileInfo fs.FileInfo
}
func (di dirInfo) IsDir() bool {
return di.fileInfo.IsDir()
}
func (di dirInfo) Type() fs.FileMode {
return di.fileInfo.Mode().Type()
}
func (di dirInfo) Info() (fs.FileInfo, error) {
return di.fileInfo, nil
}
func (di dirInfo) Name() string {
return di.fileInfo.Name()
}
// fileInfoToDirEntry returns a DirEntry that returns information from info.
// If info is nil, FileInfoToDirEntry returns nil.
func fileInfoToDirEntry(info fs.FileInfo) fs.DirEntry {
if info == nil {
return nil
}
return dirInfo{fileInfo: info}
}

View File

@@ -0,0 +1,23 @@
//go:build go1.17 && !windows && !darwin
/*
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 oci
import "io/fs"
var fileInfoToDirEntry = fs.FileInfoToDirEntry

167
pkg/oci/utils_unix_test.go Normal file
View File

@@ -0,0 +1,167 @@
//go:build !windows && !darwin
/*
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 oci
import (
"errors"
"fmt"
"os"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"github.com/containerd/containerd/v2/pkg/userns"
)
func cleanupTest() {
overrideDeviceFromPath = nil
osReadDir = os.ReadDir
usernsRunningInUserNS = userns.RunningInUserNS
}
// Based on test from runc:
// https://github.com/opencontainers/runc/blob/v1.0.0/libcontainer/devices/device_unix_test.go#L34-L47
func TestHostDevicesOSReadDirFailure(t *testing.T) {
testError := fmt.Errorf("test error: %w", os.ErrPermission)
// Override os.ReadDir to inject error.
osReadDir = func(dirname string) ([]os.DirEntry, error) {
return nil, testError
}
// Override userns.RunningInUserNS to ensure not running in user namespace.
usernsRunningInUserNS = func() bool {
return false
}
defer cleanupTest()
_, err := HostDevices()
if !errors.Is(err, testError) {
t.Fatalf("Unexpected error %v, expected %v", err, testError)
}
}
// Based on test from runc:
// https://github.com/opencontainers/runc/blob/v1.0.0/libcontainer/devices/device_unix_test.go#L34-L47
func TestHostDevicesOSReadDirFailureInUserNS(t *testing.T) {
testError := fmt.Errorf("test error: %w", os.ErrPermission)
// Override os.ReadDir to inject error.
osReadDir = func(dirname string) ([]os.DirEntry, error) {
if dirname == "/dev" {
fi, err := os.Lstat("/dev/null")
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
return []os.DirEntry{fileInfoToDirEntry(fi)}, nil
}
return nil, testError
}
// Override userns.RunningInUserNS to ensure running in user namespace.
usernsRunningInUserNS = func() bool {
return true
}
defer cleanupTest()
_, err := HostDevices()
if !errors.Is(err, nil) {
t.Fatalf("Unexpected error %v, expected %v", err, nil)
}
}
// Based on test from runc:
// https://github.com/opencontainers/runc/blob/v1.0.0/libcontainer/devices/device_unix_test.go#L49-L74
func TestHostDevicesDeviceFromPathFailure(t *testing.T) {
testError := fmt.Errorf("test error: %w", os.ErrPermission)
// Override DeviceFromPath to produce an os.ErrPermission on /dev/null.
overrideDeviceFromPath = func(path string) error {
if path == "/dev/null" {
return testError
}
return nil
}
// Override userns.RunningInUserNS to ensure not running in user namespace.
usernsRunningInUserNS = func() bool {
return false
}
defer cleanupTest()
d, err := HostDevices()
if !errors.Is(err, testError) {
t.Fatalf("Unexpected error %v, expected %v", err, testError)
}
assert.Equal(t, 0, len(d))
}
// Based on test from runc:
// https://github.com/opencontainers/runc/blob/v1.0.0/libcontainer/devices/device_unix_test.go#L49-L74
func TestHostDevicesDeviceFromPathFailureInUserNS(t *testing.T) {
testError := fmt.Errorf("test error: %w", os.ErrPermission)
// Override DeviceFromPath to produce an os.ErrPermission on all devices,
// except for /dev/null.
overrideDeviceFromPath = func(path string) error {
if path == "/dev/null" {
return nil
}
return testError
}
// Override userns.RunningInUserNS to ensure running in user namespace.
usernsRunningInUserNS = func() bool {
return true
}
defer cleanupTest()
d, err := HostDevices()
if !errors.Is(err, nil) {
t.Fatalf("Unexpected error %v, expected %v", err, nil)
}
assert.Equal(t, 1, len(d))
assert.Equal(t, d[0].Path, "/dev/null")
}
func TestHostDevicesAllValid(t *testing.T) {
devices, err := HostDevices()
if err != nil {
t.Fatalf("failed to get host devices: %v", err)
}
for _, device := range devices {
if runtime.GOOS != "freebsd" {
// On Linux, devices can't have major number 0.
if device.Major == 0 {
t.Errorf("device entry %+v has zero major number", device)
}
}
switch device.Type {
case blockDevice, charDevice:
case fifoDevice:
t.Logf("fifo devices shouldn't show up from HostDevices")
fallthrough
default:
t.Errorf("device entry %+v has unexpected type %v", device, device.Type)
}
}
}

View File

@@ -25,7 +25,7 @@ import (
transferapi "github.com/containerd/containerd/v2/api/services/transfer/v1"
transfertypes "github.com/containerd/containerd/v2/api/types/transfer"
"github.com/containerd/containerd/v2/oci"
"github.com/containerd/containerd/v2/pkg/oci"
"github.com/containerd/containerd/v2/pkg/streaming"
"github.com/containerd/containerd/v2/pkg/transfer"
tstreaming "github.com/containerd/containerd/v2/pkg/transfer/streaming"