@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
38
pkg/oci/client.go
Normal 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
73
pkg/oci/mounts.go
Normal 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
65
pkg/oci/mounts_freebsd.go
Normal 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
266
pkg/oci/spec.go
Normal 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
1674
pkg/oci/spec_opts.go
Normal file
File diff suppressed because it is too large
Load Diff
81
pkg/oci/spec_opts_linux.go
Normal file
81
pkg/oci/spec_opts_linux.go
Normal 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")
|
||||
}
|
||||
765
pkg/oci/spec_opts_linux_test.go
Normal file
765
pkg/oci/spec_opts_linux_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
36
pkg/oci/spec_opts_nonlinux.go
Normal file
36
pkg/oci/spec_opts_nonlinux.go
Normal 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)
|
||||
}
|
||||
32
pkg/oci/spec_opts_nonwindows.go
Normal file
32
pkg/oci/spec_opts_nonwindows.go
Normal 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
|
||||
}
|
||||
43
pkg/oci/spec_opts_nonwindows_test.go
Normal file
43
pkg/oci/spec_opts_nonwindows_test.go
Normal 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
773
pkg/oci/spec_opts_test.go
Normal 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
55
pkg/oci/spec_opts_unix.go
Normal 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")
|
||||
}
|
||||
72
pkg/oci/spec_opts_unix_test.go
Normal file
72
pkg/oci/spec_opts_unix_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
69
pkg/oci/spec_opts_windows.go
Normal file
69
pkg/oci/spec_opts_windows.go
Normal 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
|
||||
}
|
||||
550
pkg/oci/spec_opts_windows_test.go
Normal file
550
pkg/oci/spec_opts_windows_test.go
Normal 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
327
pkg/oci/spec_test.go
Normal 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
180
pkg/oci/utils_unix.go
Normal 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
|
||||
}
|
||||
54
pkg/oci/utils_unix_go116_test.go
Normal file
54
pkg/oci/utils_unix_go116_test.go
Normal 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}
|
||||
}
|
||||
23
pkg/oci/utils_unix_go117_test.go
Normal file
23
pkg/oci/utils_unix_go117_test.go
Normal 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
167
pkg/oci/utils_unix_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user