diff --git a/src/BuildProcess/BuildContainerImage.php b/src/BuildProcess/BuildContainerImage.php index 87aef931..b8ac02d8 100644 --- a/src/BuildProcess/BuildContainerImage.php +++ b/src/BuildProcess/BuildContainerImage.php @@ -44,6 +44,13 @@ class BuildContainerImage */ protected $cliBuildArgs; + /** + * The Docker CLI options. + * + * @var array + */ + protected $cliBuildOptions; + /** * The Docker manifest build arguments. * @@ -55,14 +62,17 @@ class BuildContainerImage * Create a new project builder. * * @param string|null $environment - * @param array $buildArgs + * @param array $cliBuildArgs + * @param array $cliBuildOptions + * @param array $manifestBuildArgs * @return void */ - public function __construct($environment = null, $cliBuildArgs = [], $manifestBuildArgs = []) + public function __construct($environment = null, $cliBuildArgs = [], $cliBuildOptions = [], $manifestBuildArgs = []) { $this->baseConstructor($environment); $this->cliBuildArgs = $cliBuildArgs; + $this->cliBuildOptions = $cliBuildOptions; $this->manifestBuildArgs = $manifestBuildArgs; } @@ -96,7 +106,8 @@ public function __invoke() $this->appPath, Manifest::name(), $this->environment, - $this->formatBuildArguments() + $this->formatBuildArguments(), + $this->cliBuildOptions ); } diff --git a/src/Commands/BuildCommand.php b/src/Commands/BuildCommand.php index 1383faaa..e9f71802 100644 --- a/src/Commands/BuildCommand.php +++ b/src/Commands/BuildCommand.php @@ -45,6 +45,7 @@ protected function configure() ->addArgument('environment', InputArgument::OPTIONAL, 'The environment name') ->addOption('asset-url', null, InputOption::VALUE_OPTIONAL, 'The asset base URL') ->addOption('build-arg', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Docker build argument') + ->addOption('build-option', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Docker build option') ->setDescription('Build the project archive'); } @@ -88,7 +89,12 @@ public function handle() new ExtractVendorToSeparateDirectory($this->argument('environment')), new CompressApplication($this->argument('environment')), new CompressVendor($this->argument('environment')), - new BuildContainerImage($this->argument('environment'), $this->option('build-arg'), Manifest::dockerBuildArgs($this->argument('environment'))), + new BuildContainerImage( + $this->argument('environment'), + $this->option('build-arg'), + $this->option('build-option'), + Manifest::dockerBuildArgs($this->argument('environment')) + ), ])->each->__invoke(); $time = (new DateTime())->diff($startedAt)->format('%im%Ss'); diff --git a/src/Commands/DeployCommand.php b/src/Commands/DeployCommand.php index 89a78b77..d8eb1a43 100644 --- a/src/Commands/DeployCommand.php +++ b/src/Commands/DeployCommand.php @@ -34,6 +34,7 @@ protected function configure() ->addOption('without-waiting', null, InputOption::VALUE_NONE, 'Deploy without waiting for progress') ->addOption('fresh-assets', null, InputOption::VALUE_NONE, 'Upload a fresh copy of all assets') ->addOption('build-arg', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Docker build argument') + ->addOption('build-option', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Docker build option') ->addOption('debug', null, InputOption::VALUE_OPTIONAL, 'Deploy with debug mode enabled', 'unset') ->setDescription('Deploy an environment'); } @@ -123,6 +124,7 @@ protected function buildProject(array $project) '--asset-url' => $this->assetDomain($project).'/'.$uuid, '--manifest' => Path::manifest(), '--build-arg' => $this->option('build-arg'), + '--build-option' => $this->option('build-option'), ]); return $this->uploadArtifact( diff --git a/src/Docker.php b/src/Docker.php index 2dd30f69..cdac0ac2 100644 --- a/src/Docker.php +++ b/src/Docker.php @@ -15,12 +15,24 @@ class Docker * @param string $project * @param string $environment * @param array $cliBuildArgs + * @param array $cliBuildOptions * @return void */ - public static function build($path, $project, $environment, $cliBuildArgs) + public static function build($path, $project, $environment, $cliBuildArgs, $cliBuildOptions) { + $buildCommand = static::buildCommand( + $project, + $environment, + $cliBuildArgs, + Manifest::dockerBuildArgs($environment), + $cliBuildOptions, + Manifest::dockerBuildOptions($environment) + ); + + Helpers::line(sprintf('Build command: %s', $buildCommand)); + Process::fromShellCommandline( - static::buildCommand($project, $environment, $cliBuildArgs, Manifest::dockerBuildArgs($environment)), + $buildCommand, $path )->setTimeout(null)->mustRun(function ($type, $line) { Helpers::write($line); @@ -34,24 +46,60 @@ public static function build($path, $project, $environment, $cliBuildArgs) * @param string $environment * @param array $cliBuildArgs * @param array $manifestBuildArgs + * @param array $cliBuildOptions + * @param array $manifestBuildOptions * @return string */ - public static function buildCommand($project, $environment, $cliBuildArgs, $manifestBuildArgs) + public static function buildCommand($project, $environment, $cliBuildArgs, $manifestBuildArgs, $cliBuildOptions, $manifestBuildOptions) { - return sprintf('docker build --pull --file=%s --tag=%s %s.', + $command = sprintf( + 'docker build --pull --file=%s --tag=%s ', Manifest::dockerfile($environment), - Str::slug($project).':'.$environment, - Collection::make($manifestBuildArgs) - ->merge(Collection::make($cliBuildArgs) - ->mapWithKeys(function ($value) { - [$key, $value] = explode('=', $value, 2); - - return [$key => $value]; - }) - )->map(function ($value, $key) { - return '--build-arg='.escapeshellarg("{$key}={$value}").' '; - })->implode('') + Str::slug($project).':'.$environment ); + + $buildArgs = Collection::make($manifestBuildArgs) + ->merge(Collection::make($cliBuildArgs) + ->mapWithKeys(function ($value) { + [$key, $value] = explode('=', $value, 2); + + return [$key => $value]; + }) + )->map(function ($value, $key) { + return '--build-arg='.escapeshellarg("{$key}={$value}"); + })->implode(' '); + + $buildOptions = Collection::make($manifestBuildOptions) + ->mapWithKeys(function ($value) { + if (is_array($value)) { + return $value; + } + + return [$value => null]; + }) + ->merge(Collection::make($cliBuildOptions) + ->mapWithKeys(function ($value) { + if (! str_contains($value, '=')) { + return [$value => null]; + } + + [$key, $value] = explode('=', $value, 2); + + return [$key => $value]; + }) + )->map(function ($value, $key) { + if ($value === null) { + return "--{$key}"; + } + + return "--{$key}=".escapeshellarg($value); + })->implode(' '); + + $command = $buildArgs ? $command.$buildArgs.' ' : $command; + $command = $buildOptions ? $command.$buildOptions.' ' : $command; + $command .= '.'; + + return $command; } /** diff --git a/src/Manifest.php b/src/Manifest.php index 07c0325b..719afd26 100644 --- a/src/Manifest.php +++ b/src/Manifest.php @@ -95,6 +95,17 @@ public static function dockerBuildArgs($environment) return static::current()['environments'][$environment]['docker-build-args'] ?? []; } + /** + * Get the Docker options. + * + * @param string $environment + * @return array + */ + public static function dockerBuildOptions($environment) + { + return static::current()['environments'][$environment]['docker-build-options'] ?? []; + } + /** * Determine if the environment uses a database proxy. * diff --git a/tests/BuildContainerImageTest.php b/tests/BuildContainerImageTest.php index 16eaedbd..1e544f66 100644 --- a/tests/BuildContainerImageTest.php +++ b/tests/BuildContainerImageTest.php @@ -55,7 +55,7 @@ public function test_docker_build_arguments_can_be_formatted_correctly() ], ])); - $buildArgs = (new BuildContainerImage('production', ['FOO=BAR', 'BAR=BAZ']))->formatBuildArguments(); + $buildArgs = (new BuildContainerImage('production', ['FOO=BAR', 'BAR=BAZ'], []))->formatBuildArguments(); $this->assertSame(['__VAPOR_RUNTIME=docker', 'FOO=BAR', 'BAR=BAZ'], $buildArgs); } @@ -72,7 +72,7 @@ public function test_runtime_variable_cannot_be_overridden() ], ])); - $buildArgs = (new BuildContainerImage('production', ['__VAPOR_RUNTIME=foo']))->formatBuildArguments(); + $buildArgs = (new BuildContainerImage('production', [], ['__VAPOR_RUNTIME=foo']))->formatBuildArguments(); $this->assertSame(['__VAPOR_RUNTIME=docker'], $buildArgs); } diff --git a/tests/DockerTest.php b/tests/DockerTest.php index 7ec017bd..34729048 100644 --- a/tests/DockerTest.php +++ b/tests/DockerTest.php @@ -24,7 +24,7 @@ protected function tearDown(): void public function test_build_command_no_build_args() { - $command = Docker::buildCommand('my-project', 'production', [], []); + $command = Docker::buildCommand('my-project', 'production', [], [], [], []); $expectedCommand = 'docker build --pull --file=production.Dockerfile --tag=my-project:production .'; $this->assertEquals($expectedCommand, $command); } @@ -32,7 +32,7 @@ public function test_build_command_no_build_args() public function test_build_command_cli_build_args() { $cliBuildArgs = ['FOO=BAR', 'FIZZ=BUZZ']; - $command = Docker::buildCommand('my-project', 'production', $cliBuildArgs, []); + $command = Docker::buildCommand('my-project', 'production', $cliBuildArgs, [], [], []); $expectedCommand = 'docker build --pull --file=production.Dockerfile --tag=my-project:production '. "--build-arg='FOO=BAR' --build-arg='FIZZ=BUZZ' ."; $this->assertEquals($expectedCommand, $command); @@ -41,7 +41,7 @@ public function test_build_command_cli_build_args() public function test_build_command_manifest_build_args() { $manifestBuildArgs = ['FOO' => 'BAR', 'FIZZ' => 'BUZZ']; - $command = Docker::buildCommand('my-project', 'production', [], $manifestBuildArgs); + $command = Docker::buildCommand('my-project', 'production', [], $manifestBuildArgs, [], []); $expectedCommand = 'docker build --pull --file=production.Dockerfile --tag=my-project:production '. "--build-arg='FOO=BAR' --build-arg='FIZZ=BUZZ' ."; $this->assertEquals($expectedCommand, $command); @@ -51,12 +51,50 @@ public function test_build_command_cli_and_manifest_build_args() { $cliBuildArgs = ['BAR=FOO', 'FIZZ=BAZZ']; $manifestBuildArgs = ['FOO' => 'BAR', 'FIZZ' => 'BUZZ']; - $command = Docker::buildCommand('my-project', 'production', $cliBuildArgs, $manifestBuildArgs); + $command = Docker::buildCommand('my-project', 'production', $cliBuildArgs, $manifestBuildArgs, [], []); $expectedCommand = 'docker build --pull --file=production.Dockerfile --tag=my-project:production '. "--build-arg='FOO=BAR' --build-arg='FIZZ=BAZZ' --build-arg='BAR=FOO' ."; $this->assertEquals($expectedCommand, $command); } + public function test_build_command_cli_docker_options() + { + $cliBuildOptions = ['BAR=FOO', 'FIZZ=BAZZ', 'FIZZLE', 'BUZZLE']; + $command = Docker::buildCommand('my-project', 'production', [], [], $cliBuildOptions, []); + $expectedCommand = 'docker build --pull --file=production.Dockerfile --tag=my-project:production '. + "--BAR='FOO' --FIZZ='BAZZ' --FIZZLE --BUZZLE ."; + $this->assertEquals($expectedCommand, $command); + } + + public function test_build_command_manifest_docker_options() + { + $manifestBuildOptions = [['FOO' => 'BAR'], ['FIZZ' => 'BUZZ'], 'FIZZLE', 'BUZZLE']; + $command = Docker::buildCommand('my-project', 'production', [], [], [], $manifestBuildOptions); + $expectedCommand = 'docker build --pull --file=production.Dockerfile --tag=my-project:production '. + "--FOO='BAR' --FIZZ='BUZZ' --FIZZLE --BUZZLE ."; + $this->assertEquals($expectedCommand, $command); + } + + public function test_build_command_cli_and_manifest_docker_args() + { + $cliBuildOptions = ['BAR=FOO', 'FIZZ=BAZZ', 'FIZZLE', 'BUZZLE']; + $manifestBuildOptions = [['FOO' => 'BAR'], ['FIZZ' => 'BUZZ'], 'FIZZLY', 'BUZZLY']; + $command = Docker::buildCommand('my-project', 'production', [], [], $cliBuildOptions, $manifestBuildOptions); + $expectedCommand = 'docker build --pull --file=production.Dockerfile --tag=my-project:production '. + "--FOO='BAR' --FIZZ='BAZZ' --FIZZLY --BUZZLY --BAR='FOO' --FIZZLE --BUZZLE ."; + $this->assertEquals($expectedCommand, $command); + } + + public function test_build_command_cli_docker_options_and_cli_build_args() + { + $cliBuildOptions = ['BAR=FOO', 'FIZZ=BAZZ']; + $cliBuildArgs = ['BAR=FOO', 'FIZZ=BAZZ']; + $command = Docker::buildCommand('my-project', 'production', $cliBuildArgs, [], $cliBuildOptions, []); + $expectedCommand = 'docker build --pull --file=production.Dockerfile --tag=my-project:production '. + "--build-arg='BAR=FOO' --build-arg='FIZZ=BAZZ' --BAR='FOO' --FIZZ='BAZZ' ."; + $this->assertEquals($expectedCommand, $command); + } + public function test_dockerfile_from_manifest() { file_put_contents(Container::getInstance()->offsetGet('manifest'), Yaml::dump([ @@ -69,7 +107,7 @@ public function test_dockerfile_from_manifest() ], ], ])); - $command = Docker::buildCommand('my-project', 'production', [], []); + $command = Docker::buildCommand('my-project', 'production', [], [], [], []); $expectedCommand = 'docker build --pull --file=docker/shared.Dockerfile --tag=my-project:production .'; $this->assertEquals($expectedCommand, $command); }