package org.wikimedia.integration import org.codehaus.groovy.GroovyException import static org.wikimedia.integration.PipelineStage.* import org.wikimedia.integration.ExecutionContext import org.wikimedia.integration.ExecutionGraph import org.wikimedia.integration.PipelineStage import org.wikimedia.integration.PipelineRunner /** * Defines a Jenkins Workflow based on a given configuration. * * The given configuration should look like this: * *

 * pipelines:
 *   serviceOne:
 *     blubberfile: serviceOne/blubber.yaml           # default based on service name for the dir
 *     directory: src/serviceOne
 *     execution:                                     # directed graph of stages to run
 *       - [unit, candidate]                          # each arc is represented horizontally
 *       - [lint, candidate]
 *       - [candidate, staging, production]           # common segments of arcs can be defined separately too
 *     stages:                                        # stage defintions
 *       - name: unit                                 # stage name (required)
 *         build: phpunit                             # build an image variant
 *         run: "${.imageID}"                         # run the built image
 *         publish:
 *           files:                                   # publish select artifact files from the built/run image
 *             paths: ["foo/*", "bar"]                # copy files {foo/*,bar} from the image fs to ./artifacts/{foo/*,bar}
 *       - name: lint                                 # default (build/run "lint" variant, no artifacts, etc.)
 *       - name: candidate
 *         build: production
 *         publish:
 *           image:                                   # publish built image to our docker registry
 *             id: "${.imageID}"                      # image reference
 *             name: "${setup.project}"               # image name
 *             tag: "${setup.timestamp}-${.stage}"    # primary tag
 *             tags: [candidate]                      # additional tags
 *         exports:                                   # export stage values under new names
 *           image: "${.imageFullName}:${.imageTag}"  # new variable name and interpolated value
 *       - name: staging
 *         deploy:                                    # deploy image to a cluster
 *           image: "${candidate.image}"              # image name:tag reference
 *           cluster: ci                              # default "ci" k8s cluster
 *           chart: http://helm/chart                 # helm chart to use for deployment
 *           test: true                               # run `helm test` on deployment
 *       - name: production
 *         deploy:
 *           cluster: production
 *           chart: http://helm/chart
 *   serviceTwo:
 *     directory: src/serviceTwo
 * 
*/ class Pipeline implements Serializable { String name String blubberfile String directory String dockerRegistry String dockerRegistryInternal private Map stagesConfig private List execution /** * Constructs a new pipeline with the given name and configuration. */ Pipeline(String pipelineName, Map config) { name = pipelineName blubberfile = config.blubberfile ?: "${name}/blubber.yaml" directory = config.directory ?: "." stagesConfig = config.stages.collectEntries{ [(it.name): PipelineStage.defaultConfig(it)] } execution = config.execution ?: [config.stages.collect { it.name }] } /** * Returns a set of node labels that will be required for this pipeline to * function correctly. */ Set getRequiredNodeLabels() { def labels = [] as Set for (def nodes in stack()) { for (def node in nodes) { labels += node.getRequiredNodeLabels() } } labels } /** * Returns the pipeline's stage stack bound with an execution context. */ List stack() { def graph = setup() + (new ExecutionGraph(execution)) + teardown() def context = new ExecutionContext(graph) graph.stack().collect { it.collect { stageName -> createStage(stageName, context.ofNode(stageName)) } } } /** * Returns a {@link PipelineRunner} for this pipeline and the given workflow * script object. */ PipelineRunner runner(ws) { def settings = [ blubberConfig: blubberfile, kubeConfig: "/etc/kubernetes/ci-staging.config", ] if (dockerRegistry) { settings["registry"] = dockerRegistry } if (dockerRegistryInternal) { settings["registryInternal"] = dockerRegistryInternal } def runner = new PipelineRunner(settings, ws) // make the PipelineRunner configPath relative to the pipeline's directory def prefix = "../" * directory.split('/').count { !(it in ["", "."]) } runner.configPath = prefix + runner.configPath runner } /** * Validates the pipeline configuration, throwing a {@link ValidationException} * if anything is amiss. */ void validate() throws ValidationException { def errors = [] // TODO expand validation if (PipelineStage.SETUP in stagesConfig) { errors += "${PipelineStage.SETUP} is a reserved stage name" } if (PipelineStage.TEARDOWN in stagesConfig) { errors += "${PipelineStage.TEARDOWN} is a reserved stage name" } if (errors) { throw new ValidationException(errors: errors) } } private ExecutionGraph setup() { new ExecutionGraph([[PipelineStage.SETUP]]) } private ExecutionGraph teardown() { new ExecutionGraph([[PipelineStage.TEARDOWN]]) } private PipelineStage createStage(stageName, context) { new PipelineStage( this, stageName, stagesConfig[stageName] ? stagesConfig[stageName] : [:], context, ) } class ValidationException extends GroovyException { def errors String getMessage() { def msgs = errors.collect { " - ${it}" }.join("\n") "Pipeline configuration validation failed:\n${msgs}" } } }