From 914e3b8c9541634addd7ced96180b9289bd50ca9 Mon Sep 17 00:00:00 2001 From: Rob Zienert Date: Fri, 3 Nov 2017 15:53:37 -0700 Subject: [PATCH] fix(pipeline_template): Deal with whitespace in jinja module kv pairs --- .../v1schema/render/tags/ModuleTag.java | 52 ++++++++++++++++++- .../v1schema/render/tags/ModuleTagSpec.groovy | 11 ++-- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/orca-pipelinetemplate/src/main/java/com/netflix/spinnaker/orca/pipelinetemplate/v1schema/render/tags/ModuleTag.java b/orca-pipelinetemplate/src/main/java/com/netflix/spinnaker/orca/pipelinetemplate/v1schema/render/tags/ModuleTag.java index 0b7f3bd4cb..76b25096a3 100644 --- a/orca-pipelinetemplate/src/main/java/com/netflix/spinnaker/orca/pipelinetemplate/v1schema/render/tags/ModuleTag.java +++ b/orca-pipelinetemplate/src/main/java/com/netflix/spinnaker/orca/pipelinetemplate/v1schema/render/tags/ModuleTag.java @@ -42,6 +42,7 @@ import java.util.Map; import java.util.Optional; import java.util.function.Supplier; +import java.util.stream.Collectors; public class ModuleTag implements Tag { @@ -60,7 +61,9 @@ public String getName() { @Override public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { - List helper = new HelperStringTokenizer(tagNode.getHelpers()).splitComma(true).allTokens(); + List helper = collapseWhitespaceInTokenPairs( + new HelperStringTokenizer(tagNode.getHelpers()).allTokens() + ); if (helper.isEmpty()) { throw new TemplateSyntaxException(tagNode.getMaster().getImage(), "Tag 'module' expects ID as first parameter: " + helper, tagNode.getLineNumber()); } @@ -164,4 +167,51 @@ public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { public String getEndTagName() { return null; } + + /** + * Look at this ungodly code. It's gross. Thanks to poor foresight, we tokenize on + * whitespace, which can break concatenation, and definitely breaks usage of filters. + * Sooo, we take the tokenized module definition and best-guess our way through collapsing + * whitespace to arrive at the real key/value pairs that we later parse for populating + * the module's internal context. + */ + private static List collapseWhitespaceInTokenPairs(List tokens) { + List combinedTokens = new ArrayList<>(); + combinedTokens.add(tokens.get(0)); + + StringBuilder buffer = new StringBuilder(); + // idx 0 is `moduleName`. Skip that guy. + for (int i = 1; i < tokens.size(); i++) { + String token = tokens.get(i); + if (token.contains("=")) { + if (buffer.length() > 0) { + combinedTokens.add(buffer.toString()); + } + buffer = new StringBuilder(); + combinedTokens.add(token); + } else { + String lastToken = combinedTokens.get(combinedTokens.size() - 1); + if (lastToken.contains("=") && !lastToken.endsWith(",")) { + buffer.append(combinedTokens.remove(combinedTokens.size() - 1)); + } + buffer.append(token); + } + } + + if (buffer.length() > 0) { + int i = combinedTokens.size() - 1; + combinedTokens.set(i, combinedTokens.get(i) + buffer.toString()); + } + + return combinedTokens.stream() + .map(ModuleTag::removeTrailingCommas) + .collect(Collectors.toList()); + } + + private static String removeTrailingCommas(String token) { + if (token.endsWith(",")) { + return token.substring(0, token.length()-1); + } + return token; + } } diff --git a/orca-pipelinetemplate/src/test/groovy/com/netflix/spinnaker/orca/pipelinetemplate/v1schema/render/tags/ModuleTagSpec.groovy b/orca-pipelinetemplate/src/test/groovy/com/netflix/spinnaker/orca/pipelinetemplate/v1schema/render/tags/ModuleTagSpec.groovy index 634a8afe93..54d708528c 100644 --- a/orca-pipelinetemplate/src/test/groovy/com/netflix/spinnaker/orca/pipelinetemplate/v1schema/render/tags/ModuleTagSpec.groovy +++ b/orca-pipelinetemplate/src/test/groovy/com/netflix/spinnaker/orca/pipelinetemplate/v1schema/render/tags/ModuleTagSpec.groovy @@ -46,18 +46,21 @@ class ModuleTagSpec extends Specification { [name: 'myStringVar', defaultValue: 'hello'] as NamedHashMap, [name: 'myOtherVar'] as NamedHashMap, [name: 'subject'] as NamedHashMap, - [name: 'job'] as NamedHashMap + [name: 'job'] as NamedHashMap, + [name: 'concat', type: 'object'] as NamedHashMap, + [name: 'filtered'] as NamedHashMap ], - definition: '{{myStringVar}} {{myOtherVar}}, {{subject}}. You triggered {{job}}') + definition: '{{myStringVar}} {{myOtherVar}}, {{subject}}. You triggered {{job}} {{concat}} {{filtered}}') ] ) RenderContext context = new DefaultRenderContext('myApp', pipelineTemplate, [job: 'myJob', buildNumber: 1234]) context.variables.put("testerName", "Mr. Tester Testington") + context.variables.put("m", [myKey: 'myValue']) when: - def result = renderer.render('{% module myModule myOtherVar=world, subject=testerName, job=trigger.job %}', context) + def result = renderer.render("{% module myModule myOtherVar=world, subject=testerName, job=trigger.job, concat=m['my' + 'Key'], filtered=trigger.nonExist|default('hello', True) %}", context) then: - result == 'hello world, Mr. Tester Testington. You triggered myJob' + result == 'hello world, Mr. Tester Testington. You triggered myJob myValue hello' } }