From 67fe100ad5e784c8d273e4de768974ecc31a8d59 Mon Sep 17 00:00:00 2001 From: Jess Frazelle Date: Tue, 20 Mar 2018 13:29:18 -0400 Subject: [PATCH] api: add configurable MaskedPaths and ReadOnlyPaths to the API This adds MaskedPaths and ReadOnlyPaths options to HostConfig for containers so that a user can override the default values. When the value sent through the API is nil the default is used. Otherwise the default is overridden. Adds integration tests for MaskedPaths and ReadonlyPaths. Signed-off-by: Jess Frazelle Upstream-commit: 3694c1e34e40fa2e255a97b5541645cec9c8d1d5 Component: engine --- components/engine/api/swagger.yaml | 10 ++ .../engine/api/types/container/host_config.go | 6 + components/engine/daemon/create_unix.go | 11 ++ components/engine/daemon/oci_linux.go | 8 + .../integration/container/create_test.go | 164 ++++++++++++++++++ 5 files changed, 199 insertions(+) diff --git a/components/engine/api/swagger.yaml b/components/engine/api/swagger.yaml index af3bd6d4847..3cdfe2988ca 100644 --- a/components/engine/api/swagger.yaml +++ b/components/engine/api/swagger.yaml @@ -772,6 +772,16 @@ definitions: - "default" - "process" - "hyperv" + MaskedPaths: + type: "array" + description: "The list of paths to be masked inside the container (this overrides the default set of paths)" + items: + type: "string" + ReadonlyPaths: + type: "array" + description: "The list of paths to be set as read-only inside the container (this overrides the default set of paths)" + items: + type: "string" ContainerConfig: description: "Configuration for a container that is portable between hosts" diff --git a/components/engine/api/types/container/host_config.go b/components/engine/api/types/container/host_config.go index 02271ecd985..4ef26fa6c87 100644 --- a/components/engine/api/types/container/host_config.go +++ b/components/engine/api/types/container/host_config.go @@ -401,6 +401,12 @@ type HostConfig struct { // Mounts specs used by the container Mounts []mount.Mount `json:",omitempty"` + // MaskedPaths is the list of paths to be masked inside the container (this overrides the default set of paths) + MaskedPaths []string + + // ReadonlyPaths is the list of paths to be set as read-only inside the container (this overrides the default set of paths) + ReadonlyPaths []string + // Run a custom init inside the container, if null, use the daemon's configured settings Init *bool `json:",omitempty"` } diff --git a/components/engine/daemon/create_unix.go b/components/engine/daemon/create_unix.go index 9ea74e7c1f9..eb9b6537300 100644 --- a/components/engine/daemon/create_unix.go +++ b/components/engine/daemon/create_unix.go @@ -11,6 +11,7 @@ import ( containertypes "github.com/docker/docker/api/types/container" mounttypes "github.com/docker/docker/api/types/mount" "github.com/docker/docker/container" + "github.com/docker/docker/oci" "github.com/docker/docker/pkg/stringid" volumeopts "github.com/docker/docker/volume/service/opts" "github.com/opencontainers/selinux/go-selinux/label" @@ -29,6 +30,16 @@ func (daemon *Daemon) createContainerOSSpecificSettings(container *container.Con return err } + // Set the default masked and readonly paths with regard to the host config options if they are not set. + if hostConfig.MaskedPaths == nil && !hostConfig.Privileged { + hostConfig.MaskedPaths = oci.DefaultSpec().Linux.MaskedPaths // Set it to the default if nil + container.HostConfig.MaskedPaths = hostConfig.MaskedPaths + } + if hostConfig.ReadonlyPaths == nil && !hostConfig.Privileged { + hostConfig.ReadonlyPaths = oci.DefaultSpec().Linux.ReadonlyPaths // Set it to the default if nil + container.HostConfig.ReadonlyPaths = hostConfig.ReadonlyPaths + } + for spec := range config.Volumes { name := stringid.GenerateNonCryptoID() destination := filepath.Clean(spec) diff --git a/components/engine/daemon/oci_linux.go b/components/engine/daemon/oci_linux.go index b675eaf406c..9b39a64ee73 100644 --- a/components/engine/daemon/oci_linux.go +++ b/components/engine/daemon/oci_linux.go @@ -903,6 +903,14 @@ func (daemon *Daemon) createSpec(c *container.Container) (retSpec *specs.Spec, e s.Process.OOMScoreAdj = &c.HostConfig.OomScoreAdj s.Linux.MountLabel = c.MountLabel + // Set the masked and readonly paths with regard to the host config options if they are set. + if c.HostConfig.MaskedPaths != nil { + s.Linux.MaskedPaths = c.HostConfig.MaskedPaths + } + if c.HostConfig.ReadonlyPaths != nil { + s.Linux.ReadonlyPaths = c.HostConfig.ReadonlyPaths + } + return &s, nil } diff --git a/components/engine/integration/container/create_test.go b/components/engine/integration/container/create_test.go index cea8c059cc7..9c466448903 100644 --- a/components/engine/integration/container/create_test.go +++ b/components/engine/integration/container/create_test.go @@ -2,14 +2,21 @@ package container // import "github.com/docker/docker/integration/container" import ( "context" + "encoding/json" + "fmt" "strconv" "testing" + "time" + "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/network" + ctr "github.com/docker/docker/integration/internal/container" "github.com/docker/docker/internal/test/request" + "github.com/docker/docker/oci" "github.com/gotestyourself/gotestyourself/assert" is "github.com/gotestyourself/gotestyourself/assert/cmp" + "github.com/gotestyourself/gotestyourself/poll" "github.com/gotestyourself/gotestyourself/skip" ) @@ -137,3 +144,160 @@ func TestCreateTmpfsMountsTarget(t *testing.T) { assert.Check(t, is.ErrorContains(err, tc.expectedError)) } } +func TestCreateWithCustomMaskedPaths(t *testing.T) { + skip.If(t, testEnv.DaemonInfo.OSType != "linux") + + defer setupTest(t)() + client := request.NewAPIClient(t) + ctx := context.Background() + + testCases := []struct { + maskedPaths []string + expected []string + }{ + { + maskedPaths: []string{}, + expected: []string{}, + }, + { + maskedPaths: nil, + expected: oci.DefaultSpec().Linux.MaskedPaths, + }, + { + maskedPaths: []string{"/proc/kcore", "/proc/keys"}, + expected: []string{"/proc/kcore", "/proc/keys"}, + }, + } + + checkInspect := func(t *testing.T, ctx context.Context, name string, expected []string) { + _, b, err := client.ContainerInspectWithRaw(ctx, name, false) + assert.NilError(t, err) + + var inspectJSON map[string]interface{} + err = json.Unmarshal(b, &inspectJSON) + assert.NilError(t, err) + + cfg, ok := inspectJSON["HostConfig"].(map[string]interface{}) + assert.Check(t, is.Equal(true, ok), name) + + maskedPaths, ok := cfg["MaskedPaths"].([]interface{}) + assert.Check(t, is.Equal(true, ok), name) + + mps := []string{} + for _, mp := range maskedPaths { + mps = append(mps, mp.(string)) + } + + assert.DeepEqual(t, expected, mps) + } + + for i, tc := range testCases { + name := fmt.Sprintf("create-masked-paths-%d", i) + config := container.Config{ + Image: "busybox", + Cmd: []string{"true"}, + } + hc := container.HostConfig{} + if tc.maskedPaths != nil { + hc.MaskedPaths = tc.maskedPaths + } + + // Create the container. + c, err := client.ContainerCreate(context.Background(), + &config, + &hc, + &network.NetworkingConfig{}, + name, + ) + assert.NilError(t, err) + + checkInspect(t, ctx, name, tc.expected) + + // Start the container. + err = client.ContainerStart(ctx, c.ID, types.ContainerStartOptions{}) + assert.NilError(t, err) + + poll.WaitOn(t, ctr.IsInState(ctx, client, c.ID, "exited"), poll.WithDelay(100*time.Millisecond)) + + checkInspect(t, ctx, name, tc.expected) + } +} + +func TestCreateWithCustomReadonlyPaths(t *testing.T) { + skip.If(t, testEnv.DaemonInfo.OSType != "linux") + + defer setupTest(t)() + client := request.NewAPIClient(t) + ctx := context.Background() + + testCases := []struct { + doc string + readonlyPaths []string + expected []string + }{ + { + readonlyPaths: []string{}, + expected: []string{}, + }, + { + readonlyPaths: nil, + expected: oci.DefaultSpec().Linux.ReadonlyPaths, + }, + { + readonlyPaths: []string{"/proc/asound", "/proc/bus"}, + expected: []string{"/proc/asound", "/proc/bus"}, + }, + } + + checkInspect := func(t *testing.T, ctx context.Context, name string, expected []string) { + _, b, err := client.ContainerInspectWithRaw(ctx, name, false) + assert.NilError(t, err) + + var inspectJSON map[string]interface{} + err = json.Unmarshal(b, &inspectJSON) + assert.NilError(t, err) + + cfg, ok := inspectJSON["HostConfig"].(map[string]interface{}) + assert.Check(t, is.Equal(true, ok), name) + + readonlyPaths, ok := cfg["ReadonlyPaths"].([]interface{}) + assert.Check(t, is.Equal(true, ok), name) + + rops := []string{} + for _, rop := range readonlyPaths { + rops = append(rops, rop.(string)) + } + assert.DeepEqual(t, expected, rops) + } + + for i, tc := range testCases { + name := fmt.Sprintf("create-readonly-paths-%d", i) + config := container.Config{ + Image: "busybox", + Cmd: []string{"true"}, + } + hc := container.HostConfig{} + if tc.readonlyPaths != nil { + hc.ReadonlyPaths = tc.readonlyPaths + } + + // Create the container. + c, err := client.ContainerCreate(context.Background(), + &config, + &hc, + &network.NetworkingConfig{}, + name, + ) + assert.NilError(t, err) + + checkInspect(t, ctx, name, tc.expected) + + // Start the container. + err = client.ContainerStart(ctx, c.ID, types.ContainerStartOptions{}) + assert.NilError(t, err) + + poll.WaitOn(t, ctr.IsInState(ctx, client, c.ID, "exited"), poll.WithDelay(100*time.Millisecond)) + + checkInspect(t, ctx, name, tc.expected) + } +}