diff --git a/README.md b/README.md index 166ed86..918a88f 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,8 @@ blueprints: - Environment variable lookup with default value fallback: `{env::}` -> value of environment variable 'var' falling back to 'defaultValue' if env var is not set - Stack/global variable lookup: `{var:}` -> value variable 'var' - Current timestamp: `{tstamp}` -> e.g. '1453151115' +- Clean: `{clean:2.1.7}` -> '217' (removes all characters that aren't allowed in stack names +- Switch profile: `[profile::...]` will switch to a different profile and evaluate the second parameter there. This is useful in cross account setups. Output and resource lookup allow you to "connect" stacks to each other by wiring the output or resources created in one stack to the input parameters needed in another stack that sits on top of the first one without manually @@ -152,6 +154,14 @@ blueprints: [...] ``` +Switch Profile Example (in this example an AMI is baked in a different account and shared with this account) +``` +blueprints: + - stackname: mystack + parameters: + BaseAmi: '[profile:myDevAccountProfile:{output:bakestack:BaseAmi}]' +``` + ### Conditional parameter values You might end up deploying the same stacks to multiple environments or accounts. Instead of duplicating the blueprints (or using YAML reference) you'll probably diff --git a/src/AwsInspector/Command/Profile/EnableCommand.php b/src/AwsInspector/Command/Profile/EnableCommand.php old mode 100755 new mode 100644 index 1e1bf6f..b7efea0 --- a/src/AwsInspector/Command/Profile/EnableCommand.php +++ b/src/AwsInspector/Command/Profile/EnableCommand.php @@ -2,6 +2,7 @@ namespace AwsInspector\Command\Profile; +use StackFormation\Profile\Manager; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -28,7 +29,7 @@ protected function interact(InputInterface $input, OutputInterface $output) $profile = $input->getArgument('profile'); if (empty($profile)) { - $profileManager = new \AwsInspector\ProfileManager(); + $profileManager = new Manager(); $helper = $this->getHelper('question'); $question = new ChoiceQuestion( @@ -47,7 +48,7 @@ protected function interact(InputInterface $input, OutputInterface $output) protected function execute(InputInterface $input, OutputInterface $output) { - $profileManager = new \AwsInspector\ProfileManager(); + $profileManager = new Manager(); $file = $profileManager->writeProfileToDotEnv($input->getArgument('profile')); $output->writeln('File written: ' . $file); } diff --git a/src/AwsInspector/Command/Profile/ListCommand.php b/src/AwsInspector/Command/Profile/ListCommand.php old mode 100755 new mode 100644 index 319471f..c082fdb --- a/src/AwsInspector/Command/Profile/ListCommand.php +++ b/src/AwsInspector/Command/Profile/ListCommand.php @@ -2,6 +2,7 @@ namespace AwsInspector\Command\Profile; +use StackFormation\Profile\Manager; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -18,7 +19,7 @@ protected function configure() protected function execute(InputInterface $input, OutputInterface $output) { - $profileManager = new \AwsInspector\ProfileManager(); + $profileManager = new Manager(); $rows=[]; foreach($profileManager->listAllProfiles() as $profileName) { diff --git a/src/AwsInspector/SdkFactory.php b/src/AwsInspector/SdkFactory.php old mode 100755 new mode 100644 index ffc210a..1075ac1 --- a/src/AwsInspector/SdkFactory.php +++ b/src/AwsInspector/SdkFactory.php @@ -2,42 +2,23 @@ namespace AwsInspector; +/** + * @deprecated + */ class SdkFactory { - protected static $sdks=[]; - - /** - * @return \Aws\Sdk - */ - public static function getSdk($profile='default') - { - if (!isset(self::$sdks[$profile])) { - $params = [ - 'version' => 'latest', - 'region' => getenv('AWS_DEFAULT_REGION'), - 'retries' => 20 - ]; - if ($profile != 'default') { - $profileManager = new ProfileManager(); - $profileConfig = $profileManager->getProfileConfig($profile); - $params['region'] = $profileConfig['region']; - $params['credentials'] = [ - 'key' => $profileConfig['access_key'], - 'secret' => $profileConfig['secret_key'] - ]; - } - self::$sdks[$profile] = new \Aws\Sdk($params); - } - return self::$sdks[$profile]; - } - /** * @param string $client * @return \Aws\AwsClientInterface * @throws \Exception + * @deprecated */ - public static function getClient($client, $profile='default', array $args=[]) { - return self::getSdk($profile)->createClient($client, $args); + public static function getClient($client, $profile=null, array $args=[]) { + static $profileManager; + if (empty($profileManager)) { + $profileManager = new \StackFormation\Profile\Manager(); + } + return $profileManager->getClient($client, $profile, $args); } } diff --git a/src/StackFormation/Blueprint.php b/src/StackFormation/Blueprint.php index fa04f5c..07983c5 100644 --- a/src/StackFormation/Blueprint.php +++ b/src/StackFormation/Blueprint.php @@ -9,22 +9,15 @@ class Blueprint { */ protected $name; protected $blueprintConfig; - protected $placeholderResolver; protected $valueResolver; - public function __construct( - $name, - array $blueprintConfig, - PlaceholderResolver $placeholderResolver, - ConditionalValueResolver $valueResolver - ) + public function __construct($name, array $blueprintConfig, ValueResolver $valueResolver) { if (!is_string($name)) { throw new \InvalidArgumentException('Name must be a string'); } $this->name = $name; $this->blueprintConfig = $blueprintConfig; - $this->placeholderResolver = $placeholderResolver; $this->valueResolver = $valueResolver; } @@ -39,7 +32,7 @@ public function getTags($resolvePlaceholders=true) if (isset($this->blueprintConfig['tags'])) { foreach ($this->blueprintConfig['tags'] as $key => $value) { if ($resolvePlaceholders) { - $value = $this->placeholderResolver->resolvePlaceholders($value, $this, 'tag', $key); + $value = $this->valueResolver->resolvePlaceholders($value, $this, 'tag', $key); } $tags[] = ['Key' => $key, 'Value' => $value]; } @@ -49,40 +42,25 @@ public function getTags($resolvePlaceholders=true) public function getStackName() { - return $this->placeholderResolver->resolvePlaceholders($this->name, $this, 'stackname'); + $stackName = $this->valueResolver->resolvePlaceholders($this->name, $this, 'stackname'); + Helper::validateStackname($stackName); + return $stackName; } - public function getProfile() + public function getProfile($resolvePlaceholders=true) { if (isset($this->blueprintConfig['profile'])) { $value = $this->blueprintConfig['profile']; - if (is_array($value)) { - $value = $this->valueResolver->resolveConditionalValue($value, $this); + if ($resolvePlaceholders) { + $value = $this->valueResolver->resolvePlaceholders($this->blueprintConfig['profile'], $this, 'profile'); } - $value = $this->placeholderResolver->resolvePlaceholders($value, $this, 'profile'); return $value; } - return false; - } - - public function enforceProfile() - { - // TODO: loading profiles shouldn't be done within a blueprint! - if ($profile = $this->getProfile()) { - if ($profile == 'USE_IAM_INSTANCE_PROFILE') { - echo "Using IAM instance profile\n"; - } else { - $profileManager = new \AwsInspector\ProfileManager(); - $profileManager->loadProfile($profile); - echo "Loading Profile: $profile\n"; - } - } + return null; } public function getPreprocessedTemplate() { - $this->enforceProfile(); - if (empty($this->blueprintConfig['template']) || !is_array($this->blueprintConfig['template'])) { throw new \Exception('No template(s) found'); } @@ -108,8 +86,6 @@ public function getParameters($resolvePlaceholders=true) { $parameters = []; - $this->enforceProfile(); - if (!isset($this->blueprintConfig['parameters'])) { return []; } @@ -120,18 +96,15 @@ public function getParameters($resolvePlaceholders=true) throw new \Exception("Invalid parameter key '$parameterKey'."); } - if (is_array($parameterValue)) { - $parameterValue = $this->valueResolver->resolveConditionalValue($parameterValue, $this); - } if (is_null($parameterValue)) { throw new \Exception("Parameter $parameterKey is null."); } - if (!is_scalar($parameterValue)) { - throw new \Exception('Invalid type for value'); - } if ($resolvePlaceholders) { - $parameterValue = $this->placeholderResolver->resolvePlaceholders($parameterValue, $this, 'parameter', $parameterKey); + $parameterValue = $this->valueResolver->resolvePlaceholders($parameterValue, $this, 'parameter', $parameterKey); + } + if (!is_scalar($parameterValue)) { + throw new \Exception('Invalid type for value'); } $tmp = [ @@ -164,7 +137,11 @@ public function getParameters($resolvePlaceholders=true) return $parameters; } - + + /** + * @return array + * @throws \Exception + */ public function getBeforeScripts() { $scripts = []; @@ -172,7 +149,8 @@ public function getBeforeScripts() $scripts = $this->blueprintConfig['before']; } foreach ($scripts as &$script) { - $script = $this->placeholderResolver->resolvePlaceholders($script, $this, 'script'); + $script = $this->valueResolver->resolvePlaceholders($script, $this, 'script'); + $script = str_replace('###CWD###', CWD, $script); } return $scripts; } @@ -218,29 +196,12 @@ public function getVars() return isset($this->blueprintConfig['vars']) ? $this->blueprintConfig['vars'] : []; } - public function executeBeforeScripts() - { - $scripts = $this->getBeforeScripts(); - if (count($scripts) == 0) { - return; - } - - $cwd = getcwd(); - chdir($this->getBasePath()); - - passthru(implode("\n", $scripts), $returnVar); - if ($returnVar !== 0) { - throw new \Exception('Error executing commands'); - } - chdir($cwd); - } - public function getBlueprintReference() { // this is how we reference a stack back to its blueprint $blueprintReference = array_merge( ['Name' => $this->name], - $this->placeholderResolver->getDependencyTracker()->getUsedEnvironmentVariables() + $this->valueResolver->getDependencyTracker()->getUsedEnvironmentVariables() ); $encodedValues = http_build_query($blueprintReference); diff --git a/src/StackFormation/BlueprintAction.php b/src/StackFormation/BlueprintAction.php index 6186279..4467263 100644 --- a/src/StackFormation/BlueprintAction.php +++ b/src/StackFormation/BlueprintAction.php @@ -2,65 +2,120 @@ namespace StackFormation; +use Aws\CloudFormation\Exception\CloudFormationException; use StackFormation\Exception\StackNotFoundException; +use Symfony\Component\Console\Output\OutputInterface; class BlueprintAction { protected $cfnClient; + protected $blueprint; + protected $profileManager; + protected $output; + + public function __construct( + Blueprint $blueprint, + \StackFormation\Profile\Manager $profileManager, + OutputInterface $output=null + ) + { + $this->blueprint = $blueprint; + $this->profileManager = $profileManager; + $this->output = $output; + } - public function __construct(\Aws\CloudFormation\CloudFormationClient $cfnClient) + /** + * @return \Aws\CloudFormation\CloudFormationClient + */ + protected function getCfnClient() { - $this->cfnClient = $cfnClient; + if (is_null($this->cfnClient)) { + $this->cfnClient = $this->profileManager->getClient('CloudFormation', $this->blueprint->getProfile()); + } + return $this->cfnClient; } - public function validateTemplate(Blueprint $blueprint) + public function validateTemplate() { - $this->cfnClient->validateTemplate(['TemplateBody' => $blueprint->getPreprocessedTemplate()]); + $this->getCfnClient()->validateTemplate(['TemplateBody' => $this->blueprint->getPreprocessedTemplate()]); // will throw an exception if there's a problem } + + public function executeBeforeScripts() + { + $scripts = $this->blueprint->getBeforeScripts(); + if (count($scripts) == 0) { + return; + } + + if ($this->output && !$this->output->isQuiet()) { + $this->output->writeln("Running scripts:"); + } + + $envVars = $this->profileManager->getEnvVarsFromProfile($this->blueprint->getProfile()); + if (empty($envVars)) { + $envVars = []; + } + + $basePath = $this->blueprint->getBasePath(); + + $tmpfile = tempnam(sys_get_temp_dir(), 'before_scripts_'); + file_put_contents($tmpfile, implode("\n", $scripts)); + + $command = "cd $basePath && " . implode(' ', $envVars) . " /usr/bin/env bash -x $tmpfile"; + passthru($command, $returnVar); + unlink($tmpfile); + if ($returnVar !== 0) { + throw new \Exception('Error executing commands'); + } + } /** - * @param Blueprint $blueprint - * @param bool $verbose * @return \Aws\Result * @throws \Exception */ - public function getChangeSet(Blueprint $blueprint, $verbose=true) + public function getChangeSet() { - $arguments = $this->prepareArguments($blueprint); + $arguments = $this->prepareArguments(); - $blueprint->executeBeforeScripts(); + try { + $this->executeBeforeScripts(); - if (isset($arguments['StackPolicyBody'])) { - unset($arguments['StackPolicyBody']); - } - $arguments['ChangeSetName'] = 'stackformation' . time(); - - $res = $this->cfnClient->createChangeSet($arguments); - $changeSetId = $res->get('Id'); - $result = Poller::poll(function() use ($changeSetId, $verbose) { - $result = $this->cfnClient->describeChangeSet(['ChangeSetName' => $changeSetId]); - if ($verbose) { - echo "Status: {$result['Status']}\n"; - } - if ($result['Status'] == 'FAILED') { - throw new \Exception($result['StatusReason']); + if (isset($arguments['StackPolicyBody'])) { + unset($arguments['StackPolicyBody']); } - return ($result['Status'] != 'CREATE_COMPLETE') ? false : $result; - }); + $arguments['ChangeSetName'] = 'stackformation' . time(); + + $res = $this->getCfnClient()->createChangeSet($arguments); + $changeSetId = $res->get('Id'); + + $result = Poller::poll(function () use ($changeSetId) { + $result = $this->getCfnClient()->describeChangeSet(['ChangeSetName' => $changeSetId]); + if ($this->output && !$this->output->isQuiet()) { + $this->output->writeln("Status: {$result['Status']}"); + } + if ($result['Status'] == 'FAILED') { + throw new \Exception($result['StatusReason']); + } + return ($result['Status'] != 'CREATE_COMPLETE') ? false : $result; + }); + } catch (CloudFormationException $e) { + throw Helper::refineException($e); // will try to create a StackNotFoundException + } return $result; } - public function deploy(Blueprint $blueprint, $dryRun=false, StackFactory $stackFactory) + public function deploy($dryRun=false) { - $arguments = $this->prepareArguments($blueprint); + $arguments = $this->prepareArguments(); if (!$dryRun) { - $blueprint->executeBeforeScripts(); + $this->executeBeforeScripts(); } try { - $stackStatus = $stackFactory->getStackStatus($blueprint->getStackName()); + $stackFactory = $this->profileManager->getStackFactory($this->blueprint->getProfile()); + $stackStatus = $stackFactory->getStackStatus($this->blueprint->getStackName()); } catch (StackNotFoundException $e) { $stackStatus = false; } @@ -69,28 +124,36 @@ public function deploy(Blueprint $blueprint, $dryRun=false, StackFactory $stackF throw new \Exception("Stack can't be updated right now. Status: $stackStatus"); } elseif (!empty($stackStatus) && $stackStatus != 'DELETE_COMPLETE') { if (!$dryRun) { - $this->cfnClient->updateStack($arguments); + $this->getCfnClient()->updateStack($arguments); } } else { - $arguments['OnFailure'] = $blueprint->getOnFailure(); + $arguments['OnFailure'] = $this->blueprint->getOnFailure(); if (!$dryRun) { - $this->cfnClient->createStack($arguments); + $this->getCfnClient()->createStack($arguments); } } } - protected function prepareArguments(Blueprint $blueprint) + protected function prepareArguments() { + if ($this->output && !$this->output->isQuiet()) { $this->output->write("Preparing parameters... "); } + $parameters = $this->blueprint->getParameters(); + if ($this->output && !$this->output->isQuiet()) { $this->output->writeln("done."); } + + if ($this->output && !$this->output->isQuiet()) { $this->output->write("Preparing template... "); } + $template = $this->blueprint->getPreprocessedTemplate(); + if ($this->output && !$this->output->isQuiet()) { $this->output->writeln("done."); } + $arguments = [ - 'StackName' => $blueprint->getStackName(), - 'Parameters' => $blueprint->getParameters(), - 'TemplateBody' => $blueprint->getPreprocessedTemplate(), - 'Tags' => $blueprint->getTags() + 'StackName' => $this->blueprint->getStackName(), + 'Parameters' => $parameters, + 'TemplateBody' => $template, + 'Tags' => $this->blueprint->getTags() ]; - if ($capabilities = $blueprint->getCapabilities()) { + if ($capabilities = $this->blueprint->getCapabilities()) { $arguments['Capabilities'] = $capabilities; } - if ($policy = $blueprint->getStackPolicy()) { + if ($policy = $this->blueprint->getStackPolicy()) { $arguments['StackPolicyBody'] = $policy; } @@ -98,7 +161,7 @@ protected function prepareArguments(Blueprint $blueprint) try { $arguments['Tags'][] = [ 'Key' => 'stackformation:blueprint', - 'Value' => $blueprint->getBlueprintReference() + 'Value' => $this->blueprint->getBlueprintReference() ]; } catch (\Exception $e) { // TODO: ignoring this for now... diff --git a/src/StackFormation/BlueprintFactory.php b/src/StackFormation/BlueprintFactory.php index 7c5d63a..7abf31b 100644 --- a/src/StackFormation/BlueprintFactory.php +++ b/src/StackFormation/BlueprintFactory.php @@ -9,18 +9,15 @@ class BlueprintFactory { protected $config; - protected $placeholderResolver; - protected $conditionalValueResolver; + protected $valueResolver; - public function __construct( - Config $config, - PlaceholderResolver $placeholderResolver, - ConditionalValueResolver $conditionalValueResolver - ) + public function __construct(Config $config=null, ValueResolver $valueResolver=null) { - $this->config = $config; - $this->placeholderResolver = $placeholderResolver; - $this->conditionalValueResolver = $conditionalValueResolver; + $this->config = $config ? $config : new Config(); + if (is_null($valueResolver)) { + $valueResolver = new ValueResolver(null, null, $this->config, null); + } + $this->valueResolver = $valueResolver; } public function getBlueprint($blueprintName) @@ -31,8 +28,7 @@ public function getBlueprint($blueprintName) $blueprint = new Blueprint( $blueprintName, $this->config->getBlueprintConfig($blueprintName), - $this->placeholderResolver, - $this->conditionalValueResolver + $this->valueResolver ); return $blueprint; } diff --git a/src/StackFormation/Command/AbstractCommand.php b/src/StackFormation/Command/AbstractCommand.php index 3aa4dd8..79818a4 100644 --- a/src/StackFormation/Command/AbstractCommand.php +++ b/src/StackFormation/Command/AbstractCommand.php @@ -3,14 +3,12 @@ namespace StackFormation\Command; use Aws\CloudFormation\Exception\CloudFormationException; -use StackFormation\BlueprintAction; use StackFormation\BlueprintFactory; -use StackFormation\ConditionalValueResolver; use StackFormation\Config; use StackFormation\DependencyTracker; -use StackFormation\PlaceholderResolver; +use StackFormation\Profile\Manager; +use StackFormation\ValueResolver; use StackFormation\StackFactory; -use StackFormation\SdkFactory; use Symfony\Component\Console\Helper\FormatterHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -28,20 +26,32 @@ abstract class AbstractCommand extends Command /* @var DependencyTracker */ protected $dependencyTracker; - /* @var BlueprintAction */ - protected $blueprintAction; + /* @var Manager */ + protected $profileManager; protected function initialize(InputInterface $input, OutputInterface $output) { parent::initialize($input, $output); - $cfnClient = SdkFactory::getCfnClient(); - $this->stackFactory = new StackFactory($cfnClient); + $this->profileManager = new Manager(null, $output); $config = new Config(); $this->dependencyTracker = new DependencyTracker(); - $placeholderResolver = new PlaceholderResolver($this->dependencyTracker, $this->stackFactory, $config); - $conditionalValueResolver = new ConditionalValueResolver($placeholderResolver); - $this->blueprintFactory = new BlueprintFactory($config, $placeholderResolver, $conditionalValueResolver); - $this->blueprintAction = new BlueprintAction($cfnClient); + $this->blueprintFactory = new BlueprintFactory( + $config, + new ValueResolver( + $this->dependencyTracker, + $this->profileManager, + $config, + null // don't load a specific profile + ) + ); + } + + protected function getStackFactory() + { + if (is_null($this->stackFactory)) { + $this->stackFactory = $this->profileManager->getStackFactory(null); + } + return $this->stackFactory; } protected function interactAskForBlueprint(InputInterface $input, OutputInterface $output) @@ -73,7 +83,7 @@ protected function interactAskForBlueprint(InputInterface $input, OutputInterfac protected function getStacks($nameFilter=null, $statusFilter=null) { - return array_keys($this->stackFactory->getStacksFromApi(false, $nameFilter, $statusFilter)); + return array_keys($this->getStackFactory()->getStacksFromApi(false, $nameFilter, $statusFilter)); } public function interactAskForStack(InputInterface $input, OutputInterface $output, $nameFilter=null, $statusFilter=null) diff --git a/src/StackFormation/Command/Blueprint/DeployCommand.php b/src/StackFormation/Command/Blueprint/DeployCommand.php index 9f7ce1a..0113ec3 100644 --- a/src/StackFormation/Command/Blueprint/DeployCommand.php +++ b/src/StackFormation/Command/Blueprint/DeployCommand.php @@ -3,11 +3,20 @@ namespace StackFormation\Command\Blueprint; use Aws\CloudFormation\Exception\CloudFormationException; +use StackFormation\BlueprintAction; +use StackFormation\Exception\OperationAbortedException; +use StackFormation\Exception\StackCannotBeUpdatedException; +use StackFormation\Exception\StackNotFoundException; +use StackFormation\Exception\StackNoUpdatesToBePerformedException; +use StackFormation\Helper\ChangeSetTable; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Helper\Table; 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\Console\Question\Question; class DeployCommand extends \StackFormation\Command\AbstractCommand { @@ -24,7 +33,7 @@ protected function configure() ) ->addOption( 'no-observe', - 'no', + 's', InputOption::VALUE_NONE, 'Don\'t observe stack after deploying' ) @@ -34,6 +43,18 @@ protected function configure() InputOption::VALUE_NONE, 'Deprecated. Deployments are being observed by default now' ) + ->addOption( + 'review-parameters', + 'p', + InputOption::VALUE_NONE, + 'Review parameters before deploying' + ) + ->addOption( + 'review-changeset', + 'c', + InputOption::VALUE_NONE, + 'Review changeset before deploying' + ) ->addOption( 'deleteOnTerminate', null, @@ -62,7 +83,7 @@ protected function interact(InputInterface $input, OutputInterface $output) protected function execute(InputInterface $input, OutputInterface $output) { if ($input->getOption('observe')) { - $output->writeln('-/--observe is deprecated now. Deployments are being observed by default. Please remove this option.'); + $output->writeln('-o/--observe is deprecated now. Deployments are being observed by default. Please remove this option.'); } $blueprint = $this->blueprintFactory->getBlueprint($input->getArgument('blueprint')); @@ -73,65 +94,99 @@ protected function execute(InputInterface $input, OutputInterface $output) $noObserve = $input->getOption('no-observe'); if ($deleteOnTerminate && $noObserve) { - throw new \Exception('--deleteOnTerminate cannot be used with --no-observe'); + throw new \InvalidArgumentException('--deleteOnTerminate cannot be used with --no-observe'); } - try { + $blueprint = $this->blueprintFactory->getBlueprint($input->getArgument('blueprint')); + $blueprintAction = new BlueprintAction($blueprint, $this->profileManager, $output); - $this->blueprintAction->deploy($blueprint, $dryRun, $this->stackFactory); + $stackFactory = $this->profileManager->getStackFactory($blueprint->getProfile()); - } catch (CloudFormationException $exception) { + try { + try { + + if ($input->getOption('review-parameters')) { + $output->writeln("\n\n== Review parameters: =="); + $table = new Table($output); + $table->setHeaders(['Key', 'Value'])->setRows($blueprint->getParameters()); + $table->render(); + + $questionHelper = $this->getHelper('question'); + $question = new ConfirmationQuestion("Do you want to proceed? [y/N] ", false); + if (!$questionHelper->ask($input, $output, $question)) { + throw new OperationAbortedException('blueprint:deploy', 'review-parameters'); + } + } + if ($input->getOption('review-changeset')) { + $output->writeln("\n\n== Review change set: =="); + try { + $changeSetResult = $blueprintAction->getChangeSet(); + $table = new ChangeSetTable($output); + $table->render($changeSetResult); + + $questionHelper = $this->getHelper('question'); + $question = new ConfirmationQuestion("Do you want to proceed? [y/N] ", false); + if (!$questionHelper->ask($input, $output, $question)) { + throw new OperationAbortedException('blueprint:deploy', 'review-changeset'); + } + } catch (StackNotFoundException $e) { + $questionHelper = $this->getHelper('question'); + $question = new ConfirmationQuestion("This stack does not exist yet. Do you want to proceed creating it? [y/N] ", false); + if (!$questionHelper->ask($input, $output, $question)) { + throw new OperationAbortedException('blueprint:deploy', 'Stack does not exist'); + } + } + } - $message = \StackFormation\Helper::extractMessage($exception); + $blueprintAction = new BlueprintAction($blueprint, $this->profileManager, $output); + $blueprintAction->deploy($dryRun); + $output->writeln("Triggered deployment of stack '$stackName'."); - if (strpos($message, 'No updates are to be performed.') !== false) { - $output->writeln('No updates are to be performed.'); - return 0; // exit code + } catch (CloudFormationException $exception) { + throw \StackFormation\Helper::refineException($exception); } - - // TODO: we're already checking the status in deploy(). This should be handled there - if (strpos($message, 'is in CREATE_FAILED state and can not be updated.') !== false) { - $helper = $this->getHelper('question'); - $question = new ConfirmationQuestion('Stack is in CREATE_FAILED state. Do you want to delete it first? [Y/n]'); - $confirmed = $helper->ask($input, $output, $question); - if ($confirmed) { - $output->writeln('Deleting failed stack ' . $stackName); - $this->stackFactory->getStack($stackName)->delete()->observe($output, $this->stackFactory); - $output->writeln('Deletion completed. Now deploying stack: ' . $stackName); - $this->blueprintAction->deploy($blueprint, $dryRun, $this->stackFactory); - } - } elseif (strpos($message, 'is in DELETE_IN_PROGRESS state and can not be updated.') !== false) { - $helper = $this->getHelper('question'); - $question = new ConfirmationQuestion('Stack is in DELETE_IN_PROGRESS state. Do you want to wait and deploy then? [Y/n]'); - $confirmed = $helper->ask($input, $output, $question); - if ($confirmed) { - $this->stackFactory->getStack($stackName)->observe($output, $this->stackFactory); - $output->writeln('Deletion completed. Now deploying stack: ' . $stackName); - $this->blueprintAction->deploy($blueprint, $dryRun, $this->stackFactory); - } - } elseif (strpos($message, 'is in UPDATE_IN_PROGRESS state and can not be updated.') !== false) { - $helper = $this->getHelper('question'); - $question = new ConfirmationQuestion('Stack is in UPDATE_IN_PROGRESS state. Do you want to cancel the current update and deploy then? [Y/n]'); - $confirmed = $helper->ask($input, $output, $question); - if ($confirmed) { - $output->writeln('Cancelling update for ' . $stackName); - $this->stackFactory->getStack($stackName)->cancelUpdate()->observe($output, $this->stackFactory); - $output->writeln('Cancellation completed. Now deploying stack: ' . $stackName); - $this->blueprintAction->deploy($blueprint, $dryRun, $this->stackFactory); - } - } else { - throw $exception; + } catch (StackNoUpdatesToBePerformedException $e) { + $output->writeln('No updates are to be performed.'); + return 0; // exit code + } catch (StackCannotBeUpdatedException $e) { + $questionHelper = $this->getHelper('question'); /* @var $questionHelper QuestionHelper */ + $stack = $stackFactory->getStack($stackName, true); + switch ($e->getState()) { + case 'CREATE_FAILED': + if ($questionHelper->ask($input, $output, new ConfirmationQuestion('Stack is in CREATE_FAILED state. Do you want to delete it first? [Y/n]'))) { + $output->writeln('Deleting failed stack ' . $stackName); + $stack->delete()->observe($output, $stackFactory); + $output->writeln('Deletion completed. Now deploying stack: ' . $stackName); + $blueprintAction->deploy($dryRun); + } + break; + case 'DELETE_IN_PROGRESS': + if ($questionHelper->ask($input, $output, new ConfirmationQuestion('Stack is in DELETE_IN_PROGRESS state. Do you want to wait and deploy then? [Y/n]'))) { + $output->writeln('Waiting until deletion completes for ' . $stackName); + $stack->observe($output, $stackFactory); + $output->writeln('Deletion completed. Now deploying stack: ' . $stackName); + $blueprintAction->deploy($dryRun); + } + break; + case 'UPDATE_IN_PROGRESS': + if ($questionHelper->ask($input, $output, new ConfirmationQuestion('Stack is in UPDATE_IN_PROGRESS state. Do you want to cancel the current update and deploy then? [Y/n]'))) { + $output->writeln('Cancelling update for ' . $stackName); + $stack->cancelUpdate()->observe($output, $stackFactory); + $output->writeln('Cancellation completed. Now deploying stack: ' . $stackName); + $blueprintAction->deploy($dryRun); + } + break; + default: throw $e; } } if (!$dryRun) { - $output->writeln("Triggered deployment of stack '$stackName'."); - if ($noObserve) { $output->writeln("\n-> Run this to observe the stack creation/update:"); $output->writeln("{$GLOBALS['argv'][0]} stack:observe $stackName\n"); } else { - return $this->stackFactory->getStack($stackName, true)->observe($output, $this->stackFactory, $deleteOnTerminate); + $stack = $stackFactory->getStack($stackName, true); + return $stack->observe($output, $stackFactory, $deleteOnTerminate); } } } diff --git a/src/StackFormation/Command/Blueprint/Show/ChangesetCommand.php b/src/StackFormation/Command/Blueprint/Show/ChangesetCommand.php index fe6f3e9..b41800d 100644 --- a/src/StackFormation/Command/Blueprint/Show/ChangesetCommand.php +++ b/src/StackFormation/Command/Blueprint/Show/ChangesetCommand.php @@ -2,6 +2,7 @@ namespace StackFormation\Command\Blueprint\Show; +use StackFormation\BlueprintAction; use StackFormation\Helper; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputArgument; @@ -31,26 +32,9 @@ protected function interact(InputInterface $input, OutputInterface $output) protected function execute(InputInterface $input, OutputInterface $output) { $blueprint = $this->blueprintFactory->getBlueprint($input->getArgument('blueprint')); - - $changeSetResult = $this->blueprintAction->getChangeSet($blueprint, true); - - $rows = []; - foreach ($changeSetResult->search('Changes[]') as $change) { - $resourceChange = $change['ResourceChange']; - $rows[] = [ - // $change['Type'], // would this ever show anything other than 'Resource'? - Helper::decorateChangesetAction($resourceChange['Action']), - $resourceChange['LogicalResourceId'], - isset($resourceChange['PhysicalResourceId']) ? $resourceChange['PhysicalResourceId'] : '', - $resourceChange['ResourceType'], - isset($resourceChange['Replacement']) ? Helper::decorateChangesetReplacement($resourceChange['Replacement']) : '', - ]; - } - - $table = new Table($output); - $table - ->setHeaders(['Action', 'LogicalResourceId', 'PhysicalResourceId', 'ResourceType', 'Replacement']) - ->setRows($rows); - $table->render(); + $blueprintAction = new BlueprintAction($blueprint, $this->profileManager, $output); + $changeSetResult = $blueprintAction->getChangeSet(); + $table = new Helper\ChangeSetTable($output); + $table->render($changeSetResult); } } diff --git a/src/StackFormation/Command/Blueprint/ValidateCommand.php b/src/StackFormation/Command/Blueprint/ValidateCommand.php index 38f926f..ddc0ee6 100644 --- a/src/StackFormation/Command/Blueprint/ValidateCommand.php +++ b/src/StackFormation/Command/Blueprint/ValidateCommand.php @@ -31,8 +31,9 @@ protected function interact(InputInterface $input, OutputInterface $output) protected function execute(InputInterface $input, OutputInterface $output) { $blueprint = $this->blueprintFactory->getBlueprint($input->getArgument('blueprint')); - $this->blueprintAction->validateTemplate($blueprint); - // will throw an exception if there's a problem + + $blueprintAction = new BlueprintAction($blueprint, $this->profileManager, $output); + $blueprintAction->validateTemplate(); // will throw an exception if there's a problem $formatter = new FormatterHelper(); $formattedBlock = $formatter->formatBlock(['No validation errors found.'], 'info', true); diff --git a/src/StackFormation/Command/Stack/CompareAllCommand.php b/src/StackFormation/Command/Stack/CompareAllCommand.php index 27549f0..2d6d570 100644 --- a/src/StackFormation/Command/Stack/CompareAllCommand.php +++ b/src/StackFormation/Command/Stack/CompareAllCommand.php @@ -24,7 +24,7 @@ protected function configure() protected function execute(InputInterface $input, OutputInterface $output) { - $stacks = $this->stackFactory->getStacksFromApi(false); + $stacks = $this->getStackFactory()->getStacksFromApi(false); $data = []; foreach ($stacks as $stackName => $stack) { /* @var $stack Stack */ diff --git a/src/StackFormation/Command/Stack/DeleteCommand.php b/src/StackFormation/Command/Stack/DeleteCommand.php index 5cd47be..b857305 100644 --- a/src/StackFormation/Command/Stack/DeleteCommand.php +++ b/src/StackFormation/Command/Stack/DeleteCommand.php @@ -83,7 +83,7 @@ protected function execute(InputInterface $input, OutputInterface $output) } foreach ($stacks as $stackName) { - $this->stackFactory->getStack($stackName)->delete(); + $this->getStackFactory()->getStack($stackName)->delete(); $output->writeln("Triggered deletion of stack '$stackName'."); } } diff --git a/src/StackFormation/Command/Stack/DiffCommand.php b/src/StackFormation/Command/Stack/DiffCommand.php index 97854d8..4c060ee 100644 --- a/src/StackFormation/Command/Stack/DiffCommand.php +++ b/src/StackFormation/Command/Stack/DiffCommand.php @@ -3,6 +3,7 @@ namespace StackFormation\Command\Stack; use StackFormation\Diff; +use StackFormation\Helper; use Symfony\Component\Console\Helper\FormatterHelper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -31,7 +32,9 @@ protected function interact(InputInterface $input, OutputInterface $output) protected function execute(InputInterface $input, OutputInterface $output) { - $stack = $this->stackFactory->getStack($input->getArgument('stack')); + $stack = $this->getStackFactory()->getStack($input->getArgument('stack')); + Helper::validateStackname($stack); + $blueprint = $this->blueprintFactory->getBlueprintByStack($stack); $diff = new Diff($output); diff --git a/src/StackFormation/Command/Stack/ListCommand.php b/src/StackFormation/Command/Stack/ListCommand.php index 3349099..2c57c09 100644 --- a/src/StackFormation/Command/Stack/ListCommand.php +++ b/src/StackFormation/Command/Stack/ListCommand.php @@ -35,7 +35,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $nameFilter = $input->getOption('nameFilter'); $statusFilter = $input->getOption('statusFilter'); - $stacks = $this->stackFactory->getStacksFromApi(false, $nameFilter, $statusFilter); + $stacks = $this->getStackFactory()->getStacksFromApi(false, $nameFilter, $statusFilter); $rows = []; foreach ($stacks as $stackName => $stack) { /* @var $stack Stack */ diff --git a/src/StackFormation/Command/Stack/ObserveCommand.php b/src/StackFormation/Command/Stack/ObserveCommand.php index ee8d2c1..150c82e 100644 --- a/src/StackFormation/Command/Stack/ObserveCommand.php +++ b/src/StackFormation/Command/Stack/ObserveCommand.php @@ -2,6 +2,7 @@ namespace StackFormation\Command\Stack; +use StackFormation\Helper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -35,8 +36,10 @@ protected function interact(InputInterface $input, OutputInterface $output) protected function execute(InputInterface $input, OutputInterface $output) { - $stack = $this->stackFactory->getStack($input->getArgument('stack')); + $stack = $this->getStackFactory()->getStack($input->getArgument('stack')); + Helper::validateStackname($stack); + $deleteOnTerminate = $input->getOption('deleteOnTerminate'); - return $stack->observe($output, $this->stackFactory, $deleteOnTerminate); + return $stack->observe($output, $this->getStackFactory(), $deleteOnTerminate); } } diff --git a/src/StackFormation/Command/Stack/Show/AbstractShowCommand.php b/src/StackFormation/Command/Stack/Show/AbstractShowCommand.php index 5e989ac..a24aa09 100644 --- a/src/StackFormation/Command/Stack/Show/AbstractShowCommand.php +++ b/src/StackFormation/Command/Stack/Show/AbstractShowCommand.php @@ -2,6 +2,7 @@ namespace StackFormation\Command\Stack\Show; +use StackFormation\Helper; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -40,7 +41,8 @@ protected function interact(InputInterface $input, OutputInterface $output) protected function execute(InputInterface $input, OutputInterface $output) { - $stack = $this->stackFactory->getStack($input->getArgument('stack')); + $stack = $this->getStackFactory()->getStack($input->getArgument('stack')); + Helper::validateStackname($stack); $methodName = 'get'.ucfirst($this->property); diff --git a/src/StackFormation/Command/Stack/Show/DependantsCommand.php b/src/StackFormation/Command/Stack/Show/DependantsCommand.php index 771870e..a6e90e2 100644 --- a/src/StackFormation/Command/Stack/Show/DependantsCommand.php +++ b/src/StackFormation/Command/Stack/Show/DependantsCommand.php @@ -2,6 +2,7 @@ namespace StackFormation\Command\Stack\Show; +use StackFormation\Helper; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -29,7 +30,8 @@ protected function interact(InputInterface $input, OutputInterface $output) protected function execute(InputInterface $input, OutputInterface $output) { - $stack = $this->stackFactory->getStack($input->getArgument('stack')); + $stack = $this->getStackFactory()->getStack($input->getArgument('stack')); + Helper::validateStackname($stack); $this->dependencyTracker->reset(); foreach ($this->blueprintFactory->getAllBlueprints() as $blueprint) { diff --git a/src/StackFormation/Command/Stack/TimelineCommand.php b/src/StackFormation/Command/Stack/TimelineCommand.php index 613d494..ddf7be8 100644 --- a/src/StackFormation/Command/Stack/TimelineCommand.php +++ b/src/StackFormation/Command/Stack/TimelineCommand.php @@ -2,6 +2,7 @@ namespace StackFormation\Command\Stack; +use StackFormation\Helper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -28,7 +29,8 @@ protected function interact(InputInterface $input, OutputInterface $output) protected function execute(InputInterface $input, OutputInterface $output) { - $stack = $this->stackFactory->getStack($input->getArgument('stack')); + $stack = $this->getStackFactory()->getStack($input->getArgument('stack')); + Helper::validateStackname($stack); $events = $stack->getEvents(); diff --git a/src/StackFormation/ConditionalValueResolver.php b/src/StackFormation/ConditionalValueResolver.php deleted file mode 100644 index 5584fd3..0000000 --- a/src/StackFormation/ConditionalValueResolver.php +++ /dev/null @@ -1,63 +0,0 @@ -placeholderResolver = $placeholderResolver; - } - - /** - * Resolve conditional value - * - * @param array $values - * @param Blueprint|null $sourceBlueprint - * @return string - * @throws \Exception - */ - public function resolveConditionalValue(array $values, Blueprint $sourceBlueprint=null) - { - foreach ($values as $condition => $value) { - if ($this->isTrue($condition, $sourceBlueprint)) { - return $value; - } - } - return ''; - } - - /** - * Evaluate is key 'is true' - * - * @param $condition - * @param Blueprint|null $sourceBlueprint - * @return bool - * @throws \Exception - */ - public function isTrue($condition, Blueprint $sourceBlueprint=null) - { - // resolve placeholders - $condition = $this->placeholderResolver->resolvePlaceholders($condition, $sourceBlueprint, 'conditional_value', $condition); - - if ($condition == 'default') { - return true; - } - if (strpos($condition, '==') !== false) { - list($left, $right) = explode('==', $condition, 2); - $left = trim($left); - $right = trim($right); - return ($left == $right); - } elseif (strpos($condition, '!=') !== false) { - list($left, $right) = explode('!=', $condition, 2); - $left = trim($left); - $right = trim($right); - return ($left != $right); - } else { - throw new \Exception('Invalid condition'); - } - } - -} \ No newline at end of file diff --git a/src/StackFormation/Config.php b/src/StackFormation/Config.php index 63531d6..f9b68ec 100644 --- a/src/StackFormation/Config.php +++ b/src/StackFormation/Config.php @@ -87,6 +87,15 @@ public function blueprintExists($blueprintName) return isset($this->conf['blueprints'][$blueprintName]); } + public function getGlobalVar($var) + { + $vars = $this->getGlobalVars(); + if (!isset($vars[$var])) { + throw new \Exception("Variable '$var' not found"); + } + return $vars[$var]; + } + public function getGlobalVars() { return isset($this->conf['vars']) ? $this->conf['vars'] : []; @@ -112,17 +121,17 @@ public function convertBlueprintNameIntoRegex($blueprintName) return '/^'.preg_replace('/\{[^\}]+?\}/', '(.*)', $blueprintName) .'$/'; } - /** - * TODO: this should not be here... - * - * @return mixed - */ - public function getCurrentUsersAccountId() - { - $iamClient = SdkFactory::getClient('Iam'); /* @var $iamClient \Aws\Iam\IamClient */ - $res = $iamClient->getUser(); - $arn = $res->search('User.Arn'); - $parts = explode(':', $arn); - return $parts[4]; - } + ///** + // * TODO: this should not be here... + // * + // * @return mixed + // */ + //public function getCurrentUsersAccountId() + //{ + // $iamClient = SdkFactory::getClient('Iam'); /* @var $iamClient \Aws\Iam\IamClient */ + // $res = $iamClient->getUser(); + // $arn = $res->search('User.Arn'); + // $parts = explode(':', $arn); + // return $parts[4]; + //} } diff --git a/src/StackFormation/Exception/OperationAbortedException.php b/src/StackFormation/Exception/OperationAbortedException.php new file mode 100644 index 0000000..e69e0af --- /dev/null +++ b/src/StackFormation/Exception/OperationAbortedException.php @@ -0,0 +1,13 @@ +stackName = $stackName; + $this->state = $state; + parent::__construct("Stack '$stackName' not found (state: $state)", 0, $previous); + } + + public function getStackName() { + return $this->stackName; + } + + public function getState() { + return $this->state; + } + +} diff --git a/src/StackFormation/Exception/StackNoUpdatesToBePerformedException.php b/src/StackFormation/Exception/StackNoUpdatesToBePerformedException.php new file mode 100644 index 0000000..a9de5fa --- /dev/null +++ b/src/StackFormation/Exception/StackNoUpdatesToBePerformedException.php @@ -0,0 +1,21 @@ +stackName = $stackName; + parent::__construct("No updates to be performened on stack $stackName", 0, $previous); + } + + public function getStackName() { + return $this->stackName; + } + +} diff --git a/src/StackFormation/Exception/StackNotFoundException.php b/src/StackFormation/Exception/StackNotFoundException.php index 8f42c4b..49f2eaa 100644 --- a/src/StackFormation/Exception/StackNotFoundException.php +++ b/src/StackFormation/Exception/StackNotFoundException.php @@ -5,4 +5,10 @@ class StackNotFoundException extends \Exception { + public function __construct($stackName, \Exception $previous=null) + { + $message = "Stack '$stackName' not found"; + parent::__construct($message, 0, $previous); + } + } diff --git a/src/StackFormation/Helper.php b/src/StackFormation/Helper.php index 7869a61..2afc94f 100644 --- a/src/StackFormation/Helper.php +++ b/src/StackFormation/Helper.php @@ -2,6 +2,10 @@ namespace StackFormation; +use StackFormation\Exception\StackCannotBeUpdatedException; +use StackFormation\Exception\StackNotFoundException; +use StackFormation\Exception\StackNoUpdatesToBePerformedException; + class Helper { @@ -40,10 +44,25 @@ public static function extractMessage(\Aws\CloudFormation\Exception\CloudFormati if ($xml !== false && $xml->Error->Message) { return $xml->Error->Message; } - return $exception->getMessage(); } + public static function refineException(\Aws\CloudFormation\Exception\CloudFormationException $exception) + { + $message = self::extractMessage($exception); + $matches = []; + if (preg_match('/^Stack \[(.+)\] does not exist$/', $message, $matches)) { + return new StackNotFoundException($matches[1], $exception); + } + if (preg_match('/.+stack\/(.+)\/.+is in ([A-Z_]+) state and can not be updated./', $message, $matches)) { + return new StackCannotBeUpdatedException($matches[1], $matches[2], $exception); + } + if (strpos($message, 'No updates are to be performed.') !== false) { + return new StackNoUpdatesToBePerformedException('TBD'); + } + return $exception; + } + public static function decorateStatus($status) { if (strpos($status, 'IN_PROGRESS') !== false) { @@ -59,20 +78,6 @@ public static function decorateStatus($status) return $status; } - public static function decorateChangesetAction($changeSetAction) - { - if ($changeSetAction == 'Modify') { - return "$changeSetAction"; - } - if ($changeSetAction == 'Add') { - return "$changeSetAction"; - } - if ($changeSetAction == 'Remove') { - return "$changeSetAction"; - } - return $changeSetAction; - } - public static function decorateChangesetReplacement($changeSetReplacement) { if ($changeSetReplacement == 'Conditional') { @@ -109,6 +114,15 @@ public static function findCloudWatchLogGroupByStream($stream, $logGroupNamePref return null; } + public static function validateStackname($stackName) + { + // A stack name can contain only alphanumeric characters (case sensitive) and hyphens. + // It must start with an alphabetic character and cannot be longer than 128 characters. + if (!preg_match('/^[a-zA-Z][a-zA-Z0-9-]{0,127}$/', $stackName)) { + throw new \Exception('Invalid stack name: ' . $stackName); + } + } + public static function validateTags(array $tags) { // @see http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#tag-restrictions diff --git a/src/StackFormation/Helper/ChangeSetTable.php b/src/StackFormation/Helper/ChangeSetTable.php new file mode 100644 index 0000000..29cfd31 --- /dev/null +++ b/src/StackFormation/Helper/ChangeSetTable.php @@ -0,0 +1,40 @@ +setHeaders(['Action', 'LogicalResourceId', 'PhysicalResourceId', 'ResourceType', 'Replacement']); + $this->setRows($this->getRows($changeSetResult)); + parent::render(); + } + + protected function getRows(\Aws\Result $changeSetResult) { + $rows = []; + foreach ($changeSetResult->search('Changes[]') as $change) { + $resourceChange = $change['ResourceChange']; + $rows[] = [ + // $change['Type'], // would this ever show anything other than 'Resource'? + $this->decorateChangesetAction($resourceChange['Action']), + $resourceChange['LogicalResourceId'], + isset($resourceChange['PhysicalResourceId']) ? $resourceChange['PhysicalResourceId'] : '', + $resourceChange['ResourceType'], + isset($resourceChange['Replacement']) ? Helper::decorateChangesetReplacement($resourceChange['Replacement']) : '', + ]; + } + return $rows; + } + + protected function decorateChangesetAction($changeSetAction) { + switch ($changeSetAction) { + case 'Modify': return "$changeSetAction"; + case 'Add': return "$changeSetAction"; + case 'Remove': return "$changeSetAction"; + } + return $changeSetAction; + } + +} \ No newline at end of file diff --git a/src/StackFormation/Observer.php b/src/StackFormation/Observer.php index edec105..0293bbb 100644 --- a/src/StackFormation/Observer.php +++ b/src/StackFormation/Observer.php @@ -3,6 +3,7 @@ namespace StackFormation; use Aws\CloudFormation\Exception\CloudFormationException; +use StackFormation\Exception\StackNotFoundException; use Symfony\Component\Console\Helper\FormatterHelper; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Filesystem\Exception\FileNotFoundException; @@ -33,6 +34,8 @@ public function observeStackActivity($pollInterval = 10) $returnValue = 0; $printedEvents = []; $first = true; + $stackGone = false; + $lastStatus = ''; do { if ($first) { $first = false; @@ -40,14 +43,13 @@ public function observeStackActivity($pollInterval = 10) sleep($pollInterval); } - // load fresh instance for updated status - $this->stack = $this->stackFactory->getStack($this->stack->getName(), true); - $status = $this->stack->getStatus(); + try { + // load fresh instance for updated status + $this->stack = $this->stackFactory->getStack($this->stack->getName(), true); + $lastStatus = $this->stack->getStatus(); - $this->output->writeln("-> Polling... (Stack Status: $status)"); + $this->output->writeln("-> Polling... (Stack Status: $lastStatus)"); - $stackGone = false; // while deleting - try { $events = $this->stack->getEvents(); $logMessages = []; @@ -123,17 +125,20 @@ public function observeStackActivity($pollInterval = 10) throw $exception; } + } catch (StackNotFoundException $exception) { + $stackGone = true; + $this->output->writeln("-> Stack gone."); } - } while (!$stackGone && strpos($status, 'IN_PROGRESS') !== false); + } while (!$stackGone && strpos($lastStatus, 'IN_PROGRESS') !== false); $formatter = new FormatterHelper(); - if (strpos($status, 'FAILED') !== false) { - $formattedBlock = $formatter->formatBlock(['Error!', 'Status: ' . $status], 'error', true); + if (strpos($lastStatus, 'FAILED') !== false) { + $formattedBlock = $formatter->formatBlock(['Error!', 'Last Status: ' . $lastStatus], 'error', true); } else { - $formattedBlock = $formatter->formatBlock(['Completed', 'Status: ' . $status], 'info', true); + $formattedBlock = $formatter->formatBlock(['Completed', 'Last Status: ' . $lastStatus], 'info', true); } - if (!in_array($status, ['CREATE_COMPLETE', 'UPDATE_COMPLETE'])) { + if (!in_array($lastStatus, ['CREATE_COMPLETE', 'UPDATE_COMPLETE', 'DELETE_IN_PROGRESS'])) { $returnValue = 1; } diff --git a/src/StackFormation/Profile/Manager.php b/src/StackFormation/Profile/Manager.php new file mode 100644 index 0000000..de2236c --- /dev/null +++ b/src/StackFormation/Profile/Manager.php @@ -0,0 +1,121 @@ +credentialProvider = is_null($credentialProvider) ? new YamlCredentialProvider() : $credentialProvider; + $this->output = $output; + } + + protected function getSdk() + { + if (is_null($this->sdk)) { + $region = getenv('AWS_DEFAULT_REGION'); + if (empty($region)) { + throw new \Exception('Environment variable AWS_DEFAULT_REGION not set.'); + } + $this->sdk = new \Aws\Sdk([ + 'version' => 'latest', + 'region' => $region, + 'retries' => 20 + ]); + } + return $this->sdk; + } + + /** + * @param string $client + * @param string $profile + * @param array $args + * @return \Aws\AwsClientInterface + * @throws \Exception + */ + public function getClient($client, $profile=null, array $args=[]) { + if (!is_string($client)) { + throw new \InvalidArgumentException('Client parameter must be a string'); + } + if (!is_null($profile) && !is_string($profile)) { + throw new \InvalidArgumentException('Profile parameter must be a string'); + } + $cacheKey = $client .'-'. ($profile ? $profile : '__empty__'); + if (!isset($this->clients[$cacheKey])) { + if ($profile) { + $args['credentials'] = $this->credentialProvider->getCredentialsForProfile($profile); + } + $this->printDebug($client, $profile); + $this->clients[$cacheKey] = $this->getSdk()->createClient($client, $args); + } + return $this->clients[$cacheKey]; + } + + protected function printDebug($client, $profile) { + if (!$this->output || !$this->output->isVerbose()) { + return; + } + $message = "[ProfileManager] Created '$client' client"; + if ($profile) { + $message .= " for profile '$profile'"; + } elseif ($profileFromEnv = getenv('AWSINSPECTOR_PROFILE')) { + $message .= " for profile '$profileFromEnv' with default credentials provider (env/ini/instance)"; + } else { + $message .= " with default credentials provider (env/ini/instance)"; + } + $this->output->writeln($message); + } + + /** + * @return \Aws\CloudFormation\CloudFormationClient + */ + public function getCfnClient($profile=null, array $args=[]) { + return $this->getClient('CloudFormation', $profile, $args); + } + + public function listAllProfiles() + { + return $this->credentialProvider->listAllProfiles(); + } + + public function getEnvVarsFromProfile($profile) { + $tmp = []; + foreach ($this->credentialProvider->getEnvVarsForProfile($profile) as $var => $value) { + $tmp[] = "$var=$value"; + } + return $tmp; + } + + public function writeProfileToDotEnv($profile, $file='.env') { + $tmp = $this->getEnvVarsFromProfile($profile); + $res = file_put_contents($file, implode("\n", $tmp)); + if ($res === false) { + throw new \Exception('Error while writing file .env'); + } + return $file; + } + + /** + * "StackFactory" Factory :) + * + * @param $profile + * @return StackFactory + */ + public function getStackFactory($profile=null) { + $cachKey = ($profile ? $profile : '__empty__'); + if (!isset($this->stackFactories[$cachKey])) { + $this->stackFactories[$cachKey] = new StackFactory($this->getCfnClient($profile)); + } + return $this->stackFactories[$cachKey]; + } + +} \ No newline at end of file diff --git a/src/AwsInspector/ProfileManager.php b/src/StackFormation/Profile/YamlCredentialProvider.php similarity index 57% rename from src/AwsInspector/ProfileManager.php rename to src/StackFormation/Profile/YamlCredentialProvider.php index 4e93c95..a0de069 100644 --- a/src/AwsInspector/ProfileManager.php +++ b/src/StackFormation/Profile/YamlCredentialProvider.php @@ -1,47 +1,85 @@ getConfig()); - } + protected $config; - public function getProfileConfig($profile) { + /** + * @param $profile string + * @return Credentials + * @throws \Exception + */ + public function getCredentialsForProfile($profile) { if (!$this->isValidProfile($profile)) { - throw new \InvalidArgumentException('Invalid profile ' . $profile); + throw new \Exception("Invalid profile: $profile"); } $config = $this->getConfig(); - return $config[$profile]; + $profileConfig = $config[$profile]; + if (empty($profileConfig['access_key'])) { + throw new \Exception("Invalid access_key in profile $profile"); + } + if (empty($profileConfig['secret_key'])) { + throw new \Exception("Invalid secret_key in profile $profile"); + } + return new Credentials( + $profileConfig['access_key'], + $profileConfig['secret_key'] + ); } - public function isValidProfile($profile) { + /** + * @param $profile string + * @return Credentials + * @throws \Exception + */ + public function getEnvVarsForProfile($profile) { + if (!$this->isValidProfile($profile)) { + throw new \Exception("Invalid profile: $profile"); + } $config = $this->getConfig(); - return isset($config[$profile]); - } - - public function loadProfile($profile) { - foreach ($this->getEnvVars($profile) as $envVar) { - putenv($envVar); + $profileConfig = $config[$profile]; + if (empty($profileConfig['access_key'])) { + throw new \Exception("Invalid access_key in profile $profile"); } + if (empty($profileConfig['secret_key'])) { + throw new \Exception("Invalid secret_key in profile $profile"); + } + return [ + 'AWSINSPECTOR_PROFILE' => $profile, // this isn't really used except for debugging + 'AWS_ACCESS_KEY_ID' => $profileConfig['access_key'], + 'AWS_SECRET_ACCESS_KEY' => $profileConfig['secret_key'], + ]; } - public function getLoadedFiles() { - return $this->loadedFiles; + public function listAllProfiles() { + return array_keys($this->getConfig()); } - public function writeProfileToDotEnv($profile, $file='.env') { - $tmp = $this->getEnvVars($profile); - $res = file_put_contents($file, implode("\n", $tmp)); - if ($res === false) { - throw new \Exception('Error while writing file .env'); + public function isValidProfile($profile) { + if (!is_string($profile) || empty($profile)) { + throw new \InvalidArgumentException('Invalid profile'); } - return $file; + $config = $this->getConfig(); + return isset($config[$profile]); + } + + protected function getConfig() { + if (is_null($this->config)) { + $this->config = []; + foreach ($this->findAllProfileFiles() as $file) { + $this->config = array_merge( + $this->config, + $this->loadFile($file) + ); + } + } + return $this->config; } protected function getDecryptedFilecontent($encryptedFilename) { @@ -72,7 +110,12 @@ protected function getFileContent($filename) { } if (!is_file($filename)) { // try if there's an encrpyted version of this file - return $this->getDecryptedFilecontent($this->getEncryptedFileName($filename)); + try { + $encryptedFilename = $this->getEncryptedFileName($filename); + return $this->getDecryptedFilecontent($encryptedFilename); + } catch (FileNotFoundException $e) { + throw new FileNotFoundException("Could not find '$filename' or '$encryptedFilename'", 0, $e); + } } return file_get_contents($filename); } @@ -86,7 +129,6 @@ protected function loadFile($filename) { if (!is_array($config['profiles']) || count($config['profiles']) == 0) { throw new \Exception('Could not find any profiles'); } - $this->loadedFiles[] = $filename; return $config['profiles']; } @@ -100,40 +142,5 @@ protected function findAllProfileFiles() return $files; } - protected function getConfig() { - if (is_null($this->config)) { - $this->config = []; - foreach ($this->findAllProfileFiles() as $file) { - $this->config = array_merge( - $this->config, - $this->loadFile($file) - ); - } - } - return $this->config; - } - - protected function getEnvVars($profile) { - $profileConfig = $this->getProfileConfig($profile); - $mapping = [ - 'region' => 'AWS_DEFAULT_REGION', - 'access_key' => 'AWS_ACCESS_KEY_ID', - 'secret_key' => 'AWS_SECRET_ACCESS_KEY', - 'assume_role' => 'AWS_ASSUME_ROLE' - ]; - - $tmp = []; - $tmp[] = 'AWSINSPECTOR_PROFILE='.$profile; - foreach ($mapping as $key => $value) { - if (empty($profileConfig[$key])) { - if ($key == 'assume_role') { - continue; - } - throw new \Exception('Mising configuration: ' . $key); - } - $tmp[] = $mapping[$key].'='.$profileConfig[$key]; - } - return $tmp; - } } \ No newline at end of file diff --git a/src/StackFormation/SdkFactory.php b/src/StackFormation/SdkFactory.php deleted file mode 100644 index be06da2..0000000 --- a/src/StackFormation/SdkFactory.php +++ /dev/null @@ -1,87 +0,0 @@ - $region, - 'version' => 'latest' - ]); - - if ($assumeRole) { - $stsClient = $sdk->createSts(); - $res = $stsClient->assumeRole([ - 'RoleArn' => getenv('AWS_ASSUME_ROLE'), - 'RoleSessionName' => 'StackFormation' - ]); - $credentials = $stsClient->createCredentials($res); - - // replace SDK object with new one that is assuming the role - $sdk = new Sdk([ - 'region' => $region, - 'version' => 'latest', - 'credentials' => $credentials - ]); - } - - self::$sdk[$conf] = $sdk; - } - - return self::$sdk[$conf] ; - } - - /** - * @param string $client - * @param array $args - * - * @return \Aws\AwsClientInterface - * @throws \Exception - */ - public static function getClient($client, array $args = []) - { - $key = $client . serialize($args); - if (!isset(self::$clients[$key])) { - self::$clients[$key] = self::getSdk()->createClient($client, $args); - } - - return self::$clients[$key]; - } - - /** - * @return \Aws\CloudFormation\CloudFormationClient - */ - public static function getCfnClient() - { - return self::getClient('CloudFormation'); - } -} diff --git a/src/StackFormation/Stack.php b/src/StackFormation/Stack.php index f74d218..30e57e3 100644 --- a/src/StackFormation/Stack.php +++ b/src/StackFormation/Stack.php @@ -89,7 +89,7 @@ public function getOutput($key) * * @return array */ - public function getOutputs() + public function getOutputs() { $outputs = []; $res = isset($this->data['Outputs']) ? $this->data['Outputs'] : []; diff --git a/src/StackFormation/StackFactory.php b/src/StackFormation/StackFactory.php index 012cdfb..31c1beb 100644 --- a/src/StackFormation/StackFactory.php +++ b/src/StackFormation/StackFactory.php @@ -7,6 +7,7 @@ class StackFactory { protected $cfnClient; + protected $stacksCache; public function __construct(\Aws\CloudFormation\CloudFormationClient $cfnClient) { @@ -23,7 +24,7 @@ public function getStack($stackName, $fresh=false) $stackName = $this->resolveWildcard($stackName); $stacksFromApi = $this->getStacksFromApi($fresh); if (!isset($stacksFromApi[$stackName])) { - throw new StackNotFoundException("Stack $stackName not found."); + throw new StackNotFoundException($stackName); } return $stacksFromApi[$stackName]; } @@ -81,34 +82,16 @@ public function resolveWildcard($stackName) */ public function getStacksFromApi($fresh=false, $nameFilter=null, $statusFilter=null) { - $stacks = StaticCache::get('stacks-from-api', function () { - //$res = $this->cfnClient->listStacks([ - // 'StackStatusFilter' => [ - // 'CREATE_IN_PROGRESS', - // 'CREATE_FAILED', - // 'CREATE_COMPLETE', - // 'ROLLBACK_IN_PROGRESS', - // 'ROLLBACK_FAILED', - // 'ROLLBACK_COMPLETE', - // 'DELETE_IN_PROGRESS', - // 'DELETE_FAILED', - // 'UPDATE_IN_PROGRESS', - // 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS', - // 'UPDATE_COMPLETE', - // 'UPDATE_ROLLBACK_IN_PROGRESS', - // 'UPDATE_ROLLBACK_FAILED', - // 'UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS', - // 'UPDATE_ROLLBACK_COMPLETE', - // ]] - //); - + if ($fresh || is_null($this->stacksCache)) { $res = $this->cfnClient->describeStacks(); - $stacks = []; + $this->stacksCache = []; foreach ($res->get('Stacks') as $stack) { - $stacks[$stack['StackName']] = new Stack($stack, $this->cfnClient); + $this->stacksCache[$stack['StackName']] = new Stack($stack, $this->cfnClient); } - return $stacks; - }, $fresh); + } + + $stacks = $this->stacksCache; + ksort($stacks); if (is_null($nameFilter)) { if ($filter = getenv('STACKFORMATION_NAME_FILTER')) { @@ -116,8 +99,6 @@ public function getStacksFromApi($fresh=false, $nameFilter=null, $statusFilter=n } } - ksort($stacks); - // filter names if (!is_null($nameFilter)) { foreach (array_keys($stacks) as $stackName) { diff --git a/src/StackFormation/PlaceholderResolver.php b/src/StackFormation/ValueResolver.php similarity index 62% rename from src/StackFormation/PlaceholderResolver.php rename to src/StackFormation/ValueResolver.php index 46c0995..98a5b3f 100644 --- a/src/StackFormation/PlaceholderResolver.php +++ b/src/StackFormation/ValueResolver.php @@ -4,24 +4,29 @@ use Aws\CloudFormation\Exception\CloudFormationException; use StackFormation\Exception\MissingEnvVarException; +use StackFormation\Exception\StackNotFoundException; +use StackFormation\Profile\Manager; -class PlaceholderResolver { +class ValueResolver { protected $dependencyTracker; - protected $stackFactory; + protected $profileManager; protected $config; + protected $forceProfile; /** * PlaceholderResolver constructor. * * @param DependencyTracker $dependencyTracker - * @param StackFactory $stackFactory + * @param Manager $profileManager * @param Config $config + * @param string $forceProfile */ - public function __construct(DependencyTracker $dependencyTracker, StackFactory $stackFactory, Config $config) + public function __construct(DependencyTracker $dependencyTracker=null, Manager $profileManager=null, Config $config, $forceProfile=null) { - $this->dependencyTracker = $dependencyTracker; - $this->stackFactory = $stackFactory; + $this->dependencyTracker = $dependencyTracker ? $dependencyTracker : new DependencyTracker(); + $this->profileManager = $profileManager ? $profileManager : new Manager(); + $this->forceProfile = $forceProfile; $this->config = $config; } @@ -46,9 +51,12 @@ public function resolvePlaceholders($string, Blueprint $sourceBlueprint=null, $s $exceptionMsgAppendix = $this->getExceptionMessageAppendix($sourceBlueprint, $sourceType, $sourceKey); + $string = $this->switchProfile($string, $sourceBlueprint, $sourceType, $sourceKey, $exceptionMsgAppendix); + $string = $this->resolveEnv($string, $sourceBlueprint, $sourceType, $sourceKey, $exceptionMsgAppendix); $string = $this->resolveEnvWithFallback($string, $sourceBlueprint, $sourceType, $sourceKey); $string = $this->resolveVar($string, $sourceBlueprint, $exceptionMsgAppendix); + $string = $this->resolveConditionalValue($string, $sourceBlueprint); $string = $this->resolveTstamp($string); $string = $this->resolveOutput($string, $sourceBlueprint, $sourceType, $sourceKey, $exceptionMsgAppendix); $string = $this->resolveResource($string, $sourceBlueprint, $sourceType, $sourceKey, $exceptionMsgAppendix); @@ -139,7 +147,14 @@ function ($matches) use ($sourceBlueprint, $exceptionMsgAppendix) { if (!isset($vars[$matches[1]])) { throw new \Exception("Variable '{$matches[1]}' not found$exceptionMsgAppendix"); } - return $vars[$matches[1]]; + $value = $vars[$matches[1]]; + if (is_array($value)) { + $value = $this->resolveConditionalValue($value, $sourceBlueprint); + } + if ($value == $matches[0]) { + throw new \Exception('Direct circular reference detected'); + } + return $value; }, $string ); @@ -179,7 +194,9 @@ protected function resolveOutput($string, Blueprint $sourceBlueprint=null, $sour function ($matches) use ($exceptionMsgAppendix, $sourceBlueprint, $sourceType, $sourceKey) { try { $this->dependencyTracker->trackStackDependency('output', $matches[1], $matches[2], $sourceBlueprint, $sourceType, $sourceKey); - return $this->stackFactory->getStackOutput($matches[1], $matches[2]); + return $this->getStackFactory($sourceBlueprint)->getStackOutput($matches[1], $matches[2]); + } catch (StackNotFoundException $e) { + throw new \Exception("Error resolving '{$matches[0]}'$exceptionMsgAppendix", 0, $e); } catch (CloudFormationException $e) { $extractedMessage = Helper::extractMessage($e); throw new \Exception("Error resolving '{$matches[0]}'$exceptionMsgAppendix (CloudFormation error: $extractedMessage)"); @@ -207,7 +224,9 @@ protected function resolveResource($string, Blueprint $sourceBlueprint=null, $so function ($matches) use ($exceptionMsgAppendix, $sourceBlueprint, $sourceType, $sourceKey) { try { $this->dependencyTracker->trackStackDependency('resource', $matches[1], $matches[2], $sourceBlueprint, $sourceType, $sourceKey); - return $this->stackFactory->getStackResource($matches[1], $matches[2]); + return $this->getStackFactory($sourceBlueprint)->getStackResource($matches[1], $matches[2]); + } catch (StackNotFoundException $e) { + throw new \Exception("Error resolving '{$matches[0]}'$exceptionMsgAppendix", 0, $e); } catch (CloudFormationException $e) { $extractedMessage = Helper::extractMessage($e); throw new \Exception("Error resolving '{$matches[0]}'$exceptionMsgAppendix (CloudFormation error: $extractedMessage)"); @@ -235,7 +254,9 @@ protected function resolveParameter($string, Blueprint $sourceBlueprint=null, $s function ($matches) use ($exceptionMsgAppendix, $sourceBlueprint, $sourceType, $sourceKey) { try { $this->dependencyTracker->trackStackDependency('parameter', $matches[1], $matches[2], $sourceBlueprint, $sourceType, $sourceKey); - return $this->stackFactory->getStackParameter($matches[1], $matches[2]); + return $this->getStackFactory($sourceBlueprint)->getStackParameter($matches[1], $matches[2]); + } catch (StackNotFoundException $e) { + throw new \Exception("Error resolving '{$matches[0]}'$exceptionMsgAppendix", 0, $e); } catch (CloudFormationException $e) { $extractedMessage = Helper::extractMessage($e); throw new \Exception("Error resolving '{$matches[0]}'$exceptionMsgAppendix (CloudFormation error: $extractedMessage)"); @@ -264,6 +285,110 @@ function ($matches) { return $string; } + /** + * Resolve conditional value + * + * @param array $values + * @param Blueprint|null $sourceBlueprint + * @return string + * @throws \Exception + */ + public function resolveConditionalValue($values, Blueprint $sourceBlueprint=null) + { + if (!is_array($values)) { + return $values; + } + foreach ($values as $condition => $value) { + if ($this->isTrue($condition, $sourceBlueprint)) { + return $value; + } + } + return ''; + } + + /** + * {profile:...:...} + * + * @param $string + * @param Blueprint $sourceBlueprint + * @param $sourceType + * @param $sourceKey + * @param $exceptionMsgAppendix + * @return mixed + */ + protected function switchProfile($string, Blueprint $sourceBlueprint=null, $sourceType=null, $sourceKey=null, $exceptionMsgAppendix) + { + $string = preg_replace_callback( + '/\[profile:([^:\]\[]+?):([^\]\[]+?)\]/', + function ($matches) use ($exceptionMsgAppendix, $sourceBlueprint, $sourceType, $sourceKey) { + try { + $profile = $matches[1]; + $substring = $matches[2]; + + // recursively create another ValueResolver, but this time with a different profile + $subValueResolver = new ValueResolver( + $this->dependencyTracker, + $this->profileManager, + $this->config, + $profile + ); + return $subValueResolver->resolvePlaceholders($substring, $sourceBlueprint, $sourceType, $sourceKey); + } catch (StackNotFoundException $e) { + throw new \Exception("Error resolving '{$matches[0]}'$exceptionMsgAppendix", 0, $e); + } catch (CloudFormationException $e) { + $extractedMessage = Helper::extractMessage($e); + throw new \Exception("Error resolving '{$matches[0]}'$exceptionMsgAppendix (CloudFormation error: $extractedMessage)"); + } + }, + $string + ); + return $string; + } + + protected function getStackFactory(Blueprint $sourceBlueprint=null) + { + if (!is_null($this->forceProfile)) { + return $this->profileManager->getStackFactory($this->forceProfile); + } + return $this->profileManager->getStackFactory($sourceBlueprint ? $sourceBlueprint->getProfile() : null); + } + + /** + * Evaluate is key 'is true' + * + * @param $condition + * @param Blueprint|null $sourceBlueprint + * @return bool + * @throws \Exception + */ + public function isTrue($condition, Blueprint $sourceBlueprint=null) + { + // resolve placeholders + $condition = $this->resolvePlaceholders($condition, $sourceBlueprint, 'conditional_value', $condition); + + if ($condition == 'default') { + return true; + } + if (strpos($condition, '==') !== false) { + list($left, $right) = explode('==', $condition, 2); + $left = trim($left); + $right = trim($right); + return ($left == $right); + } elseif (strpos($condition, '!=') !== false) { + list($left, $right) = explode('!=', $condition, 2); + $left = trim($left); + $right = trim($right); + return ($left != $right); + } elseif (strpos($condition, '~=') !== false) { + list($subject, $pattern) = explode('~=', $condition, 2); + $subject = trim($subject); + $pattern = trim($pattern); + return preg_match($pattern, $subject); + } else { + throw new \Exception('Invalid condition: ' . $condition); + } + } + /** * Craft exception message appendix * diff --git a/src/dotenv.php b/src/dotenv.php old mode 100755 new mode 100644 index 956e719..bf88b7a --- a/src/dotenv.php +++ b/src/dotenv.php @@ -1,10 +1,13 @@ overload(); -} elseif (is_readable(getcwd() . DIRECTORY_SEPARATOR . '.env.default')) { - $dotenv = new Dotenv\Dotenv(getcwd(), '.env.default'); +} +if (is_readable(CWD . DIRECTORY_SEPARATOR . '.env')) { + $dotenv = new Dotenv\Dotenv(CWD); $dotenv->overload(); } diff --git a/tests/BlueprintActionTest.php b/tests/BlueprintActionTest.php index 3be247b..40c85b3 100644 --- a/tests/BlueprintActionTest.php +++ b/tests/BlueprintActionTest.php @@ -2,74 +2,138 @@ class BlueprintActionTest extends PHPUnit_Framework_TestCase { - public function testBeforeScriptsAreBeingExecutedWhenDeploying() + + /** @var PHPUnit_Framework_MockObject_MockObject */ + protected $profileManagerMock; + + /** @var PHPUnit_Framework_MockObject_MockObject */ + protected $cfnClientMock; + + /** @var PHPUnit_Framework_MockObject_MockObject */ + protected $blueprintMock; + + public function setUp() { - $stackFactoryMock = $this->getMock('\StackFormation\StackFactory', [], [], '', false); - $stackFactoryMock->method('getStackStatus')->willReturn('CREATE_COMPLETE'); + parent::setUp(); - $blueprintMock = $this->getMock('\StackFormation\Blueprint', [], [], '', false); - $blueprintMock->method('getBlueprintReference')->willReturn('FOO'); - $blueprintMock->expects($this->once())->method('executeBeforeScripts'); + $this->cfnClientMock = $this->getMock('\Aws\CloudFormation\CloudFormationClient', ['createChangeSet', 'UpdateStack', 'DescribeChangeSet', 'ValidateTemplate'], [], '', false); + $this->cfnClientMock->method('createChangeSet')->willReturn(new \Aws\Result(['id' => 'foo_id'])); + + $this->profileManagerMock = $this->getMock('\StackFormation\Profile\Manager', [], [], '', false); + $this->profileManagerMock->method('getClient')->willReturn($this->cfnClientMock); + $this->profileManagerMock->method('getStackFactory')->willReturnCallback(function () { + $stackFactoryMock = $this->getMock('\StackFormation\StackFactory', [], [], '', false); + $stackFactoryMock->method('getStackStatus')->willReturn('CREATE_COMPLETE'); + return $stackFactoryMock; + }); - $cfnClientMock = $this->getMock('\Aws\CloudFormation\CloudFormationClient', [], [], '', false); - $blueprintAction = new \StackFormation\BlueprintAction($cfnClientMock); - $blueprintAction->deploy($blueprintMock, false, $stackFactoryMock); + $this->blueprintMock = $this->getMock('\StackFormation\Blueprint', [], [], '', false); + $this->blueprintMock->method('getBlueprintReference')->willReturn('FOO'); } - public function testBeforeScriptsAreNotBeingExecutedWhenDeployingWithDryRun() + public function testFailingChangeSet() { - $stackFactoryMock = $this->getMock('\StackFormation\StackFactory', [], [], '', false); - $stackFactoryMock->method('getStackStatus')->willReturn('CREATE_COMPLETE'); + $this->cfnClientMock->method('describeChangeSet')->willReturn(new \Aws\Result(['Status' => 'FAILED', 'StatusReason' => 'FOO REASON'])); - $blueprintMock = $this->getMock('\StackFormation\Blueprint', [], [], '', false); - $blueprintMock->method('getBlueprintReference')->willReturn('FOO'); - $blueprintMock->expects($this->never())->method('executeBeforeScripts'); + $this->setExpectedException('Exception', 'FOO REASON'); - $cfnClientMock = $this->getMock('\Aws\CloudFormation\CloudFormationClient', [], [], '', false); - $blueprintAction = new \StackFormation\BlueprintAction($cfnClientMock); - $blueprintAction->deploy($blueprintMock, true, $stackFactoryMock); + $blueprintAction = new \StackFormation\BlueprintAction( + $this->blueprintMock, + $this->profileManagerMock + ); + $blueprintAction->getChangeSet(); } - public function testBeforeScriptsAreBeingExecutedWhenRequestingChangeSet() + public function testValidateTemplate() { + $this->cfnClientMock->expects($this->once())->method('validateTemplate'); + + $blueprintAction = new \StackFormation\BlueprintAction( + $this->blueprintMock, + $this->profileManagerMock + ); + $blueprintAction->validateTemplate(); + } + + + /** + * @test + */ + public function runBeforeScripts() + { + $testfile = tempnam(sys_get_temp_dir(), __FUNCTION__); + $blueprintMock = $this->getMock('\StackFormation\Blueprint', [], [], '', false); $blueprintMock->method('getBlueprintReference')->willReturn('FOO'); - $blueprintMock->expects($this->once())->method('executeBeforeScripts'); + $blueprintMock->method('getBasePath')->willReturn(sys_get_temp_dir()); + $blueprintMock->method('getBeforeScripts')->willReturn([ + 'echo -n "HELLO WORLD" > '.$testfile + ]); + + $blueprintAction = new \StackFormation\BlueprintAction( + $blueprintMock, + $this->profileManagerMock + ); - $cfnClientMock = $this->getMock('\Aws\CloudFormation\CloudFormationClient', ['createChangeSet', 'describeChangeSet'], [], '', false); - $cfnClientMock->method('createChangeSet')->willReturn(new \Aws\Result(['id' => 'foo_id'])); - $cfnClientMock->method('describeChangeSet')->willReturn(new \Aws\Result(['Status' => 'CREATE_COMPLETE'])); + $blueprintAction->executeBeforeScripts(); - $blueprintAction = new \StackFormation\BlueprintAction($cfnClientMock); - $blueprintAction->getChangeSet($blueprintMock, false); + $this->assertStringEqualsFile($testfile, 'HELLO WORLD'); + unlink($testfile); } - public function testFailingChangeSet() + + /** + * @test + */ + public function runBeforeScriptsInCorrectLocation() { + $testfile = tempnam(sys_get_temp_dir(), __FUNCTION__); + $blueprintMock = $this->getMock('\StackFormation\Blueprint', [], [], '', false); $blueprintMock->method('getBlueprintReference')->willReturn('FOO'); - $blueprintMock->expects($this->once())->method('executeBeforeScripts'); + $blueprintMock->method('getBasePath')->willReturn(FIXTURE_ROOT.'RunBeforeScript'); + $blueprintMock->method('getBeforeScripts')->willReturn([ + 'cat foo.txt > '.$testfile + ]); - $cfnClientMock = $this->getMock('\Aws\CloudFormation\CloudFormationClient', ['createChangeSet', 'describeChangeSet'], [], '', false); - $cfnClientMock->method('createChangeSet')->willReturn(new \Aws\Result(['id' => 'foo_id'])); - $cfnClientMock->method('describeChangeSet')->willReturn(new \Aws\Result(['Status' => 'FAILED', 'StatusReason' => 'FOO REASON'])); + $blueprintAction = new \StackFormation\BlueprintAction( + $blueprintMock, + $this->profileManagerMock + ); - $this->setExpectedException('\Exception', 'FOO REASON'); + $blueprintAction->executeBeforeScripts(); - $blueprintAction = new \StackFormation\BlueprintAction($cfnClientMock); - $blueprintAction->getChangeSet($blueprintMock, false); + $this->assertStringEqualsFile($testfile, 'HELLO WORLD FROM FILE'); + unlink($testfile); } - public function testValidateTemplate() + /** + * @test + */ + public function beforeScriptsHaveProfilesEnvVarsSet() { + chdir(FIXTURE_ROOT.'ProfileManager/fixture_before_scripts'); + $testfile = tempnam(sys_get_temp_dir(), __FUNCTION__); + + $profileManager = new \StackFormation\Profile\Manager(); + $blueprintMock = $this->getMock('\StackFormation\Blueprint', [], [], '', false); + $blueprintMock->method('getProfile')->willReturn('before_scripts_profile'); $blueprintMock->method('getBlueprintReference')->willReturn('FOO'); + $blueprintMock->method('getBasePath')->willReturn(sys_get_temp_dir()); + $blueprintMock->method('getBeforeScripts')->willReturn([ + 'echo -n "${AWS_ACCESS_KEY_ID}:${AWS_SECRET_ACCESS_KEY}" > '.$testfile + ]); + + $blueprintAction = new \StackFormation\BlueprintAction( + $blueprintMock, + $profileManager + ); - $cfnClientMock = $this->getMock('\Aws\CloudFormation\CloudFormationClient', ['validateTemplate'], [], '', false); - $cfnClientMock->expects($this->once())->method('validateTemplate'); + $blueprintAction->executeBeforeScripts(); - $blueprintAction = new \StackFormation\BlueprintAction($cfnClientMock); - $blueprintAction->validateTemplate($blueprintMock); + $this->assertStringEqualsFile($testfile, 'TESTACCESSKEY1:TESTSECRETKEY1'); + unlink($testfile); } diff --git a/tests/BlueprintGetParameterTest.php b/tests/BlueprintGetParameterTest.php index 8b77bd0..418f56a 100644 --- a/tests/BlueprintGetParameterTest.php +++ b/tests/BlueprintGetParameterTest.php @@ -11,18 +11,19 @@ protected function getMockedBlueprint($blueprintConfig, $name=null) $stackFactoryMock->method('getStackResource')->willReturn('dummyResource'); $stackFactoryMock->method('getStackParameter')->willReturn('dummyParameter'); - $placeholderResolver = new \StackFormation\PlaceholderResolver( + $profileManagerMock = $this->getMock('\StackFormation\Profile\Manager', [], [], '', false); + $profileManagerMock->method('getStackFactory')->willReturn($stackFactoryMock); + + $placeholderResolver = new \StackFormation\ValueResolver( new \StackFormation\DependencyTracker(), - $stackFactoryMock, + $profileManagerMock, $configMock ); - $conditionalValueResolver = new \StackFormation\ConditionalValueResolver($placeholderResolver); - if (is_null($name)) { $name = 'blueprint_mock_'.time(); } - return new \StackFormation\Blueprint($name, $blueprintConfig, $placeholderResolver, $conditionalValueResolver); + return new \StackFormation\Blueprint($name, $blueprintConfig, $placeholderResolver); } /** @@ -34,7 +35,7 @@ public function getParameter($rawParameterValue, $expectedResolvedValue, $putenv if ($putenv) { putenv($putenv); } - $blueprint = $this->getMockedBlueprint([ 'parameters' => [ + $blueprint = $this->getMockedBlueprint(['parameters' => [ 'Foo' => $rawParameterValue ]]); $parameters = $blueprint->getParameters(true); @@ -133,5 +134,4 @@ public function getBasePath() $this->assertEquals(FIXTURE_ROOT.'Config', $basePath); } - } \ No newline at end of file diff --git a/tests/BlueprintTest.php b/tests/BlueprintTest.php index 3824e36..2a5002b 100644 --- a/tests/BlueprintTest.php +++ b/tests/BlueprintTest.php @@ -9,14 +9,12 @@ protected function getMockedBlueprintFactory(\StackFormation\Config $config) $stackFactoryMock->method('getStackResource')->willReturn('dummyResource'); $stackFactoryMock->method('getStackParameter')->willReturn('dummyParameter'); - $placeholderResolver = new \StackFormation\PlaceholderResolver( - new \StackFormation\DependencyTracker(), - $stackFactoryMock, - $config - ); + $profileManagerMock = $this->getMock('\StackFormation\Profile\Manager', [], [], '', false); + $profileManagerMock->method('getStackFactory')->willReturn($stackFactoryMock); - $conditionalValueResolver = new \StackFormation\ConditionalValueResolver($placeholderResolver); - return new \StackFormation\BlueprintFactory($config, $placeholderResolver, $conditionalValueResolver); + $valueResolver = new \StackFormation\ValueResolver(null, $profileManagerMock, $config); + + return new \StackFormation\BlueprintFactory($config, $valueResolver); } /** @@ -30,6 +28,11 @@ public function getVariable() $this->assertEquals('BlueprintBar', $blueprintVars['BlueprintFoo']); } + public function testBlueprintVarOverridesGlobalVar() + { + $this->markTestIncomplete(); + } + /** * @test */ @@ -160,21 +163,6 @@ public function getBasepath() $this->assertEquals(FIXTURE_ROOT.'Config', $blueprint->getBasePath()); } - /** - * @test - */ - public function runBeforeScriptsWith() - { - $testfile = tempnam(sys_get_temp_dir(), __METHOD__); - putenv("TESTFILE=$testfile"); - $config = new \StackFormation\Config([FIXTURE_ROOT.'Config/blueprint.1.yml']); - $blueprint = $this->getMockedBlueprintFactory($config)->getBlueprint('fixture6'); - $blueprint->executeBeforeScripts(); - - $this->assertStringEqualsFile($testfile, 'HELLO WORLD'); - unlink($testfile); - } - /** * @test */ @@ -225,5 +213,116 @@ public function testselectProfileConditionalProvider() { ]; } + /** + * @test + * @dataProvider testConditionalGlobalProvider + */ + public function testConditionalGlobalVar($foo, $expectedValue) + { + putenv('Foo='.$foo); + $config = new \StackFormation\Config([FIXTURE_ROOT.'Config/blueprint.conditional_vars.yml']); + $blueprint = $this->getMockedBlueprintFactory($config)->getBlueprint('fixture_var_conditional_global'); + $parameters = $blueprint->getParameters(true); + $parameters = \StackFormation\Helper::flatten($parameters, 'ParameterKey', 'ParameterValue'); + $this->assertEquals($expectedValue, $parameters['Parameter1']); + } + + public function testConditionalGlobalProvider() { + return [ + ['Val1', 'a'], + ['Val2', 'b'], + ['somethingelse', 'c'], + ]; + } + + /** + * @test + * @dataProvider testConditionalGlobalProvider + */ + public function testConditionalLocalVar($foo, $expectedValue) + { + putenv('Foo='.$foo); + $config = new \StackFormation\Config([FIXTURE_ROOT.'Config/blueprint.conditional_vars.yml']); + $blueprint = $this->getMockedBlueprintFactory($config)->getBlueprint('fixture_var_conditional_local'); + $parameters = $blueprint->getParameters(true); + $parameters = \StackFormation\Helper::flatten($parameters, 'ParameterKey', 'ParameterValue'); + $this->assertEquals($expectedValue, $parameters['Parameter1']); + } + + /** + * @test + */ + public function testSwitchProfile() { + $profileManagerMock = $this->getMock('\StackFormation\Profile\Manager', [], [], '', false); + $profileManagerMock + ->expects($this->exactly(2)) + ->method('getStackFactory') + ->willReturnCallback(function($profile) { + if ($profile == 'myprofile1') { + $stackFactoryMock = $this->getMock('\StackFormation\StackFactory', [], [], 'LocalStackFactory', false); + $stackFactoryMock->method('getStackOutput')->willReturn('dummyOutputLocal'); + return $stackFactoryMock; + } + if ($profile == 'myprofile2') { + $subStackFactoryMock = $this->getMock('\StackFormation\StackFactory', [], [], 'RemoteStackFactory', false); + $subStackFactoryMock->method('getStackOutput')->willReturn('dummyOutputRemote'); + return $subStackFactoryMock; + } + return null; + }); + + $config = new \StackFormation\Config([FIXTURE_ROOT.'Config/blueprint.switch_profile.yml']); + + $valueResolver = new \StackFormation\ValueResolver( + new \StackFormation\DependencyTracker(), + $profileManagerMock, + $config + ); + + $blueprintFactory = new \StackFormation\BlueprintFactory($config, $valueResolver); + + $blueprint = $blueprintFactory->getBlueprint('switch_profile'); + $parameters = $blueprint->getParameters(true); + $parameters = \StackFormation\Helper::flatten($parameters, 'ParameterKey', 'ParameterValue'); + + $this->assertEquals('Bar1', $parameters['Foo1']); + $this->assertEquals('dummyOutputRemote', $parameters['Foo2']); + $this->assertEquals('dummyOutputLocal', $parameters['Foo3']); + } + + /** + * @test + */ + public function testSwitchProfileComplex() { + + putenv('ACCOUNT=t'); + putenv('BASE_TYPE_VERSION=42'); + + $profileManagerMock = $this->getMock('\StackFormation\Profile\Manager', [], [], '', false); + $profileManagerMock + ->method('getStackFactory') + ->willReturnCallback(function() { + $stackFactoryMock = $this->getMock('\StackFormation\StackFactory', ['getStackOutput'], [], '', false); + $stackFactoryMock->method('getStackOutput')->willReturnCallback(function($stackName, $key) { return "DummyValue|$stackName|$key"; }); + return $stackFactoryMock; + }); + + $config = new \StackFormation\Config([FIXTURE_ROOT.'Config/blueprint.switch_profile.yml']); + + $valueResolver = new \StackFormation\ValueResolver( + new \StackFormation\DependencyTracker(), + $profileManagerMock, + $config + ); + + $blueprintFactory = new \StackFormation\BlueprintFactory($config, $valueResolver); + + $blueprint = $blueprintFactory->getBlueprint('switch_profile_complex'); + $parameters = $blueprint->getParameters(true); + $parameters = \StackFormation\Helper::flatten($parameters, 'ParameterKey', 'ParameterValue'); + + $this->assertEquals('DummyValue|ecom-t-all-ami-types-42-stack|VarnishAmi', $parameters['VarnishAmi']); + } + } \ No newline at end of file diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php index 8214a64..a716798 100644 --- a/tests/ConfigTest.php +++ b/tests/ConfigTest.php @@ -72,4 +72,5 @@ public function invalidAttribute() // $vars = $config->getGlobalVars(); //} + } \ No newline at end of file diff --git a/tests/HelperTest.php b/tests/HelperTest.php new file mode 100644 index 0000000..2b782ab --- /dev/null +++ b/tests/HelperTest.php @@ -0,0 +1,41 @@ +setExpectedException('Exception'); + StackFormation\Helper::validateStackname($stackName); + } + + public function invalidStackNameProvider() { + return [ + ['ecom_t_stack'], + [''], + ['stackname with whitespace'], + ['123ecom'], + [str_repeat('a', 129)], + ]; + } + +} \ No newline at end of file diff --git a/tests/ProfileManagerTest.php b/tests/ProfileManagerTest.php index 00bdf45..759d3af 100644 --- a/tests/ProfileManagerTest.php +++ b/tests/ProfileManagerTest.php @@ -7,10 +7,16 @@ class ProfileManagerTest extends PHPUnit_Framework_TestCase { protected $originalCwd; + /** + * @var \StackFormation\Profile\Manager + */ + protected $profileManager; + public function setUp() { parent::setUp(); $this->originalCwd = getcwd(); + $this->profileManager = new \StackFormation\Profile\Manager(); } public function tearDown() @@ -22,47 +28,38 @@ public function tearDown() public function testListProfiles() { chdir(FIXTURE_ROOT.'ProfileManager/fixture_basic'); - $profileManager = new \AwsInspector\ProfileManager(); $this->assertEquals( ['test1', 'test2', 'test3'], - $profileManager->listAllProfiles() - ); - $this->assertEquals( - ['profiles.yml'], - $profileManager->getLoadedFiles() + $this->profileManager->listAllProfiles() ); } public function testInvalidProfile() { chdir(FIXTURE_ROOT . 'ProfileManager/fixture_basic'); - $profileManager = new \AwsInspector\ProfileManager(); - $this->assertFalse($profileManager->isValidProfile('invalidProfile')); + $this->setExpectedException('Exception', 'Invalid profile: invalidProfile'); + $this->profileManager->getClient('CloudFormation', 'invalidProfile'); } public function testLoadProfile() { chdir(FIXTURE_ROOT . 'ProfileManager/fixture_basic'); - $profileManager = new \AwsInspector\ProfileManager(); - $profileManager->loadProfile('test1'); - $this->assertEquals('test1', getenv('AWSINSPECTOR_PROFILE')); - $this->assertEquals('us-east-1', getenv('AWS_DEFAULT_REGION')); - $this->assertEquals('TESTACCESSKEY1', getenv('AWS_ACCESS_KEY_ID')); - $this->assertEquals('TESTSECRETKEY1', getenv('AWS_SECRET_ACCESS_KEY')); + putenv("AWS_DEFAULT_REGION=eu-west-1"); + $cfnClient = $this->profileManager->getClient('CloudFormation', 'test1'); + + $credentials = $cfnClient->getCredentials()->wait(true); + + $this->assertEquals('TESTACCESSKEY1', $credentials->getAccessKeyId()); + $this->assertEquals('TESTSECRETKEY1', $credentials->getSecretKey()); } public function testLoadMultipleFiles() { chdir(FIXTURE_ROOT . 'ProfileManager/fixture_merge_profiles'); - $profileManager = new \AwsInspector\ProfileManager(); $this->assertEquals( ['test1', 'test2', 'test3', 'test1-personal', 'test2-personal'], - $profileManager->listAllProfiles() - ); - $this->assertEquals( - ['profiles.yml', 'profiles.personal.yml'], - $profileManager->getLoadedFiles() + $this->profileManager->listAllProfiles() ); } @@ -74,8 +71,7 @@ public function testEncryptedFileWithoutVaultVars() { putenv("VAULT_MAC_KEY="); putenv("VAULT_ENCRYPTION_KEY="); chdir(FIXTURE_ROOT . 'ProfileManager/fixture_encrpyted'); - $profileManager = new \AwsInspector\ProfileManager(); - $profileManager->listAllProfiles(); + $this->profileManager->listAllProfiles(); } public function testEncryptedFileWithWrongVaultVars() { @@ -86,8 +82,7 @@ public function testEncryptedFileWithWrongVaultVars() { putenv("VAULT_MAC_KEY=sadsad"); putenv("VAULT_ENCRYPTION_KEY=asdsad"); chdir(FIXTURE_ROOT . 'ProfileManager/fixture_encrpyted'); - $profileManager = new \AwsInspector\ProfileManager(); - $profileManager->listAllProfiles(); + $this->profileManager->listAllProfiles(); } public function testEncryptedFile() { @@ -97,14 +92,9 @@ public function testEncryptedFile() { putenv("VAULT_MAC_KEY=".self::VAULT_MAC_KEY); putenv("VAULT_ENCRYPTION_KEY=".self::VAULT_ENCRYPTION_KEY); chdir(FIXTURE_ROOT . 'ProfileManager/fixture_encrpyted'); - $profileManager = new \AwsInspector\ProfileManager(); $this->assertEquals( ['test1', 'test2', 'test3'], - $profileManager->listAllProfiles() - ); - $this->assertEquals( - ['profiles.yml'], - $profileManager->getLoadedFiles() + $this->profileManager->listAllProfiles() ); } @@ -115,14 +105,9 @@ public function testEncryptedAndUnencryptedFile() { putenv("VAULT_MAC_KEY=".self::VAULT_MAC_KEY); putenv("VAULT_ENCRYPTION_KEY=".self::VAULT_ENCRYPTION_KEY); chdir(FIXTURE_ROOT . 'ProfileManager/fixture_encrpyted_mix'); - $profileManager = new \AwsInspector\ProfileManager(); $this->assertEquals( ['test1', 'test2', 'test3', 'test1-personal', 'test2-personal'], - $profileManager->listAllProfiles() - ); - $this->assertEquals( - ['profiles.yml', 'profiles.personal.yml'], - $profileManager->getLoadedFiles() + $this->profileManager->listAllProfiles() ); } diff --git a/tests/ConditionalValueResolverTest.php b/tests/ValueResolverTest.php similarity index 62% rename from tests/ConditionalValueResolverTest.php rename to tests/ValueResolverTest.php index fbd6a6b..1e42b7f 100644 --- a/tests/ConditionalValueResolverTest.php +++ b/tests/ValueResolverTest.php @@ -1,30 +1,41 @@ conditionalValueResolver = new \StackFormation\ConditionalValueResolver($this->getMockedPlaceholderResolver()); + $this->valueResolver = $this->getMockedPlaceholderResolver(); parent::setUp(); } public function getMockedPlaceholderResolver() { $config = $this->getMock('\StackFormation\Config', [], [], '', false); - $config->method('getGlobalVars')->willReturn(['GlobalFoo' => 'GlobalBar']); + $config->method('getGlobalVars')->willReturn([ + 'GlobalFoo' => 'GlobalBar', + 'GlobalFoo2' => 'GlobalBar2', + 'GlobalBar' => 'GlobalFoo3', + 'rescursiveA' => '{var:rescursiveB}', + 'rescursiveB' => 'Hello', + 'circularA' => '{var:circularB}', + 'circularB' => '{var:circularA}', + 'directCircular' => '{var:directCircular}', + ]); $stackFactoryMock = $this->getMock('\StackFormation\StackFactory', [], [], '', false); $stackFactoryMock->method('getStackOutput')->willReturn('dummyOutput'); $stackFactoryMock->method('getStackResource')->willReturn('dummyResource'); $stackFactoryMock->method('getStackParameter')->willReturn('dummyParameter'); - $placeholderResolver = new \StackFormation\PlaceholderResolver( - new \StackFormation\DependencyTracker(), - $stackFactoryMock, + $profileManagerMock = $this->getMock('\StackFormation\Profile\Manager', [], [], '', false); + + $placeholderResolver = new \StackFormation\ValueResolver( + null, + $profileManagerMock, $config ); return $placeholderResolver; @@ -35,7 +46,7 @@ public function getMockedPlaceholderResolver() { */ public function defaultIsTrue() { - $this->assertTrue($this->conditionalValueResolver->isTrue('default')); + $this->assertTrue($this->valueResolver->isTrue('default')); } /** @@ -49,7 +60,7 @@ public function checkKey($key, $expectedValue, $putenv=null) { if ($putenv) { putenv($putenv); } - $actualValue = $this->conditionalValueResolver->isTrue($key, $blueprint); + $actualValue = $this->valueResolver->isTrue($key, $blueprint); $this->assertEquals($expectedValue, $actualValue); } @@ -75,12 +86,18 @@ public function isConditionDataProvider() ['{env:FOO}=={var:GlobalFoo}', true, 'FOO=GlobalBar'], ['GlobalBar=={var:{env:FOO}}', true, 'FOO=GlobalFoo'], ['{var:BlueprintFoo}==BlueprintBar', true], + ['prod~=/^prod$/', true], + ['prod~=/^(prod|qa)$/', true], + ['prd~=/^(prod|qa)$/', false], + ['test1~=/^test.$/', true], ]; $invertedValues = []; foreach ($values as $value) { - $value[0] = str_replace('==', '!=', $value[0]); - $value[1] = !$value[1]; - $invertedValues[] = $value; + if (strpos($value[0], '==') !== false) { + $value[0] = str_replace('==', '!=', $value[0]); + $value[1] = !$value[1]; + $invertedValues[] = $value; + } } return array_merge($values, $invertedValues); } @@ -91,7 +108,7 @@ public function isConditionDataProvider() */ public function invalidCondition($key) { $this->setExpectedException('Exception', 'Invalid condition'); - $this->conditionalValueResolver->isTrue($key); + $this->valueResolver->isTrue($key); } public function invalidConditionProvider() { @@ -114,7 +131,7 @@ public function resolve(array $conditions, $expectedValue, $putenv=null) } $blueprint = $this->getMock('\StackFormation\Blueprint', [], [], '', false); $blueprint->method('getVars')->willReturn(['BlueprintFoo' => 'BlueprintBar']); - $actualValue = $this->conditionalValueResolver->resolveConditionalValue($conditions, $blueprint); + $actualValue = $this->valueResolver->resolveConditionalValue($conditions, $blueprint); $this->assertEquals($expectedValue, $actualValue); } @@ -148,7 +165,52 @@ public function resolveDataProvider() { public function missingEnv() { $this->setExpectedException('Exception', "Environment variable 'DDD' not found"); - $actualValue = $this->conditionalValueResolver->resolveConditionalValue(['{env:DDD}' => 13]); + $this->valueResolver->resolveConditionalValue(['{env:DDD}' => 13]); + } + + /** + * @test + */ + public function missingVar() + { + $this->setExpectedException('Exception', "Variable 'DDD' not found (Type:conditional_value, Key:{var:DDD})"); + $this->valueResolver->resolveConditionalValue(['{var:DDD}' => 13]); + } + + /** + * @test + */ + public function nestedVars() + { + $result = $this->valueResolver->resolvePlaceholders('{var:{var:GlobalFoo}}'); + $this->assertEquals('GlobalFoo3', $result); + } + + /** + * @test + */ + public function recursiveReferences() + { + $result = $this->valueResolver->resolvePlaceholders('{var:rescursiveA}'); + $this->assertEquals('Hello', $result); + } + + /** + * @test + */ + public function directCircularReferences() + { + $this->setExpectedException('Exception', 'Direct circular reference detected'); + $result = $this->valueResolver->resolvePlaceholders('{var:directCircular}'); + } + + /** + * @test + */ + public function circularReferences() + { + $this->setExpectedException('Exception', 'Max nesting level reached. Looks like a circular dependency.'); + $this->valueResolver->resolvePlaceholders('{var:circularA}'); } -} \ No newline at end of file +} diff --git a/tests/fixtures/Config/blueprint.1.yml b/tests/fixtures/Config/blueprint.1.yml index e77a051..87141d5 100644 --- a/tests/fixtures/Config/blueprint.1.yml +++ b/tests/fixtures/Config/blueprint.1.yml @@ -35,4 +35,11 @@ blueprints: - stackname: 'fixture7' template: 'dummy.template' - stackPolicy: 'dummy_policy.json' \ No newline at end of file + stackPolicy: 'dummy_policy.json' + + + - stackname: 'fixture8' + template: 'dummy.template' + profile: 'before_scripts_profile' + before: + - 'echo -n "${AWS_ACCESS_KEY_ID}:${AWS_SECRET_ACCESS_KEY}" > {env:TESTFILE}' \ No newline at end of file diff --git a/tests/fixtures/Config/blueprint.conditional_vars.yml b/tests/fixtures/Config/blueprint.conditional_vars.yml new file mode 100644 index 0000000..383b4d2 --- /dev/null +++ b/tests/fixtures/Config/blueprint.conditional_vars.yml @@ -0,0 +1,23 @@ +vars: + + FooVar: + '{env:Foo}==Val1': a + '{env:Foo}==Val2': b + 'default': c + +blueprints: + + - stackname: 'fixture_var_conditional_global' + template: 'dummy.template' + parameters: + Parameter1: '{var:FooVar}' + + - stackname: 'fixture_var_conditional_local' + template: 'dummy.template' + vars: + LocalFooVar: + '{env:Foo}==Val1': a + '{env:Foo}==Val2': b + 'default': c + parameters: + Parameter1: '{var:LocalFooVar}' \ No newline at end of file diff --git a/tests/fixtures/Config/blueprint.switch_profile.yml b/tests/fixtures/Config/blueprint.switch_profile.yml new file mode 100644 index 0000000..138c3b6 --- /dev/null +++ b/tests/fixtures/Config/blueprint.switch_profile.yml @@ -0,0 +1,15 @@ +blueprints: + + - stackname: 'switch_profile' + template: 'dummy.template' + profile: 'myprofile1' + parameters: + Foo1: 'Bar1' + Foo2: '[profile:myprofile2:{output:remoteStack:fooField}]' + Foo3: '{output:localStack:fooField}' + + - stackname: 'switch_profile_complex' + template: 'dummy.template' + profile: 'myprofile1' + parameters: + VarnishAmi: '[profile:t.deploy-ecom:{output:ecom-{env:ACCOUNT}-all-ami-types-{env:BASE_TYPE_VERSION}-stack:VarnishAmi}]' \ No newline at end of file diff --git a/tests/fixtures/ProfileManager/fixture_before_scripts/profiles.yml b/tests/fixtures/ProfileManager/fixture_before_scripts/profiles.yml new file mode 100644 index 0000000..d323093 --- /dev/null +++ b/tests/fixtures/ProfileManager/fixture_before_scripts/profiles.yml @@ -0,0 +1,5 @@ +profiles: + before_scripts_profile: + region: 'us-east-1' + access_key: 'TESTACCESSKEY1' + secret_key: 'TESTSECRETKEY1' \ No newline at end of file diff --git a/tests/fixtures/RunBeforeScript/foo.txt b/tests/fixtures/RunBeforeScript/foo.txt new file mode 100644 index 0000000..6c02b30 --- /dev/null +++ b/tests/fixtures/RunBeforeScript/foo.txt @@ -0,0 +1 @@ +HELLO WORLD FROM FILE \ No newline at end of file