Skip to content

Fix DockerCompose@1 task to support --profile global flag correctly #21197

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
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
83 changes: 83 additions & 0 deletions Tasks/DockerComposeV1/Tests/DockerComposeUtilsTests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import * as assert from 'assert';
import * as DockerComposeUtils from '../dockercomposeutils';

describe('DockerComposeUtils Tests', function() {
describe('parseComposeArguments', function() {
it('should parse --profile flag as global argument', function() {
const result = DockerComposeUtils.parseComposeArguments('--profile build');
assert.deepEqual(result.globalArgs, ['--profile', 'build']);
assert.deepEqual(result.commandArgs, []);
});

it('should parse --profile=value flag as global argument', function() {
const result = DockerComposeUtils.parseComposeArguments('--profile=build');
assert.deepEqual(result.globalArgs, ['--profile=build']);
assert.deepEqual(result.commandArgs, []);
});

it('should separate global and command flags correctly', function() {
const result = DockerComposeUtils.parseComposeArguments('--profile build --no-cache');
assert.deepEqual(result.globalArgs, ['--profile', 'build']);
assert.deepEqual(result.commandArgs, ['--no-cache']);
});

it('should handle multiple global flags', function() {
const result = DockerComposeUtils.parseComposeArguments('--profile build --parallel 4 --no-cache');
assert.deepEqual(result.globalArgs, ['--profile', 'build', '--parallel', '4']);
assert.deepEqual(result.commandArgs, ['--no-cache']);
});

it('should handle order independence', function() {
const result = DockerComposeUtils.parseComposeArguments('--no-cache --profile build --parallel 2');
assert.deepEqual(result.globalArgs, ['--profile', 'build', '--parallel', '2']);
assert.deepEqual(result.commandArgs, ['--no-cache']);
});

it('should handle command-only arguments', function() {
const result = DockerComposeUtils.parseComposeArguments('--pull --compress');
assert.deepEqual(result.globalArgs, []);
assert.deepEqual(result.commandArgs, ['--pull', '--compress']);
});

it('should handle empty input', function() {
const result = DockerComposeUtils.parseComposeArguments('');
assert.deepEqual(result.globalArgs, []);
assert.deepEqual(result.commandArgs, []);
});

it('should handle null/undefined input', function() {
const result1 = DockerComposeUtils.parseComposeArguments(null as any);
assert.deepEqual(result1.globalArgs, []);
assert.deepEqual(result1.commandArgs, []);

const result2 = DockerComposeUtils.parseComposeArguments(undefined as any);
assert.deepEqual(result2.globalArgs, []);
assert.deepEqual(result2.commandArgs, []);
});

it('should handle quoted arguments', function() {
const result = DockerComposeUtils.parseComposeArguments('--profile "my profile" --no-cache');
assert.deepEqual(result.globalArgs, ['--profile', 'my profile']);
assert.deepEqual(result.commandArgs, ['--no-cache']);
});

it('should handle all supported global flags', function() {
const globalFlags = [
'--profile test',
'--ansi auto',
'--compatibility',
'--dry-run',
'--env-file .env',
'--parallel 2',
'--progress plain',
'--project-directory /path'
];

globalFlags.forEach(flag => {
const result = DockerComposeUtils.parseComposeArguments(flag + ' --build');
assert(result.globalArgs.length > 0, `${flag} should be parsed as global flag`);
assert.deepEqual(result.commandArgs, ['--build']);
});
});
});
});
28 changes: 28 additions & 0 deletions Tasks/DockerComposeV1/Tests/L0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,20 @@ describe('Docker Compose Suite', function() {
assert(tr.succeeded, 'task should have succeeded');
assert(tr.stdout.indexOf("[command]" + composeCommand + " -f F:\\dir2\\docker-compose.yml build --pull --parallel") != -1, "docker compose build should run with argumentss");
});

it('Runs successfully for windows docker compose service build with --profile global flag', async () => {
let tp = path.join(__dirname, 'L0Windows.js');
let tr : ttm.MockTestRunner = new ttm.MockTestRunner(tp);
process.env["__command__"] = "Build services";
process.env["__arguments__"] = "--profile build --no-cache";

await tr.runAsync();

assert(tr.invokedToolCount == 1, 'should have invoked tool one times. actual: ' + tr.invokedToolCount);
assert(tr.stderr.length == 0 || tr.errorIssues.length, 'should not have written to stderr');
assert(tr.succeeded, 'task should have succeeded');
assert(tr.stdout.indexOf("[command]" + composeCommand + " --profile build -f F:\\dir2\\docker-compose.yml build --no-cache") != -1, "docker compose build should run with --profile as global flag");
});
} else {
it('Runs successfully for linux docker compose service build', async () => {
let tp = path.join(__dirname, 'L0Linux.js');
Expand Down Expand Up @@ -304,6 +318,20 @@ describe('Docker Compose Suite', function() {
assert(tr.stdout.indexOf("[command]" + composeCommand + " -f /tmp/tempdir/100/docker-compose.yml build --pull --parallel") != -1, "docker compose build should run with argumentss");
});

it('Runs successfully for linux docker compose service build with --profile global flag', async () => {
let tp = path.join(__dirname, 'L0Linux.js');
let tr : ttm.MockTestRunner = new ttm.MockTestRunner(tp);
process.env["__command__"] = "Build services";
process.env["__arguments__"] = "--profile build --no-cache";

await tr.runAsync();

assert(tr.invokedToolCount == 1, 'should have invoked tool one times. actual: ' + tr.invokedToolCount);
assert(tr.stderr.length == 0 || tr.errorIssues.length, 'should not have written to stderr');
assert(tr.succeeded, 'task should have succeeded');
assert(tr.stdout.indexOf("[command]" + composeCommand + " --profile build -f /tmp/tempdir/100/docker-compose.yml build --no-cache") != -1, "docker compose build should run with --profile as global flag");
});

it('Runs successfully for linux docker compose command with arguments', async () => {
let tp = path.join(__dirname, 'L0Linux.js');
let tr : ttm.MockTestRunner = new ttm.MockTestRunner(tp);
Expand Down
16 changes: 12 additions & 4 deletions Tasks/DockerComposeV1/dockercomposebuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as sourceUtils from "azure-pipelines-tasks-docker-common/sourceutils";
import * as imageUtils from "azure-pipelines-tasks-docker-common/containerimageutils";
import * as dockerCommandUtils from "azure-pipelines-tasks-docker-common/dockercommandutils";
import * as utils from "./utils";
import * as DockerComposeUtils from "./dockercomposeutils";

function dockerTag(connection: DockerComposeConnection, source: string, target: string, outputUpdate: (output: any) => any) {
var command = connection.createCommand();
Expand Down Expand Up @@ -63,11 +64,18 @@ function addOtherTags(connection: DockerComposeConnection, imageName: string, ou
}

export function run(connection: DockerComposeConnection, outputUpdate: (data: string) => any): any {
var command = connection.createComposeCommand();
command.arg("build");
var arg = tl.getInput("arguments", false);
var commandArgs = dockerCommandUtils.getCommandArguments(arg || "");
command.line(commandArgs || "");
var parsedArgs = DockerComposeUtils.parseComposeArguments(arg || "");

var command = connection.createComposeCommand(parsedArgs.globalArgs);
command.arg("build");

// Add command-specific arguments
if (parsedArgs.commandArgs.length > 0) {
parsedArgs.commandArgs.forEach(cmdArg => {
command.arg(cmdArg);
});
}

return connection.execCommandWithLogging(command)
.then((output) => outputUpdate(utils.writeTaskOutput("build", output)))
Expand Down
15 changes: 11 additions & 4 deletions Tasks/DockerComposeV1/dockercomposecommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@ import * as tl from "azure-pipelines-task-lib/task";
import DockerComposeConnection from "./dockercomposeconnection";
import * as utils from "./utils";
import * as dockerCommandUtils from "azure-pipelines-tasks-docker-common/dockercommandutils";
import * as DockerComposeUtils from "./dockercomposeutils";

export function run(connection: DockerComposeConnection, outputUpdate: (data: string) => any): any {
var command = connection.createComposeCommand();
var args = tl.getInput("arguments", false);
var parsedArgs = DockerComposeUtils.parseComposeArguments(args || "");

var command = connection.createComposeCommand(parsedArgs.globalArgs);
command.line(tl.getInput("dockerComposeCommand", true));

var args = tl.getInput("arguments", false);
var commandArgs = dockerCommandUtils.getCommandArguments(args || "");
command.line(commandArgs || "");
// Add command-specific arguments
if (parsedArgs.commandArgs.length > 0) {
parsedArgs.commandArgs.forEach(cmdArg => {
command.arg(cmdArg);
});
}

return connection.execCommandWithLogging(command)
.then((output) => outputUpdate(utils.writeTaskOutput("command", output)));
Expand Down
13 changes: 10 additions & 3 deletions Tasks/DockerComposeV1/dockercomposeconnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,21 @@ export default class DockerComposeConnection extends ContainerConnection {
return output || '\n';
}

public createComposeCommand(): tr.ToolRunner {
public createComposeCommand(globalArgs?: string[]): tr.ToolRunner {
var command = tl.tool(this.dockerComposePath);

if (!tl.getInput('dockerComposePath')) {
command.arg("compose");
process.env["COMPOSE_COMPATIBILITY"] = "true";
}

// Add global arguments before -f flags and project name
if (globalArgs && globalArgs.length > 0) {
globalArgs.forEach(arg => {
command.arg(arg);
});
}

command.arg(["-f", this.dockerComposeFile]);
var basePath = path.dirname(this.dockerComposeFile);
this.additionalDockerComposeFiles.forEach(file => {
Expand All @@ -115,8 +122,8 @@ export default class DockerComposeConnection extends ContainerConnection {
return command;
}

public getCombinedConfig(imageDigestComposeFile?: string): any {
var command = this.createComposeCommand();
public getCombinedConfig(imageDigestComposeFile?: string, globalArgs?: string[]): any {
var command = this.createComposeCommand(globalArgs);
if (imageDigestComposeFile) {
command.arg(["-f", imageDigestComposeFile]);
}
Expand Down
17 changes: 12 additions & 5 deletions Tasks/DockerComposeV1/dockercomposerun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ import * as tl from "azure-pipelines-task-lib/task";
import DockerComposeConnection from "./dockercomposeconnection";
import * as dockerCommandUtils from "azure-pipelines-tasks-docker-common/dockercommandutils";
import * as utils from "./utils";
import * as DockerComposeUtils from "./dockercomposeutils";

export async function run(connection: DockerComposeConnection, outputUpdate: (data: string) => any): Promise<any> {
var command = connection.createComposeCommand();
var arg = tl.getInput("arguments", false);
var parsedArgs = DockerComposeUtils.parseComposeArguments(arg || "");

var command = connection.createComposeCommand(parsedArgs.globalArgs);
command.arg("run");

var detached = tl.getBoolInput("detached");
Expand Down Expand Up @@ -42,9 +46,12 @@ export async function run(connection: DockerComposeConnection, outputUpdate: (da
var serviceName = tl.getInput("serviceName", true);
command.arg(serviceName);

var arg = tl.getInput("arguments", false);
var commandArgs = dockerCommandUtils.getCommandArguments(arg || "");
command.line(commandArgs || "");
// Add command-specific arguments
if (parsedArgs.commandArgs.length > 0) {
parsedArgs.commandArgs.forEach(cmdArg => {
command.arg(cmdArg);
});
}

var containerCommand = tl.getInput("containerCommand");
if (containerCommand) {
Expand All @@ -57,7 +64,7 @@ export async function run(connection: DockerComposeConnection, outputUpdate: (da
} finally {
if (!detached) {

var downCommand = connection.createComposeCommand();
var downCommand = connection.createComposeCommand(parsedArgs.globalArgs);
downCommand.arg("down");

await connection.execCommandWithLogging(downCommand)
Expand Down
15 changes: 11 additions & 4 deletions Tasks/DockerComposeV1/dockercomposeup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ import * as tl from "azure-pipelines-task-lib/task";
import DockerComposeConnection from "./dockercomposeconnection";
import * as dockerCommandUtils from "azure-pipelines-tasks-docker-common/dockercommandutils";
import * as utils from "./utils";
import * as DockerComposeUtils from "./dockercomposeutils";

export function run(connection: DockerComposeConnection, outputUpdate: (data: string) => any): any {
var command = connection.createComposeCommand();
var arg = tl.getInput("arguments", false);
var parsedArgs = DockerComposeUtils.parseComposeArguments(arg || "");

var command = connection.createComposeCommand(parsedArgs.globalArgs);
command.arg("up");

var detached = tl.getBoolInput("detached");
Expand All @@ -24,9 +28,12 @@ export function run(connection: DockerComposeConnection, outputUpdate: (data: st
command.arg("--abort-on-container-exit");
}

var arg = tl.getInput("arguments", false);
var commandArgs = dockerCommandUtils.getCommandArguments(arg || "");
command.line(commandArgs || "");
// Add command-specific arguments
if (parsedArgs.commandArgs.length > 0) {
parsedArgs.commandArgs.forEach(cmdArg => {
command.arg(cmdArg);
});
}

return connection.execCommandWithLogging(command)
.then((output) => outputUpdate(utils.writeTaskOutput("up", output)))
Expand Down
86 changes: 86 additions & 0 deletions Tasks/DockerComposeV1/dockercomposeutils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,89 @@ export function findDockerFile(dockerfilepath: string, currentWorkingDirectory:
return dockerfilepath;
}
}

export interface ParsedComposeArgs {
globalArgs: string[];
commandArgs: string[];
}

export function parseComposeArguments(argsString: string): ParsedComposeArgs {
if (!argsString || argsString.trim() === "") {
return { globalArgs: [], commandArgs: [] };
}

// List of global Docker Compose flags that should be placed before the action/command
const globalFlags = [
'--profile',
'--ansi',
'--compatibility',
'--dry-run',
'--env-file',
'--parallel',
'--progress',
'--project-directory'
// Note: -f/--file and -p/--project-name are already handled separately in createComposeCommand()
];

const globalArgs: string[] = [];
const commandArgs: string[] = [];

// Simple argument parsing - split by spaces but handle quoted strings
const args = parseArgumentString(argsString);

let i = 0;
while (i < args.length) {
const arg = args[i];

// Check if this is a global flag
const isGlobalFlag = globalFlags.some(flag => arg === flag || arg.startsWith(flag + '='));

if (isGlobalFlag) {
globalArgs.push(arg);

// If it's a flag that takes a separate value (not using = syntax), include the next argument too
if (!arg.includes('=') && i + 1 < args.length && !args[i + 1].startsWith('-')) {
globalArgs.push(args[i + 1]);
i++; // Skip the next argument as we've already processed it
}
} else {
commandArgs.push(arg);
}

i++;
}

return { globalArgs, commandArgs };
}

function parseArgumentString(argsString: string): string[] {
const args: string[] = [];
let currentArg = '';
let inQuotes = false;
let quoteChar = '';

for (let i = 0; i < argsString.length; i++) {
const char = argsString[i];

if (!inQuotes && (char === '"' || char === "'")) {
inQuotes = true;
quoteChar = char;
} else if (inQuotes && char === quoteChar) {
inQuotes = false;
quoteChar = '';
} else if (!inQuotes && char === ' ') {
if (currentArg.trim()) {
args.push(currentArg.trim());
currentArg = '';
}
} else {
currentArg += char;
}
}

if (currentArg.trim()) {
args.push(currentArg.trim());
}

return args;
}