Before this change, capabilities would be sent un-normalized, un-sorted,
and could contain duplicates;
docker create --name foo --cap-add SYS_ADMIN --cap-add sys_admin --cap-add cap_sys_admin --cap-add ALL busybox
docker container inspect --format '{{json .HostConfig.CapAdd }}' foo
["SYS_ADMIN","sys_admin","cap_sys_admin","ALL"]
After this change, capabilities are sent in their normalized form, sorted,
and with duplicates removed;
docker create --name foo --cap-add SYS_ADMIN --cap-add sys_admin --cap-add cap_sys_admin --cap-add ALL busybox
docker container inspect --format '{{json .HostConfig.CapAdd }}' foo
["ALL", "CAP_SYS_ADMIN"]
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
| ... | ... |
@@ -5,6 +5,8 @@ import ( |
| 5 | 5 |
"encoding/json" |
| 6 | 6 |
"net/url" |
| 7 | 7 |
"path" |
| 8 |
+ "sort" |
|
| 9 |
+ "strings" |
|
| 8 | 10 |
|
| 9 | 11 |
"github.com/docker/docker/api/types/container" |
| 10 | 12 |
"github.com/docker/docker/api/types/network" |
| ... | ... |
@@ -52,6 +54,9 @@ func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config |
| 52 | 52 |
// When using API under 1.42, the Linux daemon doesn't respect the ConsoleSize |
| 53 | 53 |
hostConfig.ConsoleSize = [2]uint{0, 0}
|
| 54 | 54 |
} |
| 55 |
+ |
|
| 56 |
+ hostConfig.CapAdd = normalizeCapabilities(hostConfig.CapAdd) |
|
| 57 |
+ hostConfig.CapDrop = normalizeCapabilities(hostConfig.CapDrop) |
|
| 55 | 58 |
} |
| 56 | 59 |
|
| 57 | 60 |
// Since API 1.44, the container-wide MacAddress is deprecated and will trigger a WARNING if it's specified. |
| ... | ... |
@@ -108,3 +113,42 @@ func hasEndpointSpecificMacAddress(networkingConfig *network.NetworkingConfig) b |
| 108 | 108 |
} |
| 109 | 109 |
return false |
| 110 | 110 |
} |
| 111 |
+ |
|
| 112 |
+// allCapabilities is a magic value for "all capabilities" |
|
| 113 |
+const allCapabilities = "ALL" |
|
| 114 |
+ |
|
| 115 |
+// normalizeCapabilities normalizes capabilities to their canonical form, |
|
| 116 |
+// removes duplicates, and sorts the results. |
|
| 117 |
+// |
|
| 118 |
+// It is similar to [github.com/docker/docker/oci/caps.NormalizeLegacyCapabilities], |
|
| 119 |
+// but performs no validation based on supported capabilities. |
|
| 120 |
+func normalizeCapabilities(caps []string) []string {
|
|
| 121 |
+ var normalized []string |
|
| 122 |
+ |
|
| 123 |
+ unique := make(map[string]struct{})
|
|
| 124 |
+ for _, c := range caps {
|
|
| 125 |
+ c = normalizeCap(c) |
|
| 126 |
+ if _, ok := unique[c]; ok {
|
|
| 127 |
+ continue |
|
| 128 |
+ } |
|
| 129 |
+ unique[c] = struct{}{}
|
|
| 130 |
+ normalized = append(normalized, c) |
|
| 131 |
+ } |
|
| 132 |
+ |
|
| 133 |
+ sort.Strings(normalized) |
|
| 134 |
+ return normalized |
|
| 135 |
+} |
|
| 136 |
+ |
|
| 137 |
+// normalizeCap normalizes a capability to its canonical format by upper-casing |
|
| 138 |
+// and adding a "CAP_" prefix (if not yet present). It also accepts the "ALL" |
|
| 139 |
+// magic-value. |
|
| 140 |
+func normalizeCap(cap string) string {
|
|
| 141 |
+ cap = strings.ToUpper(cap) |
|
| 142 |
+ if cap == allCapabilities {
|
|
| 143 |
+ return cap |
|
| 144 |
+ } |
|
| 145 |
+ if !strings.HasPrefix(cap, "CAP_") {
|
|
| 146 |
+ cap = "CAP_" + cap |
|
| 147 |
+ } |
|
| 148 |
+ return cap |
|
| 149 |
+} |
| ... | ... |
@@ -125,3 +125,52 @@ func TestContainerCreateConnectionError(t *testing.T) {
|
| 125 | 125 |
_, err = client.ContainerCreate(context.Background(), nil, nil, nil, nil, "") |
| 126 | 126 |
assert.Check(t, is.ErrorType(err, IsErrConnectionFailed)) |
| 127 | 127 |
} |
| 128 |
+ |
|
| 129 |
+// TestContainerCreateCapabilities verifies that CapAdd and CapDrop capabilities |
|
| 130 |
+// are normalized to their canonical form. |
|
| 131 |
+func TestContainerCreateCapabilities(t *testing.T) {
|
|
| 132 |
+ inputCaps := []string{
|
|
| 133 |
+ "all", |
|
| 134 |
+ "ALL", |
|
| 135 |
+ "capability_b", |
|
| 136 |
+ "capability_a", |
|
| 137 |
+ "capability_c", |
|
| 138 |
+ "CAPABILITY_D", |
|
| 139 |
+ "CAP_CAPABILITY_D", |
|
| 140 |
+ } |
|
| 141 |
+ |
|
| 142 |
+ expectedCaps := []string{
|
|
| 143 |
+ "ALL", |
|
| 144 |
+ "CAP_CAPABILITY_A", |
|
| 145 |
+ "CAP_CAPABILITY_B", |
|
| 146 |
+ "CAP_CAPABILITY_C", |
|
| 147 |
+ "CAP_CAPABILITY_D", |
|
| 148 |
+ } |
|
| 149 |
+ |
|
| 150 |
+ client := &Client{
|
|
| 151 |
+ client: newMockClient(func(req *http.Request) (*http.Response, error) {
|
|
| 152 |
+ var config container.CreateRequest |
|
| 153 |
+ |
|
| 154 |
+ if err := json.NewDecoder(req.Body).Decode(&config); err != nil {
|
|
| 155 |
+ return nil, err |
|
| 156 |
+ } |
|
| 157 |
+ assert.Check(t, is.DeepEqual([]string(config.HostConfig.CapAdd), expectedCaps)) |
|
| 158 |
+ assert.Check(t, is.DeepEqual([]string(config.HostConfig.CapDrop), expectedCaps)) |
|
| 159 |
+ |
|
| 160 |
+ b, err := json.Marshal(container.CreateResponse{
|
|
| 161 |
+ ID: "container_id", |
|
| 162 |
+ }) |
|
| 163 |
+ if err != nil {
|
|
| 164 |
+ return nil, err |
|
| 165 |
+ } |
|
| 166 |
+ return &http.Response{
|
|
| 167 |
+ StatusCode: http.StatusOK, |
|
| 168 |
+ Body: io.NopCloser(bytes.NewReader(b)), |
|
| 169 |
+ }, nil |
|
| 170 |
+ }), |
|
| 171 |
+ version: "1.24", |
|
| 172 |
+ } |
|
| 173 |
+ |
|
| 174 |
+ _, err := client.ContainerCreate(context.Background(), nil, &container.HostConfig{CapAdd: inputCaps, CapDrop: inputCaps}, nil, nil, "")
|
|
| 175 |
+ assert.NilError(t, err) |
|
| 176 |
+} |