Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { ListContainersItem } from '@microsoft/vscode-container-client';
import assert from 'assert';
import { getComposeProjectGroup, NonComposeGroupName } from '../../../tree/containers/ContainerProperties';

function makeContainer(labels: { [key: string]: string } | undefined): ListContainersItem {
return {
labels: labels,
} as ListContainersItem;
}

suite('(unit) ContainerProperties.getComposeProjectGroup', () => {
test('Groups compose containers by their compose project name', () => {
const container = makeContainer({ 'com.docker.compose.project': 'myproject' });
assert.strictEqual(getComposeProjectGroup(container), 'myproject');
});

test('Groups swarm stack containers by their stack namespace', () => {
const container = makeContainer({ 'com.docker.stack.namespace': 'mystack' });
assert.strictEqual(getComposeProjectGroup(container), 'mystack');
});

test('Prefers the compose project name when both labels are present', () => {
const container = makeContainer({
'com.docker.compose.project': 'myproject',
'com.docker.stack.namespace': 'mystack',
});
assert.strictEqual(getComposeProjectGroup(container), 'myproject');
});

test('Returns the non-compose group name for standalone containers', () => {
assert.strictEqual(getComposeProjectGroup(makeContainer({})), NonComposeGroupName);
assert.strictEqual(getComposeProjectGroup(makeContainer(undefined)), NonComposeGroupName);
assert.strictEqual(getComposeProjectGroup(makeContainer({ 'some.other.label': 'value' })), NonComposeGroupName);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export abstract class LocalGroupTreeItemBase<TItem extends AnyContainerObject, T
return this.parent.compareChildrenImpl(ti1, ti2);
}

protected get items(): TItem[] {
return this._items;
}

public get ChildTreeItems(): AzExtTreeItem[] {
if (!this._childTreeItems) {
this._childTreeItems = this.getChildTreeItems();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { ThemeIcon, TreeItemCollapsibleState } from "vscode";
import { LocalGroupTreeItemBase } from "../LocalGroupTreeItemBase";
import { LocalRootTreeItemBase } from "../LocalRootTreeItemBase";
import { getCommonGroupIcon } from "../settings/CommonProperties";
import { ContainerProperty, getContainerStateIcon, NonComposeGroupName } from "./ContainerProperties";
import { ContainerProperty, composeProjectLabel, getContainerStateIcon, NonComposeGroupName } from "./ContainerProperties";
import { DockerContainerInfo } from "./ContainersTreeItem";

export class ContainerGroupTreeItem extends LocalGroupTreeItemBase<DockerContainerInfo, ContainerProperty> {
Expand All @@ -26,13 +26,19 @@ export class ContainerGroupTreeItem extends LocalGroupTreeItemBase<DockerContain
}

public get contextValue(): string {
if (this.parent.groupBySetting === 'Compose Project Name' && this.group !== NonComposeGroupName) {
// The compose group commands (start/stop/restart/down/logs) rely on compose labels/files, which
// Docker Swarm stacks don't have. Only expose them for actual compose projects.
if (this.parent.groupBySetting === 'Compose Project Name' && this.group !== NonComposeGroupName && this.isComposeProjectGroup()) {
return 'containerGroup;composeGroup';
}

return 'containerGroup';
}

private isComposeProjectGroup(): boolean {
return this.items.some(item => !!item.labels?.[composeProjectLabel]);
}
Comment on lines +38 to +40

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit a4ae12a. Added a protected items getter to LocalGroupTreeItemBase and updated isComposeProjectGroup() to check this.items (the raw DockerContainerInfo data) directly, avoiding any ContainerTreeItem instantiation.


public get iconPath(): ThemeIcon {
switch (this.parent.groupBySetting) {
case 'ContainerId':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const containerProperties: ITreePropertyInfo<ContainerProperty>[] = [
{ property: 'Ports', exampleValue: '8080' },
{ property: 'State', exampleValue: 'exited' },
{ property: 'Status', exampleValue: 'Exited (0) 2 hours ago' },
{ property: 'Compose Project Name', description: l10n.t('Value used to associate containers launched by a compose up command') },
{ property: 'Compose Project Name', description: l10n.t('Value used to associate containers launched by a compose up command or a Docker Swarm stack') },
{ property: 'Label', exampleValue: 'com.microsoft.created-by=visual-studio-code' },
];

Expand Down Expand Up @@ -64,7 +64,7 @@ export function getContainerPropertyValue(item: ListContainersItem, property: Co
// This normalizes things like "10 seconds" and "Less than a second" to "Less than a minute", meaning the refreshes don't happen constantly
return item.status?.replace(/(\d+ seconds?)|(Less than a second)/i, l10n.t('Less than a minute'));
case 'Compose Project Name':
return getLabelGroup(item, 'com.docker.compose.project', NonComposeGroupName);
return getComposeProjectGroup(item);
case 'Image':
return item.image.originalName;
case 'Label':
Expand All @@ -77,6 +77,21 @@ export function getContainerPropertyValue(item: ListContainersItem, property: Co
export const NonComposeGroupName = l10n.t('Individual Containers');
export const NonLabelGroupName = l10n.t('Others');

// The label Docker Compose adds to containers it creates, containing the compose project name
export const composeProjectLabel = 'com.docker.compose.project';

// The label Docker Swarm adds to containers it creates as part of a stack, containing the stack namespace (name)
export const swarmStackNamespaceLabel = 'com.docker.stack.namespace';

export function getComposeProjectGroup(container: ListContainersItem): string {
// Containers launched by `docker compose up` are grouped by their compose project name.
// Containers launched by `docker stack deploy` (Docker Swarm) are grouped by their stack namespace.
// Either way, this gives a meaningful group name for orchestrated containers, falling back to the
// non-compose group name for standalone containers.
return getLabelGroup(container, composeProjectLabel, '')
|| getLabelGroup(container, swarmStackNamespaceLabel, NonComposeGroupName);
}

export function getLabelGroup(container: ListContainersItem, label: string | undefined, defaultGroupName: string): string {
if (!label) {
return defaultGroupName;
Expand Down