diff --git a/hack/test-cri.sh b/hack/test-cri.sh index c03184b32..c9b61067b 100755 --- a/hack/test-cri.sh +++ b/hack/test-cri.sh @@ -21,7 +21,7 @@ source $(dirname "${BASH_SOURCE[0]}")/test-utils.sh # FOCUS focuses the test to run. FOCUS=${FOCUS:-} # SKIP skips the test to skip. -SKIP=${SKIP:-"RunAsUserName"} +SKIP=${SKIP:-""} REPORT_DIR=${REPORT_DIR:-"/tmp/test-cri"} if [[ -z "${GOPATH}" ]]; then diff --git a/pkg/server/container_create.go b/pkg/server/container_create.go index 0bc8857fc..afdfb38d4 100644 --- a/pkg/server/container_create.go +++ b/pkg/server/container_create.go @@ -140,8 +140,16 @@ func (c *criContainerdService) CreateContainer(ctx context.Context, r *runtime.C containerMetadataLabel: string(metaBytes), } + specOpts := containerd.WithSpec(spec) + // Set container username. This could only be done by containerd, because it needs + // access to the container rootfs. Pass user name to containerd, and let it overwrite + // the spec for us. + if username := config.GetLinux().GetSecurityContext().GetRunAsUsername(); username != "" { + specOpts = containerd.WithSpec(spec, containerd.WithUsername(username)) + } + opts = append(opts, - containerd.WithSpec(spec), + specOpts, containerd.WithRuntime(defaultRuntime), containerd.WithContainerLabels(labels)) var cntr containerd.Container @@ -185,7 +193,7 @@ func (c *criContainerdService) CreateContainer(ctx context.Context, r *runtime.C func (c *criContainerdService) generateContainerSpec(id string, sandboxPid uint32, config *runtime.ContainerConfig, sandboxConfig *runtime.PodSandboxConfig, imageConfig *imagespec.ImageConfig, extraMounts []*runtime.Mount) (*runtimespec.Spec, error) { // Creates a spec Generator with the default spec. - spec, err := containerd.GenerateSpec() + spec, err := containerd.GenerateSpec(context.Background(), nil, nil) if err != nil { return nil, err } @@ -225,6 +233,9 @@ func (c *criContainerdService) generateContainerSpec(id string, sandboxPid uint3 addOCIBindMounts(&g, append(extraMounts, config.GetMounts()...)) if securityContext.GetPrivileged() { + if !sandboxConfig.GetLinux().GetSecurityContext().GetPrivileged() { + return nil, fmt.Errorf("no privileged container allowed in sandbox") + } if err := setOCIPrivileged(&g, config); err != nil { return nil, err } @@ -238,14 +249,15 @@ func (c *criContainerdService) generateContainerSpec(id string, sandboxPid uint3 securityContext.GetCapabilities(), err) } - // TODO(random-liu): [P1] Set selinux options. - // TODO(random-liu): [P2] Add apparmor and seccomp. - // TODO: Figure out whether we should set no new privilege for sandbox container by default - g.SetProcessNoNewPrivileges(securityContext.GetNoNewPrivs()) } + // TODO: Figure out whether we should set no new privilege for sandbox container by default + g.SetProcessNoNewPrivileges(securityContext.GetNoNewPrivs()) + + // TODO(random-liu): [P1] Set selinux options. + g.SetRootReadonly(securityContext.GetReadonlyRootfs()) setOCILinuxResource(&g, config.GetLinux().GetResources()) @@ -258,9 +270,9 @@ func (c *criContainerdService) generateContainerSpec(id string, sandboxPid uint3 // Set namespaces, share namespace with sandbox container. setOCINamespaces(&g, securityContext.GetNamespaceOptions(), sandboxPid) - // TODO(random-liu): [P1] Set username. runAsUser := securityContext.GetRunAsUser() if runAsUser != nil { + // TODO(random-liu): We should also set gid. Use containerd#1425 instead. g.SetProcessUID(uint32(runAsUser.GetValue())) } diff --git a/pkg/server/container_create_test.go b/pkg/server/container_create_test.go index 6d2d80c68..717d1d9ca 100644 --- a/pkg/server/container_create_test.go +++ b/pkg/server/container_create_test.go @@ -91,6 +91,7 @@ func getCreateContainerTestData() (*runtime.ContainerConfig, *runtime.PodSandbox }, SupplementalGroups: []int64{1111, 2222}, NoNewPrivs: true, + RunAsUser: &runtime.Int64Value{Value: 255}, }, }, } @@ -143,6 +144,9 @@ func getCreateContainerTestData() (*runtime.ContainerConfig, *runtime.PodSandbox assert.NotContains(t, spec.Process.Capabilities.Permitted, "CAP_CHOWN") assert.NotContains(t, spec.Process.Capabilities.Ambient, "CAP_CHOWN") + t.Logf("Check uid") + assert.EqualValues(t, spec.Process.User.UID, 255) + t.Logf("Check supplemental groups") assert.Contains(t, spec.Process.User.AdditionalGids, uint32(1111)) assert.Contains(t, spec.Process.User.AdditionalGids, uint32(2222)) diff --git a/pkg/server/sandbox_run.go b/pkg/server/sandbox_run.go index de16cf23c..9889db5b0 100644 --- a/pkg/server/sandbox_run.go +++ b/pkg/server/sandbox_run.go @@ -207,7 +207,7 @@ func (c *criContainerdService) generateSandboxContainerSpec(id string, config *r imageConfig *imagespec.ImageConfig, nsPath string) (*runtimespec.Spec, error) { // Creates a spec Generator with the default spec. // TODO(random-liu): [P1] Compare the default settings with docker and containerd default. - spec, err := containerd.GenerateSpec() + spec, err := containerd.GenerateSpec(context.Background(), nil, nil) if err != nil { return nil, err } @@ -250,7 +250,8 @@ func (c *criContainerdService) generateSandboxContainerSpec(id string, config *r // TODO(random-liu): [P2] Set default cgroup path if cgroup parent is not specified. // Set namespace options. - nsOptions := config.GetLinux().GetSecurityContext().GetNamespaceOptions() + securityContext := config.GetLinux().GetSecurityContext() + nsOptions := securityContext.GetNamespaceOptions() if nsOptions.GetHostNetwork() { g.RemoveLinuxNamespace(string(runtimespec.NetworkNamespace)) // nolint: errcheck } else { @@ -267,11 +268,16 @@ func (c *criContainerdService) generateSandboxContainerSpec(id string, config *r // TODO(random-liu): [P1] Apply SeLinux options. - // TODO(random-liu): [P1] Set user. + runAsUser := securityContext.GetRunAsUser() + if runAsUser != nil { + // TODO(random-liu): We should also set gid. Use containerd#1425 instead. + g.SetProcessUID(uint32(runAsUser.GetValue())) + } - // TODO(random-liu): [P1] Set supplemental group. - - // TODO(random-liu): [P1] Set privileged. + supplementalGroups := securityContext.GetSupplementalGroups() + for _, group := range supplementalGroups { + g.AddProcessAdditionalGid(uint32(group)) + } // Add sysctls sysctls := config.GetLinux().GetSysctls() diff --git a/pkg/server/sandbox_run_test.go b/pkg/server/sandbox_run_test.go index df1f3874e..a69dc29e0 100644 --- a/pkg/server/sandbox_run_test.go +++ b/pkg/server/sandbox_run_test.go @@ -128,6 +128,20 @@ func TestGenerateSandboxContainerSpec(t *testing.T) { }, expectErr: true, }, + "should set user correctly": { + configChange: func(c *runtime.PodSandboxConfig) { + c.Linux.SecurityContext = &runtime.LinuxSandboxSecurityContext{ + RunAsUser: &runtime.Int64Value{Value: 255}, + SupplementalGroups: []int64{1111, 2222}, + } + }, + specCheck: func(t *testing.T, spec *runtimespec.Spec) { + require.NotNil(t, spec.Process) + assert.EqualValues(t, spec.Process.User.UID, 255) + assert.Contains(t, spec.Process.User.AdditionalGids, uint32(1111)) + assert.Contains(t, spec.Process.User.AdditionalGids, uint32(2222)) + }, + }, } { t.Logf("TestCase %q", desc) c := newTestCRIContainerdService() diff --git a/vendor.conf b/vendor.conf index 901ddd569..d498414bd 100644 --- a/vendor.conf +++ b/vendor.conf @@ -1,6 +1,6 @@ github.com/blang/semver v3.1.0 github.com/boltdb/bolt v1.3.0-58-ge9cf4fa -github.com/containerd/containerd f05281743e5ac9ad11c6e19a72be7a903eab79f5 +github.com/containerd/containerd a6ce1ef2a140d79856a8647e1d1ae5ac9ab581eb github.com/containerd/fifo fbfb6a11ec671efbe94ad1c12c2e98773f19e1e6 github.com/containernetworking/cni v0.6.0 github.com/containernetworking/plugins v0.6.0 diff --git a/vendor/github.com/containerd/containerd/README.md b/vendor/github.com/containerd/containerd/README.md index 6b9692dce..08a68145d 100644 --- a/vendor/github.com/containerd/containerd/README.md +++ b/vendor/github.com/containerd/containerd/README.md @@ -60,25 +60,25 @@ image, err := client.Pull(context, "docker.io/library/redis:latest") err := client.Push(context, "docker.io/library/redis:latest", image.Target()) ``` -### OCI Runtime Specification - -containerd fully supports the OCI runtime specification for running containers. We have built in functions to help you generate runtime specifications based on images as well as custom parameters. - -```go -spec, err := containerd.GenerateSpec(containerd.WithImageConfig(context, image)) -``` - ### Containers In containerd, a container is a metadata object. Resources such as an OCI runtime specification, image, root filesystem, and other metadata can be attached to a container. ```go -redis, err := client.NewContainer(context, "redis-master", - containerd.WithSpec(spec), -) +redis, err := client.NewContainer(context, "redis-master") defer redis.Delete(context) ``` +### OCI Runtime Specification + +containerd fully supports the OCI runtime specification for running containers. We have built in functions to help you generate runtime specifications based on images as well as custom parameters. + +You can specify options when creating a container about how to modify the specification. + +```go +redis, err := client.NewContainer(context, "redis-master", containerd.WithNewSpec(containerd.WithImageConfig(image))) +``` + ## Root Filesystems containerd allows you to use overlay or snapshot filesystems with your containers. It comes with builtin support for overlayfs and btrfs. @@ -89,16 +89,17 @@ image, err := client.Pull(context, "docker.io/library/redis:latest", containerd. // allocate a new RW root filesystem for a container based on the image redis, err := client.NewContainer(context, "redis-master", - containerd.WithSpec(spec), containerd.WithNewSnapshot("redis-rootfs", image), + containerd.WithNewSpec(containerd.WithImageConfig(image)), + ) // use a readonly filesystem with multiple containers for i := 0; i < 10; i++ { id := fmt.Sprintf("id-%s", i) container, err := client.NewContainer(ctx, id, - containerd.WithSpec(spec), containerd.WithNewSnapshotView(id, image), + containerd.WithNewSpec(containerd.WithImageConfig(image)), ) } ``` diff --git a/vendor/github.com/containerd/containerd/apparmor.go b/vendor/github.com/containerd/containerd/apparmor.go index 117b52bb1..e8485c3f5 100644 --- a/vendor/github.com/containerd/containerd/apparmor.go +++ b/vendor/github.com/containerd/containerd/apparmor.go @@ -2,11 +2,16 @@ package containerd -import specs "github.com/opencontainers/runtime-spec/specs-go" +import ( + "context" + + "github.com/containerd/containerd/containers" + specs "github.com/opencontainers/runtime-spec/specs-go" +) // WithApparmor sets the provided apparmor profile to the spec func WithApparmorProfile(profile string) SpecOpts { - return func(s *specs.Spec) error { + return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error { s.Process.ApparmorProfile = profile return nil } diff --git a/vendor/github.com/containerd/containerd/spec.go b/vendor/github.com/containerd/containerd/spec.go index 8567ace67..fc1201467 100644 --- a/vendor/github.com/containerd/containerd/spec.go +++ b/vendor/github.com/containerd/containerd/spec.go @@ -1,16 +1,21 @@ package containerd -import specs "github.com/opencontainers/runtime-spec/specs-go" +import ( + "context" + + "github.com/containerd/containerd/containers" + specs "github.com/opencontainers/runtime-spec/specs-go" +) // GenerateSpec will generate a default spec from the provided image // for use as a containerd container -func GenerateSpec(opts ...SpecOpts) (*specs.Spec, error) { +func GenerateSpec(ctx context.Context, client *Client, c *containers.Container, opts ...SpecOpts) (*specs.Spec, error) { s, err := createDefaultSpec() if err != nil { return nil, err } for _, o := range opts { - if err := o(s); err != nil { + if err := o(ctx, client, c, s); err != nil { return nil, err } } diff --git a/vendor/github.com/containerd/containerd/spec_opts.go b/vendor/github.com/containerd/containerd/spec_opts.go index 66cf5b3d7..811e1eeba 100644 --- a/vendor/github.com/containerd/containerd/spec_opts.go +++ b/vendor/github.com/containerd/containerd/spec_opts.go @@ -1,13 +1,19 @@ package containerd -import specs "github.com/opencontainers/runtime-spec/specs-go" +import ( + "context" + + "github.com/containerd/containerd/containers" + "github.com/containerd/containerd/typeurl" + specs "github.com/opencontainers/runtime-spec/specs-go" +) // SpecOpts sets spec specific information to a newly generated OCI spec -type SpecOpts func(s *specs.Spec) error +type SpecOpts func(context.Context, *Client, *containers.Container, *specs.Spec) error // WithProcessArgs replaces the args on the generated spec func WithProcessArgs(args ...string) SpecOpts { - return func(s *specs.Spec) error { + return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error { s.Process.Args = args return nil } @@ -15,8 +21,46 @@ func WithProcessArgs(args ...string) SpecOpts { // WithHostname sets the container's hostname func WithHostname(name string) SpecOpts { - return func(s *specs.Spec) error { + return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error { s.Hostname = name return nil } } + +// WithNewSpec generates a new spec for a new container +func WithNewSpec(opts ...SpecOpts) NewContainerOpts { + return func(ctx context.Context, client *Client, c *containers.Container) error { + s, err := createDefaultSpec() + if err != nil { + return err + } + for _, o := range opts { + if err := o(ctx, client, c, s); err != nil { + return err + } + } + any, err := typeurl.MarshalAny(s) + if err != nil { + return err + } + c.Spec = any + return nil + } +} + +// WithSpec sets the provided spec on the container +func WithSpec(s *specs.Spec, opts ...SpecOpts) NewContainerOpts { + return func(ctx context.Context, client *Client, c *containers.Container) error { + for _, o := range opts { + if err := o(ctx, client, c, s); err != nil { + return err + } + } + any, err := typeurl.MarshalAny(s) + if err != nil { + return err + } + c.Spec = any + return nil + } +} diff --git a/vendor/github.com/containerd/containerd/spec_opts_unix.go b/vendor/github.com/containerd/containerd/spec_opts_unix.go index 3e35c18c8..8aec1f22b 100644 --- a/vendor/github.com/containerd/containerd/spec_opts_unix.go +++ b/vendor/github.com/containerd/containerd/spec_opts_unix.go @@ -6,23 +6,28 @@ import ( "context" "encoding/json" "fmt" + "io/ioutil" + "os" "path/filepath" "strconv" "strings" + "golang.org/x/sys/unix" + "github.com/containerd/containerd/containers" "github.com/containerd/containerd/content" "github.com/containerd/containerd/images" "github.com/containerd/containerd/namespaces" - "github.com/containerd/containerd/typeurl" "github.com/opencontainers/image-spec/identity" "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/opencontainers/runc/libcontainer/user" specs "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" ) // WithTTY sets the information on the spec as well as the environment variables for // using a TTY -func WithTTY(s *specs.Spec) error { +func WithTTY(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error { s.Process.Terminal = true s.Process.Env = append(s.Process.Env, "TERM=xterm") return nil @@ -30,7 +35,7 @@ func WithTTY(s *specs.Spec) error { // WithHostNamespace allows a task to run inside the host's linux namespace func WithHostNamespace(ns specs.LinuxNamespaceType) SpecOpts { - return func(s *specs.Spec) error { + return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error { for i, n := range s.Linux.Namespaces { if n.Type == ns { s.Linux.Namespaces = append(s.Linux.Namespaces[:i], s.Linux.Namespaces[i+1:]...) @@ -44,7 +49,7 @@ func WithHostNamespace(ns specs.LinuxNamespaceType) SpecOpts { // WithLinuxNamespace uses the passed in namespace for the spec. If a namespace of the same type already exists in the // spec, the existing namespace is replaced by the one provided. func WithLinuxNamespace(ns specs.LinuxNamespace) SpecOpts { - return func(s *specs.Spec) error { + return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error { for i, n := range s.Linux.Namespaces { if n.Type == ns.Type { before := s.Linux.Namespaces[:i] @@ -60,11 +65,11 @@ func WithLinuxNamespace(ns specs.LinuxNamespace) SpecOpts { } // WithImageConfig configures the spec to from the configuration of an Image -func WithImageConfig(ctx context.Context, i Image) SpecOpts { - return func(s *specs.Spec) error { +func WithImageConfig(i Image) SpecOpts { + return func(ctx context.Context, client *Client, c *containers.Container, s *specs.Spec) error { var ( image = i.(*image) - store = image.client.ContentStore() + store = client.ContentStore() ) ic, err := image.i.Config(ctx, store) if err != nil { @@ -100,6 +105,10 @@ func WithImageConfig(ctx context.Context, i Image) SpecOpts { case 1: v, err := strconv.ParseUint(parts[0], 0, 10) if err != nil { + // if we cannot parse as a uint they try to see if it is a username + if err := WithUsername(config.User)(ctx, client, c, s); err != nil { + return err + } return err } uid, gid = uint32(v), uint32(v) @@ -129,7 +138,7 @@ func WithImageConfig(ctx context.Context, i Image) SpecOpts { // WithRootFSPath specifies unmanaged rootfs path. func WithRootFSPath(path string, readonly bool) SpecOpts { - return func(s *specs.Spec) error { + return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error { s.Root = &specs.Root{ Path: path, Readonly: readonly, @@ -139,18 +148,6 @@ func WithRootFSPath(path string, readonly bool) SpecOpts { } } -// WithSpec sets the provided spec for a new container -func WithSpec(spec *specs.Spec) NewContainerOpts { - return func(ctx context.Context, client *Client, c *containers.Container) error { - any, err := typeurl.MarshalAny(spec) - if err != nil { - return err - } - c.Spec = any - return nil - } -} - // WithResources sets the provided resources on the spec for task updates func WithResources(resources *specs.LinuxResources) UpdateTaskOpts { return func(ctx context.Context, client *Client, r *UpdateTaskInfo) error { @@ -160,13 +157,13 @@ func WithResources(resources *specs.LinuxResources) UpdateTaskOpts { } // WithNoNewPrivileges sets no_new_privileges on the process for the container -func WithNoNewPrivileges(s *specs.Spec) error { +func WithNoNewPrivileges(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error { s.Process.NoNewPrivileges = true return nil } // WithHostHostsFile bind-mounts the host's /etc/hosts into the container as readonly -func WithHostHostsFile(s *specs.Spec) error { +func WithHostHostsFile(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error { s.Mounts = append(s.Mounts, specs.Mount{ Destination: "/etc/hosts", Type: "bind", @@ -177,7 +174,7 @@ func WithHostHostsFile(s *specs.Spec) error { } // WithHostResolvconf bind-mounts the host's /etc/resolv.conf into the container as readonly -func WithHostResolvconf(s *specs.Spec) error { +func WithHostResolvconf(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error { s.Mounts = append(s.Mounts, specs.Mount{ Destination: "/etc/resolv.conf", Type: "bind", @@ -188,7 +185,7 @@ func WithHostResolvconf(s *specs.Spec) error { } // WithHostLocaltime bind-mounts the host's /etc/localtime into the container as readonly -func WithHostLocaltime(s *specs.Spec) error { +func WithHostLocaltime(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error { s.Mounts = append(s.Mounts, specs.Mount{ Destination: "/etc/localtime", Type: "bind", @@ -201,7 +198,7 @@ func WithHostLocaltime(s *specs.Spec) error { // WithUserNamespace sets the uid and gid mappings for the task // this can be called multiple times to add more mappings to the generated spec func WithUserNamespace(container, host, size uint32) SpecOpts { - return func(s *specs.Spec) error { + return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error { var hasUserns bool for _, ns := range s.Linux.Namespaces { if ns.Type == specs.UserNamespace { @@ -271,7 +268,7 @@ func WithRemappedSnapshot(id string, i Image, uid, gid uint32) NewContainerOpts // WithCgroup sets the container's cgroup path func WithCgroup(path string) SpecOpts { - return func(s *specs.Spec) error { + return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error { s.Linux.CgroupsPath = path return nil } @@ -279,13 +276,69 @@ func WithCgroup(path string) SpecOpts { // WithNamespacedCgroup uses the namespace set on the context to create a // root directory for containers in the cgroup with the id as the subcgroup -func WithNamespacedCgroup(ctx context.Context, id string) SpecOpts { - return func(s *specs.Spec) error { +func WithNamespacedCgroup() SpecOpts { + return func(ctx context.Context, _ *Client, c *containers.Container, s *specs.Spec) error { namespace, err := namespaces.NamespaceRequired(ctx) if err != nil { return err } - s.Linux.CgroupsPath = filepath.Join("/", namespace, id) + s.Linux.CgroupsPath = filepath.Join("/", namespace, c.ID) + return nil + } +} + +// WithUserIDs allows the UID and GID for the Process to be set +func WithUserIDs(uid, gid uint32) SpecOpts { + return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error { + s.Process.User.UID = uid + s.Process.User.GID = gid + return nil + } +} + +// WithUsername sets the correct UID and GID for the container +// based on the the image's /etc/passwd contents. +// id is the snapshot id that is used +func WithUsername(username string) SpecOpts { + return func(ctx context.Context, client *Client, c *containers.Container, s *specs.Spec) error { + if c.Snapshotter == "" { + return errors.Errorf("no snapshotter set for container") + } + if c.RootFS == "" { + return errors.Errorf("rootfs not created for container") + } + snapshotter := client.SnapshotService(c.Snapshotter) + mounts, err := snapshotter.Mounts(ctx, c.RootFS) + if err != nil { + return err + } + root, err := ioutil.TempDir("", "ctd-username") + if err != nil { + return err + } + defer os.RemoveAll(root) + for _, m := range mounts { + if err := m.Mount(root); err != nil { + return err + } + } + defer unix.Unmount(root, 0) + f, err := os.Open(filepath.Join(root, "/etc/passwd")) + if err != nil { + return err + } + defer f.Close() + users, err := user.ParsePasswdFilter(f, func(u user.User) bool { + return u.Name == username + }) + if err != nil { + return err + } + if len(users) == 0 { + return errors.Errorf("no users found for %s", username) + } + u := users[0] + s.Process.User.UID, s.Process.User.GID = uint32(u.Uid), uint32(u.Gid) return nil } } diff --git a/vendor/github.com/containerd/containerd/spec_opts_windows.go b/vendor/github.com/containerd/containerd/spec_opts_windows.go index f4dc8b25b..8f0fa6139 100644 --- a/vendor/github.com/containerd/containerd/spec_opts_windows.go +++ b/vendor/github.com/containerd/containerd/spec_opts_windows.go @@ -10,16 +10,15 @@ import ( "github.com/containerd/containerd/containers" "github.com/containerd/containerd/content" "github.com/containerd/containerd/images" - "github.com/containerd/containerd/typeurl" "github.com/opencontainers/image-spec/specs-go/v1" specs "github.com/opencontainers/runtime-spec/specs-go" ) -func WithImageConfig(ctx context.Context, i Image) SpecOpts { - return func(s *specs.Spec) error { +func WithImageConfig(i Image) SpecOpts { + return func(ctx context.Context, client *Client, _ *containers.Container, s *specs.Spec) error { var ( image = i.(*image) - store = image.client.ContentStore() + store = client.ContentStore() ) ic, err := image.i.Config(ctx, store) if err != nil { @@ -52,7 +51,7 @@ func WithImageConfig(ctx context.Context, i Image) SpecOpts { } func WithTTY(width, height int) SpecOpts { - return func(s *specs.Spec) error { + return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error { s.Process.Terminal = true s.Process.ConsoleSize.Width = uint(width) s.Process.ConsoleSize.Height = uint(height) @@ -60,17 +59,6 @@ func WithTTY(width, height int) SpecOpts { } } -func WithSpec(spec *specs.Spec) NewContainerOpts { - return func(ctx context.Context, client *Client, c *containers.Container) error { - any, err := typeurl.MarshalAny(spec) - if err != nil { - return err - } - c.Spec = any - return nil - } -} - func WithResources(resources *specs.WindowsResources) UpdateTaskOpts { return func(ctx context.Context, client *Client, r *UpdateTaskInfo) error { r.Resources = resources diff --git a/vendor/github.com/opencontainers/runc/libcontainer/user/lookup.go b/vendor/github.com/opencontainers/runc/libcontainer/user/lookup.go new file mode 100644 index 000000000..bf491c89c --- /dev/null +++ b/vendor/github.com/opencontainers/runc/libcontainer/user/lookup.go @@ -0,0 +1,111 @@ +package user + +import ( + "errors" + + "golang.org/x/sys/unix" +) + +var ( + // The current operating system does not provide the required data for user lookups. + ErrUnsupported = errors.New("user lookup: operating system does not provide passwd-formatted data") + // No matching entries found in file. + ErrNoPasswdEntries = errors.New("no matching entries in passwd file") + ErrNoGroupEntries = errors.New("no matching entries in group file") +) + +func lookupUser(filter func(u User) bool) (User, error) { + // Get operating system-specific passwd reader-closer. + passwd, err := GetPasswd() + if err != nil { + return User{}, err + } + defer passwd.Close() + + // Get the users. + users, err := ParsePasswdFilter(passwd, filter) + if err != nil { + return User{}, err + } + + // No user entries found. + if len(users) == 0 { + return User{}, ErrNoPasswdEntries + } + + // Assume the first entry is the "correct" one. + return users[0], nil +} + +// CurrentUser looks up the current user by their user id in /etc/passwd. If the +// user cannot be found (or there is no /etc/passwd file on the filesystem), +// then CurrentUser returns an error. +func CurrentUser() (User, error) { + return LookupUid(unix.Getuid()) +} + +// LookupUser looks up a user by their username in /etc/passwd. If the user +// cannot be found (or there is no /etc/passwd file on the filesystem), then +// LookupUser returns an error. +func LookupUser(username string) (User, error) { + return lookupUser(func(u User) bool { + return u.Name == username + }) +} + +// LookupUid looks up a user by their user id in /etc/passwd. If the user cannot +// be found (or there is no /etc/passwd file on the filesystem), then LookupId +// returns an error. +func LookupUid(uid int) (User, error) { + return lookupUser(func(u User) bool { + return u.Uid == uid + }) +} + +func lookupGroup(filter func(g Group) bool) (Group, error) { + // Get operating system-specific group reader-closer. + group, err := GetGroup() + if err != nil { + return Group{}, err + } + defer group.Close() + + // Get the users. + groups, err := ParseGroupFilter(group, filter) + if err != nil { + return Group{}, err + } + + // No user entries found. + if len(groups) == 0 { + return Group{}, ErrNoGroupEntries + } + + // Assume the first entry is the "correct" one. + return groups[0], nil +} + +// CurrentGroup looks up the current user's group by their primary group id's +// entry in /etc/passwd. If the group cannot be found (or there is no +// /etc/group file on the filesystem), then CurrentGroup returns an error. +func CurrentGroup() (Group, error) { + return LookupGid(unix.Getgid()) +} + +// LookupGroup looks up a group by its name in /etc/group. If the group cannot +// be found (or there is no /etc/group file on the filesystem), then LookupGroup +// returns an error. +func LookupGroup(groupname string) (Group, error) { + return lookupGroup(func(g Group) bool { + return g.Name == groupname + }) +} + +// LookupGid looks up a group by its group id in /etc/group. If the group cannot +// be found (or there is no /etc/group file on the filesystem), then LookupGid +// returns an error. +func LookupGid(gid int) (Group, error) { + return lookupGroup(func(g Group) bool { + return g.Gid == gid + }) +} diff --git a/vendor/github.com/opencontainers/runc/libcontainer/user/lookup_unix.go b/vendor/github.com/opencontainers/runc/libcontainer/user/lookup_unix.go new file mode 100644 index 000000000..758b734c2 --- /dev/null +++ b/vendor/github.com/opencontainers/runc/libcontainer/user/lookup_unix.go @@ -0,0 +1,30 @@ +// +build darwin dragonfly freebsd linux netbsd openbsd solaris + +package user + +import ( + "io" + "os" +) + +// Unix-specific path to the passwd and group formatted files. +const ( + unixPasswdPath = "/etc/passwd" + unixGroupPath = "/etc/group" +) + +func GetPasswdPath() (string, error) { + return unixPasswdPath, nil +} + +func GetPasswd() (io.ReadCloser, error) { + return os.Open(unixPasswdPath) +} + +func GetGroupPath() (string, error) { + return unixGroupPath, nil +} + +func GetGroup() (io.ReadCloser, error) { + return os.Open(unixGroupPath) +} diff --git a/vendor/github.com/opencontainers/runc/libcontainer/user/lookup_unsupported.go b/vendor/github.com/opencontainers/runc/libcontainer/user/lookup_unsupported.go new file mode 100644 index 000000000..721794887 --- /dev/null +++ b/vendor/github.com/opencontainers/runc/libcontainer/user/lookup_unsupported.go @@ -0,0 +1,21 @@ +// +build !darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris + +package user + +import "io" + +func GetPasswdPath() (string, error) { + return "", ErrUnsupported +} + +func GetPasswd() (io.ReadCloser, error) { + return nil, ErrUnsupported +} + +func GetGroupPath() (string, error) { + return "", ErrUnsupported +} + +func GetGroup() (io.ReadCloser, error) { + return nil, ErrUnsupported +} diff --git a/vendor/github.com/opencontainers/runc/libcontainer/user/user.go b/vendor/github.com/opencontainers/runc/libcontainer/user/user.go new file mode 100644 index 000000000..2471535a7 --- /dev/null +++ b/vendor/github.com/opencontainers/runc/libcontainer/user/user.go @@ -0,0 +1,441 @@ +package user + +import ( + "bufio" + "fmt" + "io" + "os" + "strconv" + "strings" +) + +const ( + minId = 0 + maxId = 1<<31 - 1 //for 32-bit systems compatibility +) + +var ( + ErrRange = fmt.Errorf("uids and gids must be in range %d-%d", minId, maxId) +) + +type User struct { + Name string + Pass string + Uid int + Gid int + Gecos string + Home string + Shell string +} + +type Group struct { + Name string + Pass string + Gid int + List []string +} + +func parseLine(line string, v ...interface{}) { + if line == "" { + return + } + + parts := strings.Split(line, ":") + for i, p := range parts { + // Ignore cases where we don't have enough fields to populate the arguments. + // Some configuration files like to misbehave. + if len(v) <= i { + break + } + + // Use the type of the argument to figure out how to parse it, scanf() style. + // This is legit. + switch e := v[i].(type) { + case *string: + *e = p + case *int: + // "numbers", with conversion errors ignored because of some misbehaving configuration files. + *e, _ = strconv.Atoi(p) + case *[]string: + // Comma-separated lists. + if p != "" { + *e = strings.Split(p, ",") + } else { + *e = []string{} + } + default: + // Someone goof'd when writing code using this function. Scream so they can hear us. + panic(fmt.Sprintf("parseLine only accepts {*string, *int, *[]string} as arguments! %#v is not a pointer!", e)) + } + } +} + +func ParsePasswdFile(path string) ([]User, error) { + passwd, err := os.Open(path) + if err != nil { + return nil, err + } + defer passwd.Close() + return ParsePasswd(passwd) +} + +func ParsePasswd(passwd io.Reader) ([]User, error) { + return ParsePasswdFilter(passwd, nil) +} + +func ParsePasswdFileFilter(path string, filter func(User) bool) ([]User, error) { + passwd, err := os.Open(path) + if err != nil { + return nil, err + } + defer passwd.Close() + return ParsePasswdFilter(passwd, filter) +} + +func ParsePasswdFilter(r io.Reader, filter func(User) bool) ([]User, error) { + if r == nil { + return nil, fmt.Errorf("nil source for passwd-formatted data") + } + + var ( + s = bufio.NewScanner(r) + out = []User{} + ) + + for s.Scan() { + if err := s.Err(); err != nil { + return nil, err + } + + line := strings.TrimSpace(s.Text()) + if line == "" { + continue + } + + // see: man 5 passwd + // name:password:UID:GID:GECOS:directory:shell + // Name:Pass:Uid:Gid:Gecos:Home:Shell + // root:x:0:0:root:/root:/bin/bash + // adm:x:3:4:adm:/var/adm:/bin/false + p := User{} + parseLine(line, &p.Name, &p.Pass, &p.Uid, &p.Gid, &p.Gecos, &p.Home, &p.Shell) + + if filter == nil || filter(p) { + out = append(out, p) + } + } + + return out, nil +} + +func ParseGroupFile(path string) ([]Group, error) { + group, err := os.Open(path) + if err != nil { + return nil, err + } + + defer group.Close() + return ParseGroup(group) +} + +func ParseGroup(group io.Reader) ([]Group, error) { + return ParseGroupFilter(group, nil) +} + +func ParseGroupFileFilter(path string, filter func(Group) bool) ([]Group, error) { + group, err := os.Open(path) + if err != nil { + return nil, err + } + defer group.Close() + return ParseGroupFilter(group, filter) +} + +func ParseGroupFilter(r io.Reader, filter func(Group) bool) ([]Group, error) { + if r == nil { + return nil, fmt.Errorf("nil source for group-formatted data") + } + + var ( + s = bufio.NewScanner(r) + out = []Group{} + ) + + for s.Scan() { + if err := s.Err(); err != nil { + return nil, err + } + + text := s.Text() + if text == "" { + continue + } + + // see: man 5 group + // group_name:password:GID:user_list + // Name:Pass:Gid:List + // root:x:0:root + // adm:x:4:root,adm,daemon + p := Group{} + parseLine(text, &p.Name, &p.Pass, &p.Gid, &p.List) + + if filter == nil || filter(p) { + out = append(out, p) + } + } + + return out, nil +} + +type ExecUser struct { + Uid int + Gid int + Sgids []int + Home string +} + +// GetExecUserPath is a wrapper for GetExecUser. It reads data from each of the +// given file paths and uses that data as the arguments to GetExecUser. If the +// files cannot be opened for any reason, the error is ignored and a nil +// io.Reader is passed instead. +func GetExecUserPath(userSpec string, defaults *ExecUser, passwdPath, groupPath string) (*ExecUser, error) { + var passwd, group io.Reader + + if passwdFile, err := os.Open(passwdPath); err == nil { + passwd = passwdFile + defer passwdFile.Close() + } + + if groupFile, err := os.Open(groupPath); err == nil { + group = groupFile + defer groupFile.Close() + } + + return GetExecUser(userSpec, defaults, passwd, group) +} + +// GetExecUser parses a user specification string (using the passwd and group +// readers as sources for /etc/passwd and /etc/group data, respectively). In +// the case of blank fields or missing data from the sources, the values in +// defaults is used. +// +// GetExecUser will return an error if a user or group literal could not be +// found in any entry in passwd and group respectively. +// +// Examples of valid user specifications are: +// * "" +// * "user" +// * "uid" +// * "user:group" +// * "uid:gid +// * "user:gid" +// * "uid:group" +// +// It should be noted that if you specify a numeric user or group id, they will +// not be evaluated as usernames (only the metadata will be filled). So attempting +// to parse a user with user.Name = "1337" will produce the user with a UID of +// 1337. +func GetExecUser(userSpec string, defaults *ExecUser, passwd, group io.Reader) (*ExecUser, error) { + if defaults == nil { + defaults = new(ExecUser) + } + + // Copy over defaults. + user := &ExecUser{ + Uid: defaults.Uid, + Gid: defaults.Gid, + Sgids: defaults.Sgids, + Home: defaults.Home, + } + + // Sgids slice *cannot* be nil. + if user.Sgids == nil { + user.Sgids = []int{} + } + + // Allow for userArg to have either "user" syntax, or optionally "user:group" syntax + var userArg, groupArg string + parseLine(userSpec, &userArg, &groupArg) + + // Convert userArg and groupArg to be numeric, so we don't have to execute + // Atoi *twice* for each iteration over lines. + uidArg, uidErr := strconv.Atoi(userArg) + gidArg, gidErr := strconv.Atoi(groupArg) + + // Find the matching user. + users, err := ParsePasswdFilter(passwd, func(u User) bool { + if userArg == "" { + // Default to current state of the user. + return u.Uid == user.Uid + } + + if uidErr == nil { + // If the userArg is numeric, always treat it as a UID. + return uidArg == u.Uid + } + + return u.Name == userArg + }) + + // If we can't find the user, we have to bail. + if err != nil && passwd != nil { + if userArg == "" { + userArg = strconv.Itoa(user.Uid) + } + return nil, fmt.Errorf("unable to find user %s: %v", userArg, err) + } + + var matchedUserName string + if len(users) > 0 { + // First match wins, even if there's more than one matching entry. + matchedUserName = users[0].Name + user.Uid = users[0].Uid + user.Gid = users[0].Gid + user.Home = users[0].Home + } else if userArg != "" { + // If we can't find a user with the given username, the only other valid + // option is if it's a numeric username with no associated entry in passwd. + + if uidErr != nil { + // Not numeric. + return nil, fmt.Errorf("unable to find user %s: %v", userArg, ErrNoPasswdEntries) + } + user.Uid = uidArg + + // Must be inside valid uid range. + if user.Uid < minId || user.Uid > maxId { + return nil, ErrRange + } + + // Okay, so it's numeric. We can just roll with this. + } + + // On to the groups. If we matched a username, we need to do this because of + // the supplementary group IDs. + if groupArg != "" || matchedUserName != "" { + groups, err := ParseGroupFilter(group, func(g Group) bool { + // If the group argument isn't explicit, we'll just search for it. + if groupArg == "" { + // Check if user is a member of this group. + for _, u := range g.List { + if u == matchedUserName { + return true + } + } + return false + } + + if gidErr == nil { + // If the groupArg is numeric, always treat it as a GID. + return gidArg == g.Gid + } + + return g.Name == groupArg + }) + if err != nil && group != nil { + return nil, fmt.Errorf("unable to find groups for spec %v: %v", matchedUserName, err) + } + + // Only start modifying user.Gid if it is in explicit form. + if groupArg != "" { + if len(groups) > 0 { + // First match wins, even if there's more than one matching entry. + user.Gid = groups[0].Gid + } else { + // If we can't find a group with the given name, the only other valid + // option is if it's a numeric group name with no associated entry in group. + + if gidErr != nil { + // Not numeric. + return nil, fmt.Errorf("unable to find group %s: %v", groupArg, ErrNoGroupEntries) + } + user.Gid = gidArg + + // Must be inside valid gid range. + if user.Gid < minId || user.Gid > maxId { + return nil, ErrRange + } + + // Okay, so it's numeric. We can just roll with this. + } + } else if len(groups) > 0 && uidErr != nil { + // Supplementary group ids only make sense if in the implicit form for non-numeric users. + user.Sgids = make([]int, len(groups)) + for i, group := range groups { + user.Sgids[i] = group.Gid + } + } + } + + return user, nil +} + +// GetAdditionalGroups looks up a list of groups by name or group id +// against the given /etc/group formatted data. If a group name cannot +// be found, an error will be returned. If a group id cannot be found, +// or the given group data is nil, the id will be returned as-is +// provided it is in the legal range. +func GetAdditionalGroups(additionalGroups []string, group io.Reader) ([]int, error) { + var groups = []Group{} + if group != nil { + var err error + groups, err = ParseGroupFilter(group, func(g Group) bool { + for _, ag := range additionalGroups { + if g.Name == ag || strconv.Itoa(g.Gid) == ag { + return true + } + } + return false + }) + if err != nil { + return nil, fmt.Errorf("Unable to find additional groups %v: %v", additionalGroups, err) + } + } + + gidMap := make(map[int]struct{}) + for _, ag := range additionalGroups { + var found bool + for _, g := range groups { + // if we found a matched group either by name or gid, take the + // first matched as correct + if g.Name == ag || strconv.Itoa(g.Gid) == ag { + if _, ok := gidMap[g.Gid]; !ok { + gidMap[g.Gid] = struct{}{} + found = true + break + } + } + } + // we asked for a group but didn't find it. let's check to see + // if we wanted a numeric group + if !found { + gid, err := strconv.Atoi(ag) + if err != nil { + return nil, fmt.Errorf("Unable to find group %s", ag) + } + // Ensure gid is inside gid range. + if gid < minId || gid > maxId { + return nil, ErrRange + } + gidMap[gid] = struct{}{} + } + } + gids := []int{} + for gid := range gidMap { + gids = append(gids, gid) + } + return gids, nil +} + +// GetAdditionalGroupsPath is a wrapper around GetAdditionalGroups +// that opens the groupPath given and gives it as an argument to +// GetAdditionalGroups. +func GetAdditionalGroupsPath(additionalGroups []string, groupPath string) ([]int, error) { + var group io.Reader + + if groupFile, err := os.Open(groupPath); err == nil { + group = groupFile + defer groupFile.Close() + } + return GetAdditionalGroups(additionalGroups, group) +}