Browse code

client: ContainerCreate: normalize CapAdd, CapDrop capabilities

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>

Sebastiaan van Stijn authored on 2024/09/26 21:48:16
Showing 2 changed files
... ...
@@ -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
+}