Skip to content

parseDockerLikeLabels corrupts label values containing commas (e.g. compose config_files with multiple files) #380

Description

@bwateratmsft

Package

@microsoft/vscode-container-client

Summary

parseDockerLikeLabels corrupts any label value that contains a comma, because it splits the entire raw label string on ,. The most common real-world victim is com.docker.compose.project.config_files for a compose project started with multiple -f files, where the value is itself a comma-separated list of paths.

File: packages/vscode-container-client/src/clients/DockerClientBase/parseDockerLikeLabels.ts

export function parseDockerLikeLabels(rawLabels: string): Labels {
    return rawLabels.split(',').reduce((labels, labelPair) => {
        const index = labelPair.indexOf('=');
        labels[labelPair.substring(0, index)] = labelPair.substring(index + 1);
        return labels;
    }, {} as Labels);
}

docker container ls --format '{{json .}}' (and the equivalent ls commands for networks/volumes) return the Labels field as a single comma-joined key=value,key=value string with no escaping of commas inside values. So for a container with:

com.docker.compose.project.config_files=/abs/path/docker-compose.base.yml,/abs/path/docker-compose.local.yml

the raw Labels string contains ...config_files=/abs/path/docker-compose.base.yml,/abs/path/docker-compose.local.yml,.... Splitting on , yields the fragments:

  • com.docker.compose.project.config_files=/abs/path/docker-compose.base.yml → key set correctly, value = only the first file
  • /abs/path/docker-compose.local.yml → has no =, so indexOf('=') returns -1; substring(0, -1) is '' and substring(0) is the whole fragment. This writes a bogus labels[''] = '/abs/path/docker-compose.local.yml' and, worse, silently drops the second file from the intended label.

Net effect: the config_files label value is truncated to the first file.

Downstream impact

microsoft/vscode-containers#522 — "Compose Start / Compose Stop only use the first compose file when multiple files are in com.docker.compose.project.config_files". The extension reads the (truncated) list label and only passes the first --file to docker compose start/stop, so services defined only in later compose files are never started/stopped.

That extension is adding a workaround (inspecting the container, whose Config.Labels is a proper JSON map, to recover the accurate value), but the underlying ls-based parser is lossy for every consumer.

Steps to reproduce

parseDockerLikeLabels('com.docker.compose.project.config_files=/a/base.yml,/a/local.yml,com.docker.compose.project=demo')

Actual:

{
  'com.docker.compose.project.config_files': '/a/base.yml',
  '': '/a/local.yml',
  'com.docker.compose.project': 'demo'
}

Expected:

{
  'com.docker.compose.project.config_files': '/a/base.yml,/a/local.yml',
  'com.docker.compose.project': 'demo'
}

Proposed fix

Treat a fragment with no = as a continuation of the previous label's value (re-joining the comma that split removed), rather than as a new label:

export function parseDockerLikeLabels(rawLabels: string): Labels {
    const labels: Labels = {};
    let lastKey: string | undefined;

    for (const fragment of rawLabels.split(',')) {
        const index = fragment.indexOf('=');

        if (index < 0) {
            // No '=' means this fragment is a continuation of the previous label's
            // value, which itself contained a comma (e.g. multiple compose config
            // files in `com.docker.compose.project.config_files`). `docker ... ls`
            // joins labels with commas and does not escape commas inside values, so
            // we stitch the value back together here.
            if (lastKey !== undefined) {
                labels[lastKey] += `,${fragment}`;
            }
            continue;
        }

        lastKey = fragment.substring(0, index);
        labels[lastKey] = fragment.substring(index + 1);
    }

    return labels;
}

Known limitation

This is a heuristic. It fully recovers values that contain commas (the overwhelmingly common case, including config_files), but a value containing both a comma and an = (e.g. key=a,b=c) would still be misparsed, because the b=c fragment is indistinguishable from a new label in the flattened ls output. The only fully robust source is docker inspect (proper JSON label map), so consumers that need exactness for comma-bearing values should prefer inspect. Happy to open a PR with the above change plus a unit test if it looks good.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions