Skip to content

Commit

Permalink
First code import
Browse files Browse the repository at this point in the history
  • Loading branch information
javiereguiluz committed May 26, 2017
1 parent 558f648 commit c66db26
Show file tree
Hide file tree
Showing 40 changed files with 3,102 additions and 2 deletions.
39 changes: 39 additions & 0 deletions .php_cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

$fileHeaderComment = <<<COMMENT
This file is part of the EasyDeploy project.
(c) Javier Eguiluz <[email protected]>
For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.
COMMENT;

return PhpCsFixer\Config::create()
->setRiskyAllowed(true)
->setRules([
'@Symfony' => true,
'@Symfony:risky' => true,
'array_syntax' => ['syntax' => 'short'],
'header_comment' => ['header' => $fileHeaderComment, 'separate' => 'both'],
'linebreak_after_opening_tag' => true,
'list_syntax' => ['syntax' => 'short'],
'mb_str_functions' => true,
'no_php4_constructor' => true,
'no_unreachable_default_argument_value' => true,
'no_useless_else' => true,
'no_useless_return' => true,
'ordered_class_elements' => true,
'ordered_imports' => true,
'php_unit_strict' => true,
'phpdoc_order' => true,
'pow_to_exponentiation' => true,
'random_api_migration' => true,
'return_type_declaration' => ['space_before' => 'one'],
'semicolon_after_instruction' => true,
'strict_comparison' => true,
'strict_param' => true,
'ternary_to_null_coalescing' => true,
])
->setCacheFile(__DIR__.'/.php_cs.cache')
;
20 changes: 20 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>

<!-- https://phpunit.de/manual/current/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.8/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php"
>
<php>
<ini name="error_reporting" value="-1" />
<server name="KERNEL_DIR" value="src/" />
</php>

<testsuites>
<testsuite name="Project Test Suite">
<directory>tests/</directory>
</testsuite>
</testsuites>
</phpunit>
113 changes: 113 additions & 0 deletions src/Command/DeployCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php

/*
* This file is part of the EasyDeploy project.
*
* (c) Javier Eguiluz <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace EasyCorp\Bundle\EasyDeployBundle\Command;

use EasyCorp\Bundle\EasyDeployBundle\Context;
use EasyCorp\Bundle\EasyDeployBundle\Exception\SymfonyVersionException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpKernel\Config\FileLocator;
use Symfony\Component\HttpKernel\Kernel;

class DeployCommand extends Command
{
private $fileLocator;
private $projectDir;
private $logDir;
private $configFilePath;

public function __construct(FileLocator $fileLocator, string $projectDir, string $logDir)
{
$this->fileLocator = $fileLocator;
$this->projectDir = realpath($projectDir);
$this->logDir = $logDir;

parent::__construct();
}

protected function configure()
{
$this
->setName('deploy')
->setDescription('Deploys a Symfony application to one or more remote servers.')
->setHelp('...')
->addArgument('stage', InputArgument::OPTIONAL, 'The stage to deploy to ("production", "staging", etc.)', 'prod')
->addOption('configuration', 'c', InputOption::VALUE_REQUIRED, 'Load configuration from the given file path')
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Shows the commands to perform the deployment without actually executing them')
;
}

protected function initialize(InputInterface $input, OutputInterface $output)
{
$customConfigPath = $input->getOption('configuration');
if (null !== $customConfigPath && !is_readable($customConfigPath)) {
throw new \RuntimeException(sprintf("The given configuration file ('%s') does not exist or it's not readable.", $customConfigPath));
}

if (null !== $customConfigPath && is_readable($customConfigPath)) {
return $this->configFilePath = $customConfigPath;
}

$defaultConfigPath = $this->getDefaultConfigPath($input->getArgument('stage'));
if (is_readable($defaultConfigPath)) {
return $this->configFilePath = $defaultConfigPath;
}

$this->createDefaultConfigFile($input, $output, $defaultConfigPath, $input->getArgument('stage'));
}

protected function execute(InputInterface $input, OutputInterface $output)
{
$logFilePath = sprintf('%s/deploy_%s.log', $this->logDir, $input->getArgument('stage'));
$context = new Context($input, $output, $this->projectDir, $logFilePath, true === $input->getOption('dry-run'), $output->isVerbose());

$deployer = include $this->configFilePath;
$deployer->initialize($context);
$deployer->doDeploy();
}

private function getDefaultConfigPath(string $stageName) : string
{
$symfonyVersion = Kernel::MAJOR_VERSION;
$defaultConfigPaths = [
2 => sprintf('%s/app/config/deploy_%s.php', $this->projectDir, $stageName),
3 => sprintf('%s/app/config/deploy_%s.php', $this->projectDir, $stageName),
4 => sprintf('%s/etc/%s/deploy.php', $this->projectDir, $stageName),
];

if (!isset($defaultConfigPaths[$symfonyVersion])) {
throw new SymfonyVersionException($symfonyVersion);
}

return $defaultConfigPaths[$symfonyVersion];
}

private function createDefaultConfigFile(InputInterface $input, OutputInterface $output, string $defaultConfigPath, string $stageName) : void
{
$helper = $this->getHelper('question');
$question = new ConfirmationQuestion(sprintf("\n<bg=yellow> WARNING </> There is no config file to deploy '%s' stage.\nDo you want to create a minimal config file for it? [Y/n] ", $stageName), true);

if (!$helper->ask($input, $output, $question)) {
$output->writeln(sprintf('<fg=green>OK</>, but before running this command again, create this config file: %s', $defaultConfigPath));
} else {
(new Filesystem())->copy($this->fileLocator->locate('@EasyDeployBundle/Resources/skeleton/deploy.php.dist'), $defaultConfigPath);
$output->writeln(sprintf('<fg=green>OK</>, now edit the "%s" config file and run this command again.', $defaultConfigPath));
}

exit(0);
}
}
93 changes: 93 additions & 0 deletions src/Command/RollbackCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

/*
* This file is part of the EasyDeploy project.
*
* (c) Javier Eguiluz <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace EasyCorp\Bundle\EasyDeployBundle\Command;

use EasyCorp\Bundle\EasyDeployBundle\Context;
use EasyCorp\Bundle\EasyDeployBundle\Exception\SymfonyVersionException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\HttpKernel\Kernel;

class RollbackCommand extends Command
{
private $projectDir;
private $logDir;
private $configFilePath;

public function __construct(string $projectDir, string $logDir)
{
$this->projectDir = $projectDir;
$this->logDir = $logDir;

parent::__construct();
}

protected function configure()
{
$this
->setName('rollback')
->setDescription('Deploys a Symfony application to one or more remote servers.')
->setHelp('...')
->addArgument('stage', InputArgument::OPTIONAL, 'The stage to roll back ("production", "staging", etc.)', 'prod')
->addOption('configuration', 'c', InputOption::VALUE_REQUIRED, 'Load configuration from the given file path')
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Shows the commands to perform the roll back without actually executing them')
;
}

protected function initialize(InputInterface $input, OutputInterface $output)
{
$customConfigPath = $input->getOption('configuration');
if (null !== $customConfigPath && !is_readable($customConfigPath)) {
throw new \RuntimeException(sprintf("The given configuration file ('%s') does not exist or it's not readable.", $customConfigPath));
}

if (null !== $customConfigPath && is_readable($customConfigPath)) {
return $this->configFilePath = $customConfigPath;
}

$defaultConfigPath = $this->getDefaultConfigPath($input->getArgument('stage'));
if (is_readable($defaultConfigPath)) {
return $this->configFilePath = $defaultConfigPath;
}

throw new \RuntimeException(sprintf("The default configuration file does not exist or it's not readable, and no custom configuration file was given either. Create the '%s' configuration file and run this command again.", $defaultConfigPath));
}

protected function execute(InputInterface $input, OutputInterface $output)
{
$logFilePath = sprintf('%s/deploy_%s.log', $this->logDir, $input->getArgument('stage'));
$context = new Context($input, $output, $this->projectDir, $logFilePath, true === $input->getOption('dry-run'), $output->isVerbose());

$deployer = include $this->configFilePath;
$deployer->initialize($context);
$deployer->doRollback();
}

private function getDefaultConfigPath(string $stageName) : string
{
$symfonyVersion = Kernel::MAJOR_VERSION;
$defaultConfigPaths = [
2 => sprintf('%s/app/config/deploy_%s.php', $this->projectDir, $stageName),
3 => sprintf('%s/app/config/deploy_%s.php', $this->projectDir, $stageName),
4 => sprintf('%s/etc/%s/deploy.php', $this->projectDir, $stageName),
];

if (!isset($defaultConfigPaths[$symfonyVersion])) {
throw new SymfonyVersionException($symfonyVersion);
}

return $defaultConfigPaths[$symfonyVersion];
}
}
51 changes: 51 additions & 0 deletions src/Configuration/AbstractConfiguration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

/*
* This file is part of the EasyDeploy project.
*
* (c) Javier Eguiluz <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace EasyCorp\Bundle\EasyDeployBundle\Configuration;

use EasyCorp\Bundle\EasyDeployBundle\Exception\InvalidConfigurationException;
use EasyCorp\Bundle\EasyDeployBundle\Server\Property;
use EasyCorp\Bundle\EasyDeployBundle\Server\Server;
use EasyCorp\Bundle\EasyDeployBundle\Server\ServerRepository;

/**
* It implements the "Builder" pattern to define the configuration of the deployer.
* This is the base builder extended by the specific builder used by each deployer.
*/
abstract class AbstractConfiguration
{
private const RESERVED_SERVER_PROPERTIES = [Property::use_ssh_agent_forwarding];
protected $servers;
protected $useSshAgentForwarding = true;

public function __construct()
{
$this->servers = new ServerRepository();
}

public function server(string $sshDsn, array $roles = [Server::ROLE_APP], array $properties = [])
{
$reservedProperties = array_merge(self::RESERVED_SERVER_PROPERTIES, $this->getReservedServerProperties());
$reservedPropertiesUsed = array_intersect($reservedProperties, array_keys($properties));
if (!empty($reservedPropertiesUsed)) {
throw new InvalidConfigurationException(sprintf('These properties set for the "%s" server are reserved: %s. Use different property names.', $sshDsn, implode(', ', $reservedPropertiesUsed)));
}

$this->servers->add(new Server($sshDsn, $roles, $properties));
}

public function useSshAgentForwarding(bool $useIt)
{
$this->useSshAgentForwarding = $useIt;
}

abstract protected function getReservedServerProperties() : array;
}
70 changes: 70 additions & 0 deletions src/Configuration/ConfigurationAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

/*
* This file is part of the EasyDeploy project.
*
* (c) Javier Eguiluz <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace EasyCorp\Bundle\EasyDeployBundle\Configuration;

use EasyCorp\Bundle\EasyDeployBundle\Helper\Str;
use Symfony\Component\HttpFoundation\ParameterBag;

/**
* It implements the "Adapter" pattern to allow working with the configuration
* in a consistent manner, even if the configuration of each deployer is
* completely different and defined using incompatible objects.
*/
final class ConfigurationAdapter
{
private $config;
/** @var ParameterBag */
private $options;

public function __construct(AbstractConfiguration $config)
{
$this->config = $config;
}

public function __toString() : string
{
return Str::formatAsTable($this->getOptions()->all());
}

public function get(string $optionName)
{
if (!$this->getOptions()->has($optionName)) {
throw new \InvalidArgumentException(sprintf('The "%s" option is not defined.', $optionName));
}

return $this->getOptions()->get($optionName);
}

private function getOptions() : ParameterBag
{
if (null !== $this->options) {
return $this->options;
}

// it's not the most beautiful code possible, but making the properties
// private and the methods public allows to configure the deployment using
// a config builder and the IDE autocompletion. Here we need to access
// those private properties and their values
$options = new ParameterBag();
$r = new \ReflectionObject($this->config);
foreach ($r->getProperties() as $property) {
try {
$property->setAccessible(true);
$options->set($property->getName(), $property->getValue($this->config));
} catch (\ReflectionException $e) {
// ignore this error
}
}

return $this->options = $options;
}
}
Loading

0 comments on commit c66db26

Please sign in to comment.