diff --git a/daemon/container_linux.go b/daemon/container_linux.go index ccab94c99c..8c5b15be0a 100644 --- a/daemon/container_linux.go +++ b/daemon/container_linux.go @@ -297,8 +297,8 @@ func populateCommand(c *Container, env []string) error { Resources: resources, AllowedDevices: allowedDevices, AutoCreatedDevices: autoCreatedDevices, - CapAdd: c.hostConfig.CapAdd, - CapDrop: c.hostConfig.CapDrop, + CapAdd: c.hostConfig.CapAdd.Slice(), + CapDrop: c.hostConfig.CapDrop.Slice(), ProcessConfig: processConfig, ProcessLabel: c.GetProcessLabel(), MountLabel: c.GetMountLabel(), diff --git a/daemon/container_windows.go b/daemon/container_windows.go index bed2d52010..8b8ef3e040 100644 --- a/daemon/container_windows.go +++ b/daemon/container_windows.go @@ -111,8 +111,8 @@ func populateCommand(c *Container, env []string) error { Network: en, Pid: pid, Resources: resources, - CapAdd: c.hostConfig.CapAdd, - CapDrop: c.hostConfig.CapDrop, + CapAdd: c.hostConfig.CapAdd.Slice(), + CapDrop: c.hostConfig.CapDrop.Slice(), ProcessConfig: processConfig, ProcessLabel: c.GetProcessLabel(), MountLabel: c.GetMountLabel(), diff --git a/integration-cli/docker_api_containers_test.go b/integration-cli/docker_api_containers_test.go index 9581221eec..56fba690b5 100644 --- a/integration-cli/docker_api_containers_test.go +++ b/integration-cli/docker_api_containers_test.go @@ -1705,3 +1705,24 @@ func (s *DockerSuite) TestPostContainersCreateWithStringOrSliceCmd(c *check.C) { out, _ = dockerCmd(c, "start", "-a", "echotest2") c.Assert(strings.TrimSpace(out), check.Equals, "hello world") } + +// regression #14318 +func (s *DockerSuite) TestPostContainersCreateWithStringOrSliceCapAddDrop(c *check.C) { + config := struct { + Image string + CapAdd string + CapDrop string + }{"busybox", "NET_ADMIN", "SYS_ADMIN"} + status, _, err := sockRequest("POST", "/containers/create?name=capaddtest0", config) + c.Assert(err, check.IsNil) + c.Assert(status, check.Equals, http.StatusCreated) + + config2 := struct { + Image string + CapAdd []string + CapDrop []string + }{"busybox", []string{"NET_ADMIN", "SYS_ADMIN"}, []string{"SETGID"}} + status, _, err = sockRequest("POST", "/containers/create?name=capaddtest1", config2) + c.Assert(err, check.IsNil) + c.Assert(status, check.Equals, http.StatusCreated) +} diff --git a/runconfig/hostconfig.go b/runconfig/hostconfig.go index 8223ef6067..7972fd9309 100644 --- a/runconfig/hostconfig.go +++ b/runconfig/hostconfig.go @@ -173,6 +173,53 @@ func NewLxcConfig(values []KeyValuePair) *LxcConfig { return &LxcConfig{values} } +type CapList struct { + caps []string +} + +func (c *CapList) MarshalJSON() ([]byte, error) { + if c == nil { + return []byte{}, nil + } + return json.Marshal(c.Slice()) +} + +func (c *CapList) UnmarshalJSON(b []byte) error { + if len(b) == 0 { + return nil + } + + var caps []string + if err := json.Unmarshal(b, &caps); err != nil { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + caps = append(caps, s) + } + c.caps = caps + + return nil +} + +func (c *CapList) Len() int { + if c == nil { + return 0 + } + return len(c.caps) +} + +func (c *CapList) Slice() []string { + if c == nil { + return nil + } + return c.caps +} + +func NewCapList(caps []string) *CapList { + return &CapList{caps} +} + type HostConfig struct { Binds []string ContainerIDFile string @@ -199,8 +246,8 @@ type HostConfig struct { IpcMode IpcMode PidMode PidMode UTSMode UTSMode - CapAdd []string - CapDrop []string + CapAdd *CapList + CapDrop *CapList RestartPolicy RestartPolicy SecurityOpt []string ReadonlyRootfs bool diff --git a/runconfig/hostconfig_test.go b/runconfig/hostconfig_test.go index 7aedfd06a1..ab81220d5f 100644 --- a/runconfig/hostconfig_test.go +++ b/runconfig/hostconfig_test.go @@ -2,6 +2,7 @@ package runconfig import ( "bytes" + "encoding/json" "fmt" "io/ioutil" "testing" @@ -254,12 +255,49 @@ func TestDecodeHostConfig(t *testing.T) { t.Fatalf("Expected 1 bind, found %v\n", c.Binds) } - if len(c.CapAdd) != 1 && c.CapAdd[0] != "NET_ADMIN" { + if c.CapAdd.Len() != 1 && c.CapAdd.Slice()[0] != "NET_ADMIN" { t.Fatalf("Expected CapAdd NET_ADMIN, got %v", c.CapAdd) } - if len(c.CapDrop) != 1 && c.CapDrop[0] != "NET_ADMIN" { + if c.CapDrop.Len() != 1 && c.CapDrop.Slice()[0] != "NET_ADMIN" { t.Fatalf("Expected CapDrop MKNOD, got %v", c.CapDrop) } } } + +func TestCapListUnmarshalSliceAndString(t *testing.T) { + var cl *CapList + cap0, err := json.Marshal([]string{"CAP_SOMETHING"}) + if err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(cap0, &cl); err != nil { + t.Fatal(err) + } + + slice := cl.Slice() + if len(slice) != 1 { + t.Fatalf("expected 1 element after unmarshal: %q", slice) + } + + if slice[0] != "CAP_SOMETHING" { + t.Fatalf("expected `CAP_SOMETHING`, got: %q", slice[0]) + } + + cap1, err := json.Marshal("CAP_SOMETHING") + if err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(cap1, &cl); err != nil { + t.Fatal(err) + } + + slice = cl.Slice() + if len(slice) != 1 { + t.Fatalf("expected 1 element after unmarshal: %q", slice) + } + + if slice[0] != "CAP_SOMETHING" { + t.Fatalf("expected `CAP_SOMETHING`, got: %q", slice[0]) + } +} diff --git a/runconfig/parse.go b/runconfig/parse.go index 5798e900a4..552a648330 100644 --- a/runconfig/parse.go +++ b/runconfig/parse.go @@ -353,8 +353,8 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe PidMode: pidMode, UTSMode: utsMode, Devices: deviceMappings, - CapAdd: flCapAdd.GetAll(), - CapDrop: flCapDrop.GetAll(), + CapAdd: NewCapList(flCapAdd.GetAll()), + CapDrop: NewCapList(flCapDrop.GetAll()), RestartPolicy: restartPolicy, SecurityOpt: flSecurityOpt.GetAll(), ReadonlyRootfs: *flReadonlyRootfs,