diff --git a/oci/spec_opts.go b/oci/spec_opts.go index 675be3970..96f6b3496 100644 --- a/oci/spec_opts.go +++ b/oci/spec_opts.go @@ -27,6 +27,18 @@ import ( // SpecOpts sets spec specific information to a newly generated OCI spec type SpecOpts func(context.Context, Client, *containers.Container, *specs.Spec) error +// Compose converts a sequence of spec operations into a single operation +func Compose(opts ...SpecOpts) SpecOpts { + return func(ctx context.Context, client Client, c *containers.Container, s *specs.Spec) error { + for _, o := range opts { + if err := o(ctx, client, c, s); err != nil { + return err + } + } + return nil + } +} + // setProcess sets Process to empty if unset func setProcess(s *specs.Spec) { if s.Process == nil { diff --git a/oci/spec_opts_unix.go b/oci/spec_opts_unix.go index 27d78178b..46053aa55 100644 --- a/oci/spec_opts_unix.go +++ b/oci/spec_opts_unix.go @@ -443,20 +443,23 @@ func WithUsername(username string) SpecOpts { } } -// WithAllCapabilities set all linux capabilities for the process -func WithAllCapabilities(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error { - setCapabilities(s) +// WithCapabilities sets Linux capabilities on the process +func WithCapabilities(caps []string) SpecOpts { + return func(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error { + setCapabilities(s) - caps := getAllCapabilities() + s.Process.Capabilities.Bounding = caps + s.Process.Capabilities.Effective = caps + s.Process.Capabilities.Permitted = caps + s.Process.Capabilities.Inheritable = caps - s.Process.Capabilities.Bounding = caps - s.Process.Capabilities.Effective = caps - s.Process.Capabilities.Permitted = caps - s.Process.Capabilities.Inheritable = caps - - return nil + return nil + } } +// WithAllCapabilities sets all linux capabilities for the process +var WithAllCapabilities = WithCapabilities(getAllCapabilities()) + func getAllCapabilities() []string { last := capability.CAP_LAST_CAP // hack for RHEL6 which has no /proc/sys/kernel/cap_last_cap @@ -512,3 +515,93 @@ func getGIDFromPath(root string, filter func(user.Group) bool) (gid uint32, err func isRootfsAbs(root string) bool { return filepath.IsAbs(root) } + +// WithMaskedPaths sets the masked paths option +func WithMaskedPaths(paths []string) SpecOpts { + return func(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error { + setLinux(s) + s.Linux.MaskedPaths = paths + return nil + } +} + +// WithReadonlyPaths sets the read only paths option +func WithReadonlyPaths(paths []string) SpecOpts { + return func(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error { + setLinux(s) + s.Linux.ReadonlyPaths = paths + return nil + } +} + +// WithWriteableSysfs makes any sysfs mounts writeable +func WithWriteableSysfs(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error { + for i, m := range s.Mounts { + if m.Type == "sysfs" { + var options []string + for _, o := range m.Options { + if o == "ro" { + o = "rw" + } + options = append(options, o) + } + s.Mounts[i].Options = options + } + } + return nil +} + +// WithWriteableCgroupfs makes any cgroup mounts writeable +func WithWriteableCgroupfs(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error { + for i, m := range s.Mounts { + if m.Type == "cgroup" { + var options []string + for _, o := range m.Options { + if o == "ro" { + o = "rw" + } + options = append(options, o) + } + s.Mounts[i].Options = options + } + } + return nil +} + +// WithSelinuxLabel sets the process SELinux label +func WithSelinuxLabel(label string) SpecOpts { + return func(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error { + setProcess(s) + s.Process.SelinuxLabel = label + return nil + } +} + +// WithApparmorProfile sets the Apparmor profile for the process +func WithApparmorProfile(profile string) SpecOpts { + return func(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error { + setProcess(s) + s.Process.ApparmorProfile = profile + return nil + } +} + +// WithSeccompUnconfined clears the seccomp profile +func WithSeccompUnconfined(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error { + setLinux(s) + s.Linux.Seccomp = nil + return nil +} + +// WithPrivileged sets up options for a privileged container +// TODO(justincormack) device handling +var WithPrivileged = Compose( + WithAllCapabilities, + WithMaskedPaths(nil), + WithReadonlyPaths(nil), + WithWriteableSysfs, + WithWriteableCgroupfs, + WithSelinuxLabel(""), + WithApparmorProfile(""), + WithSeccompUnconfined, +) diff --git a/oci/spec_unix_test.go b/oci/spec_unix_test.go index 93c6aee85..9cbcf42b7 100644 --- a/oci/spec_unix_test.go +++ b/oci/spec_unix_test.go @@ -108,3 +108,116 @@ func TestWithLinuxNamespace(t *testing.T) { } } } + +func TestWithCapabilities(t *testing.T) { + t.Parallel() + + ctx := namespaces.WithNamespace(context.Background(), "testing") + + s, err := GenerateSpec(ctx, nil, &containers.Container{ID: t.Name()}, + WithCapabilities([]string{"CAP_SYS_ADMIN"}), + ) + 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) != 1 || s.Process.Capabilities.Inheritable[0] != "CAP_SYS_ADMIN" { + t.Error("Unexpected capabilities set") + } +} + +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 TestWithPrivileged(t *testing.T) { + t.Parallel() + + ctx := namespaces.WithNamespace(context.Background(), "testing") + + s, err := GenerateSpec(ctx, nil, &containers.Container{ID: t.Name()}, + WithCapabilities(nil), + WithMounts([]specs.Mount{ + {Type: "cgroup", Destination: "/sys/fs/cgroup", Options: []string{"ro"}}, + }), + WithPrivileged, + ) + if err != nil { + t.Fatal(err) + } + + 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") + } +}