Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(pipeline_template): Render configuration for templated pipelines…
Browse files Browse the repository at this point in the history
… with dynamic source

* Tries to fetch the last pipeline execution and use the context of that to render the source
jervi committed Oct 19, 2017

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 9a442c9 commit ff05273
Showing 5 changed files with 103 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -15,27 +15,68 @@
*/
package com.netflix.spinnaker.orca.pipelinetemplate;

import com.netflix.spinnaker.orca.pipeline.model.Pipeline;
import com.netflix.spinnaker.orca.pipeline.persistence.ExecutionRepository;
import com.netflix.spinnaker.orca.pipeline.persistence.ExecutionRepository.ExecutionCriteria;
import com.netflix.spinnaker.orca.pipelinetemplate.loader.TemplateLoader;
import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.TemplateMerge;
import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.model.PipelineTemplate;
import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.model.TemplateConfiguration.TemplateSource;
import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.render.DefaultRenderContext;
import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.render.Renderer;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.Nullable;
import java.util.List;
import java.util.NoSuchElementException;

@Component
public class PipelineTemplateService {

private final TemplateLoader templateLoader;

private final ExecutionRepository executionRepository;

private final Renderer renderer;

@Autowired
public PipelineTemplateService(TemplateLoader templateLoader) {
public PipelineTemplateService(TemplateLoader templateLoader, ExecutionRepository executionRepository, Renderer renderer) {
this.templateLoader = templateLoader;
this.executionRepository = executionRepository;
this.renderer = renderer;
}

public PipelineTemplate resolveTemplate(TemplateSource templateSource) {
public PipelineTemplate resolveTemplate(TemplateSource templateSource, @Nullable String pipelineConfigId) {
if (containsJinja(templateSource.getSource()) && pipelineConfigId != null) {
ExecutionCriteria criteria = new ExecutionCriteria().setLimit(1);
try {
Pipeline pipeline = executionRepository.retrievePipelinesForPipelineConfigId(pipelineConfigId, criteria)
.toSingle()
.toBlocking()
.value();
String renderedSource = render(templateSource.getSource(), pipeline);
if (StringUtils.isNotBlank(renderedSource)) {
templateSource.setSource(renderedSource);
}

} catch (NoSuchElementException e) {
// Do nothing
}
}

List<PipelineTemplate> templates = templateLoader.load(templateSource);
return TemplateMerge.merge(templates);
}

private String render(String templateString, Pipeline pipeline) {
DefaultRenderContext rc = new DefaultRenderContext(pipeline.getApplication(), null, pipeline.getTrigger());
return renderer.render(templateString, rc);
}

private static boolean containsJinja(String string) {
return string.contains("{%") || string.contains("{{");
}

}
Original file line number Diff line number Diff line change
@@ -84,6 +84,7 @@ class V1TemplateLoaderHandler(
val value = v.defaultValue
if (value != null && value is String) {
v.defaultValue = renderer.renderGraph(value.toString(), renderContext)
renderContext.variables.putIfAbsent(v.name, v.defaultValue)
}
}
}
Original file line number Diff line number Diff line change
@@ -16,9 +16,6 @@

package com.netflix.spinnaker.orca.controllers

import com.netflix.spinnaker.orca.pipeline.util.ArtifactResolver

import javax.servlet.http.HttpServletResponse
import com.fasterxml.jackson.databind.ObjectMapper
import com.netflix.spinnaker.kork.web.exceptions.InvalidRequestException
import com.netflix.spinnaker.kork.web.exceptions.ValidationException
@@ -29,6 +26,7 @@ import com.netflix.spinnaker.orca.pipeline.OrchestrationLauncher
import com.netflix.spinnaker.orca.pipeline.PipelineLauncher
import com.netflix.spinnaker.orca.pipeline.model.Pipeline
import com.netflix.spinnaker.orca.pipeline.persistence.ExecutionRepository
import com.netflix.spinnaker.orca.pipeline.util.ArtifactResolver
import com.netflix.spinnaker.orca.pipeline.util.ContextParameterProcessor
import com.netflix.spinnaker.orca.webhook.service.WebhookService
import com.netflix.spinnaker.security.AuthenticatedRequest
@@ -40,6 +38,8 @@ import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.bind.annotation.RestController
import static net.logstash.logback.argument.StructuredArguments.value

import javax.servlet.http.HttpServletResponse

@RestController
@Slf4j
class OperationsController {
@@ -115,6 +115,20 @@ class OperationsController {
private void parsePipelineTrigger(ExecutionRepository executionRepository, BuildService buildService, Map pipeline) {
if (!(pipeline.trigger instanceof Map)) {
pipeline.trigger = [:]
if (pipeline.plan && pipeline.type == "templatedPipeline") {
// If possible, initialize the config with the previous execution trigger context, to be able to resolve
// dynamic parameters in jinja expressions
try {
def criteria = new ExecutionRepository.ExecutionCriteria(limit: 1)
def previousExecution = executionRepository.retrievePipelinesForPipelineConfigId(pipeline.id, criteria)
.toSingle()
.toBlocking()
.value()
pipeline.trigger = previousExecution.trigger
} catch (NoSuchElementException ignore) {
// Do nothing
}
}
}

if (!pipeline.trigger.type) {
Original file line number Diff line number Diff line change
@@ -38,12 +38,11 @@ class PipelineTemplateController {
PipelineTemplateService pipelineTemplateService

@RequestMapping(value = "/pipelineTemplate", method = RequestMethod.GET)
PipelineTemplate getPipelineTemplate(@RequestParam("source") String source) {
if (source == null || source?.empty) {
PipelineTemplate getPipelineTemplate(@RequestParam String source, @RequestParam(required = false) String pipelineConfigId) {
if (!source) {
throw new InvalidRequestException("template source must not be empty")
}

pipelineTemplateService.resolveTemplate(new TemplateSource(source: source))
pipelineTemplateService.resolveTemplate(new TemplateSource(source: source), pipelineConfigId)
}

@RequestMapping(value = "/convertPipelineToTemplate", method = RequestMethod.POST, produces = 'text/x-yaml')
Original file line number Diff line number Diff line change
@@ -16,6 +16,8 @@

package com.netflix.spinnaker.orca.controllers

import rx.Observable

import javax.servlet.http.HttpServletResponse
import com.netflix.spinnaker.kork.web.exceptions.InvalidRequestException
import com.netflix.spinnaker.orca.igor.BuildArtifactFilter
@@ -39,6 +41,7 @@ import spock.lang.Specification
import spock.lang.Subject
import spock.lang.Unroll
import static com.netflix.spinnaker.orca.ExecutionStatus.CANCELED
import static com.netflix.spinnaker.orca.ExecutionStatus.SUCCEEDED
import static com.netflix.spinnaker.orca.test.model.ExecutionBuilder.pipeline
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post

@@ -197,6 +200,40 @@ class OperationsControllerSpec extends Specification {
]
}

def "should get pipeline execution context from previous execution if not provided and attribute plan is truthy"() {
given:
Pipeline startedPipeline = null
pipelineLauncher.start(_) >> { String json ->
startedPipeline = mapper.readValue(json, Pipeline)
startedPipeline.id = UUID.randomUUID().toString()
startedPipeline
}

Pipeline previousExecution = pipeline {
name = "Last executed pipeline"
status = SUCCEEDED
id = "12345"
application = "covfefe"
trigger << [
type: "travis"
]
}

when:
def orchestration = controller.orchestrate(requestedPipeline, Mock(HttpServletResponse))

then:
1 * executionRepository.retrievePipelinesForPipelineConfigId("54321", _) >> Observable.just(previousExecution)
orchestration.trigger.type == "travis"

where:
requestedPipeline = [
id: "54321",
plan: true,
type: "templatedPipeline"
]
}

def "trigger user takes precedence over query parameter"() {
given:
Pipeline startedPipeline = null
@@ -492,6 +529,7 @@ class OperationsControllerSpec extends Specification {
given:
def pipelineConfig = [
plan : true,
type : "templatedPipeline",
errors: [
'things broke': 'because of the way it is'
]
@@ -503,6 +541,7 @@ class OperationsControllerSpec extends Specification {

then:
thrown(InvalidRequestException)
1 * executionRepository.retrievePipelinesForPipelineConfigId(_, _) >> Observable.empty()
0 * pipelineLauncher.start(_)
}

0 comments on commit ff05273

Please sign in to comment.