package org.wikimedia.integration import com.cloudbees.groovy.cps.NonCPS import static org.wikimedia.integration.Utility.timestampLabel import org.wikimedia.integration.ExecutionContext import org.wikimedia.integration.PatchSet import org.wikimedia.integration.Pipeline class PipelineStage implements Serializable { static final String SETUP = 'setup' static final String TEARDOWN = 'teardown' static final List STEPS = ['build', 'run', 'publish', 'deploy', 'exports'] Pipeline pipeline String name Map config private def context /** * Returns an config based on the given one but with default values * inserted. * * @example Shorthand stage config (providing only a stage name) *
* def cfg = [name: "foo"]
*
* assert PipelineStage.defaultConfig(cfg) == [
* name: "foo",
* build: '${.stage}', // builds a variant by the same name
* run: [
* image: '${.imageID}', // runs the variant built by this stage
* arguments: [],
* ],
* ]
*
*
* @example Configuring `run: true` means run the variant built by this
* stage
*
* def cfg = [name: "foo", build: "foo", run: true]
*
* assert PipelineStage.defaultConfig(cfg) == [
* name: "foo",
* build: "foo",
* run: [
* image: '${.imageID}', // runs the variant built by this stage
* arguments: [],
* ],
* ]
*
*
* @example Publish image default configuration
*
* def cfg = [image: true]
* def defaults = PipelineStage.defaultConfig(cfg)
*
* // publish.image.id defaults to the previously built image
* assert defaults.publish.image.id == '${.imageID}'
*
* // publish.image.name defaults to the project name
* assert defaults.publish.image.name == '${setup.project}'
*
* // publish.image.tag defaults to {timestamp}-{stage name}
* assert defaults.publish.image.tag == '${setup.timestamp}-${.stage}'
*
*/
@NonCPS
static Map defaultConfig(Map cfg) {
Map dcfg
// shorthand with just name is: build and run a variant
if (cfg.size() == 1 && cfg["name"]) {
dcfg = cfg + [
build: '${.stage}',
run: [
image: '${.imageID}',
]
]
} else {
dcfg = cfg.clone()
}
if (dcfg.run) {
// run: true means run the built image
if (dcfg.run == true) {
dcfg.run = [
image: '${.imageID}',
]
} else {
dcfg.run = dcfg.run.clone()
}
// run.image defaults to previously built image
dcfg.run.image = dcfg.run.image ?: '${.imageID}'
// run.arguments defaults to []
dcfg.run.arguments = dcfg.run.arguments ?: []
}
if (dcfg.publish) {
def pcfg = dcfg.publish.clone()
if (pcfg.image) {
if (pcfg.image == true) {
pcfg.image = [:]
} else {
pcfg.image = pcfg.image.clone()
}
// publish.image.id defaults to the previously built image
pcfg.image.id = pcfg.image.id ?: '${.imageID}'
// publish.image.name defaults to the project name
pcfg.image.name = pcfg.image.name ?: "\${${SETUP}.project}"
// publish.image.tag defaults to {timestamp}-{stage name}
pcfg.image.tag = pcfg.image.tag ?: "\${${SETUP}.timestamp}-\${.stage}"
pcfg.image.tags = (pcfg.image.tags ?: []).clone()
}
if (pcfg.files) {
pcfg.files.paths = pcfg.files.paths.clone()
}
dcfg.publish = pcfg
}
if (dcfg.deploy) {
dcfg.deploy = dcfg.deploy.clone()
dcfg.deploy.image = dcfg.deploy.image ?: '${.publishedImage}'
dcfg.deploy.cluster = dcfg.deploy.cluster ?: "ci"
dcfg.deploy.test = dcfg.deploy.test == null ? true : dcfg.deploy.test
}
dcfg
}
PipelineStage(Pipeline pline, String stageName, Map stageConfig, nodeContext) {
pipeline = pline
name = stageName
config = stageConfig
context = nodeContext
}
/**
* Constructs and retruns a closure for this pipeline stage using the given
* Jenkins workflow script object.
*/
Closure closure(ws) {
({
def runner = pipeline.runner(ws)
context["stage"] = name
switch (name) {
case SETUP:
setup(ws, runner)
break
case TEARDOWN:
teardown(ws, runner)
break
default:
ws.echo("running steps in ${pipeline.directory} with config: ${config.inspect()}")
ws.dir(pipeline.directory) {
for (def stageStep in STEPS) {
if (config[stageStep]) {
ws.echo("step: ${stageStep}, config: ${config.inspect()}")
this."${stageStep}"(ws, runner)
}
}
}
}
})
}
/**
* Returns a set of node labels that will be required for this stage to
* function correctly.
*/
Set getRequiredNodeLabels() {
def labels = [] as Set
if (config.build || config.run) {
labels.add("blubber")
}
if (config.publish) {
if (config.publish.files) {
labels.add("blubber")
}
if (config.publish.image) {
labels.add("dockerPublish")
}
}
labels
}
/**
* Performs setup steps, checkout out the repo and binding useful values to
* be used by all other stages (default image labels, project identifier,
* timestamp, etc).
*
* ${setup.project}
${setup.timestamp}
${setup.imageLabels}
jenkins.job
,
* jenkins.build
,
* ci.project
,
* ci.pipeline
* build
* stages:
* - name: candidate
* build: production
*
*
* ${[stage].imageID}
run
run: true
expands to
* run: { image: '${.imageID}' }
* (i.e. the image built in this stage)image
{$.imageID}
arguments
[]
* stages:
* - name: test
* build: test
* run: true
*
*
*
* stages:
* - name: built
* - name: lint
* run:
* image: '${built.imageID}'
* arguments: [lint]
* - name: test
* run:
* image: '${built.imageID}'
* arguments: [test]
*
*/
void run(ws, runner) {
runner.run(
context % config.run.image,
config.run.arguments.collect { context % it },
)
}
/**
* Publish artifacts, either files or a built image variant (pushed to the
* WMF Docker registry).
*
* publish
image
${.imageID}
(image built in this stage)${setup.project}
(project identifier;
* see {@link setup()})${setup.timestamp}-${.stage}
files
${[stage].imageName}
${[stage].imageFullName}
${[stage].imageTag}
${[stage].publishedImage}
${.imageFullName}:${.imageTag}
)deploy
${.publishedImage}
(image published in the
* {@link publish() publish step} of this stage)"ci"
helm test
against this deploymenttrue
${[stage].releaseName}
exports
* stages:
* - name: candidate
* build: production
* exports:
* image: '${.imageID}'
* tag: '${.imageTag}-my-tag'
* - name: published
* publish:
* image:
* id: '${candidate.image}'
* tags: ['${candidate.tag}']
*
*
* ${[name].[value]}