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>
This commit is contained in:
Sebastiaan van Stijn
2024-09-26 14:48:16 +02:00
parent b9cd744f99
commit 5bdbc2f026
2 changed files with 93 additions and 0 deletions

View File

@@ -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
}

View File

@@ -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)
}