Browse code

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 <acidburn@microsoft.com>

Jess Frazelle authored on 2018/03/21 02:29:18
Showing 5 changed files
... ...
@@ -772,6 +772,16 @@ definitions:
772 772
               - "default"
773 773
               - "process"
774 774
               - "hyperv"
775
+          MaskedPaths:
776
+            type: "array"
777
+            description: "The list of paths to be masked inside the container (this overrides the default set of paths)"
778
+            items:
779
+              type: "string"
780
+          ReadonlyPaths:
781
+            type: "array"
782
+            description: "The list of paths to be set as read-only inside the container (this overrides the default set of paths)"
783
+            items:
784
+              type: "string"
775 785
 
776 786
   ContainerConfig:
777 787
     description: "Configuration for a container that is portable between hosts"
... ...
@@ -401,6 +401,12 @@ type HostConfig struct {
401 401
 	// Mounts specs used by the container
402 402
 	Mounts []mount.Mount `json:",omitempty"`
403 403
 
404
+	// MaskedPaths is the list of paths to be masked inside the container (this overrides the default set of paths)
405
+	MaskedPaths []string
406
+
407
+	// ReadonlyPaths is the list of paths to be set as read-only inside the container (this overrides the default set of paths)
408
+	ReadonlyPaths []string
409
+
404 410
 	// Run a custom init inside the container, if null, use the daemon's configured settings
405 411
 	Init *bool `json:",omitempty"`
406 412
 }
... ...
@@ -11,6 +11,7 @@ import (
11 11
 	containertypes "github.com/docker/docker/api/types/container"
12 12
 	mounttypes "github.com/docker/docker/api/types/mount"
13 13
 	"github.com/docker/docker/container"
14
+	"github.com/docker/docker/oci"
14 15
 	"github.com/docker/docker/pkg/stringid"
15 16
 	volumeopts "github.com/docker/docker/volume/service/opts"
16 17
 	"github.com/opencontainers/selinux/go-selinux/label"
... ...
@@ -29,6 +30,16 @@ func (daemon *Daemon) createContainerOSSpecificSettings(container *container.Con
29 29
 		return err
30 30
 	}
31 31
 
32
+	// Set the default masked and readonly paths with regard to the host config options if they are not set.
33
+	if hostConfig.MaskedPaths == nil && !hostConfig.Privileged {
34
+		hostConfig.MaskedPaths = oci.DefaultSpec().Linux.MaskedPaths // Set it to the default if nil
35
+		container.HostConfig.MaskedPaths = hostConfig.MaskedPaths
36
+	}
37
+	if hostConfig.ReadonlyPaths == nil && !hostConfig.Privileged {
38
+		hostConfig.ReadonlyPaths = oci.DefaultSpec().Linux.ReadonlyPaths // Set it to the default if nil
39
+		container.HostConfig.ReadonlyPaths = hostConfig.ReadonlyPaths
40
+	}
41
+
32 42
 	for spec := range config.Volumes {
33 43
 		name := stringid.GenerateNonCryptoID()
34 44
 		destination := filepath.Clean(spec)
... ...
@@ -903,6 +903,14 @@ func (daemon *Daemon) createSpec(c *container.Container) (retSpec *specs.Spec, e
903 903
 	s.Process.OOMScoreAdj = &c.HostConfig.OomScoreAdj
904 904
 	s.Linux.MountLabel = c.MountLabel
905 905
 
906
+	// Set the masked and readonly paths with regard to the host config options if they are set.
907
+	if c.HostConfig.MaskedPaths != nil {
908
+		s.Linux.MaskedPaths = c.HostConfig.MaskedPaths
909
+	}
910
+	if c.HostConfig.ReadonlyPaths != nil {
911
+		s.Linux.ReadonlyPaths = c.HostConfig.ReadonlyPaths
912
+	}
913
+
906 914
 	return &s, nil
907 915
 }
908 916
 
... ...
@@ -2,14 +2,21 @@ package container // import "github.com/docker/docker/integration/container"
2 2
 
3 3
 import (
4 4
 	"context"
5
+	"encoding/json"
6
+	"fmt"
5 7
 	"strconv"
6 8
 	"testing"
9
+	"time"
7 10
 
11
+	"github.com/docker/docker/api/types"
8 12
 	"github.com/docker/docker/api/types/container"
9 13
 	"github.com/docker/docker/api/types/network"
14
+	ctr "github.com/docker/docker/integration/internal/container"
10 15
 	"github.com/docker/docker/internal/test/request"
16
+	"github.com/docker/docker/oci"
11 17
 	"github.com/gotestyourself/gotestyourself/assert"
12 18
 	is "github.com/gotestyourself/gotestyourself/assert/cmp"
19
+	"github.com/gotestyourself/gotestyourself/poll"
13 20
 	"github.com/gotestyourself/gotestyourself/skip"
14 21
 )
15 22
 
... ...
@@ -137,3 +144,160 @@ func TestCreateTmpfsMountsTarget(t *testing.T) {
137 137
 		assert.Check(t, is.ErrorContains(err, tc.expectedError))
138 138
 	}
139 139
 }
140
+func TestCreateWithCustomMaskedPaths(t *testing.T) {
141
+	skip.If(t, testEnv.DaemonInfo.OSType != "linux")
142
+
143
+	defer setupTest(t)()
144
+	client := request.NewAPIClient(t)
145
+	ctx := context.Background()
146
+
147
+	testCases := []struct {
148
+		maskedPaths []string
149
+		expected    []string
150
+	}{
151
+		{
152
+			maskedPaths: []string{},
153
+			expected:    []string{},
154
+		},
155
+		{
156
+			maskedPaths: nil,
157
+			expected:    oci.DefaultSpec().Linux.MaskedPaths,
158
+		},
159
+		{
160
+			maskedPaths: []string{"/proc/kcore", "/proc/keys"},
161
+			expected:    []string{"/proc/kcore", "/proc/keys"},
162
+		},
163
+	}
164
+
165
+	checkInspect := func(t *testing.T, ctx context.Context, name string, expected []string) {
166
+		_, b, err := client.ContainerInspectWithRaw(ctx, name, false)
167
+		assert.NilError(t, err)
168
+
169
+		var inspectJSON map[string]interface{}
170
+		err = json.Unmarshal(b, &inspectJSON)
171
+		assert.NilError(t, err)
172
+
173
+		cfg, ok := inspectJSON["HostConfig"].(map[string]interface{})
174
+		assert.Check(t, is.Equal(true, ok), name)
175
+
176
+		maskedPaths, ok := cfg["MaskedPaths"].([]interface{})
177
+		assert.Check(t, is.Equal(true, ok), name)
178
+
179
+		mps := []string{}
180
+		for _, mp := range maskedPaths {
181
+			mps = append(mps, mp.(string))
182
+		}
183
+
184
+		assert.DeepEqual(t, expected, mps)
185
+	}
186
+
187
+	for i, tc := range testCases {
188
+		name := fmt.Sprintf("create-masked-paths-%d", i)
189
+		config := container.Config{
190
+			Image: "busybox",
191
+			Cmd:   []string{"true"},
192
+		}
193
+		hc := container.HostConfig{}
194
+		if tc.maskedPaths != nil {
195
+			hc.MaskedPaths = tc.maskedPaths
196
+		}
197
+
198
+		// Create the container.
199
+		c, err := client.ContainerCreate(context.Background(),
200
+			&config,
201
+			&hc,
202
+			&network.NetworkingConfig{},
203
+			name,
204
+		)
205
+		assert.NilError(t, err)
206
+
207
+		checkInspect(t, ctx, name, tc.expected)
208
+
209
+		// Start the container.
210
+		err = client.ContainerStart(ctx, c.ID, types.ContainerStartOptions{})
211
+		assert.NilError(t, err)
212
+
213
+		poll.WaitOn(t, ctr.IsInState(ctx, client, c.ID, "exited"), poll.WithDelay(100*time.Millisecond))
214
+
215
+		checkInspect(t, ctx, name, tc.expected)
216
+	}
217
+}
218
+
219
+func TestCreateWithCustomReadonlyPaths(t *testing.T) {
220
+	skip.If(t, testEnv.DaemonInfo.OSType != "linux")
221
+
222
+	defer setupTest(t)()
223
+	client := request.NewAPIClient(t)
224
+	ctx := context.Background()
225
+
226
+	testCases := []struct {
227
+		doc           string
228
+		readonlyPaths []string
229
+		expected      []string
230
+	}{
231
+		{
232
+			readonlyPaths: []string{},
233
+			expected:      []string{},
234
+		},
235
+		{
236
+			readonlyPaths: nil,
237
+			expected:      oci.DefaultSpec().Linux.ReadonlyPaths,
238
+		},
239
+		{
240
+			readonlyPaths: []string{"/proc/asound", "/proc/bus"},
241
+			expected:      []string{"/proc/asound", "/proc/bus"},
242
+		},
243
+	}
244
+
245
+	checkInspect := func(t *testing.T, ctx context.Context, name string, expected []string) {
246
+		_, b, err := client.ContainerInspectWithRaw(ctx, name, false)
247
+		assert.NilError(t, err)
248
+
249
+		var inspectJSON map[string]interface{}
250
+		err = json.Unmarshal(b, &inspectJSON)
251
+		assert.NilError(t, err)
252
+
253
+		cfg, ok := inspectJSON["HostConfig"].(map[string]interface{})
254
+		assert.Check(t, is.Equal(true, ok), name)
255
+
256
+		readonlyPaths, ok := cfg["ReadonlyPaths"].([]interface{})
257
+		assert.Check(t, is.Equal(true, ok), name)
258
+
259
+		rops := []string{}
260
+		for _, rop := range readonlyPaths {
261
+			rops = append(rops, rop.(string))
262
+		}
263
+		assert.DeepEqual(t, expected, rops)
264
+	}
265
+
266
+	for i, tc := range testCases {
267
+		name := fmt.Sprintf("create-readonly-paths-%d", i)
268
+		config := container.Config{
269
+			Image: "busybox",
270
+			Cmd:   []string{"true"},
271
+		}
272
+		hc := container.HostConfig{}
273
+		if tc.readonlyPaths != nil {
274
+			hc.ReadonlyPaths = tc.readonlyPaths
275
+		}
276
+
277
+		// Create the container.
278
+		c, err := client.ContainerCreate(context.Background(),
279
+			&config,
280
+			&hc,
281
+			&network.NetworkingConfig{},
282
+			name,
283
+		)
284
+		assert.NilError(t, err)
285
+
286
+		checkInspect(t, ctx, name, tc.expected)
287
+
288
+		// Start the container.
289
+		err = client.ContainerStart(ctx, c.ID, types.ContainerStartOptions{})
290
+		assert.NilError(t, err)
291
+
292
+		poll.WaitOn(t, ctr.IsInState(ctx, client, c.ID, "exited"), poll.WithDelay(100*time.Millisecond))
293
+
294
+		checkInspect(t, ctx, name, tc.expected)
295
+	}
296
+}