diff --git a/src/StackFormation/Helper/Div.php b/src/StackFormation/Helper/Div.php index f477f75..952e177 100644 --- a/src/StackFormation/Helper/Div.php +++ b/src/StackFormation/Helper/Div.php @@ -35,4 +35,17 @@ public static function isProgramInstalled($program) @exec('which ' . $program, $out, $return); return $return === 0; } + + /** + * @param string $string + * @return bool + */ + public static function isJson($string) { + $string = trim($string); + + // TODO just a workaround to check if the string is a valid JSON + // we could not check that with json_decode because, it could be that the JSON file has some comments inside + // and the StringPreProcessor/StripComments is invoked after that check!! + return preg_match('/\A\{(.*)/', $string); + } } diff --git a/src/StackFormation/Helper/Pipeline.php b/src/StackFormation/Helper/Pipeline.php index 9bd1176..71ea6d8 100644 --- a/src/StackFormation/Helper/Pipeline.php +++ b/src/StackFormation/Helper/Pipeline.php @@ -24,6 +24,14 @@ public function addStage(callable $stage) return $this; } + /** + * @return \callable[] + */ + public function getStages() + { + return $this->stages; + } + /** * Process the payload. * diff --git a/src/StackFormation/PreProcessor/RecursiveArrayObject.php b/src/StackFormation/PreProcessor/RecursiveArrayObject.php new file mode 100644 index 0000000..6d06330 --- /dev/null +++ b/src/StackFormation/PreProcessor/RecursiveArrayObject.php @@ -0,0 +1,82 @@ +$v) { + $this->__set($k, $v); + } + } + + /** + * @param string $name + * @param mixed $value + */ + public function __set($name, $value){ + if (is_array($value) || is_object($value)) { + $this->offsetSet($name, (new self($value))); + } else { + $this->offsetSet($name, $value); + } + } + + /** + * @param string $name + * @return mixed + */ + public function __get($name){ + if ($this->offsetExists($name)) { + return $this->offsetGet($name); + } elseif (array_key_exists($name, $this)) { + return $this[$name]; + } else { + throw new \InvalidArgumentException(sprintf('$this have not prop `%s`',$name)); + } + } + + /** + * @return array + */ + public function getArrayCopy() + { + $array = parent::getArrayCopy(); + $assocArray = false; + foreach ($array as $key => $value) { + if (is_string($key)) { + $assocArray = true; + } + if ($value instanceof RecursiveArrayObject) { + $array[$key] = $value->getArrayCopy(); + } + } + return $assocArray ? $array : array_values($array); + } + + /** + * @param string $name + * @return bool + */ + public function __isset($name){ + return array_key_exists($name, $this); + } + + /** + * @param string $name + */ + public function __unset($name){ + unset($this[$name]); + } +} diff --git a/src/StackFormation/PreProcessor/Rootline.php b/src/StackFormation/PreProcessor/Rootline.php new file mode 100644 index 0000000..a83017d --- /dev/null +++ b/src/StackFormation/PreProcessor/Rootline.php @@ -0,0 +1,44 @@ +getArrayCopy()); + } + + /** + * @param $index + * @return mixed + */ + public function indexGet($index) { + $keys = $this->getKeys(); + return $this->offsetGet($keys[$index]); + } + + public function removeLast() { + $keys = $this->getKeys(); + + // TODO + @$this->offsetUnset($keys[$this->count()]); + } + + /** + * @param int $generation + * @return mixed + */ + public function parent($generation = 1) { + $keys = $this->getKeys(); + return $this->offsetGet($keys[$this->count() - $generation]); + } +} diff --git a/src/StackFormation/PreProcessor/RootlineItem.php b/src/StackFormation/PreProcessor/RootlineItem.php new file mode 100644 index 0000000..9af6aaf --- /dev/null +++ b/src/StackFormation/PreProcessor/RootlineItem.php @@ -0,0 +1,38 @@ +key = $key; + $this->value = $value; + } + + /** + * @return string + */ + public function getKey() { + return $this->key; + } + + /** + * @return mixed + */ + public function getValue() { + return $this->value; + } +} diff --git a/src/StackFormation/PreProcessor/Stage/AbstractStringPreProcessorStage.php b/src/StackFormation/PreProcessor/Stage/AbstractStringPreProcessorStage.php new file mode 100644 index 0000000..78d820e --- /dev/null +++ b/src/StackFormation/PreProcessor/Stage/AbstractStringPreProcessorStage.php @@ -0,0 +1,36 @@ +stringPreProcessor = $stringPreProcessor; + } + + /** + * @param string $content + * @throws \StackFormation\Exception\StringPreProcessorException + */ + public function __invoke($content) + { + try { + return $this->invoke($content); + } catch (\Exception $e) { + + throw new \Exception('TODO create StringPreProcessorException'); + // TODO + //throw new \StackFormation\Exception\StringPreProcessorException($content, $e); + } + } + + /** + * @param string $content + */ + abstract function invoke($content); +} diff --git a/src/StackFormation/PreProcessor/Stage/AbstractTreePreProcessorStage.php b/src/StackFormation/PreProcessor/Stage/AbstractTreePreProcessorStage.php new file mode 100644 index 0000000..f694fef --- /dev/null +++ b/src/StackFormation/PreProcessor/Stage/AbstractTreePreProcessorStage.php @@ -0,0 +1,30 @@ +treePreProcessor = $treePreProcessor; + $this->basePath = $basePath; + } + + public function __invoke() {} + + /** + * @param string $path + * @param string $value + * @param Rootline $rootLineReferences + * @return mixed + */ + abstract function invoke($path, $value, Rootline $rootLineReferences); +} diff --git a/src/StackFormation/PreProcessor/Stage/String/StripComments.php b/src/StackFormation/PreProcessor/Stage/String/StripComments.php new file mode 100644 index 0000000..2cd6a36 --- /dev/null +++ b/src/StackFormation/PreProcessor/Stage/String/StripComments.php @@ -0,0 +1,24 @@ +parent(1); /* @var $parentRootlineItem RootlineItem */ + $parent = $parentRootlineItem->getValue(); /* @var $parent RecursiveArrayObject */ + + $grandParentRootlineItem = $rootLineReferences->parent(2); /* @var $grandParentRootlineItem RootlineItem */ + $grandParent = $grandParentRootlineItem->getValue(); /* @var $grandParent RecursiveArrayObject */ + + // remove original item + $grandParent->offsetUnset($parentRootlineItem->getKey()); + + // add a new line for every csl item + foreach (explode(',', $value) as $cidrIp) { + $newItem = clone $parent; + $newItem->CidrIp = $cidrIp; + $grandParent->append($newItem); + } + + return true; // indicate that something has changed + } +} diff --git a/src/StackFormation/PreProcessor/Stage/Tree/ExpandCslPort.php b/src/StackFormation/PreProcessor/Stage/Tree/ExpandCslPort.php new file mode 100644 index 0000000..59879f4 --- /dev/null +++ b/src/StackFormation/PreProcessor/Stage/Tree/ExpandCslPort.php @@ -0,0 +1,45 @@ +parent(1); /* @var $parentRootlineItem RootlineItem */ + $parent = $parentRootlineItem->getValue(); /* @var $parent RecursiveArrayObject */ + + $grandParentRootlineItem = $rootLineReferences->parent(2); /* @var $grandParentRootlineItem RootlineItem */ + $grandParent = $grandParentRootlineItem->getValue(); /* @var $grandParent RecursiveArrayObject */ + + // remove original item + $grandParent->offsetUnset($parentRootlineItem->getKey()); + + // add a new line for every csl item + foreach (explode(',', $value) as $port) { + $newItem = clone $parent; + $newItem->FromPort = $port; + $newItem->ToPort = $port; + $newItem->offsetUnset('Port'); + $grandParent->append($newItem); + } + + return true; // indicate that something has changed + } +} diff --git a/src/StackFormation/PreProcessor/Stage/Tree/InjectFilecontent.php b/src/StackFormation/PreProcessor/Stage/Tree/InjectFilecontent.php new file mode 100644 index 0000000..c4a3191 --- /dev/null +++ b/src/StackFormation/PreProcessor/Stage/Tree/InjectFilecontent.php @@ -0,0 +1,107 @@ +parent(1); /* @var RootlineItem $parentRootlineItem */ + $parent = $parentRootlineItem->getValue(); /* @var RecursiveArrayObject $parentData */ + $lines = $this->renderFileContent(trim(end($matches)), $matches[1]); + + $parent->offsetUnset($fnBase64); + $parent[$fnBase64][$fnJoin] = ['', [implode('', $lines)]]; + + return true; + } + + if (preg_match('+Fn::FileContent(|Unpretty|TrimLines|Minify)$+', $path, $matches)) { + + $parentRootlineItem = $rootLineReferences->parent(1); /* @var RootlineItem $parentRootlineItem */ + $parent = $parentRootlineItem->getValue(); /* @var RecursiveArrayObject $parentData */ + $lines = $this->renderFileContent($value, $matches[1]); + + $parent->offsetUnset($matches[0]); + $parent[$fnJoin] = ['', [implode('', $lines)]]; + + return true; + } + + return false; + } + + /** + * @param string $file + * @param string $modus + * @return array + * @throws \Exception + */ + protected function renderFileContent($file, $modus) + { + $file = $this->basePath . '/' . $file; + if (!is_file($file)) { + throw new FileNotFoundException("File '$file' not found"); + } + + $ext = pathinfo($file, PATHINFO_EXTENSION); + if ($modus == 'Minify' && $ext != 'js') { + throw new \Exception('Fn::FileContentMinify is only supported for *.js files. (File: ' . $file . ')'); + } + + $fileContent = file_get_contents($file); + + # TODO in own stage ? + #$fileContent = $this->injectInclude($fileContent, dirname(realpath($file))); + + if ($ext === 'js') { + if ($modus == 'Minify') { + $fileContent = \JShrink\Minifier::minify($fileContent, ['flaggedComments' => false]); + } + + $size = strlen($fileContent); + if ($size > self::MAX_JS_FILE_INCLUDE_SIZE) { + // this is assuming you are uploading an inline JS file to AWS Lambda + throw new \Exception(sprintf("JS file is larger than %s bytes (actual size: %s bytes)", self::MAX_JS_FILE_INCLUDE_SIZE, $size)); + } + } + + // TODO: this isn't optimal. Why are we processing this here in between? + #$fileContent = $this->base64encodedJson($fileContent); + + $lines = explode("\n", $fileContent); + foreach ($lines as $lineKey => &$line) { + if ($modus == 'TrimLines') { + $line = trim($line); + if (empty($line)) { + unset($lines[$lineKey]); + } + } + $line .= "\n"; + } + + #$whitespace = trim($matches[1], "\n"); + #$result = str_replace("\n", "\n" . $whitespace, $result); + + return $lines; + } +} diff --git a/src/StackFormation/PreProcessor/StringPreProcessor.php b/src/StackFormation/PreProcessor/StringPreProcessor.php new file mode 100644 index 0000000..4596f50 --- /dev/null +++ b/src/StackFormation/PreProcessor/StringPreProcessor.php @@ -0,0 +1,26 @@ +addStage(new $stageClass($this)); + } + + return $pipeline->process($content); + } +} diff --git a/src/StackFormation/PreProcessor/TreePreProcessor.php b/src/StackFormation/PreProcessor/TreePreProcessor.php new file mode 100644 index 0000000..9668256 --- /dev/null +++ b/src/StackFormation/PreProcessor/TreePreProcessor.php @@ -0,0 +1,95 @@ + ArrayObject (so we can restructure it since all the child elements are passed by reference) + $data = new RecursiveArrayObject($data, \ArrayObject::ARRAY_AS_PROPS); + + $stageClasses = [ + '\StackFormation\PreProcessor\Stage\Tree\ExpandCslPort', + '\StackFormation\PreProcessor\Stage\Tree\ExpandCidrIp', + '\StackFormation\PreProcessor\Stage\Tree\InjectFilecontent', + + # TODO, check also if we still need that + #'\StackFormation\PreProcessor\Stage\Tree\ParseRefInDoubleQuotedStrings', + #'\StackFormation\PreProcessor\Stage\Tree\Base64encodedJson', + #'\StackFormation\PreProcessor\Stage\Tree\Split', + #'\StackFormation\PreProcessor\Stage\Tree\ReplaceFnGetAttr', + #'\StackFormation\PreProcessor\Stage\Tree\ReplaceRef', + #'\StackFormation\PreProcessor\Stage\Tree\ReplaceMarkers', + ]; + + $this->pipeline = new Pipeline(); + foreach ($stageClasses as $stageClass) { + $this->pipeline->addStage(new $stageClass($this, $basePath)); + } + + // traverse the object (depth search) and call all transformers on every node. If any transformer changes something start over + $c = 0; + while ($this->traverse($data)) { + if ($c++ > 100) { throw new \Exception('Too many iteraitions. Are we stuck in a loop here?'); } + // Changes detected. Repeating ... + } + return $data->getArrayCopy(); + } + + /** + * This is where the magic happens + * + * @param RecursiveArrayObject $array + * @param string $parentPath (INTERNAL USE ONLY - when being called recursively) + * @param Rootline|null $rootline (INTERNAL USE ONLY - when being called recursively) + * @return bool (true indicates that something has changed, false shows that nothing has been touched) + */ + function traverse(RecursiveArrayObject $array, $parentPath = '', Rootline $rootline = null) { + if (null === $rootline) { + $rootline = new Rootline(); + } + + foreach ($array as $key => $value) { + $path = $parentPath . '/' . $key; + if ($value instanceof RecursiveArrayObject) { + // add element to the rootline stack + $rootline->append(new RootlineItem($key, $value)); + + if ($this->traverse($value, $parentPath . '/' . $key, $rootline)) { + // if somethine has changed (return value true) we abort and start over + // since the object structure is different now which will confusue the iterators + return true; + } + + // remove element from the rootline stack + $rootline->removeLast(); + } + + foreach ($this->pipeline->getStages() as $stage) { + if ($stage->invoke($path, $value, $rootline)) { + // if somethine has changed (return value true) we abort and start over + // since the object structure is different now which will confusue the iterators + return true; + } + } + } + + return false; + } +} diff --git a/src/StackFormation/PrefixedTemplate.php b/src/StackFormation/PrefixedTemplate.php index e379e7d..6fc45db 100644 --- a/src/StackFormation/PrefixedTemplate.php +++ b/src/StackFormation/PrefixedTemplate.php @@ -51,11 +51,11 @@ public function getProcessedTemplate() } } - public function getDecodedJson() + public function getData() { if ($this->prefix) { if (!$this->cache->has(__METHOD__)) { - $array = parent::getDecodedJson(); + $array = parent::getData(); foreach ($array as $topLevelKey => $topLevelData) { if (is_array($topLevelData)) { @@ -71,7 +71,7 @@ public function getDecodedJson() return $this->cache->get(__METHOD__); } else { - return parent::getDecodedJson(); + return parent::getData(); } } diff --git a/src/StackFormation/Preprocessor.php b/src/StackFormation/Preprocessor.php deleted file mode 100644 index f1bd545..0000000 --- a/src/StackFormation/Preprocessor.php +++ /dev/null @@ -1,228 +0,0 @@ -stripComments($json); - $json = $this->parseRefInDoubleQuotedStrings($json); - $json = $this->expandPort($json); - $json = $this->injectFilecontent($json, $basePath); - $json = $this->base64encodedJson($json); - $json = $this->split($json); - $json = $this->replaceFnGetAttr($json); - $json = $this->replaceRef($json); - $json = $this->replaceMarkers($json); - return $json; - } - - protected function stripComments($json) - { - // there's a problem with '"http://example.com"' being converted to '"http:' - // $json = preg_replace('~//[^\r\n]*|/\*.*?\*/~s', '', $json); - - // there's a problem with "arn:aws:s3:::my-bucket/*" - // $json = preg_replace('~/\*.*?\*/~s', '', $json); - - // quick workaround: don't allow quotes - $json = preg_replace('~/\*[^"]*?\*/~s', '', $json); - return $json; - } - - protected function parseRefInDoubleQuotedStrings($json) - { - $json = preg_replace_callback( - '/"([^"]*){Ref:(.+?)}([^"]*)"/', - function ($matches) { - $snippet = $matches[0]; - $snippet = trim($snippet, '"'); - $pieces = preg_split('/({Ref:.+})/U', $snippet, -1, PREG_SPLIT_DELIM_CAPTURE); - $processedPieces = []; - foreach ($pieces as $piece) { - if (empty($piece)) { - continue; - } - if (substr($piece, 0, 5) == '{Ref:') { - $processedPieces[] = preg_replace('/{Ref:(.+)}/', '{"Ref":"$1"}', $piece); - } else { - $processedPieces[] = '"' . $piece . '"'; - } - } - return '{"Fn::Join": ["", [' . implode(', ', $processedPieces) . ']]}'; - }, - $json - ); - return $json; - } - - protected function replaceMarkers($json) - { - $markers = [ - '###TIMESTAMP###' => date(\DateTime::ISO8601), - ]; - $json = str_replace(array_keys($markers), array_values($markers), $json); - - $json = preg_replace_callback( - '/###ENV:([^#:]+)###/', - function ($matches) { - if (!getenv($matches[1])) { - throw new \Exception("Environment variable '{$matches[1]}' not found"); - } - - return getenv($matches[1]); - }, - $json - ); - - return $json; - } - - protected function expandPort($jsonString) - { - return preg_replace('/([\{,]\s*)"Port"\s*:\s*"(\d+)"/', '\1"FromPort": "\2", "ToPort": "\2"', $jsonString); - } - - protected function injectFilecontent($jsonString, $basePath) - { - $jsonString = preg_replace_callback( - '/(\s*)(.*){\s*"Fn::FileContent(Unpretty|TrimLines|Minify)?"\s*:\s*"(.+?)"\s*}/', - function (array $matches) use ($basePath) { - $file = $basePath . '/' . end($matches); - if (!is_file($file)) { - throw new FileNotFoundException("File '$file' not found"); - } - $ext = pathinfo($file, PATHINFO_EXTENSION); - if ($matches[3] == 'Minify' && $ext != 'js') { - throw new \Exception('Fn::FileContentMinify is only supported for *.js files. (File: ' . $file . ')'); - } - - $fileContent = file_get_contents($file); - $fileContent = $this->injectInclude($fileContent, dirname(realpath($file))); - - if ($ext === 'js') { - if ($matches[3] == 'Minify') { - $fileContent = \JShrink\Minifier::minify($fileContent, ['flaggedComments' => false]); - } - - $size = strlen($fileContent); - if ($size > self::MAX_JS_FILE_INCLUDE_SIZE) { - // this is assuming you are uploading an inline JS file to AWS Lambda - throw new \Exception(sprintf("JS file is larger than %s bytes (actual size: %s bytes)", self::MAX_JS_FILE_INCLUDE_SIZE, $size)); - } - } - - // TODO: this isn't optimal. Why are we processing this here in between? - $fileContent = $this->base64encodedJson($fileContent); - - $lines = explode("\n", $fileContent); - foreach ($lines as $key => &$line) { - if ($matches[3] == 'TrimLines') { - $line = trim($line); - if (empty($line)) { - unset($lines[$key]); - } - } - $line .= "\n"; - } - - if ($matches[3] == 'Unpretty') { - $result = ' {"Fn::Join": ["", ' . json_encode(array_values($lines)) . ']}'; - } else { - $result = ' {"Fn::Join": ["", ' . json_encode(array_values($lines), JSON_PRETTY_PRINT) . ']}'; - } - - $whitespace = trim($matches[1], "\n"); - $result = str_replace("\n", "\n" . $whitespace, $result); - - return $matches[1] . $matches[2] . $result; - }, - $jsonString - ); - - return $jsonString; - } - - protected function split($jsonString) - { - return preg_replace_callback( - '/(\s*)(.*){\s*"Fn::Split"\s*:\s*\[\s*"(.*?)"\s*,\s*"(.*?)"\s*\]\s*}/', - function (array $matches) { - if (empty($matches[3])) { - throw new \Exception('Delimiter cannot be empty'); - } - if (empty($matches[4])) { - throw new \Exception('String cannot be empty'); - } - $pieces = explode($matches[3], $matches[4]); - return $matches[1] . $matches[2] . '["' . implode('", "', $pieces).'"]'; - }, - $jsonString - ); - } - - protected function injectInclude($string, $basePath) - { - return preg_replace_callback( - '/###INCLUDE:(.+)/', - function (array $matches) use ($basePath) { - $file = $basePath . '/' . $matches[1]; - - # Parse ENV vars in file names... - $file = $this->replaceMarkers($file); - - if (!is_file($file)) { - throw new FileNotFoundException("File $file not found"); - } - - $fileContent = file_get_contents($file); - $fileContent = trim($fileContent); - - return $fileContent; - }, - $string - ); - } - - protected function replaceRef($jsonString) - { - return preg_replace('/\{\s*Ref\s*:\s*([a-zA-Z0-9:]+?)\s*\}/', '", {"Ref": "$1"}, "', $jsonString); - } - - /** - * @param $jsonString - * @return mixed - */ - protected function base64encodedJson($jsonString) - { - $jsonString = preg_replace_callback( - '/###JSON###(.+?)######/', - function (array $m) { - return '", ' . base64_decode($m[1]) . ', "'; - }, - $jsonString - ); - return $jsonString; - } - - /** - * transforms {Fn::GetAtt:[resource,attribute]} to inline statement - * - * @param $jsonstring - * @return mixed - */ - protected function replaceFnGetAttr($jsonstring) - { - return preg_replace('/\{\s*Fn\s*::\s*GetAtt\s*:\s*\[\s*([a-zA-Z0-9:]+?)\s*,\s*([a-zA-Z0-9:]+?)\s*\]\s*\}/', - '", {"Fn::GetAtt": ["$1", "$2"]} ,"', $jsonstring); - } -} diff --git a/src/StackFormation/Template.php b/src/StackFormation/Template.php index 7cc4120..d70fd0e 100644 --- a/src/StackFormation/Template.php +++ b/src/StackFormation/Template.php @@ -2,21 +2,25 @@ namespace StackFormation; +use \StackFormation\PreProcessor\StringPreProcessor; +use \StackFormation\PreProcessor\TreePreProcessor; + class Template { - protected $filepath; protected $cache; - protected $preProcessor; + protected $stringPreProcessor; + protected $treePreProcessor; - public function __construct($filePath, Preprocessor $preprocessor = null) + public function __construct($filePath, StringPreProcessor $stringPreProcessor = null, TreePreProcessor $treePreProcessor = null) { if (!is_file($filePath)) { throw new \Symfony\Component\Filesystem\Exception\FileNotFoundException("File '$filePath' not found"); } $this->filepath = $filePath; $this->cache = new \StackFormation\Helper\Cache(); - $this->preProcessor = $preprocessor ? $preprocessor : new Preprocessor(); + $this->stringPreProcessor = $stringPreProcessor ? $stringPreProcessor : new StringPreProcessor(); + $this->treePreProcessor = $treePreProcessor ? $treePreProcessor : new TreePreProcessor(); } public function getFilePath() @@ -29,34 +33,41 @@ public function getFileContent() return $this->cache->get( __METHOD__, function () { - return file_get_contents($this->filepath); + $fileContent = file_get_contents($this->filepath); + return $this->stringPreProcessor->process($fileContent); } ); } - public function getProcessedTemplate() + public function getProcessedTemplate($fileContent) { return $this->cache->get( __METHOD__, - function () { - return $this->preProcessor->processJson($this->getFileContent(), $this->getBasePath()); + function () use ($fileContent) { + if (\StackFormation\Helper\Div::isJson($fileContent)) { + // TODO Just a workaround (need to be a single line, replace \n would also delete new line char in multiline strings + $fileContent = str_replace("\n", "", $fileContent); + } + + $data = \Symfony\Component\Yaml\Yaml::parse($fileContent); + return $this->treePreProcessor->process($data, $this->getBasePath()); } ); } - public function getDecodedJson() + public function getData() { if (!$this->cache->has(__METHOD__)) { - $templateBody = $this->getProcessedTemplate(); - $array = json_decode($templateBody, true); - if (!is_array($array)) { + $fileContent = $this->getFileContent(); + $data = $this->getProcessedTemplate($fileContent); + if (!is_array($data)) { throw new TemplateDecodeException($this->getFilePath(), sprintf("Error decoding file '%s'", $this->getFilePath())); } - if ($array['AWSTemplateFormatVersion'] != '2010-09-09') { + if ($data['AWSTemplateFormatVersion'] != '2010-09-09') { throw new TemplateInvalidException($this->getFilePath(), 'Invalid AWSTemplateFormatVersion'); } - $this->cache->set(__METHOD__, $array); + $this->cache->set(__METHOD__, $data); } return $this->cache->get(__METHOD__); @@ -64,7 +75,7 @@ public function getDecodedJson() public function getDescription() { - $data = $this->getDecodedJson(); + $data = $this->getData(); return isset($data['Description']) ? $data['Description'] : ''; } diff --git a/src/StackFormation/TemplateMerger.php b/src/StackFormation/TemplateMerger.php index 324b3c2..7b6e2fa 100644 --- a/src/StackFormation/TemplateMerger.php +++ b/src/StackFormation/TemplateMerger.php @@ -39,7 +39,7 @@ public function merge(array $templates, $description = null, array $additionalDa } try { - $array = $template->getDecodedJson(); + $array = $template->getData(); // Copy the current description into the final template if (!empty($array['Description'])) { @@ -82,18 +82,25 @@ public function merge(array $templates, $description = null, array $additionalDa $mergedTemplate = array_merge_recursive($mergedTemplate, $additionalData); - $json = json_encode($mergedTemplate, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + $yaml = new \Symfony\Component\Yaml\Yaml(); + $output = $yaml->dump($mergedTemplate); // Check for max template size - if (strlen($json) > self::MAX_CF_TEMPLATE_SIZE) { - $json = json_encode($mergedTemplate, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + if (strlen($output) > self::MAX_CF_TEMPLATE_SIZE) { + $output = $yaml->dump($mergedTemplate, 1); // Re-check for max template size - if (strlen($json) > self::MAX_CF_TEMPLATE_SIZE) { - throw new \Exception(sprintf('Template too big (%s bytes). Maximum template size is %s bytes.', strlen($json), self::MAX_CF_TEMPLATE_SIZE)); + if (strlen($output) > self::MAX_CF_TEMPLATE_SIZE) { + throw new \Exception(sprintf('Template too big (%s bytes). Maximum template size is %s bytes.', strlen($output), self::MAX_CF_TEMPLATE_SIZE)); } } - return $json; + + // TODO + #print_r($mergedTemplate); + #print_r($output); + #die('MERGE_TEMPLATE_DIE'); + + return $output; } } diff --git a/tests/StackFormation/BlueprintTest.php b/tests/StackFormation/BlueprintTest.php index 786b86f..806b1d7 100644 --- a/tests/StackFormation/BlueprintTest.php +++ b/tests/StackFormation/BlueprintTest.php @@ -394,10 +394,13 @@ public function getPreprocessedTemplate() $blueprintFactory = new \StackFormation\BlueprintFactory($config, $valueResolver); $blueprint = $blueprintFactory->getBlueprint('fixture1'); $template = $blueprint->getPreprocessedTemplate(); - $template = json_decode($template, true); - $this->assertArrayHasKey('Resources', $template); - $this->assertArrayHasKey('MyResource', $template['Resources']); - $this->assertEquals('AWS::CloudFormation::WaitConditionHandle', $template['Resources']['MyResource']['Type']); + + $yamlParser = new \Symfony\Component\Yaml\Parser(); + $data = $yamlParser->parse($template); + + $this->assertArrayHasKey('Resources', $data); + $this->assertArrayHasKey('MyResource', $data['Resources']); + $this->assertEquals('AWS::CloudFormation::WaitConditionHandle', $data['Resources']['MyResource']['Type']); } /** @@ -411,18 +414,21 @@ public function getPreprocessedTemplateContainsBlueprintReference() $blueprintFactory = new \StackFormation\BlueprintFactory($config, $valueResolver); $blueprint = $blueprintFactory->getBlueprint('reference-fixture-{env:FOO1}'); $template = $blueprint->getPreprocessedTemplate(); - $template = json_decode($template, true); - $this->assertArrayHasKey('Metadata', $template); - $this->assertArrayHasKey('StackFormation', $template['Metadata']); - $this->assertArrayHasKey('Blueprint', $template['Metadata']['StackFormation']); - $this->assertEquals('reference-fixture-{env:FOO1}', $template['Metadata']['StackFormation']['Blueprint']); - $this->assertArrayHasKey('EnvironmentVariables', $template['Metadata']['StackFormation']); + $yamlParser = new \Symfony\Component\Yaml\Parser(); + $data = $yamlParser->parse($template); + + $this->assertArrayHasKey('Metadata', $data); + $this->assertArrayHasKey('StackFormation', $data['Metadata']); + $this->assertArrayHasKey('Blueprint', $data['Metadata']['StackFormation']); + $this->assertEquals('reference-fixture-{env:FOO1}', $data['Metadata']['StackFormation']['Blueprint']); + + $this->assertArrayHasKey('EnvironmentVariables', $data['Metadata']['StackFormation']); $this->assertEquals([ 'FOO1' => 'BAR1', 'FOO2' => 'BAR2', 'FOO3' => 'BAR3' - ], $template['Metadata']['StackFormation']['EnvironmentVariables']); + ], $data['Metadata']['StackFormation']['EnvironmentVariables']); } } diff --git a/tests/StackFormation/PreProcessor/Stage/String/StripCommentsTest.php b/tests/StackFormation/PreProcessor/Stage/String/StripCommentsTest.php new file mode 100644 index 0000000..1525a9e --- /dev/null +++ b/tests/StackFormation/PreProcessor/Stage/String/StripCommentsTest.php @@ -0,0 +1,30 @@ +getMock('\StackFormation\PreProcessor\StringPreProcessor', [], [], '', false); + $transformer = new \StackFormation\PreProcessor\Stage\String\StripComments($stringPreProcessor); + $output = $transformer->invoke($string); + $this->assertEquals($expected, $output); + } + + /** + * @return array + */ + public function stripCommentsDataProvider() + { + return [ + ['This is a string /** comment blabla */', 'This is a string '], + ['This is a string /** comment blabla */ with a comment between', 'This is a string with a comment between'], + ['arn:aws:s3:::my-bucket/*', 'arn:aws:s3:::my-bucket/*'], + ]; + } +} diff --git a/tests/StackFormation/PreProcessor/Stage/Tree/ExpandCidrIpTest.php b/tests/StackFormation/PreProcessor/Stage/Tree/ExpandCidrIpTest.php new file mode 100644 index 0000000..5a26dcf --- /dev/null +++ b/tests/StackFormation/PreProcessor/Stage/Tree/ExpandCidrIpTest.php @@ -0,0 +1,67 @@ + [ + 'IpProtocol' => 'tcp', + 'Port' => '80', + 'CidrIp' => '1.1.1.1/1,2.2.2.2/2,3.3.3.3/3' + ] + ]; + $parentData = $granparentData['granparent']; + + $parentRecursiveArrayObject = new \StackFormation\PreProcessor\RecursiveArrayObject($parentData, \ArrayObject::ARRAY_AS_PROPS); + $gradnparentRecursiveArrayObject = new \StackFormation\PreProcessor\RecursiveArrayObject($granparentData, \ArrayObject::ARRAY_AS_PROPS); + + $parentRootlineItem = $this->getMockBuilder('\StackFormation\PreProcessor\RootlineItem') + ->disableOriginalConstructor() + ->setMethods(['getValue', 'getKey']) + ->getMock(); + $parentRootlineItem->method('getValue')->willReturn($parentRecursiveArrayObject); + $parentRootlineItem->method('getKey')->willReturn('granparent'); + + $grandParentRootlineItem = $this->getMockBuilder('\StackFormation\PreProcessor\RootlineItem') + ->disableOriginalConstructor() + ->setMethods(['getValue']) + ->getMock(); + $grandParentRootlineItem->method('getValue')->willReturn($gradnparentRecursiveArrayObject); + + $rootline = $this->getMockBuilder('\StackFormation\PreProcessor\Rootline') + ->setMethods(['parent']) + ->getMock(); + $rootline->expects($this->any()) + ->method('parent') + ->with($this->logicalOr( + $this->equalTo(1), + $this->equalTo(2) + )) + ->will($this->returnCallback( + function($param) use ($parentRootlineItem, $grandParentRootlineItem) { + print_r($param); + if ($param == 1) return $parentRootlineItem; + if ($param == 2) return $grandParentRootlineItem; + } + )); + + $treePreProcessor = $this->getMock('\StackFormation\PreProcessor\TreePreProcessor', [], [], '', false); + $transformer = new \StackFormation\PreProcessor\Stage\Tree\ExpandCidrIp($treePreProcessor); + + $output = $transformer->invoke('/Resources/InstanceSecurityGroup/Properties/SecurityGroupIngress/1/CidrIp', '1.1.1.1/1,2.2.2.2/2,3.3.3.3/3', $rootline); + $this->assertTrue($output); + + $grandparentData = $rootline->parent(2)->getValue()->getArrayCopy(); + + $this->assertSame(3, count($grandparentData)); + $this->assertSame('1.1.1.1/1', $grandparentData[0]['CidrIp']); + $this->assertSame('2.2.2.2/2', $grandparentData[1]['CidrIp']); + $this->assertSame('3.3.3.3/3', $grandparentData[2]['CidrIp']); + } +} diff --git a/tests/StackFormation/PreProcessor/Stage/Tree/ExpandCslPortTest.php b/tests/StackFormation/PreProcessor/Stage/Tree/ExpandCslPortTest.php new file mode 100644 index 0000000..8281781 --- /dev/null +++ b/tests/StackFormation/PreProcessor/Stage/Tree/ExpandCslPortTest.php @@ -0,0 +1,68 @@ + [ + 'IpProtocol' => 'tcp', + 'Port' => '80,443', + 'CidrIp' => '1.1.1.1/1' + ] + ]; + $parentData = $granparentData['granparent']; + + $parentRecursiveArrayObject = new \StackFormation\PreProcessor\RecursiveArrayObject($parentData, \ArrayObject::ARRAY_AS_PROPS); + $gradnparentRecursiveArrayObject = new \StackFormation\PreProcessor\RecursiveArrayObject($granparentData, \ArrayObject::ARRAY_AS_PROPS); + + $parentRootlineItem = $this->getMockBuilder('\StackFormation\PreProcessor\RootlineItem') + ->disableOriginalConstructor() + ->setMethods(['getValue', 'getKey']) + ->getMock(); + $parentRootlineItem->method('getValue')->willReturn($parentRecursiveArrayObject); + $parentRootlineItem->method('getKey')->willReturn('granparent'); + + $grandParentRootlineItem = $this->getMockBuilder('\StackFormation\PreProcessor\RootlineItem') + ->disableOriginalConstructor() + ->setMethods(['getValue']) + ->getMock(); + $grandParentRootlineItem->method('getValue')->willReturn($gradnparentRecursiveArrayObject); + + $rootline = $this->getMockBuilder('\StackFormation\PreProcessor\Rootline') + ->setMethods(['parent']) + ->getMock(); + $rootline->expects($this->any()) + ->method('parent') + ->with($this->logicalOr( + $this->equalTo(1), + $this->equalTo(2) + )) + ->will($this->returnCallback( + function($param) use ($parentRootlineItem, $grandParentRootlineItem) { + print_r($param); + if ($param == 1) return $parentRootlineItem; + if ($param == 2) return $grandParentRootlineItem; + } + )); + + $treePreProcessor = $this->getMock('\StackFormation\PreProcessor\TreePreProcessor', [], [], '', false); + $transformer = new \StackFormation\PreProcessor\Stage\Tree\ExpandCslPort($treePreProcessor); + + $output = $transformer->invoke('/Resources/InstanceSecurityGroup/Properties/SecurityGroupIngress/1/Port', '80,443', $rootline); + $this->assertTrue($output); + + $grandparentData = $rootline->parent(2)->getValue()->getArrayCopy(); + + $this->assertSame(2, count($grandparentData)); + $this->assertSame('80', $grandparentData[0]['FromPort']); + $this->assertSame('80', $grandparentData[0]['ToPort']); + $this->assertSame('443', $grandparentData[1]['FromPort']); + $this->assertSame('443', $grandparentData[1]['ToPort']); + } +} diff --git a/tests/StackFormation/PreprocessorTest.php b/tests/StackFormation/PreprocessorTest.php deleted file mode 100644 index ceb116e..0000000 --- a/tests/StackFormation/PreprocessorTest.php +++ /dev/null @@ -1,90 +0,0 @@ -preprocessor = new \StackFormation\Preprocessor(); - } - - /** - * @param string $fixtureDirectory - * @throws \Exception - * @test - * @dataProvider processFileDataProvider - */ - public function processFile($fixtureDirectory) - { - $prefix = FIXTURE_ROOT . 'Preprocessor/'; - $prefix .= $fixtureDirectory . '/'; - $templatePath = $prefix . 'blueprint/input.template'; - $fileContent = file_get_contents($templatePath); - $this->assertEquals( - $this->preprocessor->processJson($fileContent, dirname($templatePath)), - file_get_contents($prefix. 'blueprint/expected.template') - ); - } - - public function processFileDataProvider() - { - $prefix = FIXTURE_ROOT . 'Preprocessor/'; - $directories = glob($prefix.'*', GLOB_ONLYDIR); - array_walk($directories, function(&$directory) use ($prefix) { - $directory = [ str_replace($prefix, '', $directory)]; - }); - return $directories; - } - - /** - * @param string $inputJson - * @param string $expectedJson - * @throws \Exception - * @test - * @dataProvider processJsonDataProvider - */ - public function processJson($inputJson, $expectedJson) - { - $this->assertEquals( - $this->preprocessor->processJson($inputJson, sys_get_temp_dir()), - $expectedJson - ); - } - - /** - * @return array - */ - public function processJsonDataProvider() - { - return [ - // strip comments - ['Hello World /* Comment */', 'Hello World '], - ['Hello World /* Comment */ Hello World', 'Hello World Hello World'], - ['/* Comment */ Hello World', ' Hello World'], - // support single quotes - ["Hello World /* 'Comment' */ Hello World", 'Hello World Hello World'], - // ignore double quotes - ['Hello World /* "Comment" */', 'Hello World /* "Comment" */'], - ['Hello World /* "Comment */', 'Hello World /* "Comment */'], - // multi-line - ["Hello World /* Multiline\nComment */ Hello World", 'Hello World Hello World'], - // parseRefInDoubleQuotedStrings - ['"Key": "Name", "Value": "magento-{Ref:Environment}-{Ref:Build}-instance"', '"Key": "Name", "Value": {"Fn::Join": ["", ["magento-", {"Ref":"Environment"}, "-", {"Ref":"Build"}, "-instance"]]}'], - // expandPort - ['{"IpProtocol": "tcp", "Port": "80", "CidrIp": "1.2.3.4/32"},', '{"IpProtocol": "tcp", "FromPort": "80", "ToPort": "80", "CidrIp": "1.2.3.4/32"},'], - // replace ref - ["WAIT_CONDITION_HANDLE='{Ref:WaitConditionHandle}'", "WAIT_CONDITION_HANDLE='\", {\"Ref\": \"WaitConditionHandle\"}, \"'"], - ["REGION='{Ref:AWS::Region}'", "REGION='\", {\"Ref\": \"AWS::Region\"}, \"'"], - ['"Aliases": { "Fn::Split": [",", "a,b,c"] }', '"Aliases": ["a", "b", "c"]'], - ['"Aliases": { "Fn::Split": ["+", "a,b,c"] }', '"Aliases": ["a,b,c"]'], - ['"Aliases": { "Fn::Split": ["+", "a+b+c"] }', '"Aliases": ["a", "b", "c"]'], - ]; - } -}