fix: auto-inject -f and --env-file flags into custom Docker Compose commands

This commit is contained in:
Andras Bacsai
2025-11-18 13:07:12 +01:00
parent 274c37e333
commit f86ccfaa9a
4 changed files with 441 additions and 7 deletions

View File

@@ -1,16 +1,17 @@
<?php
/**
* Test to verify that custom Docker Compose build commands properly inject environment variables.
* Test to verify that custom Docker Compose build commands properly inject flags.
*
* This test suite verifies that when using a custom build command, the system automatically
* injects the --env-file flag to ensure build-time environment variables are available during
* the build process. This fixes the issue where environment variables were lost when using
* custom build commands.
* injects the -f (compose file path) and --env-file flags to ensure the correct compose file
* is used and build-time environment variables are available during the build process.
*
* The fix ensures that:
* - -f flag with compose file path is automatically injected after 'docker compose'
* - --env-file /artifacts/build-time.env is automatically injected after 'docker compose'
* - Users can still provide their own --env-file flag to override the default behavior
* - Users can still provide their own -f or --env-file flags to override the default behavior
* - Both flags are injected in a single str_replace operation
* - Build arguments are appended when not using build secrets
*/
it('injects --env-file flag into custom build command', function () {
@@ -131,3 +132,360 @@ it('handles DOCKER_BUILDKIT prefix with env-file injection', function () {
expect($customCommand)->toStartWith('DOCKER_BUILDKIT=1');
expect($customCommand)->toContain('--env-file /artifacts/build-time.env');
});
// Tests for -f flag injection
it('injects -f flag with compose file path into custom build command', function () {
$customCommand = 'docker compose build';
$composeFilePath = '/artifacts/deployment-uuid/backend/docker-compose.yaml';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, $composeFilePath, '/artifacts/build-time.env');
expect($customCommand)->toBe('docker compose -f /artifacts/deployment-uuid/backend/docker-compose.yaml --env-file /artifacts/build-time.env build');
expect($customCommand)->toContain('-f /artifacts/deployment-uuid/backend/docker-compose.yaml');
expect($customCommand)->toContain('--env-file /artifacts/build-time.env');
});
it('does not duplicate -f flag when already present', function () {
$customCommand = 'docker compose -f ./custom/docker-compose.yaml build';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
expect($customCommand)->toBe('docker compose --env-file /artifacts/build-time.env -f ./custom/docker-compose.yaml build');
expect(substr_count($customCommand, ' -f '))->toBe(1);
expect($customCommand)->toContain('--env-file /artifacts/build-time.env');
});
it('does not duplicate --file flag when already present', function () {
$customCommand = 'docker compose --file ./custom/docker-compose.yaml build';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
expect($customCommand)->toBe('docker compose --env-file /artifacts/build-time.env --file ./custom/docker-compose.yaml build');
expect(substr_count($customCommand, '--file '))->toBe(1);
expect($customCommand)->toContain('--env-file /artifacts/build-time.env');
});
it('injects both -f and --env-file flags in single operation', function () {
$customCommand = 'docker compose build --no-cache';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/uuid/app/docker-compose.prod.yaml', '/artifacts/build-time.env');
expect($customCommand)->toBe('docker compose -f /artifacts/uuid/app/docker-compose.prod.yaml --env-file /artifacts/build-time.env build --no-cache');
expect($customCommand)->toContain('-f /artifacts/uuid/app/docker-compose.prod.yaml');
expect($customCommand)->toContain('--env-file /artifacts/build-time.env');
expect($customCommand)->toContain('build --no-cache');
});
it('respects user-provided -f and --env-file flags', function () {
$customCommand = 'docker compose -f ./my-compose.yaml --env-file .env build';
// Use the helper function - should not inject anything since both flags are already present
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
expect($customCommand)->toBe('docker compose -f ./my-compose.yaml --env-file .env build');
expect(substr_count($customCommand, ' -f '))->toBe(1);
expect(substr_count($customCommand, '--env-file'))->toBe(1);
});
// Tests for custom start command -f and --env-file injection
it('injects -f and --env-file flags into custom start command', function () {
$customCommand = 'docker compose up -d';
$serverWorkdir = '/var/lib/docker/volumes/coolify-data/_data/applications/app-uuid';
$composeLocation = '/docker-compose.yaml';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, "{$serverWorkdir}{$composeLocation}", "{$serverWorkdir}/.env");
expect($customCommand)->toBe('docker compose -f /var/lib/docker/volumes/coolify-data/_data/applications/app-uuid/docker-compose.yaml --env-file /var/lib/docker/volumes/coolify-data/_data/applications/app-uuid/.env up -d');
expect($customCommand)->toContain('-f /var/lib/docker/volumes/coolify-data/_data/applications/app-uuid/docker-compose.yaml');
expect($customCommand)->toContain('--env-file /var/lib/docker/volumes/coolify-data/_data/applications/app-uuid/.env');
});
it('does not duplicate -f flag in start command when already present', function () {
$customCommand = 'docker compose -f ./custom-compose.yaml up -d';
$serverWorkdir = '/var/lib/docker/volumes/coolify-data/_data/applications/app-uuid';
$composeLocation = '/docker-compose.yaml';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, "{$serverWorkdir}{$composeLocation}", "{$serverWorkdir}/.env");
expect($customCommand)->toBe('docker compose --env-file /var/lib/docker/volumes/coolify-data/_data/applications/app-uuid/.env -f ./custom-compose.yaml up -d');
expect(substr_count($customCommand, ' -f '))->toBe(1);
expect($customCommand)->toContain('--env-file');
});
it('does not duplicate --env-file flag in start command when already present', function () {
$customCommand = 'docker compose --env-file ./my.env up -d';
$serverWorkdir = '/var/lib/docker/volumes/coolify-data/_data/applications/app-uuid';
$composeLocation = '/docker-compose.yaml';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, "{$serverWorkdir}{$composeLocation}", "{$serverWorkdir}/.env");
expect($customCommand)->toBe('docker compose -f /var/lib/docker/volumes/coolify-data/_data/applications/app-uuid/docker-compose.yaml --env-file ./my.env up -d');
expect(substr_count($customCommand, '--env-file'))->toBe(1);
expect($customCommand)->toContain('-f');
});
it('respects both user-provided flags in start command', function () {
$customCommand = 'docker compose -f ./my-compose.yaml --env-file ./.env up -d';
$serverWorkdir = '/var/lib/docker/volumes/coolify-data/_data/applications/app-uuid';
$composeLocation = '/docker-compose.yaml';
// Use the helper function - should not inject anything since both flags are already present
$customCommand = injectDockerComposeFlags($customCommand, "{$serverWorkdir}{$composeLocation}", "{$serverWorkdir}/.env");
expect($customCommand)->toBe('docker compose -f ./my-compose.yaml --env-file ./.env up -d');
expect(substr_count($customCommand, ' -f '))->toBe(1);
expect(substr_count($customCommand, '--env-file'))->toBe(1);
});
it('injects both flags in start command with additional parameters', function () {
$customCommand = 'docker compose up -d --remove-orphans';
$serverWorkdir = '/workdir/app';
$composeLocation = '/backend/docker-compose.prod.yaml';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, "{$serverWorkdir}{$composeLocation}", "{$serverWorkdir}/.env");
expect($customCommand)->toBe('docker compose -f /workdir/app/backend/docker-compose.prod.yaml --env-file /workdir/app/.env up -d --remove-orphans');
expect($customCommand)->toContain('-f /workdir/app/backend/docker-compose.prod.yaml');
expect($customCommand)->toContain('--env-file /workdir/app/.env');
expect($customCommand)->toContain('--remove-orphans');
});
// Security tests: Prevent bypass vectors for flag detection
it('detects -f flag with equals sign format (bypass vector)', function () {
$customCommand = 'docker compose -f=./custom/docker-compose.yaml build';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Should NOT inject -f flag since -f= is already present
expect($customCommand)->toBe('docker compose --env-file /artifacts/build-time.env -f=./custom/docker-compose.yaml build');
expect($customCommand)->not->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
expect($customCommand)->toContain('--env-file /artifacts/build-time.env');
});
it('detects --file flag with equals sign format (bypass vector)', function () {
$customCommand = 'docker compose --file=./custom/docker-compose.yaml build';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Should NOT inject -f flag since --file= is already present
expect($customCommand)->toBe('docker compose --env-file /artifacts/build-time.env --file=./custom/docker-compose.yaml build');
expect($customCommand)->not->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
expect($customCommand)->toContain('--env-file /artifacts/build-time.env');
});
it('detects --env-file flag with equals sign format (bypass vector)', function () {
$customCommand = 'docker compose --env-file=./custom/.env build';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Should NOT inject --env-file flag since --env-file= is already present
expect($customCommand)->toBe('docker compose -f /artifacts/deployment-uuid/docker-compose.yaml --env-file=./custom/.env build');
expect($customCommand)->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
expect($customCommand)->not->toContain('--env-file /artifacts/build-time.env');
});
it('detects -f flag with tab character whitespace (bypass vector)', function () {
$customCommand = "docker compose\t-f\t./custom/docker-compose.yaml build";
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Should NOT inject -f flag since -f with tab is already present
expect($customCommand)->toBe("docker compose --env-file /artifacts/build-time.env\t-f\t./custom/docker-compose.yaml build");
expect($customCommand)->not->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
expect($customCommand)->toContain('--env-file /artifacts/build-time.env');
});
it('detects --env-file flag with tab character whitespace (bypass vector)', function () {
$customCommand = "docker compose\t--env-file\t./custom/.env build";
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Should NOT inject --env-file flag since --env-file with tab is already present
expect($customCommand)->toBe("docker compose -f /artifacts/deployment-uuid/docker-compose.yaml\t--env-file\t./custom/.env build");
expect($customCommand)->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
expect($customCommand)->not->toContain('--env-file /artifacts/build-time.env');
});
it('detects -f flag with multiple spaces (bypass vector)', function () {
$customCommand = 'docker compose -f ./custom/docker-compose.yaml build';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Should NOT inject -f flag since -f with multiple spaces is already present
expect($customCommand)->toBe('docker compose --env-file /artifacts/build-time.env -f ./custom/docker-compose.yaml build');
expect($customCommand)->not->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
expect($customCommand)->toContain('--env-file /artifacts/build-time.env');
});
it('detects --file flag with multiple spaces (bypass vector)', function () {
$customCommand = 'docker compose --file ./custom/docker-compose.yaml build';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Should NOT inject -f flag since --file with multiple spaces is already present
expect($customCommand)->toBe('docker compose --env-file /artifacts/build-time.env --file ./custom/docker-compose.yaml build');
expect($customCommand)->not->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
expect($customCommand)->toContain('--env-file /artifacts/build-time.env');
});
it('detects -f flag at start of command (edge case)', function () {
$customCommand = '-f ./custom/docker-compose.yaml docker compose build';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Should NOT inject -f flag since -f is at start of command
expect($customCommand)->toBe('-f ./custom/docker-compose.yaml docker compose --env-file /artifacts/build-time.env build');
expect($customCommand)->not->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
expect($customCommand)->toContain('--env-file /artifacts/build-time.env');
});
it('detects --env-file flag at start of command (edge case)', function () {
$customCommand = '--env-file=./custom/.env docker compose build';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Should NOT inject --env-file flag since --env-file is at start of command
expect($customCommand)->toBe('--env-file=./custom/.env docker compose -f /artifacts/deployment-uuid/docker-compose.yaml build');
expect($customCommand)->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
expect($customCommand)->not->toContain('--env-file /artifacts/build-time.env');
});
it('handles mixed whitespace correctly (comprehensive test)', function () {
$customCommand = "docker compose\t-f=./custom/docker-compose.yaml --env-file\t./custom/.env build";
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Should NOT inject any flags since both are already present with various whitespace
expect($customCommand)->toBe("docker compose\t-f=./custom/docker-compose.yaml --env-file\t./custom/.env build");
expect($customCommand)->not->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
expect($customCommand)->not->toContain('--env-file /artifacts/build-time.env');
});
// Tests for concatenated -f flag format (no space, no equals)
it('detects -f flag in concatenated format -fvalue (bypass vector)', function () {
$customCommand = 'docker compose -f./custom/docker-compose.yaml build';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Should NOT inject -f flag since -f is concatenated with value
expect($customCommand)->toBe('docker compose --env-file /artifacts/build-time.env -f./custom/docker-compose.yaml build');
expect($customCommand)->not->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
expect($customCommand)->toContain('--env-file /artifacts/build-time.env');
});
it('detects -f flag concatenated with path containing slash', function () {
$customCommand = 'docker compose -f/path/to/compose.yml up -d';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Should NOT inject -f flag since -f is concatenated
expect($customCommand)->toBe('docker compose --env-file /artifacts/build-time.env -f/path/to/compose.yml up -d');
expect($customCommand)->not->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
expect($customCommand)->toContain('-f/path/to/compose.yml');
});
it('detects -f flag concatenated at start of command', function () {
$customCommand = '-f./compose.yaml docker compose build';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Should NOT inject -f flag since -f is already present (even at start)
expect($customCommand)->toBe('-f./compose.yaml docker compose --env-file /artifacts/build-time.env build');
expect($customCommand)->not->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
});
it('detects concatenated -f flag with relative path', function () {
$customCommand = 'docker compose -f../docker-compose.prod.yaml build --no-cache';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Should NOT inject -f flag
expect($customCommand)->toBe('docker compose --env-file /artifacts/build-time.env -f../docker-compose.prod.yaml build --no-cache');
expect($customCommand)->not->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
expect($customCommand)->toContain('-f../docker-compose.prod.yaml');
});
it('correctly injects when no -f flag is present (sanity check after concatenated fix)', function () {
$customCommand = 'docker compose build --no-cache';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// SHOULD inject both flags
expect($customCommand)->toBe('docker compose -f /artifacts/deployment-uuid/docker-compose.yaml --env-file /artifacts/build-time.env build --no-cache');
expect($customCommand)->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
expect($customCommand)->toContain('--env-file /artifacts/build-time.env');
});
// Edge case tests: First occurrence only replacement
it('only replaces first docker compose occurrence in chained commands', function () {
$customCommand = 'docker compose pull && docker compose build';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Only the FIRST 'docker compose' should get the flags
expect($customCommand)->toBe('docker compose -f /artifacts/uuid/docker-compose.yaml --env-file /artifacts/build-time.env pull && docker compose build');
expect($customCommand)->toContain('docker compose -f /artifacts/uuid/docker-compose.yaml --env-file /artifacts/build-time.env pull');
expect($customCommand)->toContain(' && docker compose build');
// Verify the second occurrence is NOT modified
expect(substr_count($customCommand, '-f /artifacts/uuid/docker-compose.yaml'))->toBe(1);
expect(substr_count($customCommand, '--env-file /artifacts/build-time.env'))->toBe(1);
});
it('does not modify docker compose string in echo statements', function () {
$customCommand = 'docker compose build && echo "docker compose finished successfully"';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Only the FIRST 'docker compose' (the command) should get flags, NOT the echo message
expect($customCommand)->toBe('docker compose -f /artifacts/uuid/docker-compose.yaml --env-file /artifacts/build-time.env build && echo "docker compose finished successfully"');
expect($customCommand)->toContain('docker compose -f /artifacts/uuid/docker-compose.yaml --env-file /artifacts/build-time.env build');
expect($customCommand)->toContain('echo "docker compose finished successfully"');
// Verify echo message is NOT modified
expect(substr_count($customCommand, 'docker compose', 0))->toBe(2); // Two total occurrences
expect(substr_count($customCommand, '-f /artifacts/uuid/docker-compose.yaml'))->toBe(1); // Only first has flags
});
it('does not modify docker compose string in bash comments', function () {
$customCommand = 'docker compose build # This runs docker compose to build the image';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Only the FIRST 'docker compose' (the command) should get flags, NOT the comment
expect($customCommand)->toBe('docker compose -f /artifacts/uuid/docker-compose.yaml --env-file /artifacts/build-time.env build # This runs docker compose to build the image');
expect($customCommand)->toContain('docker compose -f /artifacts/uuid/docker-compose.yaml --env-file /artifacts/build-time.env build');
expect($customCommand)->toContain('# This runs docker compose to build the image');
// Verify comment is NOT modified
expect(substr_count($customCommand, 'docker compose', 0))->toBe(2); // Two total occurrences
expect(substr_count($customCommand, '-f /artifacts/uuid/docker-compose.yaml'))->toBe(1); // Only first has flags
});