diff --git a/client/container_create.go b/client/container_create.go index 522a194c8c..94925d7b7f 100644 --- a/client/container_create.go +++ b/client/container_create.go @@ -5,6 +5,8 @@ import ( "encoding/json" "net/url" "path" + "sort" + "strings" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/network" @@ -52,6 +54,9 @@ func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config // When using API under 1.42, the Linux daemon doesn't respect the ConsoleSize hostConfig.ConsoleSize = [2]uint{0, 0} } + + hostConfig.CapAdd = normalizeCapabilities(hostConfig.CapAdd) + hostConfig.CapDrop = normalizeCapabilities(hostConfig.CapDrop) } // 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 } return false } + +// allCapabilities is a magic value for "all capabilities" +const allCapabilities = "ALL" + +// normalizeCapabilities normalizes capabilities to their canonical form, +// removes duplicates, and sorts the results. +// +// It is similar to [github.com/docker/docker/oci/caps.NormalizeLegacyCapabilities], +// but performs no validation based on supported capabilities. +func normalizeCapabilities(caps []string) []string { + var normalized []string + + unique := make(map[string]struct{}) + for _, c := range caps { + c = normalizeCap(c) + if _, ok := unique[c]; ok { + continue + } + unique[c] = struct{}{} + normalized = append(normalized, c) + } + + sort.Strings(normalized) + return normalized +} + +// normalizeCap normalizes a capability to its canonical format by upper-casing +// and adding a "CAP_" prefix (if not yet present). It also accepts the "ALL" +// magic-value. +func normalizeCap(cap string) string { + cap = strings.ToUpper(cap) + if cap == allCapabilities { + return cap + } + if !strings.HasPrefix(cap, "CAP_") { + cap = "CAP_" + cap + } + return cap +} diff --git a/client/container_create_test.go b/client/container_create_test.go index 46e8cf79e2..1a1ea62280 100644 --- a/client/container_create_test.go +++ b/client/container_create_test.go @@ -125,3 +125,52 @@ func TestContainerCreateConnectionError(t *testing.T) { _, err = client.ContainerCreate(context.Background(), nil, nil, nil, nil, "") assert.Check(t, is.ErrorType(err, IsErrConnectionFailed)) } + +// TestContainerCreateCapabilities verifies that CapAdd and CapDrop capabilities +// are normalized to their canonical form. +func TestContainerCreateCapabilities(t *testing.T) { + inputCaps := []string{ + "all", + "ALL", + "capability_b", + "capability_a", + "capability_c", + "CAPABILITY_D", + "CAP_CAPABILITY_D", + } + + expectedCaps := []string{ + "ALL", + "CAP_CAPABILITY_A", + "CAP_CAPABILITY_B", + "CAP_CAPABILITY_C", + "CAP_CAPABILITY_D", + } + + client := &Client{ + client: newMockClient(func(req *http.Request) (*http.Response, error) { + var config container.CreateRequest + + if err := json.NewDecoder(req.Body).Decode(&config); err != nil { + return nil, err + } + assert.Check(t, is.DeepEqual([]string(config.HostConfig.CapAdd), expectedCaps)) + assert.Check(t, is.DeepEqual([]string(config.HostConfig.CapDrop), expectedCaps)) + + b, err := json.Marshal(container.CreateResponse{ + ID: "container_id", + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader(b)), + }, nil + }), + version: "1.24", + } + + _, err := client.ContainerCreate(context.Background(), nil, &container.HostConfig{CapAdd: inputCaps, CapDrop: inputCaps}, nil, nil, "") + assert.NilError(t, err) +}