Skip to content

Commit

Permalink
feat(pipelinetemplates): Adding basic plan capability
Browse files Browse the repository at this point in the history
  • Loading branch information
robzienert committed Mar 8, 2017
1 parent 504a786 commit 0ffa509
Show file tree
Hide file tree
Showing 15 changed files with 239 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.netflix.spectator.api.Registry;
import com.netflix.spinnaker.orca.extensionpoint.pipeline.PipelinePreprocessor;
import com.netflix.spinnaker.orca.pipelinetemplate.exceptions.IllegalTemplateConfigurationException;
import com.netflix.spinnaker.orca.pipelinetemplate.exceptions.TemplateLoaderException;
import com.netflix.spinnaker.orca.pipelinetemplate.exceptions.TemplateRenderException;
import com.netflix.spinnaker.orca.pipelinetemplate.generator.ExecutionGenerator;
import com.netflix.spinnaker.orca.pipelinetemplate.loader.TemplateLoader;
import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.TemplateMerge;
Expand Down Expand Up @@ -62,6 +65,18 @@ public PipelineTemplatePipelinePreprocessor(ObjectMapper pipelineTemplateObjectM

@Override
public Map<String, Object> process(Map<String, Object> pipeline) {
try {
return processInternal(pipeline);
} catch (TemplateLoaderException e) {
return ErrorResponse.builder().addError("failed loading template", e.getMessage()).toMap();
} catch (TemplateRenderException e) {
return ErrorResponse.builder().addError("failed rendering handlebars template", e.getMessage()).toMap();
} catch (IllegalTemplateConfigurationException e) {
return ErrorResponse.builder().addError("malformed template configuration", e.getMessage()).toMap();
}
}

private Map<String, Object> processInternal(Map<String, Object> pipeline) {
TemplatedPipelineRequest request = pipelineTemplateObjectMapper.convertValue(pipeline, TemplatedPipelineRequest.class);
if (!request.isTemplatedPipelineRequest()) {
return pipeline;
Expand All @@ -81,16 +96,27 @@ public Map<String, Object> process(Map<String, Object> pipeline) {

ExecutionGenerator executionGenerator = new V1SchemaExecutionGenerator();

return executionGenerator.generate(template, templateConfiguration);
Map<String, Object> generatedPipeline = executionGenerator.generate(template, templateConfiguration);

validateGeneratedPipeline(generatedPipeline);

return generatedPipeline;
}

private static void validateGeneratedPipeline(Map<String, Object> generatedPipeline) {
if (generatedPipeline.get("application") == null) {
throw new IllegalTemplateConfigurationException("Configuration is missing 'application' value");
}
}

private static class TemplatedPipelineRequest {
String type;
Map<String, Object> trigger;
TemplateConfiguration config;
Boolean plan;

public boolean isTemplatedPipelineRequest() {
return type != null && type.equals("templatedPipeline");
return "templatedPipeline".equals(type);
}

public String getType() {
Expand All @@ -116,5 +142,32 @@ public Map<String, Object> getTrigger() {
public void setTrigger(Map<String, Object> trigger) {
this.trigger = trigger;
}

public Boolean getPlan() {
return plan;
}

public void setPlan(Boolean plan) {
this.plan = plan;
}
}

private static class ErrorResponse {
Map<String, String> errors = new HashMap<>();

static ErrorResponse builder() {
return new ErrorResponse();
}

ErrorResponse addError(String message, String cause) {
errors.put(message, cause);
return this;
}

Map<String, Object> toMap() {
Map<String, Object> m = new HashMap<>();
m.put("errors", errors);
return m;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/
package com.netflix.spinnaker.orca.pipelinetemplate.exceptions;

public class IllegalTemplateConfigurationException extends RuntimeException {
public class IllegalTemplateConfigurationException extends IllegalStateException {

public IllegalTemplateConfigurationException(String message) {
super(message);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ public Map<String, Object> generate(PipelineTemplate template, TemplateConfigura
stage.put("refId", s.getId());
stage.put("type", s.getType());
stage.put("name", s.getName());
stage.put("context", s.getConfig());
stage.put("requisiteStageRefIds", getStageRequisiteIds(s, template.getStages()));
stage.putAll(s.getConfig());
return stage;
})
.collect(Collectors.toList()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package com.netflix.spinnaker.orca.pipelinetemplate.v1schema.graph.transform;

import com.netflix.spinnaker.orca.pipelinetemplate.exceptions.IllegalTemplateConfigurationException;
import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.PipelineTemplateVisitor;
import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.model.PipelineTemplate;
import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.model.StageDefinition;
Expand All @@ -23,7 +24,12 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
Expand Down Expand Up @@ -97,7 +103,7 @@ private static void dfs(String stageId,

for (String n : stage.getRequisiteStageRefIds()) {
Status status = state.get(n);
if (status == Status.VISITING) throw new IllegalStateException("Cycle detected in graph");
if (status == Status.VISITING) throw new IllegalTemplateConfigurationException(String.format("Cycle detected in graph (discovered on stage: %s)", stageId));
if (status == Status.VISITED) continue;
dfs(n, result, state, graph, outOrder);
}
Expand Down Expand Up @@ -155,7 +161,7 @@ private static void injectStages(List<StageDefinition> stages, List<StageDefinit
return;
}

throw new IllegalStateException(String.format("stage did not have any valid injections defined (id: %s)", s.getId()));
throw new IllegalTemplateConfigurationException(String.format("stage did not have any valid injections defined (id: %s)", s.getId()));
});
}

Expand Down Expand Up @@ -230,7 +236,7 @@ private static void injectAfter(StageDefinition stage, String targetId, List<Sta

StageDefinition target = graph.get(targetId);
if (target == null) {
throw new RuntimeException(String.format("could not inject '%s' stage: unknown target stage id '%s'", stage.getId(), targetId));
throw new IllegalTemplateConfigurationException(String.format("could not inject '%s' stage: unknown target stage id '%s'", stage.getId(), targetId));
}

stage.getRequisiteStageRefIds().add(target.getId());
Expand Down Expand Up @@ -261,7 +267,7 @@ private static StageDefinition getInjectionTarget(String stageId, String targetI
.stream()
.filter(pts -> pts.getId().equals(targetId))
.findFirst()
.orElseThrow(() -> new RuntimeException(
.orElseThrow(() -> new IllegalTemplateConfigurationException(
String.format("could not inject '%s' stage: unknown target stage id '%s'", stageId, targetId)
));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ public void setVariables(List<Variable> variables) {
}

public Configuration getConfiguration() {
if (configuration == null) {
configuration = new Configuration();
}
return configuration;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ public void setId(String id) {
}

public String getName() {
return name;
return Optional.ofNullable(name).orElse(id);
}

public void setName(String name) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@
import com.github.jknack.handlebars.Context;
import com.github.jknack.handlebars.EscapingStrategy;
import com.github.jknack.handlebars.Handlebars;
import com.github.jknack.handlebars.HandlebarsException;
import com.github.jknack.handlebars.Template;
import com.netflix.spinnaker.orca.pipelinetemplate.exceptions.TemplateRenderException;
import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.render.helper.ConditionHelper;
import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.render.helper.JsonHelper;
import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.render.helper.ModuleHelper;
import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.render.helper.UnknownIdentifierHelper;
import org.apache.commons.lang3.math.NumberUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -50,6 +52,7 @@ public HandlebarsRenderer(ObjectMapper pipelineTemplateObjectMapper) {

handlebars = new Handlebars()
.with(EscapingStrategy.NOOP)
.registerHelperMissing(new UnknownIdentifierHelper())
.registerHelper("json", new JsonHelper(pipelineTemplateObjectMapper))
.registerHelper("module", new ModuleHelper(this, pipelineTemplateObjectMapper))
;
Expand All @@ -73,6 +76,8 @@ public String render(String template, RenderContext configuration) {
return tmpl.apply(context);
} catch (IOException e) {
throw new TemplateRenderException("could not apply context to template", e);
} catch (HandlebarsException e) {
throw new TemplateRenderException(e.getMessage(), e.getCause());
}
}

Expand All @@ -93,7 +98,7 @@ public Object renderGraph(String template, RenderContext context) {
}
} else if (rendered.equals("true") || rendered.equals("false")) {
return Boolean.parseBoolean(rendered);
} else if (!rendered.startsWith("{") && !rendered.startsWith("[")) {
} else if (rendered.startsWith("{{") || (!rendered.startsWith("{") && !rendered.startsWith("["))) {
return rendered;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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.helper;

import com.github.jknack.handlebars.Helper;
import com.github.jknack.handlebars.Options;

import java.io.IOException;

public class UnknownIdentifierHelper implements Helper<Object> {
@Override
public Object apply(Object context, Options options) throws IOException {
return options.fn.text();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,27 +71,9 @@ class PipelineTemplatePipelinePreprocessorSpec extends Specification {

def 'should process simple template'() {
given:
def request = [
type: 'templatedPipeline',
trigger: [
type: "jenkins",
master: "master",
job: "job",
buildNumber: 1111
],
config: [
id: 'myTemplate',
pipeline: [
application: 'myapp',
template: [
source: getClass().getResource("/templates/simple-001.yml").toURI()
],
variables: [
regions: ['us-east-1', 'us-west-2']
]
]
]
]
def request = createTemplateRequest('simple-001.yml', [
regions: ['us-east-1', 'us-west-2']
])

when:
def result = subject.process(request)
Expand All @@ -112,29 +94,67 @@ class PipelineTemplatePipelinePreprocessorSpec extends Specification {
type: 'bake',
name: 'Bake',
requisiteStageRefIds: [],
context: [
regions: ['us-east-1', 'us-west-2'],
package: 'myapp-package',
baseOs: 'trusty',
vmType: 'hvm',
storeType: 'ebs',
baseLabel: 'release'
]
regions: ['us-east-1', 'us-west-2'],
package: 'myapp-package',
baseOs: 'trusty',
vmType: 'hvm',
storeType: 'ebs',
baseLabel: 'release'
],
[
id: null,
refId: 'tagImage',
type: 'tagImage',
name: 'Tag Image',
requisiteStageRefIds: ['bake'],
context: [
tags: [
stack: 'test'
]
tags: [
stack: 'test'
]
]
]
]
assertReflectionEquals(expected, result, ReflectionComparatorMode.IGNORE_DEFAULTS)
}

def 'should render jackson mapping exceptions'() {
when:
def result = subject.process(createTemplateRequest('invalid-template-001.yml', [:], true))

then:
noExceptionThrown()
result.errors != null
result.errors.containsKey("failed loading template")
}

def 'should not render unknown handlebars identifiers'() {
when:
def result = subject.process(createTemplateRequest('invalid-handlebars-001.yml', [:], true))

then:
noExceptionThrown()
result.stages[0].regions == '{{unknown_identifier}}'
}

Map<String, Object> createTemplateRequest(String templatePath, Map<String, Object> variables = [:], boolean plan = false) {
return [
type: 'templatedPipeline',
trigger: [
type: "jenkins",
master: "master",
job: "job",
buildNumber: 1111
],
config: [
id: 'myTemplate',
pipeline: [
application: 'myapp',
template: [
source: getClass().getResource("/templates/${templatePath}").toURI()
],
variables: variables
]
],
plan: plan
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,5 +91,6 @@ class V1SchemaExecutionGeneratorSpec extends Specification {
result.keepWaitingPipelines == false
result.stages*.type == ['bake', 'tagImage']
result.stages*.requisiteStageRefIds == [[], ['bake']]
result.stages.find { it.type == 'bake' }.baseOs == 'trusty'
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,6 @@ class HandlebarsRendererSpec extends Specification {
''' || List | ['us-east-1', 'us-west-2']
'{{json objectVar}}' || Map | [key1: 'value1', key2: 'value2']
'{{json trigger}}' || Map | [job: 'job', buildNumber: 1234]
'{{unknownVar}}' || String | '{{unknownVar}}'
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
schema: "1"
id: invalidHandlebarsTemplate
stages:
- id: bake
type: bake
name: Bake
config:
regions: "{{unknown_identifier}}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# "stage" is not a valid field on a pipeline template
schema: "1"
id: invalidTemplate
stage:
- id: bake
type: bake
config:
some: value
Loading

0 comments on commit 0ffa509

Please sign in to comment.