Skip to content

Commit

Permalink
feat(pipelinetemplate): Adding a Jinja template renderer alternative (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
robzienert authored Mar 22, 2017
1 parent 352b385 commit 1f565cb
Show file tree
Hide file tree
Showing 13 changed files with 518 additions and 87 deletions.
3 changes: 3 additions & 0 deletions orca-pipelinetemplate/orca-pipelinetemplate.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ dependencies {

compile('com.github.jknack:handlebars:4.0.6')
compile('com.github.jknack:handlebars-jackson2:1.0.0')

compile('com.hubspot.jinjava:jinjava:2.1.14')

compile "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${spinnaker.version('jackson')}"
compile('com.jayway.jsonpath:json-path:2.2.0')

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@
import com.fasterxml.jackson.databind.SerializationFeature;
import com.netflix.spinnaker.orca.pipelinetemplate.PipelineTemplateModule;
import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.render.HandlebarsRenderer;
import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.render.JinjaRenderer;
import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.render.JsonRenderedValueConverter;
import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.render.RenderedValueConverter;
import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.render.Renderer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
Expand All @@ -38,7 +42,19 @@ ObjectMapper pipelineTemplateObjectMapper() {
}

@Bean
Renderer renderer(ObjectMapper pipelineTemplateObjectMapper) {
return new HandlebarsRenderer(pipelineTemplateObjectMapper);
RenderedValueConverter jsonRenderedValueConverter(ObjectMapper pipelineTemplateObjectMapper) {
return new JsonRenderedValueConverter(pipelineTemplateObjectMapper);
}

@Bean
@ConditionalOnExpression("!${pipelineTemplate.jinja.enabled}")
Renderer handlebarsRenderer(RenderedValueConverter renderedValueConverter, ObjectMapper pipelineTemplateObjectMapper) {
return new HandlebarsRenderer(renderedValueConverter, pipelineTemplateObjectMapper);
}

@Bean
@ConditionalOnExpression("${pipelineTemplate.jinja.enabled}")
Renderer jinjaRenderer(RenderedValueConverter renderedValueConverter, ObjectMapper pipelineTemplateObjectMapper) {
return new JinjaRenderer(renderedValueConverter, pipelineTemplateObjectMapper);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/
package com.netflix.spinnaker.orca.pipelinetemplate.v1schema.render;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.jknack.handlebars.Context;
import com.github.jknack.handlebars.EscapingStrategy;
Expand All @@ -28,13 +27,10 @@
import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.render.helper.ModuleHelper;
import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.render.helper.UnknownIdentifierHelper;
import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.render.helper.WithMapKeyHelper;
import org.apache.commons.lang3.math.NumberUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;

/**
* Runs a two-phase render, first to process the handlebars template, then a second
Expand All @@ -46,10 +42,14 @@ public class HandlebarsRenderer implements Renderer {

Handlebars handlebars;

ObjectMapper pipelineTemplateObjectMapper;
private RenderedValueConverter renderedValueConverter;

public HandlebarsRenderer(ObjectMapper pipelineTemplateObjectMapper) {
this.pipelineTemplateObjectMapper = pipelineTemplateObjectMapper;
this(new JsonRenderedValueConverter(pipelineTemplateObjectMapper), pipelineTemplateObjectMapper);
}

public HandlebarsRenderer(RenderedValueConverter renderedValueConverter, ObjectMapper pipelineTemplateObjectMapper) {
this.renderedValueConverter = renderedValueConverter;

handlebars = new Handlebars()
.with(EscapingStrategy.NOOP)
Expand All @@ -59,6 +59,8 @@ public HandlebarsRenderer(ObjectMapper pipelineTemplateObjectMapper) {
.registerHelper("withMapKey", new WithMapKeyHelper())
;
ConditionHelper.register(handlebars);

log.info("PipelineTemplates: Using HandlebarsRenderer");
}

@Override
Expand Down Expand Up @@ -96,61 +98,6 @@ public String render(String template, RenderContext configuration) {

@Override
public Object renderGraph(String template, RenderContext context) {
String rendered = render(template, context);

// Short-circuit primitive values.
// TODO rz - having trouble getting jackson to parse primitive values outside of unit tests
if (NumberUtils.isNumber(rendered)) {
if (rendered.contains(".")) {
return NumberUtils.createDouble(rendered);
}
try {
return NumberUtils.createInteger(rendered);
} catch (NumberFormatException ignored) {
return NumberUtils.createLong(rendered);
}
} else if (rendered.equals("true") || rendered.equals("false")) {
return Boolean.parseBoolean(rendered);
} else if (rendered.startsWith("{{") || (!rendered.startsWith("{") && !rendered.startsWith("["))) {
return rendered;
}

JsonNode node;
try {
node = pipelineTemplateObjectMapper.readTree(rendered);
} catch (IOException e) {
throw new TemplateRenderException("template produced invalid json", e);
}

try {
if (node.isArray()) {
return pipelineTemplateObjectMapper.readValue(rendered, Collection.class);
}
if (node.isObject()) {
return pipelineTemplateObjectMapper.readValue(rendered, HashMap.class);
}
if (node.isBoolean()) {
return Boolean.parseBoolean(node.asText());
}
if (node.isDouble()) {
return node.doubleValue();
}
if (node.canConvertToInt()) {
return node.intValue();
}
if (node.canConvertToLong()) {
return node.longValue();
}
if (node.isTextual()) {
return node.textValue();
}
if (node.isNull()) {
return null;
}
} catch (IOException e) {
throw new TemplateRenderException("template produced invalid json", e);
}

throw new TemplateRenderException("unknown rendered object type");
return renderedValueConverter.convertRenderedValue(render(template, context));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright 2017 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.netflix.spinnaker.orca.pipelinetemplate.v1schema.render;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.hubspot.jinjava.Jinjava;
import com.hubspot.jinjava.JinjavaConfig;
import com.hubspot.jinjava.interpret.InterpretException;
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
import com.hubspot.jinjava.loader.ResourceLocator;
import com.netflix.spinnaker.orca.pipelinetemplate.exceptions.TemplateRenderException;
import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.render.tags.ModuleTag;
import com.netflix.spinnaker.orca.pipelinetemplate.validator.Errors.Error;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.charset.Charset;

public class JinjaRenderer implements Renderer {

private final Logger log = LoggerFactory.getLogger(getClass());

private Jinjava jinja;

private RenderedValueConverter renderedValueConverter;

public JinjaRenderer(ObjectMapper pipelineTemplateObjectMapper) {
this(new JsonRenderedValueConverter(pipelineTemplateObjectMapper), pipelineTemplateObjectMapper);
}

public JinjaRenderer(RenderedValueConverter renderedValueConverter, ObjectMapper pipelineTemplateObjectMapper) {
this.renderedValueConverter = renderedValueConverter;

JinjavaConfig config = new JinjavaConfig();
jinja = new Jinjava(config);
jinja.setResourceLocator(new NoopResourceLocator());
jinja.getGlobalContext().registerTag(new ModuleTag(this, pipelineTemplateObjectMapper));

log.info("PipelineTemplates: Using JinjaRenderer");
}

@Override
public String render(String template, RenderContext context) {
String rendered;
try {
rendered = jinja.render(template, context.getVariables());
} catch (InterpretException e) {
log.error("Failed rendering jinja template", e);
throw new TemplateRenderException(new Error()
.withMessage("failed rendering jinja template")
.withCause(e.getMessage())
.withLocation(context.getLocation())
);
}

rendered = rendered.trim().replaceAll("\n", "");

if (!template.equals(rendered)) {
log.debug("rendered '" + template + "' -> '" + rendered + "'");
}

return rendered;
}

@Override
public Object renderGraph(String template, RenderContext context) {
return renderedValueConverter.convertRenderedValue(render(template, context));
}

private static class NoopResourceLocator implements ResourceLocator {
@Override
public String getString(String fullName, Charset encoding, JinjavaInterpreter interpreter) throws IOException {
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright 2017 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.netflix.spinnaker.orca.pipelinetemplate.v1schema.render;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.netflix.spinnaker.orca.pipelinetemplate.exceptions.TemplateRenderException;
import org.apache.commons.lang3.math.NumberUtils;

import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;

public class JsonRenderedValueConverter implements RenderedValueConverter {

private ObjectMapper pipelineTemplateObjectMapper;

public JsonRenderedValueConverter(ObjectMapper pipelineTemplateObjectMapper) {
this.pipelineTemplateObjectMapper = pipelineTemplateObjectMapper;
}

@Override
public Object convertRenderedValue(String rendered) {
if (NumberUtils.isNumber(rendered)) {
if (rendered.contains(".")) {
return NumberUtils.createDouble(rendered);
}
try {
return NumberUtils.createInteger(rendered);
} catch (NumberFormatException ignored) {
return NumberUtils.createLong(rendered);
}
} else if (rendered.equals("true") || rendered.equals("false")) {
return Boolean.parseBoolean(rendered);
} else if (rendered.startsWith("{{") || (!rendered.startsWith("{") && !rendered.startsWith("["))) {
return rendered;
}

JsonNode node;
try {
node = pipelineTemplateObjectMapper.readTree(rendered);
} catch (IOException e) {
throw new TemplateRenderException("template produced invalid json", e);
}

try {
if (node.isArray()) {
return pipelineTemplateObjectMapper.readValue(rendered, Collection.class);
}
if (node.isObject()) {
return pipelineTemplateObjectMapper.readValue(rendered, HashMap.class);
}
if (node.isBoolean()) {
return Boolean.parseBoolean(node.asText());
}
if (node.isDouble()) {
return node.doubleValue();
}
if (node.canConvertToInt()) {
return node.intValue();
}
if (node.canConvertToLong()) {
return node.longValue();
}
if (node.isTextual()) {
return node.textValue();
}
if (node.isNull()) {
return null;
}
} catch (IOException e) {
throw new TemplateRenderException("template produced invalid json", e);
}

throw new TemplateRenderException("unknown rendered object type");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright 2017 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.netflix.spinnaker.orca.pipelinetemplate.v1schema.render;

public interface RenderedValueConverter {

Object convertRenderedValue(String renderedValue);
}
Loading

0 comments on commit 1f565cb

Please sign in to comment.