- 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:
- *
- * <pre><code>
- * 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
- * </code></pre>
- */
- class Pipeline implements Serializable {
- String name
- String blubberfile
- String directory
- String dockerRegistry
- String dockerRegistryInternal
-
- private Map stagesConfig
- private List<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}"
- }
- }
- }
|