diff --git a/bin/kloud b/bin/kloud index e5be8e3..b503b4a 100755 --- a/bin/kloud +++ b/bin/kloud @@ -1,8 +1,8 @@ #!/usr/bin/env php addCommands([ (new Command\Images\BuildCommand( - $commandRunner, + $commandRunner, __DIR__ . '/../config/', __DIR__ . '/../environments', Command\Images\BuildCommand::$defaultName, @@ -68,6 +68,80 @@ $app->addCommands([ __DIR__ . '/../compose/', Command\Stack\UpgradeCommand::$defaultName, ))->setAliases(['upgrade']), + + (new Command\Environment\InitCommand( + Command\Environment\InitCommand::$defaultName, + )), + + (new Command\Environment\Variable\AddCommand( + Command\Environment\Variable\AddCommand::$defaultName, + )), + + (new Command\Environment\Variable\UnsetCommand( + Command\Environment\Variable\UnsetCommand::$defaultName, + )), + + (new Command\Environment\Variable\GetCommand( + Command\Environment\Variable\GetCommand::$defaultName, + )), + + (new Command\Environment\Variable\SetCommand( + Command\Environment\Variable\SetCommand::$defaultName, + )), + + (new Command\Environment\Variable\ListCommand( + Command\Environment\Variable\ListCommand::$defaultName, + )), + + (new Command\Environment\DeployCommand( + Command\Environment\DeployCommand::$defaultName, + $app, + )), + + (new Command\Environment\DestroyCommand( + Command\Environment\DestroyCommand::$defaultName, + $app, + )), + + (new Command\Environment\StartCommand( + Command\Environment\StartCommand::$defaultName, + $app, + )), + + (new Command\Environment\StopCommand( + Command\Environment\StopCommand::$defaultName, + $app, + )), + + (new Command\Environment\RsyncCommand( + Command\Environment\RsyncCommand::$defaultName, + $app, + )), + + (new Command\Environment\Cache\ClearCommand( + Command\Environment\Cache\ClearCommand::$defaultName, + $app, + )), + + (new Command\Environment\Database\DumpCommand( + Command\Environment\Database\DumpCommand::$defaultName, + $app, + )), + + (new Command\Environment\Database\LoadCommand( + Command\Environment\Database\LoadCommand::$defaultName, + $app, + )), + + (new Command\Environment\ShellCommand( + Command\Environment\ShellCommand::$defaultName, + $app, + )), + + (new Command\Environment\ProxyCommand( + Command\Environment\ProxyCommand::$defaultName, + $app, + )), ]); $app->run($input, $output); diff --git a/composer.json b/composer.json index da271da..7153998 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,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 2eeaf8b..b9744b6 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": "26880f37b6892f4e5b66345648ffd0d6", + "content-hash": "218e72c75e07b49f15a0b7d9ab9cf7c1", "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", @@ -245,6 +354,56 @@ "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", @@ -709,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" }, { @@ -772,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" }, { @@ -1085,20 +1216,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-10-23T14:02:19+00:00" }, { @@ -1297,20 +1414,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" }, { @@ -1530,12 +1633,6 @@ "Xdebug", "performance" ], - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - } - ], "time": "2020-03-01T12:26:26+00:00" }, { @@ -2024,12 +2121,6 @@ "object", "object graph" ], - "funding": [ - { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" - } - ], "time": "2020-06-29T13:22:24+00:00" }, { @@ -2897,16 +2988,6 @@ "testing", "xunit" ], - "funding": [ - { - "url": "https://phpunit.de/donate.html", - "type": "custom" - }, - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], "time": "2020-06-22T07:06:58+00:00" }, { @@ -3398,20 +3479,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" }, { @@ -3840,6 +3907,5 @@ "php": "^7.4", "ext-json": "*" }, - "platform-dev": [], - "plugin-api-version": "1.1.0" + "platform-dev": [] } diff --git a/docs/usage.md b/docs/usage.md index 1d9b17f..bdc7e1f 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -63,3 +63,24 @@ Test the required images If you need to test if the images you are using are following every constraint you would expect: `kloud image:test` + +Environment commands +--- + +After `kloud stack:init`, you can use several environment commands to do several things on a remote server + +* `kloud environment:init`: Initialize the environment file in local workspace. +* `kloud environment:variable:add`: Add an environment variable. +* `kloud environment:variable:unset`: Unset an environment variable value. +* `kloud environment:variable:get`: Print an environment variable value. +* `kloud environment:variable:set`: Change an environment variable value. +* `kloud environment:variable:list`: Print the list of environment variables and their value. +* `kloud environment:deploy`: Deploy the application to a remote server using rsync and initialize docker services. +* `kloud environment:destroy`: Destroy the docker infrastructure with associated volumes and remove remote directory. +* `kloud environment:start`: Start docker services on the remote server. +* `kloud environment:stop`: Stop docker services on the remote server. +* `kloud environment:rsync`: Synchronize remote directory according to local directory. +* `kloud environment:cache:clear`: Clear cache and restart FPM service. +* `kloud environment:database:dump`: Dump the database in the current state. +* `kloud environment:database:load`: Load a database dump. +* `kloud environment:shell`: Start a shell session for a service. diff --git a/src/Domain/Environment/DTO/Context.php b/src/Domain/Environment/DTO/Context.php new file mode 100644 index 0000000..b975a6d --- /dev/null +++ b/src/Domain/Environment/DTO/Context.php @@ -0,0 +1,111 @@ +deployment = $deployment; + $this->database = $database; + $this->environmentVariables = []; + } + + public function addVariable(EnvironmentVariableInterface ...$variable): void + { + array_push($this->environmentVariables, ...$variable); + } + + public function getVariable(string $variableName): EnvironmentVariableInterface + { + foreach ($this->environmentVariables as $variable) { + if ($variableName !== (string) $variable->getVariable()) { + continue; + } + + return $variable; + } + + throw new VariableNotFoundException(strtr('The variable %name% does not exist.', ['%name%' => $variableName])); + } + + public function setVariable(EnvironmentVariableInterface $newVariable): void + { + $i = 0; + foreach ($this->environmentVariables as $variable) { + if ((string) $newVariable->getVariable() !== (string) $variable->getVariable()) { + ++$i; + continue; + } + $this->environmentVariables[$i] = $newVariable; + + return; + } + } + + public function denormalize(DenormalizerInterface $denormalizer, $data, string $format = null, array $context = []) + { + $this->deployment = $denormalizer->denormalize($data['deployment'], Deployment::class, $format, $context); + $this->database = $denormalizer->denormalize($data['database'], Database::class, $format, $context); + $this->environmentVariables = []; + + $parser = new ExpressionParser(); + foreach ($data['environment'] as $variable) { + if (isset($variable['value'])) { + $this->environmentVariables[] = new DirectValueEnvironmentVariable( + new Variable($variable['name']), + $variable['value'] + ); + } elseif (isset($variable['secret'])) { + $this->environmentVariables[] = new SecretValueEnvironmentVariable( + new Variable($variable['name']), + $variable['secret'] + ); + } else { + $this->environmentVariables[] = new EnvironmentVariable( + new Variable($variable['name']) + ); + } + } + } + + public function normalize(NormalizerInterface $normalizer, string $format = null, array $context = []) + { + return [ + 'deployment' => $normalizer->normalize($this->deployment, $format, $context), + 'database' => $normalizer->normalize($this->database, $format, $context), + 'environment' => iterator_to_array((function ($variables) { + /** @var EnvironmentVariableInterface $variable */ + foreach ($variables as $variable) { + if ($variable instanceof DirectValueEnvironmentVariable) { + yield [ + 'name' => (string) $variable->getVariable(), + 'value' => $variable->getValue(), + ]; + } elseif ($variable instanceof SecretValueEnvironmentVariable) { + yield [ + 'name' => (string) $variable->getVariable(), + 'secret' => $variable->getSecret(), + ]; + } else { + yield [ + 'name' => (string) $variable->getVariable(), + ]; + } + } + })($this->environmentVariables)), + ]; + } +} diff --git a/src/Domain/Environment/DTO/Database.php b/src/Domain/Environment/DTO/Database.php new file mode 100644 index 0000000..8e84b70 --- /dev/null +++ b/src/Domain/Environment/DTO/Database.php @@ -0,0 +1,19 @@ +databaseName = $databaseName; + $this->username = $username; + $this->password = $password; + } +} diff --git a/src/Domain/Environment/DTO/Deployment.php b/src/Domain/Environment/DTO/Deployment.php new file mode 100644 index 0000000..0ef1dc6 --- /dev/null +++ b/src/Domain/Environment/DTO/Deployment.php @@ -0,0 +1,17 @@ +server = $server; + $this->path = $path; + } +} diff --git a/src/Domain/Environment/DTO/DirectValueEnvironmentVariable.php b/src/Domain/Environment/DTO/DirectValueEnvironmentVariable.php new file mode 100644 index 0000000..8822746 --- /dev/null +++ b/src/Domain/Environment/DTO/DirectValueEnvironmentVariable.php @@ -0,0 +1,36 @@ +variable = $variable; + $this->value = $value; + } + + public function getVariable(): Variable + { + return $this->variable; + } + + /** + * @return int|Expression|Variable|string + */ + public function getValue() + { + return $this->value; + } + + public function setValue(string $value) + { + $this->value = $value; + } +} diff --git a/src/Domain/Environment/DTO/EnvironmentVariable.php b/src/Domain/Environment/DTO/EnvironmentVariable.php new file mode 100644 index 0000000..bbe038c --- /dev/null +++ b/src/Domain/Environment/DTO/EnvironmentVariable.php @@ -0,0 +1,20 @@ +variable = $variable; + } + + public function getVariable(): Variable + { + return $this->variable; + } +} diff --git a/src/Domain/Environment/DTO/EnvironmentVariableInterface.php b/src/Domain/Environment/DTO/EnvironmentVariableInterface.php new file mode 100644 index 0000000..2c18121 --- /dev/null +++ b/src/Domain/Environment/DTO/EnvironmentVariableInterface.php @@ -0,0 +1,10 @@ +elements = $elements; + } + + public function __toString() + { + return implode('', array_map(function ($item) { + return (string) $item; + }, $this->elements)); + } + + public function normalize(NormalizerInterface $normalizer, string $format = null, array $context = []) + { + return (string) $this; + } + + public function denormalize(DenormalizerInterface $denormalizer, $data, string $format = null, array $context = []) + { + // TODO: Implement denormalize() method. + } +} diff --git a/src/Domain/Environment/DTO/ExpressionParser.php b/src/Domain/Environment/DTO/ExpressionParser.php new file mode 100644 index 0000000..4bf0f99 --- /dev/null +++ b/src/Domain/Environment/DTO/ExpressionParser.php @@ -0,0 +1,34 @@ + 0) { + $elements[] = $matches[1]; + } + + if (isset($matches[3])) { + $elements[] = new Variable($matches[3]); + } elseif (isset($matches[2])) { + $elements[] = new Variable($matches[2]); + } + + $offset += strlen($matches[0]); + } + + return new Expression(...$elements); + } +} diff --git a/src/Domain/Environment/DTO/SecretValueEnvironmentVariable.php b/src/Domain/Environment/DTO/SecretValueEnvironmentVariable.php new file mode 100644 index 0000000..eaab579 --- /dev/null +++ b/src/Domain/Environment/DTO/SecretValueEnvironmentVariable.php @@ -0,0 +1,32 @@ +variable = $variable; + $this->secret = $secret; + } + + public function getVariable(): Variable + { + return $this->variable; + } + + public function getSecret(): string + { + return $this->secret; + } + + public function setSecret(string $secret) + { + $this->secret = $secret; + } +} diff --git a/src/Domain/Environment/DTO/Server.php b/src/Domain/Environment/DTO/Server.php new file mode 100644 index 0000000..c63f8c8 --- /dev/null +++ b/src/Domain/Environment/DTO/Server.php @@ -0,0 +1,19 @@ +hostname = $hostname; + $this->port = $port; + $this->username = $username; + } +} diff --git a/src/Domain/Environment/DTO/ValuedEnvironmentVariableInterface.php b/src/Domain/Environment/DTO/ValuedEnvironmentVariableInterface.php new file mode 100644 index 0000000..5c7befa --- /dev/null +++ b/src/Domain/Environment/DTO/ValuedEnvironmentVariableInterface.php @@ -0,0 +1,15 @@ +name = $name; + } + + public function __toString() + { + return $this->name; + } +} diff --git a/src/Domain/Environment/Exception/VariableNotFoundException.php b/src/Domain/Environment/Exception/VariableNotFoundException.php new file mode 100644 index 0000000..b9a534d --- /dev/null +++ b/src/Domain/Environment/Exception/VariableNotFoundException.php @@ -0,0 +1,7 @@ +repository = $repository; diff --git a/src/Platform/Console/Command/Environment/Cache/ClearCommand.php b/src/Platform/Console/Command/Environment/Cache/ClearCommand.php new file mode 100644 index 0000000..b497d2d --- /dev/null +++ b/src/Platform/Console/Command/Environment/Cache/ClearCommand.php @@ -0,0 +1,127 @@ +console = $console; + $this->wizard = new EnvironmentWizard(); + parent::__construct($name); + } + + protected function configure() + { + $this->setDescription('Clear cache and restart FPM service'); + + $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; + } + + $env = $format->askQuestion(new ChoiceQuestion('For what environment ?', ['prod', 'dev', 'test'], 'prod')); + + $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 = [ + 'cache:clear' => 'cd '.$context->deployment->path.'/'.$projectName.' && docker-compose exec -T sh bin/console cache:clear --env='.$env, + 'docker:restart-fpm' => 'cd '.$context->deployment->path.'/'.$projectName.' && docker-compose restart fpm', + ]; + + 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/Database/DumpCommand.php b/src/Platform/Console/Command/Environment/Database/DumpCommand.php new file mode 100644 index 0000000..7f4b9bf --- /dev/null +++ b/src/Platform/Console/Command/Environment/Database/DumpCommand.php @@ -0,0 +1,163 @@ +console = $console; + $this->wizard = new EnvironmentWizard(); + parent::__construct($name); + } + + protected function configure() + { + $this->setDescription('Dump the database in the current state'); + + $this->wizard->configureConsoleCommand($this); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $workingDirectory = $input->getOption('working-directory') ?: getcwd(); + + $environmentFinder = (new Finder()) + ->files() + ->ignoreDotFiles(false) + ->in($workingDirectory) + ->name('/^\.?kloud.environment.ya?ml$/'); + + $stackFinder = (new Finder()) + ->files() + ->ignoreDotFiles(false) + ->in($workingDirectory) + ->name('/^\.?kloud.ya?ml$/'); + + $format = new SymfonyStyle($input, $output); + + $serializer = new Serializer( + [ + new StackContextNormalizer(), + new CustomNormalizer(), + new PropertyNormalizer(), + ], + [ + new YamlEncoder(), + ] + ); + + if ($environmentFinder->hasResults()) { + foreach ($environmentFinder as $environmentFile) { + try { + /** @var EnvironmentContext $environementContext */ + $environementContext = $serializer->deserialize($environmentFile->getContents(), EnvironmentContext::class, 'yaml'); + } catch (\Throwable $exception) { + $format->error($exception->getMessage()); + continue; + } + + break; + } + foreach ($stackFinder as $stackFile) { + try { + /** @var StackContext $stackContext */ + $stackContext = $serializer->deserialize($stackFile->getContents(), StackContext::class, 'yaml'); + } catch (\Throwable $exception) { + $format->error($exception->getMessage()); + continue; + } + + break; + } + } + + if (!isset($environementContext)) { + $format->error('No .kloud.environment.yaml file found in your directory. You must initialize it using environment:init command'); + + return 1; + } + + $host = new Host($environementContext->deployment->server->hostname); + $host->port($environementContext->deployment->server->port); + $host->user($environementContext->deployment->server->username); + + $directories = explode('/', $workingDirectory); + $projectName = end($directories); + $remoteProjectPath = $environementContext->deployment->path.'/'.$projectName; + + $sqlService = $format->askQuestion(new Question('What is the name of your SQL service?', 'sql')); + $process = new Process(['ssh', '-t', $host->getUser().'@'.$host->getHostname(), 'cd', $remoteProjectPath, '&&', 'docker-compose', 'ps', '-q', $sqlService]); + try { + $process->mustRun(); + $containerIds = rtrim($process->getOutput(), PHP_EOL); + } catch (\Exception $exception) { + $format->error($exception->getMessage()); + + return 1; + } + + if (!empty($stackContext->dbms)) { + $dbms = $stackContext->dbms; + } else { + $dbms = strtolower($format->askQuestion(new ChoiceQuestion('Is it a MySQL or PostgreSQL database?', ['MySQL', 'PostgreSQL']))); + } + + $dumpName = $format->askQuestion(new Question('How do you want to name it?', 'dump.sql')); + $dumpPath = $remoteProjectPath.'/.docker/'.$dumpName; + $databaseName = $environementContext->database->databaseName; + $username = $environementContext->database->username; + $password = $environementContext->database->password; + + if ('postgresql' === $dbms) { + $process2 = new Process(['ssh', '-t', $host->getUser().'@'.$host->getHostname(), 'docker', 'exec', '-i', $containerIds, 'pg_dump', '-U', $username, $databaseName, '>', $dumpPath]); + try { + $process2->setTimeout(0)->mustRun(); + $format->success('Dump well created at '.$host->getUser().'@'.$host->getHostname().':'.$dumpPath); + + return 0; + } catch (\Exception $exception) { + $format->error($exception->getMessage()); + + return 1; + } + } else { + $process2 = new Process(['ssh', '-t', $host->getUser().'@'.$host->getHostname(), 'docker', 'exec', $containerIds, '/usr/bin/mysqldump', '-u', $username, '--password='.$password, $databaseName, '>', $dumpPath]); + try { + $process2->setTimeout(0)->mustRun(); + $format->success('Dump well created at '.$host->getUser().'@'.$host->getHostname().':'.$dumpPath); + + return 0; + } catch (\Exception $exception) { + $format->error($exception->getMessage()); + + return 1; + } + } + } +} diff --git a/src/Platform/Console/Command/Environment/Database/LoadCommand.php b/src/Platform/Console/Command/Environment/Database/LoadCommand.php new file mode 100644 index 0000000..a2612c8 --- /dev/null +++ b/src/Platform/Console/Command/Environment/Database/LoadCommand.php @@ -0,0 +1,163 @@ +console = $console; + $this->wizard = new EnvironmentWizard(); + parent::__construct($name); + } + + protected function configure() + { + $this->setDescription('Load a database dump'); + + $this->wizard->configureConsoleCommand($this); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $workingDirectory = $input->getOption('working-directory') ?: getcwd(); + + $environmentFinder = (new Finder()) + ->files() + ->ignoreDotFiles(false) + ->in($workingDirectory) + ->name('/^\.?kloud.environment.ya?ml$/'); + + $stackFinder = (new Finder()) + ->files() + ->ignoreDotFiles(false) + ->in($workingDirectory) + ->name('/^\.?kloud.ya?ml$/'); + + $format = new SymfonyStyle($input, $output); + + $serializer = new Serializer( + [ + new StackContextNormalizer(), + new CustomNormalizer(), + new PropertyNormalizer(), + ], + [ + new YamlEncoder(), + ] + ); + + if ($environmentFinder->hasResults()) { + foreach ($environmentFinder->name('/^\.?kloud.environment.ya?ml$/') as $environmentFile) { + try { + /** @var EnvironmentContext $environementContext */ + $environementContext = $serializer->deserialize($environmentFile->getContents(), EnvironmentContext::class, 'yaml'); + } catch (\Throwable $exception) { + $format->error($exception->getMessage()); + continue; + } + + break; + } + foreach ($stackFinder->name('/^\.?kloud.ya?ml$/') as $stackFile) { + try { + /** @var StackContext $stackContext */ + $stackContext = $serializer->deserialize($stackFile->getContents(), StackContext::class, 'yaml'); + } catch (\Throwable $exception) { + $format->error($exception->getMessage()); + continue; + } + + break; + } + } + + if (!isset($environementContext)) { + $format->error('No .kloud.environment.yaml file found in your directory. You must initialize it using environment:init command'); + + return 1; + } + + $host = new Host($environementContext->deployment->server->hostname); + $host->port($environementContext->deployment->server->port); + $host->user($environementContext->deployment->server->username); + + $directories = explode('/', $workingDirectory); + $projectName = end($directories); + $remoteProjectPath = $environementContext->deployment->path.'/'.$projectName; + + $sqlService = $format->askQuestion(new Question('What is the name of your SQL service?', 'sql')); + $process = new Process(['ssh', '-t', $host->getUser().'@'.$host->getHostname(), 'cd', $remoteProjectPath, '&&', 'docker-compose', 'ps', '-q', $sqlService]); + try { + $process->mustRun(); + $containerIds = rtrim($process->getOutput(), PHP_EOL); + } catch (\Exception $exception) { + $format->error($exception->getMessage()); + + return 1; + } + + if (!empty($stackContext->dbms)) { + $dbms = $stackContext->dbms; + } else { + $dbms = strtolower($format->askQuestion(new ChoiceQuestion('Is it a MySQL or PostgreSQL database?', ['MySQL', 'PostgreSQL']))); + } + + $dumpName = $format->askQuestion(new Question('Name of your SQL dump to load')); + $dumpPath = $remoteProjectPath.'/.docker/'.$dumpName; + $databaseName = $environementContext->database->databaseName; + $username = $environementContext->database->username; + $password = $environementContext->database->password; + + if ('postgresql' === $dbms) { + $process2 = new Process(['ssh', '-t', $host->getUser().'@'.$host->getHostname(), 'docker', 'exec', '-i', $containerIds, 'psql', '-U', $username, $databaseName, '<', $dumpPath]); + try { + $process2->setTimeout(0)->mustRun(); + $format->success('Dump well loaded'); + + return 0; + } catch (\Exception $exception) { + $format->error($exception->getMessage()); + + return 1; + } + } else { + $process2 = new Process(['ssh', '-t', $host->getUser().'@'.$host->getHostname(), 'docker', 'exec', '-i', $containerIds, 'mysql', '-u', $username, '--password='.$password, $databaseName, '<', $dumpPath]); + try { + $process2->setTimeout(0)->mustRun(); + $format->success('Dump well loaded'); + + return 0; + } catch (\Exception $exception) { + $format->error($exception->getMessage()); + + return 1; + } + } + } +} diff --git a/src/Platform/Console/Command/Environment/DeployCommand.php b/src/Platform/Console/Command/Environment/DeployCommand.php new file mode 100644 index 0000000..00b21c8 --- /dev/null +++ b/src/Platform/Console/Command/Environment/DeployCommand.php @@ -0,0 +1,146 @@ +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 services'); + + $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..2efdc24 --- /dev/null +++ b/src/Platform/Console/Command/Environment/DestroyCommand.php @@ -0,0 +1,125 @@ +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); + $cd = 'cd '.$context->deployment->path; + + $commands = [ + 'docker:down' => $cd.'/'.$projectName.' && docker-compose down -v', + 'directory:remove' => $cd.' && 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/InitCommand.php b/src/Platform/Console/Command/Environment/InitCommand.php new file mode 100644 index 0000000..e0534b2 --- /dev/null +++ b/src/Platform/Console/Command/Environment/InitCommand.php @@ -0,0 +1,122 @@ +wizard = new EnvironmentWizard(); + parent::__construct($name); + } + + protected function configure() + { + $this->setDescription('Initialize the environment file in local workspace'); + + $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) { + $format->error('The directory was already initialized with an environment file. You should update it using commands listed in environment:variable'); + + return 0; + } + } + + $format = new SymfonyStyle($input, $output); + + $context = new Context( + new Deployment( + new Server( + $format->askQuestion(new Question('Please provide the SSH host of your remote environment')), + $format->askQuestion(new Question('Please provide the SSH port of your remote environment', 22)), + $format->askQuestion(new Question('Please provide the SSH user name of your remote environment', 'root')), + ), + $format->askQuestion(new Question('Please provide the path to your remote environment')), + ), + new Database( + $format->askQuestion(new Question('Please provide the name of your database')), + $format->askQuestion(new Question('Please provide the user\'s name of your database')), + $format->askQuestion(new Question('Please provide the user\'s password of your database')), + ), + ); + + $envDistPath = getcwd().'/.env.dist'; + if (file_exists($envDistPath)) { + $envDist = parse_ini_file($envDistPath); + foreach (array_keys($envDist) as $name) { + $value = $format->askQuestion(new Question('Value of '.$name)); + + $isSecret = false; + if ($value) { + $isSecret = $format->askQuestion(new ConfirmationQuestion('Is this a secret variable ?', false)); + } + + if ($isSecret) { + $context->addVariable(new SecretValueEnvironmentVariable(new Variable($name), $value)); + } else { + $context->addVariable(new DirectValueEnvironmentVariable(new Variable($name), $value)); + } + } + } + + $format->note('Writing a new .kloud.environment.yaml file.'); + file_put_contents($workingDirectory.'/.kloud.environment.yaml', $serializer->serialize($context, 'yaml', [ + 'yaml_inline' => 4, + 'yaml_indent' => 0, + 'yaml_flags' => 0, + ])); + + return 0; + } +} diff --git a/src/Platform/Console/Command/Environment/ProxyCommand.php b/src/Platform/Console/Command/Environment/ProxyCommand.php new file mode 100644 index 0000000..2663e6f --- /dev/null +++ b/src/Platform/Console/Command/Environment/ProxyCommand.php @@ -0,0 +1,111 @@ +console = $console; + $this->wizard = new EnvironmentWizard(); + parent::__construct($name); + } + + protected function configure() + { + $this->setDescription('Port forwarding using ssh tunnel'); + + $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 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; + } + + $host = new Host($context->deployment->server->hostname); + $host->port($context->deployment->server->port); + $host->user($context->deployment->server->username); + + $type = $format->askQuestion(new ChoiceQuestion('What type of port forwarding is it?', ['Local to remote', 'Remote to local'])); + $localPort = $format->askQuestion(new Question('What is the local port you want to tunnel?')); + $remotePort = $format->askQuestion(new Question('What is the remote port you want to tunnel?')); + + if ('Local' === $type) { + $process = new Process(['ssh', '-L', $localPort.':'.$host->getHostname().':'.$remotePort, $host->getUser().'@'.$host->getHostname()]); + } else { + $process = new Process(['ssh', '-R', $remotePort.':127.0.0.1:'.$localPort, $host->getUser().'@'.$host->getHostname()]); + } + + try { + $process->setTty(Process::isTtySupported())->setTimeout(0)->mustRun(); + } catch (\Exception $exception) { + $format->error($exception->getMessage()); + + return 1; + } + + 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..4de23c5 --- /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('Synchronize remote directory according to local 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; + $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/ShellCommand.php b/src/Platform/Console/Command/Environment/ShellCommand.php new file mode 100644 index 0000000..9df1656 --- /dev/null +++ b/src/Platform/Console/Command/Environment/ShellCommand.php @@ -0,0 +1,116 @@ +console = $console; + $this->wizard = new EnvironmentWizard(); + parent::__construct($name); + } + + protected function configure() + { + $this->setDescription('Start a shell session for a service'); + + $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 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; + } + + $host = new Host($context->deployment->server->hostname); + $host->port($context->deployment->server->port); + $host->user($context->deployment->server->username); + + $directories = explode('/', $workingDirectory); + $projectName = end($directories); + $remoteProjectPath = $context->deployment->path.'/'.$projectName; + + $service = $format->askQuestion(new Question('For what service you want to start a shell session?')); + $process = new Process(['ssh', '-t', $host->getUser().'@'.$host->getHostname(), 'cd', $remoteProjectPath, '&&', 'docker-compose', 'ps', '-q', $service]); + try { + $process->mustRun(); + $containerIds = rtrim($process->getOutput(), PHP_EOL); + } catch (\Exception $exception) { + $format->error($exception->getMessage()); + + return 1; + } + + $process2 = new Process(['ssh', '-t', $host->getUser().'@'.$host->getHostname(), 'cd', $remoteProjectPath, '&&', 'docker', 'exec', '-ti', $containerIds, 'sh']); + try { + $process2->setTty(Process::isTtySupported())->setTimeout(0)->mustRun(); + } catch (\Exception $exception) { + $format->error($exception->getMessage()); + + return 1; + } + + return 0; + } +} diff --git a/src/Platform/Console/Command/Environment/StartCommand.php b/src/Platform/Console/Command/Environment/StartCommand.php new file mode 100644 index 0000000..c12b3fa --- /dev/null +++ b/src/Platform/Console/Command/Environment/StartCommand.php @@ -0,0 +1,119 @@ +console = $console; + $this->wizard = new EnvironmentWizard(); + parent::__construct($name); + } + + protected function configure() + { + $this->setDescription('Start docker services on the remote server'); + + $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); + + $command = 'cd '.$context->deployment->path.'/'.$projectName.' && docker-compose start'; + + array_push($tasks, new Task('docker:start', 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/StopCommand.php b/src/Platform/Console/Command/Environment/StopCommand.php new file mode 100644 index 0000000..c4684a7 --- /dev/null +++ b/src/Platform/Console/Command/Environment/StopCommand.php @@ -0,0 +1,119 @@ +console = $console; + $this->wizard = new EnvironmentWizard(); + parent::__construct($name); + } + + protected function configure() + { + $this->setDescription('Stop docker services on the remote server'); + + $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); + + $command = 'cd '.$context->deployment->path.'/'.$projectName.' && docker-compose stop'; + + array_push($tasks, new Task('docker:stop', 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/Variable/AddCommand.php b/src/Platform/Console/Command/Environment/Variable/AddCommand.php new file mode 100644 index 0000000..e418f29 --- /dev/null +++ b/src/Platform/Console/Command/Environment/Variable/AddCommand.php @@ -0,0 +1,109 @@ +wizard = new EnvironmentWizard(); + parent::__construct($name); + } + + protected function configure() + { + $this->setDescription('Add an environment variable'); + + $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 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; + } + + $variableName = $format->askQuestion(new Question('Please enter a variable name')); + $variableValue = $format->askQuestion(new Question('Please enter '.$variableName.' value')); + + $isSecret = false; + if ($variableValue) { + $isSecret = $format->askQuestion(new ConfirmationQuestion('Is this a secret variable ?', false)); + } + + if ($isSecret) { + $context->addVariable(new SecretValueEnvironmentVariable(new Variable($variableName), $variableValue)); + } else { + $context->addVariable(new DirectValueEnvironmentVariable(new Variable($variableName), $variableValue)); + } + + $format->note('Writing a new .kloud.environment.yaml file.'); + file_put_contents($workingDirectory.'/.kloud.environment.yaml', $serializer->serialize($context, 'yaml', [ + 'yaml_inline' => 4, + 'yaml_indent' => 0, + 'yaml_flags' => 0, + ])); + + return 0; + } +} diff --git a/src/Platform/Console/Command/Environment/Variable/GetCommand.php b/src/Platform/Console/Command/Environment/Variable/GetCommand.php new file mode 100644 index 0000000..37298a0 --- /dev/null +++ b/src/Platform/Console/Command/Environment/Variable/GetCommand.php @@ -0,0 +1,113 @@ +wizard = new EnvironmentWizard(); + parent::__construct($name); + } + + protected function configure() + { + $this->setDescription('Print an environment variable value'); + + $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 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; + } + + $variableName = $format->askQuestion(new Question('Please enter a variable name')); + + try { + /** @var EnvironmentVariableInterface $variable */ + $variable = $context->getVariable($variableName); + } catch (VariableNotFoundException $exception) { + $format->error($exception->getMessage()); + + return 1; + } + + $format->table( + ['Variable', 'Value'], + [ + [ + $variableName, + $variable instanceof ValuedEnvironmentVariableInterface ? + $variable->getValue() : + ($variable instanceof SecretValueEnvironmentVariable ? + sprintf('SECRET: %s', $variable->getSecret()) : + null), + ], + ] + ); + + return 0; + } +} diff --git a/src/Platform/Console/Command/Environment/Variable/ListCommand.php b/src/Platform/Console/Command/Environment/Variable/ListCommand.php new file mode 100644 index 0000000..64e301d --- /dev/null +++ b/src/Platform/Console/Command/Environment/Variable/ListCommand.php @@ -0,0 +1,103 @@ +wizard = new EnvironmentWizard(); + parent::__construct($name); + } + + protected function configure() + { + $this->setDescription('Print the list of environment variables and their value'); + + $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; + } + + $format->table( + ['Variable', 'Value'], + iterator_to_array((function (Context $context): \Generator { + /** @var EnvironmentVariableInterface|ValuedEnvironmentVariableInterface $variable */ + foreach ($context->environmentVariables as $variable) { + yield [ + (string) $variable->getVariable(), + $variable instanceof ValuedEnvironmentVariableInterface ? + $variable->getValue() : + ($variable instanceof SecretValueEnvironmentVariable ? + sprintf('SECRET: %s', $variable->getSecret()) : + null), + ]; + } + })($context)), + ); + + return 0; + } +} diff --git a/src/Platform/Console/Command/Environment/Variable/SetCommand.php b/src/Platform/Console/Command/Environment/Variable/SetCommand.php new file mode 100644 index 0000000..957bcd4 --- /dev/null +++ b/src/Platform/Console/Command/Environment/Variable/SetCommand.php @@ -0,0 +1,170 @@ +wizard = new EnvironmentWizard(); + parent::__construct($name); + } + + protected function configure() + { + $this->setDescription('Change an environment variable value'); + + $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 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; + } + + $variableName = $format->askQuestion(new Question('Please enter a variable name')); + + try { + /** @var EnvironmentVariableInterface $variable */ + $variable = $context->getVariable($variableName); + } catch (VariableNotFoundException $exception) { + $format->error($exception->getMessage()); + + return 1; + } + + $format->table( + ['Variable', 'Value'], + [ + [ + $variableName, + $variable instanceof ValuedEnvironmentVariableInterface ? + $variable->getValue() : + ($variable instanceof SecretValueEnvironmentVariable ? + sprintf('SECRET: %s', $variable->getSecret()) : + null), + ], + ] + ); + + // If value is empty, $variable becomes/stay an EnvironmentVariable without any value. + if (!$value = $this->verifyValue($context, $format, $variable)) { + $this->sendResponse($context, $format, $serializer, $workingDirectory); + + return 0; + } + + $isSecret = $format->askQuestion(new ConfirmationQuestion('Is this a secret variable ?', false)); + + // Test $variable type and potentially change it according to the answer + if ($variable instanceof ValuedEnvironmentVariableInterface) { + if ($isSecret) { + $context->setVariable(new SecretValueEnvironmentVariable($variable->getVariable(), $value)); + } else { + $variable->setValue($value); + } + } + if ($variable instanceof SecretValueEnvironmentVariable) { + if ($isSecret) { + $variable->setSecret($value); + } else { + $context->setVariable(new DirectValueEnvironmentVariable($variable->getVariable(), $value)); + } + } + if ($variable instanceof EnvironmentVariable) { + if ($isSecret) { + $context->setVariable(new SecretValueEnvironmentVariable($variable->getVariable(), $value)); + } else { + $context->setVariable(new DirectValueEnvironmentVariable($variable->getVariable(), $value)); + } + } + $this->sendResponse($context, $format, $serializer, $workingDirectory); + + return 0; + } + + private function sendResponse(Context $context, SymfonyStyle $format, Serializer $serializer, string $workingDirectory): void + { + $format->success('Variable was successfully changed'); + file_put_contents($workingDirectory.'/.kloud.environment.yaml', $serializer->serialize($context, 'yaml', [ + 'yaml_inline' => 4, + 'yaml_indent' => 0, + 'yaml_flags' => 0, + ])); + } + + private function verifyValue(Context $context, SymfonyStyle $format, EnvironmentVariableInterface $variable): ?string + { + if (!$value = $format->askQuestion(new Question('Please provide the new value'))) { + $context->setVariable(new EnvironmentVariable($variable->getVariable())); + + return null; + } + + return $value; + } +} diff --git a/src/Platform/Console/Command/Environment/Variable/UnsetCommand.php b/src/Platform/Console/Command/Environment/Variable/UnsetCommand.php new file mode 100644 index 0000000..03af391 --- /dev/null +++ b/src/Platform/Console/Command/Environment/Variable/UnsetCommand.php @@ -0,0 +1,107 @@ +wizard = new EnvironmentWizard(); + parent::__construct($name); + } + + protected function configure() + { + $this->setDescription('Unset an environment variable value'); + + $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 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; + } + + $variableName = $format->askQuestion(new Question('Please enter the variable to unset')); + + try { + /** @var EnvironmentVariableInterface $variable */ + $variable = $context->getVariable($variableName); + } catch (VariableNotFoundException $exception) { + $format->error($exception->getMessage()); + + return 1; + } + + $context->setVariable(new EnvironmentVariable($variable->getVariable())); + + $format->success('Variable was successfully unset'); + file_put_contents($workingDirectory.'/.kloud.environment.yaml', $serializer->serialize($context, 'yaml', [ + 'yaml_inline' => 4, + 'yaml_indent' => 0, + 'yaml_flags' => 0, + ])); + + return 0; + } +} diff --git a/src/Platform/Console/Command/Stack/InitCommand.php b/src/Platform/Console/Command/Stack/InitCommand.php index 1f00d07..9cb00d3 100644 --- a/src/Platform/Console/Command/Stack/InitCommand.php +++ b/src/Platform/Console/Command/Stack/InitCommand.php @@ -71,7 +71,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $format->note('Writing a new .kloud.yaml file.'); file_put_contents($workingDirectory . '/.kloud.yaml', $serializer->serialize($context, 'yaml', [ 'yaml_inline' => 2, - 'yaml_indent' => 2, + 'yaml_indent' => 0, 'yaml_flags' => 0 ])); diff --git a/src/Platform/Console/EnvironmentWizard.php b/src/Platform/Console/EnvironmentWizard.php new file mode 100644 index 0000000..ceea8ba --- /dev/null +++ b/src/Platform/Console/EnvironmentWizard.php @@ -0,0 +1,16 @@ +addOption('working-directory', null, InputOption::VALUE_OPTIONAL, 'Change the working directory in which the kloud environment file will be guessed from and written.'); + } +} diff --git a/src/Platform/Serializer/Normalizer/StackContextNormalizer.php b/src/Platform/Serializer/Normalizer/StackContextNormalizer.php new file mode 100644 index 0000000..c763518 --- /dev/null +++ b/src/Platform/Serializer/Normalizer/StackContextNormalizer.php @@ -0,0 +1,29 @@ +