From 890f79b9bd2a360ff51f3c04a7f19cd1bf77763b Mon Sep 17 00:00:00 2001 From: nicolaschautard Date: Tue, 6 Oct 2020 12:22:53 +0200 Subject: [PATCH] #12 : Add deploy, destroy and rsync commands --- README.md | 1 + bin/kloud | 15 ++ composer.json | 3 +- composer.lock | 241 ++++++++++++------ src/Domain/Environment/DTO/Context.php | 2 +- .../VariableNotFoundException.php | 3 +- .../Command/Environment/DeployCommand.php | 146 +++++++++++ .../Command/Environment/DestroyCommand.php | 124 +++++++++ .../Command/Environment/RsyncCommand.php | 130 ++++++++++ .../Environment/Variable/GetCommand.php | 2 +- .../Environment/Variable/SetCommand.php | 2 +- .../Environment/Variable/UnsetCommand.php | 2 +- 12 files changed, 585 insertions(+), 86 deletions(-) rename src/Domain/Environment/{ => Exception}/VariableNotFoundException.php (56%) create mode 100644 src/Platform/Console/Command/Environment/DeployCommand.php create mode 100644 src/Platform/Console/Command/Environment/DestroyCommand.php create mode 100644 src/Platform/Console/Command/Environment/RsyncCommand.php diff --git a/README.md b/README.md index 5080f64..e49e37f 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ This project aims at building your Docker stack for [OroCommerce](https://oroinc Requirements: --- - PHP 7.4 +- rsync (for environment commands) Installation --- diff --git a/bin/kloud b/bin/kloud index c760b98..313862c 100755 --- a/bin/kloud +++ b/bin/kloud @@ -79,6 +79,21 @@ $app->addCommands([ (new Command\Environment\Variable\UnsetCommand( Command\Environment\Variable\UnsetCommand::$defaultName, )), + + (new Command\Environment\DeployCommand( + Command\Environment\DeployCommand::$defaultName, + $app, + )), + + (new Command\Environment\DestroyCommand( + Command\Environment\DestroyCommand::$defaultName, + $app, + )), + + (new Command\Environment\RsyncCommand( + Command\Environment\RsyncCommand::$defaultName, + $app, + )), ]); $app->run(new ArgvInput($argv), new ConsoleOutput()); diff --git a/composer.json b/composer.json index 75f1cbd..0694776 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,8 @@ "symfony/property-access": "^5.0", "symfony/serializer": "^5.0", "symfony/yaml": "^5.0", - "splitbrain/php-archive": "^1.1" + "splitbrain/php-archive": "^1.1", + "deployer/deployer": "^6.8" }, "require-dev": { "friends-of-phpspec/phpspec-code-coverage": "^4.0", diff --git a/composer.lock b/composer.lock index 9467444..740103f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "525a32433d3859897ef760bac6fa763b", + "content-hash": "30eee74a5479099f32df904d397eae35", "packages": [ { "name": "composer/ca-bundle", @@ -123,6 +123,115 @@ ], "time": "2020-01-13T12:06:48+00:00" }, + { + "name": "deployer/deployer", + "version": "v6.8.0", + "source": { + "type": "git", + "url": "https://github.com/deployphp/deployer.git", + "reference": "4e243a64ed61e779fbb31c5a74e258a8e52fdaff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/deployphp/deployer/zipball/4e243a64ed61e779fbb31c5a74e258a8e52fdaff", + "reference": "4e243a64ed61e779fbb31c5a74e258a8e52fdaff", + "shasum": "" + }, + "require": { + "deployer/phar-update": "~2.2", + "php": "^7.2", + "pimple/pimple": "~3.0", + "symfony/console": "~2.7|~3.0|~4.0|~5.0", + "symfony/process": "~2.7|~3.0|~4.0|~5.0", + "symfony/yaml": "~2.7|~3.0|~4.0|~5.0" + }, + "require-dev": { + "phpunit/phpunit": "^8" + }, + "bin": [ + "bin/dep" + ], + "type": "library", + "autoload": { + "psr-4": { + "Deployer\\": "src/" + }, + "files": [ + "src/Support/helpers.php", + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anton Medvedev", + "email": "anton@medv.io" + } + ], + "description": "Deployment Tool", + "homepage": "https://deployer.org", + "time": "2020-04-25T16:05:31+00:00" + }, + { + "name": "deployer/phar-update", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/deployphp/phar-update.git", + "reference": "9ad07422f2cd43a1382ee8e134bdcd3a374848e3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/deployphp/phar-update/zipball/9ad07422f2cd43a1382ee8e134bdcd3a374848e3", + "reference": "9ad07422f2cd43a1382ee8e134bdcd3a374848e3", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "symfony/console": "~2.7|~3.0|~4.0|~5.0" + }, + "require-dev": { + "mikey179/vfsstream": "1.1.0", + "phpunit/phpunit": "3.7.*", + "symfony/process": "~2.7|~3.0|~4.0|~5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Deployer\\Component\\PharUpdate\\": "src/", + "Deployer\\Component\\PHPUnit\\": "src/PHPUnit/", + "Deployer\\Component\\Version\\": "src/Version/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kevin Herrera", + "email": "kevin@herrera.io", + "homepage": "http://kevin.herrera.io" + }, + { + "name": "Anton Medvedev", + "email": "anton@medv.io", + "homepage": "https://medv.io" + } + ], + "description": "Integrates Phar Update to Symfony Console.", + "homepage": "https://github.com/deployphp/phar-update", + "keywords": [ + "console", + "phar", + "update" + ], + "abandoned": true, + "time": "2019-12-12T13:45:57+00:00" + }, { "name": "padraic/humbug_get_contents", "version": "1.1.2", @@ -242,8 +351,59 @@ "self-update", "update" ], + "abandoned": true, "time": "2018-03-30T12:52:15+00:00" }, + { + "name": "pimple/pimple", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/silexphp/Pimple.git", + "reference": "e55d12f9d6a0e7f9c85992b73df1267f46279930" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/silexphp/Pimple/zipball/e55d12f9d6a0e7f9c85992b73df1267f46279930", + "reference": "e55d12f9d6a0e7f9c85992b73df1267f46279930", + "shasum": "" + }, + "require": { + "php": "^7.2.5", + "psr/container": "^1.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^3.4|^4.4|^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3.x-dev" + } + }, + "autoload": { + "psr-0": { + "Pimple": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Pimple, a simple Dependency Injection Container", + "homepage": "https://pimple.symfony.com", + "keywords": [ + "container", + "dependency injection" + ], + "time": "2020-03-03T09:12:48+00:00" + }, { "name": "psr/container", "version": "1.0.0", @@ -708,20 +868,6 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-02-24T15:05:31+00:00" }, { @@ -771,20 +917,6 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-02-14T07:43:07+00:00" }, { @@ -1080,20 +1212,6 @@ "portable", "shim" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-03-03T16:59:03+00:00" }, { @@ -1292,20 +1410,6 @@ ], "description": "Symfony Serializer Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-02-29T10:07:09+00:00" }, { @@ -1469,12 +1573,6 @@ "Xdebug", "performance" ], - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - } - ], "time": "2020-03-01T12:26:26+00:00" }, { @@ -2804,20 +2902,6 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-02-22T20:09:08+00:00" }, { @@ -3247,6 +3331,5 @@ "php": "^7.4", "ext-json": "*" }, - "platform-dev": [], - "plugin-api-version": "1.1.0" + "platform-dev": [] } diff --git a/src/Domain/Environment/DTO/Context.php b/src/Domain/Environment/DTO/Context.php index 5ff19b2..59f171e 100644 --- a/src/Domain/Environment/DTO/Context.php +++ b/src/Domain/Environment/DTO/Context.php @@ -4,7 +4,7 @@ namespace Kiboko\Cloud\Domain\Environment\DTO; -use Kiboko\Cloud\Domain\Environment\VariableNotFoundException; +use Kiboko\Cloud\Domain\Environment\Exception\VariableNotFoundException; use Symfony\Component\Serializer\Normalizer\DenormalizableInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizableInterface; diff --git a/src/Domain/Environment/VariableNotFoundException.php b/src/Domain/Environment/Exception/VariableNotFoundException.php similarity index 56% rename from src/Domain/Environment/VariableNotFoundException.php rename to src/Domain/Environment/Exception/VariableNotFoundException.php index 8d2ed8c..b9a534d 100644 --- a/src/Domain/Environment/VariableNotFoundException.php +++ b/src/Domain/Environment/Exception/VariableNotFoundException.php @@ -1,8 +1,7 @@ console = $console; + $this->wizard = new EnvironmentWizard(); + parent::__construct($name); + } + + protected function configure() + { + $this->setDescription('Deploy the application to a remote server using rsync and initialize docker containers'); + + $this->wizard->configureConsoleCommand($this); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $workingDirectory = $input->getOption('working-directory') ?: getcwd(); + + $finder = (new Finder()) + ->files() + ->ignoreDotFiles(false) + ->in($workingDirectory); + + $format = new SymfonyStyle($input, $output); + + $serializer = new Serializer( + [ + new CustomNormalizer(), + new PropertyNormalizer(), + ], + [ + new YamlEncoder(), + ] + ); + + if ($finder->hasResults()) { + /** @var SplFileInfo $file */ + foreach ($finder->name('/^\.?kloud.environment.ya?ml$/') as $file) { + try { + /** @var \Kiboko\Cloud\Domain\Stack\DTO\Context $context */ + $context = $serializer->deserialize($file->getContents(), Context::class, 'yaml'); + } catch (\Throwable $exception) { + $format->error($exception->getMessage()); + continue; + } + + break; + } + } + + if (!isset($context)) { + $format->error('No .kloud.environment.yaml file found in your directory. You must initialize it using environment:init command'); + + return 1; + } + + $application = new Application($this->console->getName()); + $deployer = new Deployer($application); + $deployer['output'] = $output; + $deployer['log_handler'] = function ($deployer) { + return !empty($deployer->config['log_file']) + ? new FileHandler($deployer->config['log_file']) + : new NullHandler(); + }; + $deployer['logger'] = function ($deployer) { + return new Logger($deployer['log_handler']); + }; + $rsync = new Rsync(new ProcessOutputPrinter($output, $deployer['logger'])); + + $hosts = []; + $tasks = []; + + /** @var Context $context */ + $host = new Host($context->deployment->server->hostname); + $host->port($context->deployment->server->port); + $host->user($context->deployment->server->username); + array_push($hosts, $host); + + $destination = $host->getUser().'@'.$host->getHostname().':'.$context->deployment->path; + + try { + $format->note('Syncing remote directory with local directory'); + $rsync->call($host->getHostname(), $workingDirectory, $destination); + $format->success('Remote directory synced with local directory'); + } catch (ProcessFailedException $exception) { + $format->error($exception->getMessage()); + + return 1; + } + + $directories = explode('/', $workingDirectory); + $projectName = end($directories); + + $command = 'cd '.$context->deployment->path.'/'.$projectName.' && docker-compose up --no-start'; + + array_push($tasks, new Task('docker:up', function () use ($command, $host) { + run($command); + })); + + $seriesExecutor = new SeriesExecutor($input, $output, new Informer(new OutputWatcher($output))); + $seriesExecutor->run($tasks, $hosts); + + return 0; + } +} diff --git a/src/Platform/Console/Command/Environment/DestroyCommand.php b/src/Platform/Console/Command/Environment/DestroyCommand.php new file mode 100644 index 0000000..27faa0a --- /dev/null +++ b/src/Platform/Console/Command/Environment/DestroyCommand.php @@ -0,0 +1,124 @@ +console = $console; + $this->wizard = new EnvironmentWizard(); + parent::__construct($name); + } + + protected function configure() + { + $this->setDescription('Destroy the Docker infrastructure with associated volumes and remove remote directory'); + + $this->wizard->configureConsoleCommand($this); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $workingDirectory = $input->getOption('working-directory') ?: getcwd(); + + $finder = (new Finder()) + ->files() + ->ignoreDotFiles(false) + ->in($workingDirectory); + + $format = new SymfonyStyle($input, $output); + + $serializer = new Serializer( + [ + new CustomNormalizer(), + new PropertyNormalizer(), + ], + [ + new YamlEncoder(), + ] + ); + + if ($finder->hasResults()) { + /** @var SplFileInfo $file */ + foreach ($finder->name('/^\.?kloud.environment.ya?ml$/') as $file) { + try { + /** @var \Kiboko\Cloud\Domain\Stack\DTO\Context $context */ + $context = $serializer->deserialize($file->getContents(), Context::class, 'yaml'); + } catch (\Throwable $exception) { + $format->error($exception->getMessage()); + continue; + } + + break; + } + } + + if (!isset($context)) { + $format->error('No .kloud.environment.yaml file found in your directory. You must initialize it using environment:init command'); + + return 1; + } + + $application = new Application($this->console->getName()); + $deployer = new Deployer($application); + $deployer['output'] = $output; + + $hosts = []; + $tasks = []; + + /** @var Context $context */ + $host = new Host($context->deployment->server->hostname); + $host->port($context->deployment->server->port); + $host->user($context->deployment->server->username); + array_push($hosts, $host); + + $directories = explode('/', $workingDirectory); + $projectName = end($directories); + + $commands = [ + 'docker:down' => 'cd '.$context->deployment->path.'/'.$projectName.' && docker-compose down -v', + 'directory:remove' => 'cd '.$context->deployment->path.' && rm -rf '.$projectName, + ]; + + foreach ($commands as $key => $value) { + array_push($tasks, new Task($key, function () use ($value, $host) { + run($value); + })); + } + + $seriesExecutor = new SeriesExecutor($input, $output, new Informer(new OutputWatcher($output))); + $seriesExecutor->run($tasks, $hosts); + + return 0; + } +} diff --git a/src/Platform/Console/Command/Environment/RsyncCommand.php b/src/Platform/Console/Command/Environment/RsyncCommand.php new file mode 100644 index 0000000..2b8bf50 --- /dev/null +++ b/src/Platform/Console/Command/Environment/RsyncCommand.php @@ -0,0 +1,130 @@ +console = $console; + $this->wizard = new EnvironmentWizard(); + parent::__construct($name); + } + + protected function configure() + { + $this->setDescription('Deploy the application to a remote server using rsync and initialize docker containers'); + + $this->wizard->configureConsoleCommand($this); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $workingDirectory = $input->getOption('working-directory') ?: getcwd(); + + $finder = (new Finder()) + ->files() + ->ignoreDotFiles(false) + ->in($workingDirectory); + + $format = new SymfonyStyle($input, $output); + + $serializer = new Serializer( + [ + new CustomNormalizer(), + new PropertyNormalizer(), + ], + [ + new YamlEncoder(), + ] + ); + + if ($finder->hasResults()) { + /** @var SplFileInfo $file */ + foreach ($finder->name('/^\.?kloud.environment.ya?ml$/') as $file) { + try { + /** @var \Kiboko\Cloud\Domain\Stack\DTO\Context $context */ + $context = $serializer->deserialize($file->getContents(), Context::class, 'yaml'); + } catch (\Throwable $exception) { + $format->error($exception->getMessage()); + continue; + } + + break; + } + } + + if (!isset($context)) { + $format->error('No .kloud.environment.yaml file found in your directory. You must initialize it using environment:init command'); + + return 1; + } + + $application = new Application($this->console->getName()); + $deployer = new Deployer($application); + $deployer['output'] = $output; + $deployer['log_handler'] = function ($deployer) { + return !empty($deployer->config['log_file']) + ? new FileHandler($deployer->config['log_file']) + : new NullHandler(); + }; + $deployer['logger'] = function ($deployer) { + return new Logger($deployer['log_handler']); + }; + $rsync = new Rsync(new ProcessOutputPrinter($output, $deployer['logger'])); + + /** @var Context $context */ + $host = new Host($context->deployment->server->hostname); + $host->port($context->deployment->server->port); + $host->user($context->deployment->server->username); + + $destination = $host->getUser().'@'.$host->getHostname().':'.$context->deployment->path; + $config = [ + 'options' => [ + '--delete', + ], + ]; + + try { + $format->note('Syncing remote directory with local directory'); + $rsync->call($host->getHostname(), $workingDirectory, $destination, $config); + $format->success('Remote directory synced with local directory'); + } catch (ProcessFailedException $exception) { + $format->error($exception->getMessage()); + + return 1; + } + + return 0; + } +} diff --git a/src/Platform/Console/Command/Environment/Variable/GetCommand.php b/src/Platform/Console/Command/Environment/Variable/GetCommand.php index b7d36d1..8788fd4 100644 --- a/src/Platform/Console/Command/Environment/Variable/GetCommand.php +++ b/src/Platform/Console/Command/Environment/Variable/GetCommand.php @@ -7,7 +7,7 @@ use Kiboko\Cloud\Domain\Environment\DTO\Context; use Kiboko\Cloud\Domain\Environment\DTO\SecretValueEnvironmentVariable; use Kiboko\Cloud\Domain\Environment\DTO\ValuedEnvironmentVariableInterface; -use Kiboko\Cloud\Domain\Environment\VariableNotFoundException; +use Kiboko\Cloud\Domain\Environment\Exception\VariableNotFoundException; use Kiboko\Cloud\Domain\Stack\Compose\EnvironmentVariableInterface; use Kiboko\Cloud\Platform\Console\EnvironmentWizard; use Symfony\Component\Console\Command\Command; diff --git a/src/Platform/Console/Command/Environment/Variable/SetCommand.php b/src/Platform/Console/Command/Environment/Variable/SetCommand.php index d319ca0..bfbaa58 100644 --- a/src/Platform/Console/Command/Environment/Variable/SetCommand.php +++ b/src/Platform/Console/Command/Environment/Variable/SetCommand.php @@ -10,7 +10,7 @@ use Kiboko\Cloud\Domain\Environment\DTO\EnvironmentVariableInterface; use Kiboko\Cloud\Domain\Environment\DTO\SecretValueEnvironmentVariable; use Kiboko\Cloud\Domain\Environment\DTO\ValuedEnvironmentVariableInterface; -use Kiboko\Cloud\Domain\Environment\VariableNotFoundException; +use Kiboko\Cloud\Domain\Environment\Exception\VariableNotFoundException; use Kiboko\Cloud\Platform\Console\EnvironmentWizard; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; diff --git a/src/Platform/Console/Command/Environment/Variable/UnsetCommand.php b/src/Platform/Console/Command/Environment/Variable/UnsetCommand.php index 68bda77..cdcdfc1 100644 --- a/src/Platform/Console/Command/Environment/Variable/UnsetCommand.php +++ b/src/Platform/Console/Command/Environment/Variable/UnsetCommand.php @@ -7,7 +7,7 @@ use Kiboko\Cloud\Domain\Environment\DTO\Context; use Kiboko\Cloud\Domain\Environment\DTO\EnvironmentVariable; use Kiboko\Cloud\Domain\Environment\DTO\EnvironmentVariableInterface; -use Kiboko\Cloud\Domain\Environment\VariableNotFoundException; +use Kiboko\Cloud\Domain\Environment\Exception\VariableNotFoundException; use Kiboko\Cloud\Platform\Console\EnvironmentWizard; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface;