├── .github ├── pull_request_template.md └── workflows │ └── build.yml ├── .gitignore ├── CHANGELOG.md ├── Jenkinsfile.terraform-pipeline ├── LICENSE ├── README.md ├── build.gradle ├── codecov.yml ├── config └── codenarc │ └── codenarc.xml ├── docs ├── AgentNodePlugin.md ├── AnsiColorPlugin.md ├── AwssumePlugin.md ├── BuildStage.md ├── BuildWithParametersPlugin.md ├── ConditionalApplyPlugin.md ├── ConfirmApplyPlugin.md ├── ConsulBackendPlugin.md ├── CredentialsPlugin.md ├── DefaultEnvironmentPlugin.md ├── DestroyPlugin.md ├── FileParametersPlugin.md ├── FlywayMigrationPlugin.md ├── GithubPRPlanPlugin.md ├── ParameterStoreBuildWrapperPlugin.md ├── ParameterStoreExecPlugin.md ├── PassPlanFilePlugin.md ├── PlanOnlyPlugin.md ├── RegressionStage.md ├── S3BackendPlugin.md ├── TagPlugin.md ├── TargetPlugin.md ├── TerraformDirectoryPlugin.md ├── TerraformEnvironmentStage.md ├── TerraformEnvironmentStageShellHookPlugin.md ├── TerraformImportPlugin.md ├── TerraformLandscapePlugin.md ├── TerraformOutputOnlyPlugin.md ├── TerraformPlugin.md ├── TerraformStartDirectoryPlugin.md ├── TerraformTaintPlugin.md ├── TfvarsFilesPlugin.md ├── ValidateFormatPlugin.md └── WithAwsPlugin.md ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── MultibranchPipeline.png ├── NewItem.png ├── configure-project.png ├── custom-declarative-pipeline.png ├── declarative-pipeline.png ├── default-pipeline-success.png ├── destroy-pipeline.png ├── import-terraform-pipeline.png ├── restart-from-stage-numbers.png └── restart-from-stage.png ├── settings.gradle ├── src ├── AgentNodePlugin.groovy ├── AnsiColorPlugin.groovy ├── AwssumePlugin.groovy ├── BuildGraph.groovy ├── BuildStage.groovy ├── BuildStagePlugin.groovy ├── BuildWithParametersPlugin.groovy ├── ConditionalApplyPlugin.groovy ├── ConfirmApplyPlugin.groovy ├── ConsulBackendPlugin.groovy ├── CredentialsPlugin.groovy ├── DecoratableStage.groovy ├── DefaultEnvironmentPlugin.groovy ├── DestroyPlugin.groovy ├── EnvironmentVariablePlugin.groovy ├── FileParametersPlugin.groovy ├── FlywayCommand.groovy ├── FlywayMigrationPlugin.groovy ├── GithubPRPlanPlugin.groovy ├── HookPoint.groovy ├── Jenkinsfile.groovy ├── ParameterStoreBuildWrapperPlugin.groovy ├── ParameterStoreExecPlugin.groovy ├── PassPlanFilePlugin.groovy ├── PlanOnlyPlugin.groovy ├── RegressionStage.groovy ├── RegressionStagePlugin.groovy ├── Resettable.groovy ├── S3BackendPlugin.groovy ├── SemanticVersion.groovy ├── Stage.groovy ├── StageDecorations.groovy ├── TagPlugin.groovy ├── TargetPlugin.groovy ├── TerraformApplyCommand.groovy ├── TerraformApplyCommandPlugin.groovy ├── TerraformCommand.groovy ├── TerraformDirectoryPlugin.groovy ├── TerraformEnvironmentStage.groovy ├── TerraformEnvironmentStagePlugin.groovy ├── TerraformEnvironmentStageShellHookPlugin.groovy ├── TerraformFormatCommand.groovy ├── TerraformFormatCommandPlugin.groovy ├── TerraformImportCommand.groovy ├── TerraformImportCommandPlugin.groovy ├── TerraformImportPlugin.groovy ├── TerraformInitCommand.groovy ├── TerraformInitCommandPlugin.groovy ├── TerraformLandscapePlugin.groovy ├── TerraformOutputCommand.groovy ├── TerraformOutputCommandPlugin.groovy ├── TerraformOutputOnlyPlugin.groovy ├── TerraformPlanCommand.groovy ├── TerraformPlanCommandPlugin.groovy ├── TerraformPlugin.groovy ├── TerraformPluginVersion.groovy ├── TerraformPluginVersion11.groovy ├── TerraformPluginVersion12.groovy ├── TerraformPluginVersion15.groovy ├── TerraformStartDirectoryPlugin.groovy ├── TerraformTaintCommand.groovy ├── TerraformTaintCommandPlugin.groovy ├── TerraformTaintPlugin.groovy ├── TerraformUntaintCommand.groovy ├── TerraformUntaintCommandPlugin.groovy ├── TerraformValidateCommand.groovy ├── TerraformValidateCommandPlugin.groovy ├── TerraformValidateStage.groovy ├── TerraformValidateStagePlugin.groovy ├── TfvarsFilesPlugin.groovy ├── ValidateFormatPlugin.groovy ├── WhenToRun.groovy └── WithAwsPlugin.groovy ├── test ├── AgentNodePluginTest.groovy ├── AnsiColorPluginTest.groovy ├── AwssumePluginTest.groovy ├── BuildGraphTest.groovy ├── BuildStageTest.groovy ├── BuildWithParametersPluginTest.groovy ├── ConditionalApplyPluginTest.groovy ├── ConfirmApplyPluginTest.groovy ├── ConsulBackendPluginTest.groovy ├── CredentialsPluginTest.groovy ├── DefaultEnvironmentPluginTest.groovy ├── DestroyPluginTest.groovy ├── FileParametersPluginTest.groovy ├── FlywayCommandTest.groovy ├── FlywayMigrationPluginTest.groovy ├── GithubPRPlanPluginTest.groovy ├── HookPointTest.groovy ├── JenkinsfileTest.groovy ├── MockJenkinsfile.groovy ├── MockWorkflowScript.groovy ├── ParameterStoreBuildWrapperPluginTest.groovy ├── ParameterStoreExecPluginTest.groovy ├── PassPlanFilePluginTest.groovy ├── PlanOnlyPluginTest.groovy ├── RegressionStageTest.groovy ├── ResetStaticStateExtension.groovy ├── S3BackendPluginTest.groovy ├── SemanticVersionTest.groovy ├── StageDecorationsTest.groovy ├── TagPluginTest.groovy ├── TargetPluginTest.groovy ├── TerraformApplyCommandTest.groovy ├── TerraformDirectoryPluginTest.groovy ├── TerraformEnvironmentStageShellHookPluginTest.groovy ├── TerraformEnvironmentStageTest.groovy ├── TerraformFormatCommandTest.groovy ├── TerraformImportCommandTest.groovy ├── TerraformImportPluginTest.groovy ├── TerraformInitCommandTest.groovy ├── TerraformLandscapePluginTest.groovy ├── TerraformOutputCommandTest.groovy ├── TerraformOutputOnlyPluginTest.groovy ├── TerraformPlanCommandTest.groovy ├── TerraformPluginTest.groovy ├── TerraformPluginVersion11Test.groovy ├── TerraformPluginVersion12Test.groovy ├── TerraformPluginVersion15Test.groovy ├── TerraformStartDirectoryPluginTest.groovy ├── TerraformTaintCommandTest.groovy ├── TerraformTaintPluginTest.groovy ├── TerraformUntaintCommandTest.groovy ├── TerraformValidateCommandTest.groovy ├── TerraformValidateStageTest.groovy ├── TfvarsFilesPluginTest.groovy ├── ValidateFormatPluginTest.groovy └── WithAwsPluginTest.groovy └── vars ├── ApplyJenkinsfileClosure.groovy ├── Pipeline2Stage.groovy ├── Pipeline3Stage.groovy ├── Pipeline4Stage.groovy ├── Pipeline5Stage.groovy ├── Pipeline6Stage.groovy └── Pipeline7Stage.groovy /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Merge Checklist 2 | 3 | * [ ] Have you linked this Pull Request to an [Issue](https://github.com/manheim/terraform-pipeline/issues)? 4 | * [ ] Have you updated any relevant README files, to explain how this new feature works (with examples)? 5 | * [ ] Have you added relevant test cases for your change? 6 | * [ ] Do all tests and code style checks pass with `./gradlew check --info`? 7 | * [ ] Have you successfully run your change in a pipeline in a Jenkins instance? 8 | * [ ] Have you updated the [CHANGELOG](https://github.com/manheim/terraform-pipeline/blob/master/CHANGELOG.md)? 9 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # Run unit tests 2 | name: CI 3 | 4 | # Triggers the workflow on all push or pull request events 5 | on: [push, pull_request] 6 | 7 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 8 | jobs: 9 | # This workflow contains a single job called "build" 10 | build: 11 | # The type of runner that the job will run on 12 | runs-on: ubuntu-latest 13 | 14 | # Steps represent a sequence of tasks that will be executed as part of the job 15 | steps: 16 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 17 | - uses: actions/checkout@v2 18 | 19 | - name: Set up JDK 11 20 | uses: actions/setup-java@v2 21 | with: 22 | java-version: '11' 23 | distribution: 'adopt' 24 | 25 | - name: Validate Gradle wrapper 26 | uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b 27 | 28 | # Run unit tests 29 | - name: Run tests with gradle 30 | run: ./gradlew check --info 31 | 32 | # Upload code coverage data to CodeCov 33 | - name: Upload CodeCov results 34 | uses: codecov/codecov-action@v1 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | .gradle 3 | 4 | build 5 | .idea 6 | *.iml 7 | /out 8 | .history 9 | bin/ 10 | tags 11 | .vscode 12 | shell.nix 13 | nix/ 14 | -------------------------------------------------------------------------------- /Jenkinsfile.terraform-pipeline: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manheim/terraform-pipeline/0b618f10644fb16025e6cc72599b2e16e74c2646/Jenkinsfile.terraform-pipeline -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'groovy' 2 | apply plugin: 'jacoco' 3 | apply plugin: 'codenarc' 4 | 5 | repositories { 6 | mavenCentral() 7 | } 8 | 9 | dependencies { 10 | compile 'org.codehaus.groovy:groovy-all:2.4.12' 11 | 12 | testCompile 'com.lesfurets:jenkins-pipeline-unit:1.1' 13 | testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.7.0' 14 | testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.7.0' 15 | testCompile group: 'com.cyrusinnovation', name: 'mockito-groovy-support', version: '1.3' 16 | testImplementation 'org.hamcrest:hamcrest:2.2' 17 | testImplementation 'org.reflections:reflections:0.9.12' 18 | testImplementation 'org.slf4j:slf4j-api:1.7.30' 19 | } 20 | 21 | test { 22 | useJUnitPlatform() 23 | } 24 | 25 | sourceSets { 26 | test { 27 | groovy { 28 | srcDirs = ['test'] 29 | } 30 | } 31 | 32 | main { 33 | groovy { 34 | srcDirs = ['src'] 35 | } 36 | } 37 | 38 | } 39 | 40 | jacocoTestReport { 41 | reports { 42 | xml.enabled true 43 | html.enabled true 44 | } 45 | } 46 | 47 | check.dependsOn jacocoTestReport 48 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: no 3 | -------------------------------------------------------------------------------- /config/codenarc/codenarc.xml: -------------------------------------------------------------------------------- 1 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /docs/AgentNodePlugin.md: -------------------------------------------------------------------------------- 1 | ## [AgentNodePlugin](../src/AgentNodePlugin.groovy) 2 | 3 | This plugin allows you to run the terraform stages in a docker container. This **DOES NOT WORK WITH AWSSUME** you should be using iam_block with aws provider with terraform. 4 | 5 | ### Using pre-built docker image 6 | ``` 7 | // Jenkinsfile 8 | @Library(['terraform-pipeline']) _ 9 | 10 | Jenkinsfile.init(this) 11 | 12 | AgentNodePlugin.withAgentDockerImage("hashicorp/terraform:latest") 13 | .withAgentDockerImageOptions("--entrypoint=''") 14 | .init() 15 | 16 | def validate = new TerraformValidateStage() 17 | def deployQA = new TerraformEnvironmentStage('qa') 18 | def deployUat = new TerraformEnvironmentStage('uat') 19 | def deployProd = new TerraformEnvironmentStage('prod') 20 | 21 | validate.then(deployQA) 22 | .then(deployUat) 23 | .then(deployProd) 24 | .build() 25 | ``` 26 | 27 | ### Using Dockerfile in repo 28 | ``` 29 | // Jenkinsfile 30 | @Library(['terraform-pipeline']) _ 31 | 32 | Jenkinsfile.init(this) 33 | 34 | AgentNodePlugin.withAgentDockerImage('custom/terraform-ruby:latest') 35 | .withAgentDockerImageOptions("--entrypoint=''") 36 | .withAgentDockerfile() 37 | .init() 38 | 39 | def validate = new TerraformValidateStage() 40 | def deployQA = new TerraformEnvironmentStage('qa') 41 | def deployUat = new TerraformEnvironmentStage('uat') 42 | def deployProd = new TerraformEnvironmentStage('prod') 43 | 44 | validate.then(deployQA) 45 | .then(deployUat) 46 | .then(deployProd) 47 | .build() 48 | ``` 49 | 50 | ### Optionally use a Dockerfile with a different filename 51 | ``` 52 | // Jenkinsfile 53 | @Library(['terraform-pipeline']) _ 54 | 55 | Jenkinsfile.init(this) 56 | 57 | AgentNodePlugin.withAgentDockerImage('custom/terraform-ruby:latest') 58 | .withAgentDockerImageOptions("--entrypoint=''") 59 | .withAgentDockerfile('MyDockerfile') 60 | .init() 61 | 62 | def validate = new TerraformValidateStage() 63 | def deployQA = new TerraformEnvironmentStage('qa') 64 | def deployUat = new TerraformEnvironmentStage('uat') 65 | def deployProd = new TerraformEnvironmentStage('prod') 66 | 67 | validate.then(deployQA) 68 | .then(deployUat) 69 | .then(deployProd) 70 | .build() 71 | ``` 72 | 73 | -------------------------------------------------------------------------------- /docs/AnsiColorPlugin.md: -------------------------------------------------------------------------------- 1 | ## [AnsiColorPlugin](../src/AnsiColorPlugin.groovy) 2 | 3 | Enable this plugin to color the output for terraform plan and apply. 4 | 5 | One-time setup: 6 | * Install the [AnsiColorPlugin](https://wiki.jenkins.io/display/JENKINS/AnsiColor+Plugin) on your Jenkins server. 7 | 8 | ``` 9 | // Jenkinsfile 10 | @Library(['terraform-pipeline@v3.10']) _ 11 | 12 | Jenkinsfile.init(this, env) 13 | 14 | AnsiColorPlugin.init() // Decorate your TerraformEnvironmentStages with the AnsiColor plugin 15 | 16 | def validate = new TerraformValidateStage() 17 | 18 | def deployQa = new TerraformEnvironmentStage('qa') 19 | def deployUat = new TerraformEnvironmentStage('uat') 20 | def deployProd = new TerraformEnvironmentStage('prod') 21 | 22 | validate.then(deployQa) 23 | .then(deployUat) 24 | .then(deployProd) 25 | .build() 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/AwssumePlugin.md: -------------------------------------------------------------------------------- 1 | ## [AwssumePlugin](../src/AwssumePlugin.groovy) 2 | 3 | Enable this plugin to wrap your terraform commands with the [awssume](https://github.com/manheim/awssume) gem, allowing you to assume roles across accounts. 4 | 5 | One-time setup: 6 | * Install the awssume gem on your Jenkins slaves. 7 | * Optional: Define global variables that match your environment name, to a role across all pipelines with that environment: 8 | * QA_AWS_ROLE_ARN (all 'qa' environments will assume the role specified by this variable) 9 | * UAT_AWS_ROLE_ARN (all 'uat' environments will assume the role specified by this variable) 10 | * PROD_AWS_ROLE_ARN (all 'prod' environments will assume the role specified by this variable) 11 | 12 | Awssume will assume the role for any environment where a `AWS_ROLE_ARN` is defined, or for any environment that matches a global `_AWS_ROLE_ARN`. If neither variables are specified, the use of Awssume will be skipped. 13 | 14 | ``` 15 | // Jenkinsfile 16 | @Library(['terraform-pipeline@v3.10']) _ 17 | 18 | Jenkinsfile.init(this, env) 19 | 20 | AwssumePlugin.init() // Decorate your TerraformEnvironmentStages with the Awssume plugin 21 | 22 | def validate = new TerraformValidateStage() 23 | 24 | // Run terraform apply and plan using the AWS Role defined by either AWS_ROLE_ARN or QA_AWS_ROLE_ARN 25 | def deployQA = new TerraformEnvironmentStage('qa') 26 | 27 | // Run terraform apply and plan using the AWS Role defined by either AWS_ROLE_ARN or UAT_AWS_ROLE_ARN 28 | def deployUat = new TerraformEnvironmentStage('uat') 29 | 30 | // Run terraform apply and plan using the AWS Role defined by either AWS_ROLE_ARN or PROD_AWS_ROLE_ARN 31 | def deployProd = new TerraformEnvironmentStage('prod') 32 | 33 | validate.then(deployQa) 34 | .then(deployUat) 35 | .then(deployProd) 36 | .build() 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/BuildStage.md: -------------------------------------------------------------------------------- 1 | # BuildStage 2 | 3 | Some pipelines dealing with application code may to build deployment artifacts. The BuildStage can be used to accommodate this. Create a BuildStage then add it to your other linked stages in the appropriate position. Below is an example for building a deployment artifact which is later used to deploy to QA. 4 | 5 | Let terraform-pipline know which build artifacts to save and make available using the `saveArtifact` method. Artifacts that match the pattern passed to `saveArtifact` will automatically be stashed after BuildStage, and unstashed in each subsequent TerraformEnvironmentStage. 6 | 7 | ``` 8 | // Jenkinsfile 9 | ... 10 | def build = new BuildStage().saveArtifact('*/target/MyApp.war') 11 | 12 | validate.then(build) 13 | .then(deployQa) // MyApp.war will be available in the working directory 14 | ... 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/BuildWithParametersPlugin.md: -------------------------------------------------------------------------------- 1 | ## [BuildWithParametersPlugin](../src/BuildWithParametersPlugin.groovy) 2 | 3 | This plugin is enabled by default. 4 | 5 | Pipelines can prompt the user for parameters at build-time, with the Jenkinsfile "Build With Parameters" feature. This plugin lets you enable this feature, and prompt the user for parameters. If no parameters are configured, this plugin does nothing. 6 | 7 | Eg: 8 | 9 | ``` 10 | @Library(['terraform-pipeline@v5.1']) _ 11 | 12 | Jenkinsfile.init(this) 13 | BuildWithParametersPlugin.withBooleanParameter([ 14 | name: 'PIPELINE_PREFERENCE', 15 | description: 'Do you like pipelines?', 16 | defaultValue: true 17 | ]) 18 | BuildWithParametersPlugin.withStringParameter([ 19 | name: 'PIPELINE_THOUGHTS', 20 | description: 'What do you think about pipelines?', 21 | defaultValue: 'They make deployments so easy' 22 | ]) 23 | 24 | def validate = new TerraformValidateStage() 25 | def deployQa = new TerraformEnvironmentStage('qa') 26 | def deployUat = new TerraformEnvironmentStage('uat') 27 | def deployProd = new TerraformEnvironmentStage('prod') 28 | 29 | // At the start of the pipeline, the user with a checkbox and a string input 30 | // The user's responses are available in the environment variables 31 | // PIPELINE_PREFERENCE and PIPELINE_THOUGHTS 32 | validate.then(deployQa) 33 | .then(deployUat) 34 | .then(deployProd) 35 | .build() 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/ConditionalApplyPlugin.md: -------------------------------------------------------------------------------- 1 | ## [ConditionalApplyPlugin](../src/ConditionalApplyPlugin.groovy) 2 | 3 | This plugin is enabled by default. 4 | 5 | By default, changes are applied through one and only one branch - main. The 6 | ConditionalApplyPlugin enforces this by making the "Confirm" and "Apply" steps 7 | of a TerraformEnvironmentStage visible only on the main or master branch. You can 8 | continue to use branches and PullRequests, however, branches and PullRequests 9 | will only run the Plan step for each environment, and skip over the 10 | Confirm/Apply steps. 11 | 12 | This behavior can be changed by using `ConditionalApplyPlugin.withApplyOnBranch()`. This method accepts one or more branches. "Confirm" and "Apply" steps of TerraformEnvironmentStage will then be visible for each of the specified branches. Any branch or PullRequest not in that list will only run the Plan step for each environment, and skip over the Confirm/Apply steps. 13 | 14 | Example: 15 | 16 | ``` 17 | @Library(['terraform-pipeline']) _ 18 | 19 | Jenkinsfile.init(this) 20 | ConditionalApplyPlugin.withApplyOnBranch('myMainReplacement') 21 | 22 | def validate = new TerraformValidateStage() 23 | // 'qa' stage will run Plan/Confirm/Apply on the 'myMainReplacement' branch. 24 | // 'qa' stage will only run Plan for all other branches and PullRequests. 25 | def deployQa = new TerraformEnvironmentStage('qa') 26 | // 'uat' stage will run Plan/Confirm/Apply on 'myMainReplacement' branch. 27 | // 'uat' stage will only run Plan for all other branches and PullRequests. 28 | def deployUat = new TerraformEnvironmentStage('uat') 29 | // 'prod' stage will run Plan/Confirm/Apply on 'myMainReplacement' branch. 30 | // 'prod' stage will only run Plan for all other branches and PullRequests. 31 | def deployProd = new TerraformEnvironmentStage('prod') 32 | 33 | validate.then(deployQa) 34 | .then(deployUat) 35 | .then(deployProd) 36 | .build() 37 | ... 38 | ``` 39 | 40 | Alternatively, enable "Confirm" and "Apply" for specific environments with `ConditionalApplyPlugin.withApplyOnEnvironment()`. This method accepts one or more environment names. "Confirm" and "Apply" steps of TerraformEnvironmentStage will then be visible for each of the specified environments, regardless of the branch or PullRequest. 41 | 42 | Example: 43 | 44 | ``` 45 | @Library(['terraform-pipeline']) _ 46 | 47 | Jenkinsfile.init(this) 48 | ConditionalApplyPlugin.withApplyOnEnvironment('qa') 49 | 50 | def validate = new TerraformValidateStage() 51 | // 'qa' stage will run Plan/Confirm/Apply on all branches and PullRequests. 52 | def deployQa = new TerraformEnvironmentStage('qa') 53 | // 'uat' stage will run Plan/Confirm/Apply only on main, and will only run Plan on all other branches and PullRequests. 54 | def deployUat = new TerraformEnvironmentStage('uat') 55 | // 'prod' stage will run Plan/Confirm/Apply only on main, and will only run Plan on all other branches and PullRequests. 56 | def deployProd = new TerraformEnvironmentStage('prod') 57 | 58 | validate.then(deployQa) 59 | .then(deployUat) 60 | .then(deployProd) 61 | .build() 62 | ... 63 | ``` 64 | 65 | Disable this plugin, if you want to allow "Confirm" and "Apply" on any branch or PullRequest. 66 | 67 | ``` 68 | @Library(['terraform-pipeline']) _ 69 | 70 | Jenkinsfile.init(this) 71 | ConditionalApplyPlugin.disable() 72 | ... 73 | ``` 74 | -------------------------------------------------------------------------------- /docs/ConfirmApplyPlugin.md: -------------------------------------------------------------------------------- 1 | ## [ConfirmApplyPlugin](../src/ConfirmApplyPlugin.groovy) 2 | 3 | This plugin is enabled by default. 4 | 5 | It's a good practice to review the terraform plan before applying changes to any environment, to confirm that the changes that are being applied are the same changes that are expected. The ConfirmApplyPlugin will pause your pipeline after the Plan step, allowing a human to review the changes. A human must then manually Confirm the changes by clicking on the pipeline, before the changes are applied. 6 | 7 | The pipeline will pause for a limited amount of time - 15 minutes. Once that timeout is exceeded, that particular pipeline run will be canceled. 8 | 9 | This functionality of this plugin can be disabled with the following configuration: 10 | 11 | ``` 12 | ConfirmApplyPlugin.disable() 13 | -------------------------------------------------------------------------------- /docs/ConsulBackendPlugin.md: -------------------------------------------------------------------------------- 1 | ## [ConsulBackendPlugin](../src/ConsulBackendPlugin.groovy) 2 | 3 | Enable this plugin to store state in Consul. 4 | 5 | Terraform state can be stored in consul with the following configuration: 6 | 7 | ``` 8 | # main.tf 9 | 10 | terraform { 11 | backend "consul" { } 12 | } 13 | ``` 14 | 15 | See: https://www.terraform.io/docs/backends/types/consul.html 16 | 17 | The configuration above still requires you to tell Consul the path where environment states should be managed, but hardcoding that value into your terraform template prevents you from reusing the same template across all your environments. Ideally, you would provide a variable for the path for each environment, but terraform treats backend configuration as special, and you can't use normal variables. Instead terraform provides a separate `-backend-config` flag for `terraform init` to configure different backends (See: https://www.terraform.io/docs/backends/config.html#partial-configuration). 18 | 19 | By enabling this plugin, each environment state will automatically be given a unique consul path to store state, in the form `-backend-config=path=terraform///`. 20 | 21 | ``` 22 | // Jenkinsfile 23 | @Library(['terraform-pipeline@v5.0']) _ 24 | 25 | Jenkinsfile.init(this) 26 | 27 | ConsulBackendPlugin.init() 28 | 29 | def validate = new TerraformValidateStage() 30 | 31 | // terraform init -backend-config=path=///qa 32 | def deployQa = new TerraformEnvironmentStage('qa') 33 | 34 | // terraform init -backend-config=path=///uat 35 | def deployUat = new TerraformEnvironmentStage('uat') 36 | 37 | // terraform init -backend-config=path=///prod 38 | def deployProd = new TerraformEnvironmentStage('prod') 39 | 40 | validate.then(deployQA) 41 | .then(deployUat) 42 | .then(deployProd) 43 | .build() 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/CredentialsPlugin.md: -------------------------------------------------------------------------------- 1 | ## [CredentialsPlugin](../src/CredentialsPlugin.groovy) 2 | 3 | Enable this plugin to inject credentials into your stages using the [Jenkins Credentials Plugin](https://wiki.jenkins.io/display/JENKINS/Credentials+Plugin). 4 | 5 | One-time setup: 6 | * Install the [Jenkins Credentials Binding Plugin](https://www.jenkins.io/doc/pipeline/steps/credentials-binding/) on your Jenkins server. 7 | * Define a credential that you want to inject. 8 | 9 | Add any number of credentials bindings that you want to wrap your stages, with `withBinding`. Each call to this method will cumulatively add more credentials. See the [Credentials Binding Plugin homepage](https://www.jenkins.io/doc/pipeline/steps/credentials-binding/) for the list of supported bindings. 10 | 11 | ``` 12 | // Jenkinsfile 13 | @Library(['terraform-pipeline@v5.0']) _ 14 | 15 | Jenkinsfile.init(this) 16 | 17 | // Add credentials to all Stages from usernamePassword, usernameColonPassword, and string credentials 18 | CredentialsPlugin.withBinding { usernamePassword(credentialsId: 'my-user-pass', usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD') } 19 | .withBinding { usernameColonPassword(credentialsId: 'my-user-colon-pass', variable: 'USERPASS') } 20 | .withBinding { string(credentialsId: 'my-string-token', variable: 'TOKEN') } 21 | .init() 22 | 23 | def validate = new TerraformValidateStage() 24 | def build = new BuildStage() 25 | def deployQa = new TerraformEnvironmentStage('qa') 26 | def testQa = new RegressionStage() 27 | def deployUat = new TerraformEnvironmentStage('uat') 28 | def deployProd = new TerraformEnvironmentStage('prod') 29 | 30 | validate.then(build) 31 | .then(deployQa) 32 | .then(testQa) 33 | .then(deployUat) 34 | .then(deployProd) 35 | .build() 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/DefaultEnvironmentPlugin.md: -------------------------------------------------------------------------------- 1 | ## [DefaultEnvironmentPlugin](../src/DefaultEnvironmentPlugin.groovy) 2 | 3 | This plugin is enabled by default. 4 | 5 | Provides a terraform variable `environment` to all TerraformEnvironmentStages by default, by setting an environment variable in the form `TF_VAR_environment`. The value of `environment` is the same as the environment value that you passed when creating an instance of the TerraformEnvironmentStage. 6 | 7 | -------------------------------------------------------------------------------- /docs/DestroyPlugin.md: -------------------------------------------------------------------------------- 1 | ## [DestroyPlugin](../src/DestroyPlugin.groovy) 2 | 3 | Enable this plugin to use `terraform destroy` to destroy your environment(s). 4 | 5 | When this plugin is enabled, the pipeline will follow these steps: 6 | 1. Run a `terraform plan -destroy` to display which resources will get destroyed. 7 | 2. Ask for human confirmation to proceed with the destroy. 8 | 3. Run the `terraform destroy` command. 9 | 10 | 11 | ``` 12 | // Jenkinsfile 13 | @Library(['terraform-pipeline@v3.10']) _ 14 | 15 | Jenkinsfile.init(this, env) 16 | 17 | // This enables the destroy functionality 18 | DestroyPlugin.init() 19 | 20 | def validate = new TerraformValidateStage() 21 | 22 | def destroyQa = new TerraformEnvironmentStage('qa') 23 | def destroyUat = new TerraformEnvironmentStage('uat') 24 | def destroyProd = new TerraformEnvironmentStage('prod') 25 | 26 | validate.then(destroyQa) 27 | .then(destroyUat) 28 | .then(destroyProd) 29 | .build() 30 | ``` 31 | 32 | When using this plugin, your pipeline will look something like this: 33 | 34 | ![DestroyPluginPipeline](../images/destroy-pipeline.png) 35 | 36 | ## Adding arguments to the destroy command 37 | 38 | You can use `withArgument("-some-arg")` to add arguments to the `terraform destroy` command. 39 | ``` 40 | // Jenkinsfile 41 | @Library(['terraform-pipeline@v3.10']) _ 42 | 43 | Jenkinsfile.init(this, env) 44 | 45 | // This enables the destroy functionality 46 | // Set refresh to false for destroy command 47 | DestroyPlugin.withArgument("-refresh=false").init() 48 | 49 | ... 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/FileParametersPlugin.md: -------------------------------------------------------------------------------- 1 | ## [FileParametersPlugin](../src/FileParametersPlugin.groovy) 2 | 3 | Enable this plugin to inject variables from a local properties file. 4 | 5 | The properties file should have variables in the form `KEY=VALUE`, with each variable on its own line. Values can reference other existing environment variables defined elsewhere, using the [Jenkinsfile `env` variable](https://jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables), and [Groovy string interpolation](http://docs.groovy-lang.org/latest/html/documentation/#_string_interpolation). Eg: `DATABASE_URL=${env.QA_DATABASE_URL}`. 6 | 7 | You should not store sensitive data in these properties files. Instead, sensitive data should be stored with the [CredentialsPlugin](./src/CredentialsPlugin.groovy), [ParameterStoreBuildWrapperPlugin](#parameterstorebuildwrapperplugin) with encryption, or any other tool that supports safe storage. 8 | 9 | ``` 10 | // Jenkinsfile 11 | @Library(['terraform-pipeline@v5.0']) _ 12 | 13 | Jenkinsfile.init(this) 14 | 15 | FileParametersPlugin.init() // Enable FileParametersPlugin 16 | 17 | def validate = new TerraformValidateStage() 18 | 19 | // Inject all parameters in qa.properties 20 | def deployQA = new TerraformEnvironmentStage('qa') 21 | 22 | // Inject all parameters in uat.properties 23 | def deployUat = new TerraformEnvironmentStage('uat') 24 | 25 | // Inject all parameters in prod.properties 26 | def deployProd = new TerraformEnvironmentStage('prod') 27 | 28 | validate.then(deployQA) 29 | .then(deployUat) 30 | .then(deployProd) 31 | .build() 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/GithubPRPlanPlugin.md: -------------------------------------------------------------------------------- 1 | ## [GithubPRPlanPlugin](../src/GithubPRPlanPlugin.groovy) 2 | 3 | Use this to post Terraform plan results in the comments of a PR. 4 | 5 | One-Time Setup: 6 | * Your pipeline should be configured as a Multibranch Pipeline with Github Repository HTTPS URL. 7 | * Create a Github Personal Access Token that has permission to comment on your repo. By default, the a Github Personal Access Token is expected to be available in an environment variable `GITHUB_TOKEN`. A number of [credential and configuration management plugins](https://github.com/manheim/terraform-pipeline#credentials-and-configuration-management) are available to do this. Optionally, your Github Authentication token can be assigned to a different environment variable using `.withGithubTokenEnvVar()` 8 | 9 | ``` 10 | @Library(['terraform-pipeline']) _ 11 | 12 | Jenkinsfile.init(this) 13 | 14 | // A GITHUB_TOKEN environment variable should contain your Github PAT 15 | GithubPRPlanPlugin.init() 16 | 17 | // After creating a PullRequest, the plan results for each 18 | // environment are posted as a comment to the PullRequest. 19 | def validate = new TerraformValidateStage() 20 | def deployQa = new TerraformEnvironmentStage('qa') 21 | def deployUat = new TerraformEnvironmentStage('uat') 22 | def deployProd = new TerraformEnvironmentStage('prod') 23 | 24 | validate.then(deployQa) 25 | .then(deployUat) 26 | .then(deployProd) 27 | .build() 28 | ``` 29 | 30 | ## Changing Repo Host 31 | 32 | By default, this plugin will examime the SCM URL configured for a given pipeline to determine how to post comments. 33 | If you wish to change the repo host url, use the `withRepoHost()` option. 34 | 35 | Example: 36 | ``` 37 | ... 38 | 39 | // Change the repo host 40 | GithubPRPlanPlugin.withRepoHost("https://mygithubenterprise.company.com") 41 | .init() 42 | ... 43 | ~`` -------------------------------------------------------------------------------- /docs/ParameterStoreBuildWrapperPlugin.md: -------------------------------------------------------------------------------- 1 | ## [ParameterStoreBuildWrapperPlugin](../src/ParameterStoreBuildWrapperPlugin.groovy) 2 | 3 | Enable this plugin to inject variables using the [AWS Parameter Store Build Wrapper Plugin](https://plugins.jenkins.io/aws-parameter-store) 4 | 5 | One-time step: 6 | * Install the AWS Parameter Store Build Wrapper Plugin on your Jenkins server 7 | * For cross-account deployments, create an AWS Credential with the id '<ENVIRONMENT>_PARAMETER_STORE_ACCESS' that provides access to ParameterStore for that account. 8 | 9 | By default, parameters will be retrieved from the ParameterStore path constructed from your project's Git Organization, Git Repository name, and environment. Eg: If my terraform project were at https://github.com/Manheim/fake-terraform-project, then my 'qa' environment would receive parameters from the ParameterStore path '/Manheim/fake-terraform-project/qa'. 10 | 11 | ``` 12 | // Jenkinsfile 13 | @Library(['terraform-pipeline@v5.0']) _ 14 | 15 | Jenkinsfile.init(this) 16 | 17 | ParameterStoreBuildWrapperPlugin.init() // Enable ParameterStoreBuildWrapperPlugin 18 | 19 | def validate = new TerraformValidateStage() 20 | 21 | // Inject all parameters in ///qa 22 | def deployQA = new TerraformEnvironmentStage('qa') 23 | 24 | // Inject all parameters in ///uat 25 | def deployUat = new TerraformEnvironmentStage('uat') 26 | 27 | // Inject all parameters in ///prod 28 | def deployProd = new TerraformEnvironmentStage('prod') 29 | 30 | validate.then(deployQA) 31 | .then(deployUat) 32 | .then(deployProd) 33 | .build() 34 | ``` 35 | 36 | Optionally, you can override the default ParameterStore path with your own custom pattern. 37 | 38 | ``` 39 | // Jenkinsfile 40 | @Library(['terraform-pipeline@v5.0']) _ 41 | 42 | Jenkinsfile.init(this) 43 | 44 | // Enable ParameterStoreBuildWrapperPlugin with a custom path pattern 45 | ParameterStoreBuildWrapperPlugin.withPathPattern { options -> "/${options['organization']}/${options['environment']}/${options['repoName']}" } 46 | .init() 47 | 48 | def validate = new TerraformValidateStage() 49 | 50 | // Inject all parameters in //qa/ 51 | def deployQA = new TerraformEnvironmentStage('qa') 52 | 53 | // Inject all parameters in //uat/ 54 | def deployUat = new TerraformEnvironmentStage('uat') 55 | 56 | // Inject all parameters in //prod/ 57 | def deployProd = new TerraformEnvironmentStage('prod') 58 | 59 | validate.then(deployQA) 60 | .then(deployUat) 61 | .then(deployProd) 62 | .build() 63 | ``` 64 | 65 | 66 | You can also optionally support Global Parameters applied to the following stages `VALIDATE`, `PLAN`, `APPLY` 67 | 68 | 69 | ``` 70 | // Jenkinsfile 71 | @Library(['terraform-pipeline@v5.0']) _ 72 | 73 | Jenkinsfile.init(this) 74 | 75 | ParameterStoreBuildWrapperPlugin.withGlobalParameter('/somePath/') // get all keys under `/somePath/` 76 | .withGlobalParameter('/someOtherPath/', [naming: 'relative', recursive: true]) // get all keys recursively under `/someOtherPath/` 77 | .init() // Enable ParameterStoreBuildWrapperPlugin 78 | 79 | def validate = new TerraformValidateStage() 80 | 81 | // Inject all parameters in ///qa 82 | def deployQA = new TerraformEnvironmentStage('qa') 83 | 84 | // Inject all parameters in ///uat 85 | def deployUat = new TerraformEnvironmentStage('uat') 86 | 87 | // Inject all parameters in ///prod 88 | def deployProd = new TerraformEnvironmentStage('prod') 89 | 90 | validate.then(deployQA) 91 | .then(deployUat) 92 | .then(deployProd) 93 | .build() 94 | ``` -------------------------------------------------------------------------------- /docs/ParameterStoreExecPlugin.md: -------------------------------------------------------------------------------- 1 | ## [ParameterStoreExecPlugin](../src/ParameterStoreExecPlugin.groovy) 2 | 3 | Enable this plugin to inject variables from AWS ParameterStore using [parameter-store-exec](https://github.com/cultureamp/parameter-store-exec). 4 | 5 | Prefer using the [ParameterStoreBuildWrapperPlugin](#ParameterStoreBuildWrapperPlugin) above if possible. 6 | 7 | One-time setup: 8 | * Install parameter-store-exec on your Jenkins slaves. 9 | 10 | By default, parameters will be retrieved from the ParameterStore path constructed from your project's Git Organization, Git Repository name, and environment. Eg: If my terraform project were at https://github.com/Manheim/fake-terraform-project, then my 'qa' environment would receive parameters from the ParameterStore path '/Manheim/fake-terraform-project/qa'. 11 | 12 | ``` 13 | // Jenkinsfile 14 | @Library(['terraform-pipeline@v']) _ 15 | 16 | Jenkinsfile.init(this) 17 | 18 | ParameterStoreExecPlugin.init() // Enable ParameterStoreExecPlugin 19 | 20 | def validate = new TerraformValidateStage() 21 | 22 | // Inject all parameters in ///qa with parameter-store-exec 23 | def deployQA = new TerraformEnvironmentStage('qa') 24 | 25 | // Inject all parameters in ///uat with parameter-store-exec 26 | def deployUat = new TerraformEnvironmentStage('uat') 27 | 28 | // Inject all parameters in ///prod with parameter-store-exec 29 | def deployProd = new TerraformEnvironmentStage('prod') 30 | 31 | validate.then(deployQA) 32 | .then(deployUat) 33 | .then(deployProd) 34 | .build() 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/PassPlanFilePlugin.md: -------------------------------------------------------------------------------- 1 | ## [PassPlanFilePlugin](../src/PassPlanFilePlugin.groovy) 2 | 3 | Enable this plugin to pass the plan file output to `terraform apply`. 4 | 5 | This plugin stashes the plan file during the `plan` step. 6 | When `apply` is called, the plan file is unstashed and passed as an argument. 7 | 8 | For stash and unstash commands, you can either specify a directory when initializing the plugin, or it will default to the `./` directory. 9 | 10 | ``` 11 | // Jenkinsfile 12 | @Library(['terraform-pipeline@v3.10']) _ 13 | 14 | Jenkinsfile.init(this, env) 15 | 16 | // Pass the plan file to 'terraform apply' 17 | PassPlanFilePlugin.init() 18 | 19 | def validate = new TerraformValidateStage() 20 | 21 | def destroyQa = new TerraformEnvironmentStage('qa') 22 | def destroyUat = new TerraformEnvironmentStage('uat') 23 | def destroyProd = new TerraformEnvironmentStage('prod') 24 | 25 | validate.then(destroyQa) 26 | .then(destroyUat) 27 | .then(destroyProd) 28 | .build() 29 | ``` 30 | 31 | ### TerraformDirectoryPlugin Changes 32 | 33 | If you are using the TerraformDirectoryPlugin, you must specify the same directory to support stash and unstash. 34 | 35 | ``` 36 | // Jenkinsfile 37 | @Library(['terraform-pipeline@v3.10']) _ 38 | 39 | Jenkinsfile.init(this, env) 40 | 41 | // When using TerraformDirectoryPlugin, 42 | // Pass the plan file to 'terraform apply' using withDirectory 43 | PassPlanFilePlugin.withDirectory('./tf/').init() 44 | TerraformDirectoryPlugin.withDirectory('./tf/').init() 45 | 46 | 47 | def validate = new TerraformValidateStage() 48 | 49 | def destroyQa = new TerraformEnvironmentStage('qa') 50 | def destroyUat = new TerraformEnvironmentStage('uat') 51 | def destroyProd = new TerraformEnvironmentStage('prod') 52 | 53 | validate.then(destroyQa) 54 | .then(destroyUat) 55 | .then(destroyProd) 56 | .build() 57 | ``` 58 | -------------------------------------------------------------------------------- /docs/PlanOnlyPlugin.md: -------------------------------------------------------------------------------- 1 | ## [PlanOnlyPlugin](../src/PlanOnlyPlugin.groovy) 2 | 3 | Enable this plugin to add a parameter to the build which will restrict pipeline functionality to `terraform plan` only. 4 | 5 | ``` 6 | // Jenkinsfile 7 | @Library(['terraform-pipeline@v3.10']) _ 8 | 9 | Jenkinsfile.init(this, env) 10 | 11 | // This enables the "plan only" functionality 12 | PlanOnlyPlugin.init() 13 | 14 | def validate = new TerraformValidateStage() 15 | 16 | def destroyQa = new TerraformEnvironmentStage('qa') 17 | def destroyUat = new TerraformEnvironmentStage('uat') 18 | def destroyProd = new TerraformEnvironmentStage('prod') 19 | 20 | validate.then(destroyQa) 21 | .then(destroyUat) 22 | .then(destroyProd) 23 | .build() 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/RegressionStage.md: -------------------------------------------------------------------------------- 1 | # RegressionStage 2 | 3 | Some pipelines dealing with application code may need to run automation tests. The RegressionStage can be used to accommodate this. Create a RegressionStage then add it to your other linked stages in the appropriate position. Below is an example for running tests after a QA deployment: 4 | ``` 5 | // Jenkinsfile 6 | ... 7 | def testQa = new RegressionStage() 8 | 9 | validate.then(deployQa) 10 | .then(testQa) 11 | ... 12 | ``` 13 | 14 | By default, the RegressionStage will execute a `./bin/test.sh` script. If you wish to use a different path or script name, you can do so by passing it into the RegressionStage constructor: 15 | ``` 16 | // Jenkinsfile 17 | ... 18 | def testQa = new RegressionStage('./some/other/location/some_other_test.sh') 19 | 20 | validate.then(deployQa) 21 | .then(testQa) 22 | ... 23 | ``` 24 | 25 | If your test suite lives outside of the code, you can call `withScm()` and pass it the git URL of your automation test: 26 | ``` 27 | // Jenkinsfile 28 | ... 29 | def testQa = new RegressionStage().withScm('git@SomeHost:SomeTeam/SomeAutomationRepository.git') 30 | validate.then(deployQa) 31 | .then(testQa) 32 | ... 33 | 34 | ``` 35 | 36 | There could be instances where multiple repositories are required to execute automation tests (such as needing to clone application code as well as automation code). When calling `withScm()` more than once, the repositories become cumulative and it will check out all repositories into their respective subdirectories (named by the project name referenced by the git url). Below is an example of a scenario where the application repository and automation repository are both required. The test script to execute is also located in a subdirectory which we can use `changeDirectory()` so that we are in the appropriate directory. 37 | 38 | ``` 39 | // Jenkinsfile 40 | ... 41 | def testQa = new RegressionStage().withScm('git@SomeHost:SomeTeam/SomeAutomationRepository.git') 42 | .withScm('git@SomeHost:SomeTeam/SomeApplicationRepository.git') 43 | .changeDirectory('SomeAutomationRespository') 44 | validate.then(deployQa) 45 | .then(testQa) 46 | ... 47 | ``` 48 | The above example will check out `git@SomeHost:SomeTeam/SomeAutomationRepository.git` into a subdirectory named `SomeAutomationRepository` and `git@SomeHost:SomeTeam/SomeApplicationRepository.git` into a subdirectory named `SomeApplicationRepository`. 49 | 50 | Note that if withScm is only called once, there will not be a subdirectory and it will check out the repository at the root. 51 | 52 | -------------------------------------------------------------------------------- /docs/TargetPlugin.md: -------------------------------------------------------------------------------- 1 | ## [TargetPlugin](../src/TargetPlugin.groovy) 2 | 3 | Enable this plugin to run plan/apply on selective resource targets 4 | 5 | ``` 6 | // Jenkinsfile 7 | @Library(['terraform-pipeline']) _ 8 | 9 | Jenkinsfile.init(this, env) 10 | TargetPlugin.init() // Optionally limit plan/apply to specific targets 11 | 12 | 13 | def validate = new TerraformValidateStage() 14 | 15 | def deployQa = new TerraformEnvironmentStage('qa') 16 | def deployUat = new TerraformEnvironmentStage('uat') 17 | def deployProd = new TerraformEnvironmentStage('prod') 18 | 19 | 20 | // Pipeline can now be built with "Build with Parameters" 21 | // New 'target' option in parameter list can limit plan/apply to specific targets 22 | // By default, builds should skip the 'target' option, and build as-normal 23 | validate.then(deployQa) 24 | .then(deployUat) 25 | .then(deployProd) 26 | .build() 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/TerraformDirectoryPlugin.md: -------------------------------------------------------------------------------- 1 | ## [TerraformDirectoryPlugin](../src/TerraformDirectoryPlugin.groovy) 2 | 3 | This plugin allows Terraform to run in a specific directory so that the number of files at the root of any given project can be limited. 4 | 5 | It works by appending `-chdir=` before any Terraform sub-command (`init`, `plan`, etc.) run by terraform-pipeline. You can either specify a directory when initializing the plugin, or it will default to the `./terraform/` directory. 6 | 7 | ### Terraform Version Notes 8 | Previous versions of this plugin would append the directory to the end of the Terraform commands. This has been removed as of Terraform 0.15 and replaced with the `-chdir` argument. 9 | 10 | ``` 11 | // Jenkinsfile 12 | @Library(['terraform-pipeline@v5.0']) _ 13 | 14 | Jenkinsfile.init(this) 15 | 16 | // Using withDirectory to initialize here would cause all terraform 17 | // commands to run in the ./xyz/ directory 18 | TerraformDirectoryPlugin.withDirectory('./xyz/').init() 19 | 20 | def validate = new TerraformValidateStage() 21 | 22 | def deployQA = new TerraformEnvironmentStage('qa') 23 | def deployUat = new TerraformEnvironmentStage('uat') 24 | def deployProd = new TerraformEnvironmentStage('prod') 25 | 26 | validate.then(deployQA) 27 | .then(deployUat) 28 | .then(deployProd) 29 | .build() 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/TerraformEnvironmentStage.md: -------------------------------------------------------------------------------- 1 | ## [TerraformEnvironmentStage](../src/TerraformEnvironmentStage.groovy) 2 | 3 | ### Pass in Global Environment variable 4 | 5 | You can pass in global environment variables too all stages in your pipeline using `withGlobalEnv` 6 | 7 | ``` 8 | // Jenkinsfile 9 | @Library(['terraform-pipeline']) _ 10 | 11 | Jenkinsfile.init(this) 12 | 13 | TerraformEnvironmentStage.withGlobalEnv('KEY_01', 'VALUE_01') 14 | .withGlobalEnv('KEY_01', 'VALUE_02') 15 | 16 | def validate = new TerraformValidateStage() 17 | def deployQA = new TerraformEnvironmentStage('qa') 18 | def deployProd = new TerraformEnvironmentStage('prod') 19 | 20 | validate.then(deployQA) 21 | .then(deployProd) 22 | .build() 23 | ``` 24 | 25 | ### Pass in Stage Specific Environment variable 26 | 27 | You can pass in stage specific environment variables each stage in your pipeline using `withEnv` 28 | 29 | ``` 30 | // Jenkinsfile 31 | @Library(['terraform-pipeline']) _ 32 | 33 | Jenkinsfile.init(this) 34 | 35 | def validate = new TerraformValidateStage() 36 | def deployQA = new TerraformEnvironmentStage('qa').withEnv('KEY_01', 'VALUE_01') 37 | .withEnv('KEY_02', 'VALUE_02') 38 | def deployProd = new TerraformEnvironmentStage('prod').withEnv('KEY_03', 'VALUE_03') 39 | .withEnv('KEY_04', 'VALUE_04') 40 | 41 | validate.then(deployQA) 42 | .then(deployProd) 43 | .build() 44 | ``` 45 | 46 | ### For Plugin Developers 47 | 48 | #### Decorate Stages/Commands or Hook before/after command execution 49 | 50 | TerraformEnvironmentStage has support for numerous decorators, to allow wrapping the various stages, steps, and individual commands. Please see the [source code](../src/TerraformEnvironmentStage.groovy) for details. 51 | -------------------------------------------------------------------------------- /docs/TerraformEnvironmentStageShellHookPlugin.md: -------------------------------------------------------------------------------- 1 | ## [TerraformEnvironmentStageShellHookPlugin](../src/TerraformEnvironmentStageShellHookPlugin.groovy) 2 | 3 | This plugin allows inserting shell script hooks at various points in the TerraformEnvironmentStage execution. Please see the [TerraformEnvironmentStage source code](../src/TerraformEnvironmentStage.groovy) for a list of the supported hook points. At this time, they are: 4 | 5 | * ``TerraformEnvironmentStage.ALL`` 6 | * ``TerraformEnvironmentStage.INIT_COMMAND`` 7 | * ``TerraformEnvironmentStage.PLAN`` 8 | * ``TerraformEnvironmentStage.PLAN_COMMAND`` 9 | * ``TerraformEnvironmentStage.APPLY`` 10 | * ``TerraformEnvironmentStage.APPLY_COMMAND`` 11 | 12 | Each hook point "wraps" various parts of the Stage, and supports a total of four hooks: 13 | 14 | * Before the wrapped code runs (``WhenToRun.BEFORE``) 15 | * **The default,** after the wrapped code runs successfully (``WhenToRun.ON_SUCCESS``) 16 | * After the wrapped code runs and fails (``WhenToRun.ON_FAILURE``) 17 | * After the wrapped code runs, regardless of success or failure (``WhenToRun.AFTER``) 18 | 19 | ``` 20 | // Jenkinsfile 21 | @Library(['terraform-pipeline']) _ 22 | 23 | Jenkinsfile.init(this) 24 | 25 | TerraformEnvironmentStageShellHookPlugin.withHook(TerraformEnvironmentStage.APPLY_COMMAND, './bin/after_successful_apply.sh') 26 | .withHook(TerraformEnvironmentStage.INIT_COMMAND, './bin/download_deps.sh', WhenToRun.BEFORE) 27 | .withHook(TerraformEnvironmentStage.ALL, './bin/cleanup.sh', WhenToRun.AFTER) 28 | .init() 29 | 30 | def validate = new TerraformValidateStage() 31 | 32 | def deployQA = new TerraformEnvironmentStage('qa') 33 | def deployUat = new TerraformEnvironmentStage('uat') 34 | def deployProd = new TerraformEnvironmentStage('prod') 35 | 36 | validate.then(deployQA) 37 | .then(deployUat) 38 | .then(deployProd) 39 | .build() 40 | ``` 41 | 42 | **Reminder:** Please remember that plugin definition order matters. You most likely want this plugin to be defined in your Jenkinsfile _before_ any plugins that affect the environment. 43 | -------------------------------------------------------------------------------- /docs/TerraformImportPlugin.md: -------------------------------------------------------------------------------- 1 | ## [TerraformImportPlugin](../src/TerraformImportPlugin.groovy) 2 | 3 | Enable this plugin to change pipeline functionality. This plugin will import a 4 | resource into state, and adds two new job parameters. 5 | 6 | * `IMPORT_RESOURCE`: This is the resource identifier to import into state. 7 | * `IMPORT_TARGET_PATH`: This is the Terraform state path into which the 8 | resource should be imported. 9 | * `IMPORT_ENVIRONMENT`: This should be set to the terraform-pipeline 10 | environment stage that should perform the import. 11 | 12 | ``` 13 | // Jenkinsfile 14 | @Library(['terraform-pipeline@v3.10']) _ 15 | 16 | Jenkinsfile.init(this, env) 17 | 18 | // This enables the "import" functionality 19 | TerraformImportPlugin.init() 20 | 21 | def validate = new TerraformValidateStage() 22 | 23 | def qa = new TerraformEnvironmentStage('qa') 24 | def uat = new TerraformEnvironmentStage('uat') 25 | def prod = new TerraformEnvironmentStage('prod') 26 | 27 | validate.then(qa) 28 | .then(uat) 29 | .then(prod) 30 | .build() 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/TerraformLandscapePlugin.md: -------------------------------------------------------------------------------- 1 | ## [TerraformLandscapePlugin](../src/TerraformLandscapePlugin.groovy) 2 | 3 | Enable this plugin to improve Terraform's plan output commands with the [terraform-landscape](https://github.com/coinbase/terraform-landscape) gem. 4 | 5 | One-time setup: 6 | * Install the terraform-landscape gem on your Jenkins slaves. 7 | 8 | Requirements: 9 | * Enable AnsiColorPlugin for colors in Jenkins 10 | 11 | ``` 12 | // Jenkinsfile 13 | @Library(['terraform-pipeline@v3.10']) _ 14 | 15 | Jenkinsfile.init(this, env) 16 | 17 | AnsiColorPlugin.init() // REQUIRED: Decorate your TerraformEnvironmentStages with the AnsiColor plugin 18 | TerraformLandscapePlugin.init() // Use the Terraform Landscape gem to format plan output 19 | 20 | def validate = new TerraformValidateStage() 21 | 22 | def deployQa = new TerraformEnvironmentStage('qa') 23 | def deployUat = new TerraformEnvironmentStage('uat') 24 | def deployProd = new TerraformEnvironmentStage('prod') 25 | 26 | validate.then(deployQa) 27 | .then(deployUat) 28 | .then(deployProd) 29 | .build() 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/TerraformOutputOnlyPlugin.md: -------------------------------------------------------------------------------- 1 | ## [TerraformOutputOnlyPlugin](../src/TerraformOutputOnlyPlugin.groovy) 2 | 3 | Enable this plugin to change pipeline functionality. This plugin will skip the plan and apply stages and add three new job parameters. 4 | 5 | * `SHOW_OUTPUTS_ONLY`: This configures the job to skip execution of the plan and apply terraform commands. The job will perform the INIT stage and immediately perform `terraform output`. Unless this option is checked, the following options will have no effect. 6 | * `JSON_FORMAT_OUTPUTS`: This will instruct the plugin to display the output in JSON format. 7 | * `REDIRECT_OUTPUTS_TO_FILE`: Text entered into this option will be used to redirect the result of `terraform output` to a file in the current workspace. The filename should be relative to the workspace, and directories will NOT be created so they should exist beforehand. 8 | 9 | ``` 10 | // Jenkinsfile 11 | @Library(['terraform-pipeline@v3.10']) _ 12 | 13 | Jenkinsfile.init(this, env) 14 | 15 | // This enables the "output only" functionality 16 | TerraformOutputOnlyPlugin.init() 17 | 18 | def validate = new TerraformValidateStage() 19 | 20 | def destroyQa = new TerraformEnvironmentStage('qa') 21 | def destroyUat = new TerraformEnvironmentStage('uat') 22 | def destroyProd = new TerraformEnvironmentStage('prod') 23 | 24 | validate.then(destroyQa) 25 | .then(destroyUat) 26 | .then(destroyProd) 27 | .build() 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/TerraformPlugin.md: -------------------------------------------------------------------------------- 1 | ## [TerraformPlugin](../src/TerraformPlugin.groovy) 2 | 3 | This plugin provides a code point for the library to apply different arguments 4 | and behaviors based on your expected terraform version. 5 | 6 | ### Usage 7 | 8 | This is a default plugin that is automatically added to all required commands 9 | and stages. As such, there is no `init()` method to call on the class. 10 | 11 | Instead, place a `.terraform-version` file in the root of your repository 12 | containing the version of terraform you require. 13 | 14 | If this is not possible, you may declare the version in your Jenkinsfile as 15 | follows: 16 | 17 | ``` 18 | // Jenkinsfile 19 | 20 | Jenkinsfile.init(this) 21 | 22 | TerraformPlugin.withVersion('0.12.17') 23 | ``` 24 | 25 | ### Development 26 | 27 | When a version change occurs that breaks with previous behavior, you will need 28 | to update this plugin to account for that. The process is fairly 29 | straightforward. Please see documentation in the 30 | [source code](../src/TerraformPlugin.groovy) for the plugin for further 31 | information. 32 | -------------------------------------------------------------------------------- /docs/TerraformStartDirectoryPlugin.md: -------------------------------------------------------------------------------- 1 | ## [TerraformStartDirectoryPlugin](../src/TerraformStartDirectoryPlugin.groovy) 2 | 3 | This plugin changes the starting execution workspace directory. Commands will use this directory as current and any relative path will use it as base path. 4 | 5 | ``` 6 | // Jenkinsfile 7 | @Library(['terraform-pipeline']) _ 8 | 9 | Jenkinsfile.init(this) 10 | 11 | TerraformStartDirectoryPlugin.withDirectory('./xyz/').init() 12 | 13 | def validate = new TerraformValidateStage() 14 | 15 | def deployQA = new TerraformEnvironmentStage('qa') 16 | def deployUat = new TerraformEnvironmentStage('uat') 17 | def deployProd = new TerraformEnvironmentStage('prod') 18 | 19 | validate.then(deployQA) 20 | .then(deployUat) 21 | .then(deployProd) 22 | .build() 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/TerraformTaintPlugin.md: -------------------------------------------------------------------------------- 1 | ## [TerraformTaintPlugin](../src/TerraformTaintPlugin.groovy) 2 | 3 | Enable this plugin to add `TAINT_RESOURCE` and `UNTAINT_RESOURCE` parameters 4 | to the build. If a resource path is provided via one of those parameters, then 5 | the `terraform plan` command will be preceded by the corresponding Terraform 6 | command to taint or untaint the appropriate resources. 7 | 8 | Note that the untaint command will take precedence, so if for some reason the 9 | same resource is placed in both parameters, it will be tainted and immediately 10 | untainted, resulting in no change. 11 | 12 | There are several ways to customize where and when the taint/untaint can run: 13 | 14 | * `onBranch()`: This takes in a branch name as a parameter. This adds the 15 | branch to the list of approved branches. 16 | 17 | ``` 18 | // Jenkinsfile 19 | @Library(['terraform-pipeline@v3.10']) _ 20 | 21 | Jenkinsfile.init(this, env) 22 | 23 | // This enables the "taint" and "untaint" functionality 24 | // It will only apply to the main branch (default behavior) 25 | TerraformTaintPlugin.init() 26 | 27 | def validate = new TerraformValidateStage() 28 | 29 | def destroyQa = new TerraformEnvironmentStage('qa') 30 | def destroyUat = new TerraformEnvironmentStage('uat') 31 | def destroyProd = new TerraformEnvironmentStage('prod') 32 | 33 | validate.then(destroyQa) 34 | .then(destroyUat) 35 | .then(destroyProd) 36 | .build() 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/TfvarsFilesPlugin.md: -------------------------------------------------------------------------------- 1 | ## [TfvarsFilesPlugin](../src/TfvarsFilesPlugin.groovy) 2 | 3 | This plugin allows you to add `-var-file=${environment}.tfvars` to your plan 4 | and apply commands. It supports being configured for a directory relative to 5 | to the git root and global files being applied to every plan and apply command. 6 | 7 | For the following repository: 8 | 9 | ``` 10 | . 11 | ├── Jenkinsfile 12 | ├── main.tf 13 | ├── variables.tf 14 | └── tfvars 15 |    ├── qa.tfvars 16 |    ├── uat.tfvars 17 | ├── prod.tfvars 18 | └── global.tfvars 19 | ``` 20 | This Jenkinsfile setup will include a `-var-file` argument for both 21 | `qa.tfvars` and `global.tfvars` on the QA plan and apply commands. Similarly, 22 | the UAT commands will include `uat.tfvars` and `global.tfvars`. 23 | 24 | ``` 25 | // Jenkinsfile 26 | @Library(['terraform-pipeline@v5.3']) _ 27 | 28 | Jenkinsfile.init(this) 29 | 30 | TfvarsFilesPlugin.withDirectory('./tfvars') 31 | .withGlobalVarFile('global.tfvars') 32 | .init() 33 | 34 | def validate = new TerraformValidateStage() 35 | 36 | def deployQA = new TerraformEnvironmentStage('qa') 37 | def deployUat = new TerraformEnvironmentStage('uat') 38 | def deployProd = new TerraformEnvironmentStage('prod') 39 | 40 | validate.then(deployQA) 41 | .then(deployUat) 42 | .then(deployProd) 43 | .build() 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/ValidateFormatPlugin.md: -------------------------------------------------------------------------------- 1 | ## [ValidateFormatPlugin](../src/ValidateFormatPlugin.groovy) 2 | 3 | Enable this plugin to run `terraform fmt -check` as part of the TerraformValidateStage. If no changes are necessary, TerraformValidateStage will pass. If any format changes are necessary, the TerraformValidateStage will fail. 4 | 5 | ``` 6 | // Jenkinsfile 7 | @Library(['terraform-pipeline']) _ 8 | 9 | Jenkinsfile.init(this) 10 | 11 | ValidateFormatPlugin.init() 12 | 13 | // Runs `terraform fmt -check` in addition to `terraform validate`. 14 | // TerraformValidateStage fails if code requires validation. 15 | def validate = new TerraformValidateStage() 16 | def deployQA = new TerraformEnvironmentStage('qa') 17 | def deployUat = new TerraformEnvironmentStage('uat') 18 | def deployProd = new TerraformEnvironmentStage('prod') 19 | 20 | validate.then(deployQA) 21 | .then(deployUat) 22 | .then(deployProd) 23 | .build() 24 | ``` 25 | 26 | Additional options are available, to search directories recursively, and to display diffs. 27 | 28 | ``` 29 | // Jenkinsfile 30 | @Library(['terraform-pipeline']) _ 31 | 32 | Jenkinsfile.init(this) 33 | 34 | ValidateFormatPlugin.init() 35 | TerraformFormatCommand.withRecursive() 36 | .withDiff() 37 | 38 | // Runs `terraform fmt -check` in addition to `terraform validate`. 39 | // TerraformValidateStage fails if code requires validation. 40 | def validate = new TerraformValidateStage() 41 | def deployQA = new TerraformEnvironmentStage('qa') 42 | def deployUat = new TerraformEnvironmentStage('uat') 43 | def deployProd = new TerraformEnvironmentStage('prod') 44 | 45 | validate.then(deployQA) 46 | .then(deployUat) 47 | .then(deployProd) 48 | .build() 49 | ``` 50 | 51 | -------------------------------------------------------------------------------- /docs/WithAwsPlugin.md: -------------------------------------------------------------------------------- 1 | ## [WithAwsPlugin](../src/WithAwsPlugin.groovy) 2 | 3 | Enable this plugin to manage AWS Authentication with the [pipeline-aws-plugin](https://github.com/jenkinsci/pipeline-aws-plugin). 4 | 5 | One-time setup 6 | 7 | * Have the [pipeline-aws-plugin] installed on your Jenkins instance. 8 | * (Optional) Define an AWS_ROLE_ARN variable, or environment-specific `${env}_AWS_ROLE_ARN` 9 | 10 | Example pipeline using the WithAwsPlugin using an explicit role: 11 | 12 | ``` 13 | // Jenkinsfile 14 | @Library(['terraform-pipeline@v4.3']) _ 15 | 16 | Jenkinsfile.init(this) 17 | 18 | WithAwsPlugin.withRole(MY_ROLE_ARN).init() 19 | 20 | def validate = new TerraformValidateStage() 21 | 22 | // withAws(role: MY_ROLE_ARN) 23 | def deployQA = new TerraformEnvironmentStage('qa') 24 | 25 | // withAws(role: MY_ROLE_ARN) 26 | def deployUat = new TerraformEnvironmentStage('uat') 27 | 28 | // withAws(role: MY_ROLE_ARN) 29 | def deployProd = new TerraformEnvironmentStage('prod') 30 | 31 | validate.then(deployQa) 32 | .then(deployUat) 33 | .then(deployProd) 34 | .build() 35 | ``` 36 | 37 | Example pipeline using the WithAwsPlugin using implicit roles: 38 | 39 | ``` 40 | // Jenkinsfile 41 | @Library(['terraform-pipeline@v4.3']) _ 42 | 43 | Jenkinsfile.init(this) 44 | 45 | WithAwsPlugin.withRole().init() 46 | 47 | def validate = new TerraformValidateStage() 48 | 49 | // withAws(role: AWS_ROLE_ARN) or withAws(role: QA_AWS_ROLE_ARN), where either AWS_ROLE_ARN or QA_AWS_ROLE_ARN are defined. Nothing if neither is defined. 50 | def deployQA = new TerraformEnvironmentStage('qa') 51 | 52 | // withAws(role: AWS_ROLE_ARN) or withAws(role: UAT_AWS_ROLE_ARN), where either AWS_ROLE_ARN or UAT_AWS_ROLE_ARN are defined. Nothing if neither is defined. 53 | def deployUat = new TerraformEnvironmentStage('uat') 54 | 55 | // withAws(role: AWS_ROLE_ARN) or withAws(role: PROD_AWS_ROLE_ARN), where either AWS_ROLE_ARN or PROD_AWS_ROLE_ARN are defined. Nothing if neither is defined. 56 | def deployProd = new TerraformEnvironmentStage('prod') 57 | 58 | validate.then(deployQa) 59 | .then(deployUat) 60 | .then(deployProd) 61 | .build() 62 | ``` 63 | 64 | If you want to specify a role session duration other than the default of 1 hour (3600 seconds), you can do so by providing an integer duration to `withDuration()`: 65 | 66 | ``` 67 | WithAwsPlugin.withDuration(43200).init() 68 | ``` 69 | 70 | or, with a specific role ARN 71 | 72 | ``` 73 | WithAwsPlugin.withRole('MY_ROLE_ARN').withDuration(43200).init() 74 | ``` 75 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manheim/terraform-pipeline/0b618f10644fb16025e6cc72599b2e16e74c2646/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /images/MultibranchPipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manheim/terraform-pipeline/0b618f10644fb16025e6cc72599b2e16e74c2646/images/MultibranchPipeline.png -------------------------------------------------------------------------------- /images/NewItem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manheim/terraform-pipeline/0b618f10644fb16025e6cc72599b2e16e74c2646/images/NewItem.png -------------------------------------------------------------------------------- /images/configure-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manheim/terraform-pipeline/0b618f10644fb16025e6cc72599b2e16e74c2646/images/configure-project.png -------------------------------------------------------------------------------- /images/custom-declarative-pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manheim/terraform-pipeline/0b618f10644fb16025e6cc72599b2e16e74c2646/images/custom-declarative-pipeline.png -------------------------------------------------------------------------------- /images/declarative-pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manheim/terraform-pipeline/0b618f10644fb16025e6cc72599b2e16e74c2646/images/declarative-pipeline.png -------------------------------------------------------------------------------- /images/default-pipeline-success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manheim/terraform-pipeline/0b618f10644fb16025e6cc72599b2e16e74c2646/images/default-pipeline-success.png -------------------------------------------------------------------------------- /images/destroy-pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manheim/terraform-pipeline/0b618f10644fb16025e6cc72599b2e16e74c2646/images/destroy-pipeline.png -------------------------------------------------------------------------------- /images/import-terraform-pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manheim/terraform-pipeline/0b618f10644fb16025e6cc72599b2e16e74c2646/images/import-terraform-pipeline.png -------------------------------------------------------------------------------- /images/restart-from-stage-numbers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manheim/terraform-pipeline/0b618f10644fb16025e6cc72599b2e16e74c2646/images/restart-from-stage-numbers.png -------------------------------------------------------------------------------- /images/restart-from-stage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manheim/terraform-pipeline/0b618f10644fb16025e6cc72599b2e16e74c2646/images/restart-from-stage.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * The settings file is used to specify which projects to include in your build. 5 | * 6 | * Detailed information about configuring a multi-project build in Gradle can be found 7 | * in the user guide at https://docs.gradle.org/4.8.1/userguide/multi_project_builds.html 8 | */ 9 | 10 | rootProject.name = 'terraform-pipeline' 11 | -------------------------------------------------------------------------------- /src/AgentNodePlugin.groovy: -------------------------------------------------------------------------------- 1 | import static TerraformValidateStage.ALL 2 | import static TerraformEnvironmentStage.ALL 3 | 4 | public class AgentNodePlugin implements TerraformValidateStagePlugin, TerraformEnvironmentStagePlugin, Resettable { 5 | private static String dockerImage 6 | private static String dockerfile 7 | private static String dockerBuildOptions 8 | private static String dockerOptions 9 | 10 | AgentNodePlugin() { } 11 | 12 | public static void init() { 13 | def plugin = new AgentNodePlugin() 14 | 15 | TerraformValidateStage.addPlugin(plugin) 16 | TerraformEnvironmentStage.addPlugin(plugin) 17 | } 18 | 19 | public static withAgentDockerImage(String dockerImage) { 20 | this.dockerImage = dockerImage 21 | return this 22 | } 23 | 24 | public static withAgentDockerImageOptions(String dockerOptions) { 25 | this.dockerOptions = dockerOptions 26 | return this 27 | } 28 | 29 | public static withAgentDockerBuildOptions(String dockerBuildOptions) { 30 | this.dockerBuildOptions = dockerBuildOptions 31 | return this 32 | } 33 | 34 | public static withAgentDockerfile(String dockerfile = 'Dockerfile') { 35 | this.dockerfile = dockerfile 36 | return this 37 | } 38 | 39 | @Override 40 | public void apply(TerraformValidateStage stage) { 41 | stage.decorate(TerraformValidateStage.ALL, addAgent()) 42 | } 43 | 44 | @Override 45 | public void apply(TerraformEnvironmentStage stage) { 46 | stage.decorate(TerraformEnvironmentStage.ALL, addAgent()) 47 | } 48 | 49 | public Closure addAgent() { 50 | return { closure -> 51 | if (this.dockerImage && this.dockerfile == null) { 52 | docker.image(this.dockerImage).inside(this.dockerOptions) { 53 | closure() 54 | } 55 | } else if (this.dockerImage && this.dockerfile) { 56 | def buildCommand = "-f ${dockerfile} ." 57 | if (this.dockerBuildOptions) { 58 | buildCommand = "${this.dockerBuildOptions} ${buildCommand}" 59 | } 60 | 61 | docker.build(this.dockerImage, buildCommand.toString()).inside(this.dockerOptions) { 62 | closure() 63 | } 64 | } else { 65 | closure() 66 | } 67 | } 68 | } 69 | 70 | public static void reset() { 71 | dockerImage = null 72 | dockerfile = null 73 | dockerBuildOptions = null 74 | dockerOptions = null 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/AnsiColorPlugin.groovy: -------------------------------------------------------------------------------- 1 | import static TerraformEnvironmentStage.PLAN 2 | import static TerraformEnvironmentStage.APPLY 3 | 4 | class AnsiColorPlugin implements TerraformValidateStagePlugin, TerraformEnvironmentStagePlugin { 5 | 6 | public static void init() { 7 | TerraformValidateStage.addPlugin(new AnsiColorPlugin()) 8 | TerraformEnvironmentStage.addPlugin(new AnsiColorPlugin()) 9 | } 10 | 11 | @Override 12 | public void apply(TerraformValidateStage stage) { 13 | stage.decorate(addColor()) 14 | } 15 | 16 | @Override 17 | public void apply(TerraformEnvironmentStage stage) { 18 | stage.decorate(PLAN, addColor()) 19 | stage.decorate(APPLY, addColor()) 20 | } 21 | 22 | public Closure addColor() { 23 | return { closure -> ansiColor('xterm') { closure() } } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/AwssumePlugin.groovy: -------------------------------------------------------------------------------- 1 | class AwssumePlugin implements TerraformInitCommandPlugin, TerraformPlanCommandPlugin, TerraformApplyCommandPlugin { 2 | public static void init() { 3 | AwssumePlugin plugin = new AwssumePlugin() 4 | 5 | TerraformInitCommand.addPlugin(plugin) 6 | TerraformPlanCommand.addPlugin(plugin) 7 | TerraformApplyCommand.addPlugin(plugin) 8 | } 9 | 10 | @Override 11 | public void apply(TerraformInitCommand command) { 12 | applyToCommand(command) 13 | } 14 | 15 | @Override 16 | public void apply(TerraformPlanCommand command) { 17 | applyToCommand(command) 18 | } 19 | 20 | @Override 21 | public void apply(TerraformApplyCommand command) { 22 | applyToCommand(command) 23 | } 24 | 25 | // We lost type safety here - we should have a TerraformCommand interface 26 | private void applyToCommand(command) { 27 | String environment = command.getEnvironment() 28 | String region = getRegion(environment) 29 | String iamArn = getAwsRoleArn(environment) 30 | 31 | if (iamArn) { 32 | command.withPrefix("AWS_REGION=${region} AWS_ROLE_ARN=${iamArn} awssume") 33 | } else { 34 | println("No AWS_ROLE_ARN is set, so awssume will not be used for terraform in the ${environment} environment.") 35 | } 36 | } 37 | 38 | public String getAwsRoleArn(String environment) { 39 | String role = Jenkinsfile.instance.getEnv()['AWS_ROLE_ARN'] 40 | 41 | if (role == null) { 42 | role = Jenkinsfile.instance.getEnv()["${environment.toUpperCase()}_AWS_ROLE_ARN"] 43 | } 44 | 45 | if (role == null) { 46 | role = Jenkinsfile.instance.getEnv()["${environment}_AWS_ROLE_ARN"] 47 | } 48 | 49 | return role 50 | } 51 | 52 | public String getRegion(String environment) { 53 | String region = Jenkinsfile.instance.getEnv()['AWS_REGION'] 54 | 55 | if (region == null) { 56 | region = Jenkinsfile.instance.getEnv()["${environment.toUpperCase()}_AWS_REGION"] 57 | } 58 | 59 | if (region == null) { 60 | region = Jenkinsfile.instance.getEnv()['AWS_DEFAULT_REGION'] 61 | } 62 | 63 | return region 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/BuildGraph.groovy: -------------------------------------------------------------------------------- 1 | class BuildGraph implements Stage { 2 | private List stages 3 | 4 | public BuildGraph(Stage start) { 5 | this.stages = [start] 6 | } 7 | 8 | public Stage then(Stage nextStage) { 9 | this.stages << nextStage 10 | return this 11 | } 12 | 13 | public void build() { 14 | Jenkinsfile.build(stages) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/BuildStage.groovy: -------------------------------------------------------------------------------- 1 | import static TerraformEnvironmentStage.ALL 2 | 3 | class BuildStage implements Stage, DecoratableStage, TerraformEnvironmentStagePlugin, Resettable { 4 | private final String ARTIFACT_STASH_KEY = 'buildArtifact' 5 | 6 | public String buildCommand 7 | 8 | private String artifactIncludePattern 9 | private StageDecorations decorations 10 | private Jenkinsfile jenkinsfile 11 | 12 | private static plugins = [] 13 | 14 | public BuildStage() { 15 | this("./build.sh") 16 | } 17 | 18 | public BuildStage(String buildCommand) { 19 | this.buildCommand = buildCommand 20 | this.jenkinsfile = Jenkinsfile.instance 21 | this.decorations = new StageDecorations() 22 | } 23 | 24 | public BuildStage saveArtifact(String artifactIncludePattern) { 25 | this.artifactIncludePattern = artifactIncludePattern 26 | TerraformEnvironmentStage.addPlugin(this) 27 | return this 28 | } 29 | 30 | public Stage then(Stage nextStage) { 31 | return new BuildGraph(this).then(nextStage) 32 | } 33 | 34 | public void build() { 35 | Jenkinsfile.build(pipelineConfiguration()) 36 | } 37 | 38 | @Override 39 | public void apply(TerraformEnvironmentStage stage) { 40 | stage.decorate(ALL, unstashArtifact(ARTIFACT_STASH_KEY)) 41 | } 42 | 43 | private Closure unstashArtifact(String artifactStashKey) { 44 | return { closure -> 45 | unstash "${artifactStashKey}" 46 | closure() 47 | } 48 | } 49 | 50 | public void decorate(Closure decoration) { 51 | decorations.add(decoration) 52 | } 53 | 54 | protected Closure pipelineConfiguration() { 55 | applyPlugins() 56 | 57 | return { 58 | node(jenkinsfile.getNodeName()) { 59 | stage("build") { 60 | decorations.apply { 61 | checkout(scm) 62 | sh buildCommand 63 | if (artifactIncludePattern != null) { 64 | stash includes: artifactIncludePattern, name: ARTIFACT_STASH_KEY 65 | } 66 | } 67 | } 68 | } 69 | } 70 | } 71 | 72 | public static getPlugins() { 73 | return plugins 74 | } 75 | 76 | public static void reset() { 77 | this.plugins = [] 78 | } 79 | 80 | public static void addPlugin(plugin) { 81 | plugins << plugin 82 | } 83 | 84 | public void applyPlugins() { 85 | for (plugin in plugins) { 86 | plugin.apply(this) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/BuildStagePlugin.groovy: -------------------------------------------------------------------------------- 1 | interface BuildStagePlugin { 2 | public void apply(BuildStage stage) 3 | } 4 | -------------------------------------------------------------------------------- /src/BuildWithParametersPlugin.groovy: -------------------------------------------------------------------------------- 1 | public class BuildWithParametersPlugin implements BuildStagePlugin, 2 | TerraformValidateStagePlugin, 3 | TerraformEnvironmentStagePlugin, 4 | RegressionStagePlugin, 5 | Resettable { 6 | 7 | private static globalBuildParameters = [] 8 | private static appliedOnce = false 9 | 10 | public static void init() { 11 | def plugin = new BuildWithParametersPlugin() 12 | 13 | BuildStage.addPlugin(plugin) 14 | TerraformValidateStage.addPlugin(plugin) 15 | TerraformEnvironmentStage.addPlugin(plugin) 16 | RegressionStage.addPlugin(plugin) 17 | } 18 | 19 | @Override 20 | public void apply(BuildStage stage) { 21 | applyToAllStages(stage) 22 | } 23 | 24 | @Override 25 | public void apply(TerraformValidateStage stage) { 26 | applyToAllStages(stage) 27 | } 28 | 29 | @Override 30 | public void apply(TerraformEnvironmentStage stage) { 31 | applyToAllStages(stage) 32 | } 33 | 34 | @Override 35 | public void apply(RegressionStage stage) { 36 | applyToAllStages(stage) 37 | } 38 | 39 | private void applyToAllStages(DecoratableStage stage) { 40 | stage.decorate(addParameterToFirstStageOnly()) 41 | } 42 | 43 | public Closure addParameterToFirstStageOnly() { 44 | return { innerClosure -> 45 | if (hasParameters() && !appliedOnce) { 46 | properties([ 47 | parameters(getBuildParameters()) 48 | ]) 49 | appliedOnce = true 50 | } 51 | 52 | innerClosure() 53 | } 54 | } 55 | 56 | public static withBooleanParameter(Map options) { 57 | def optionDefaults = [ 58 | defaultValue: false, 59 | $class: 'hudson.model.BooleanParameterDefinition' 60 | ] 61 | 62 | globalBuildParameters << (optionDefaults + options) 63 | } 64 | 65 | public static withStringParameter(Map options) { 66 | def optionDefaults = [ 67 | defaultValue: '', 68 | $class: 'hudson.model.StringParameterDefinition' 69 | ] 70 | 71 | globalBuildParameters << (optionDefaults + options) 72 | } 73 | 74 | public static withParameter(Map options) { 75 | globalBuildParameters << options 76 | } 77 | 78 | public boolean hasParameters() { 79 | return !globalBuildParameters.isEmpty() 80 | } 81 | 82 | public List getBuildParameters() { 83 | return globalBuildParameters 84 | } 85 | 86 | public static reset() { 87 | globalBuildParameters = [] 88 | appliedOnce = false 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/ConditionalApplyPlugin.groovy: -------------------------------------------------------------------------------- 1 | import static TerraformEnvironmentStage.CONFIRM 2 | import static TerraformEnvironmentStage.APPLY 3 | 4 | public class ConditionalApplyPlugin implements TerraformEnvironmentStagePlugin, Resettable { 5 | 6 | private static enabled = true 7 | private static DEFAULT_BRANCHES = ['main', 'master'] 8 | private static branches = DEFAULT_BRANCHES 9 | private static environments = [] 10 | 11 | public static withApplyOnBranch(String... enabledBranches) { 12 | branches = enabledBranches.clone() 13 | return this 14 | } 15 | 16 | public static withApplyOnEnvironment(String... enabledEnvironments) { 17 | environments = enabledEnvironments.clone() 18 | return this 19 | } 20 | 21 | public static disable() { 22 | enabled = false 23 | } 24 | 25 | public static void reset() { 26 | branches = DEFAULT_BRANCHES 27 | enabled = true 28 | } 29 | 30 | @Override 31 | public void apply(TerraformEnvironmentStage stage) { 32 | stage.decorateAround(CONFIRM, onlyOnExpectedBranch(stage.getEnvironment())) 33 | stage.decorateAround(APPLY, onlyOnExpectedBranch(stage.getEnvironment())) 34 | } 35 | 36 | public Closure onlyOnExpectedBranch(String environment) { 37 | return { closure -> 38 | if (shouldApply(environment)) { 39 | closure() 40 | } else { 41 | echo "Skipping Confirm/Apply steps, based on the configuration of ConditionalApplyPlugin." 42 | } 43 | } 44 | } 45 | 46 | public boolean shouldApply(String environment) { 47 | if (!enabled) { 48 | return true 49 | } 50 | 51 | if (environments.contains(environment)) { 52 | return true 53 | } 54 | 55 | if (branches.contains(Jenkinsfile.instance.getEnv().BRANCH_NAME)) { 56 | println("Current branch '${Jenkinsfile.instance.getEnv().BRANCH_NAME}' matches expected branches '${branches}', stage branch-condition is met and will run.") 57 | return true 58 | } else if (null == Jenkinsfile.instance.getEnv().BRANCH_NAME) { 59 | println("Current branch is null - you're probably using a single-branch job which doesn't make your branch name available. Assume that apply should be enabled.") 60 | return true 61 | } 62 | 63 | return false 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/ConfirmApplyPlugin.groovy: -------------------------------------------------------------------------------- 1 | import static TerraformEnvironmentStage.CONFIRM 2 | 3 | class ConfirmApplyPlugin implements TerraformEnvironmentStagePlugin, Resettable { 4 | 5 | public static final String DEFAULT_SUBMITTER_PARAMETER = 'approver' 6 | public static parameters = [] 7 | public static confirmConditions = [] 8 | public static enabled = true 9 | public static String confirmMessage = 'Are you absolutely sure the plan above is correct, and should be IMMEDIATELY DEPLOYED via "terraform apply" on stage ${environment}?' 10 | public static String okMessage = 'Run terraform apply now' 11 | public static String submitter 12 | 13 | public static void init() { 14 | TerraformEnvironmentStage.addPlugin(new ConfirmApplyPlugin()) 15 | } 16 | 17 | @Override 18 | public void apply(TerraformEnvironmentStage stage) { 19 | if (enabled) { 20 | stage.decorate(CONFIRM, addConfirmation(stage.getEnvironment())) 21 | } 22 | } 23 | 24 | public Closure addConfirmation(String environment) { 25 | return { closure -> 26 | def userInput 27 | try { 28 | timeout(time: 15, unit: 'MINUTES') { 29 | userInput = input(getInputOptions(environment)) 30 | checkConfirmConditions(userInput, environment) 31 | } 32 | } catch (ex) { 33 | throw ex 34 | } 35 | 36 | closure() 37 | } 38 | } 39 | 40 | private Map getInputOptions(String environment) { 41 | Map inputOptions = interpolateMap([ 42 | message: confirmMessage, 43 | ok: okMessage, 44 | submitterParameter: submitter ?: DEFAULT_SUBMITTER_PARAMETER 45 | ], environment) 46 | 47 | if (!parameters.isEmpty()) { 48 | inputOptions['parameters'] = parameters.collect { item -> interpolateMap(item, environment) } 49 | } 50 | 51 | return inputOptions 52 | } 53 | 54 | public Map interpolateMap(Map input, String environment) { 55 | return input.inject([:]) { memo, key, value -> 56 | memo[key] = value.replaceAll('\\$\\{environment\\}', environment) 57 | memo 58 | } 59 | } 60 | 61 | public void checkConfirmConditions(userInput, environment) { 62 | if (confirmConditions.isEmpty()) { 63 | return 64 | } 65 | 66 | def options = [ input: userInput, environment: environment ] 67 | def hasFailures = confirmConditions.collect { condition -> condition.call(options) } 68 | .contains(false) 69 | 70 | if (hasFailures) { 71 | throw new RuntimeException('Confirmation Failed') 72 | } 73 | } 74 | 75 | public static withConfirmCondition(Closure condition) { 76 | confirmConditions << condition 77 | return this 78 | } 79 | 80 | public static withParameter(Map parameterOptions) { 81 | parameters << parameterOptions 82 | return this 83 | } 84 | 85 | public static withConfirmMessage(String newMessage) { 86 | this.confirmMessage = newMessage 87 | return this 88 | } 89 | 90 | public static withOkMessage(String newMessage) { 91 | this.okMessage = newMessage 92 | return this 93 | } 94 | 95 | public static withSubmitterParameter(String newParam) { 96 | this.submitter = newParam 97 | return this 98 | } 99 | 100 | public static disable() { 101 | this.enabled = false 102 | return this 103 | } 104 | 105 | public static enable() { 106 | this.enabled = true 107 | return this 108 | } 109 | 110 | public static reset() { 111 | this.enabled = true 112 | this.parameters = [] 113 | this.confirmConditions = [] 114 | this.submitter = null 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/ConsulBackendPlugin.groovy: -------------------------------------------------------------------------------- 1 | class ConsulBackendPlugin implements TerraformInitCommandPlugin { 2 | 3 | public static String defaultAddress 4 | public static Closure pathPattern 5 | 6 | public static void init() { 7 | ConsulBackendPlugin plugin = new ConsulBackendPlugin() 8 | 9 | TerraformInitCommand.addPlugin(plugin) 10 | } 11 | 12 | @Override 13 | public void apply(TerraformInitCommand command) { 14 | String environment = command.getEnvironment() 15 | String backendPath = getBackendPath(environment) 16 | command.withBackendConfig("path=${backendPath}") 17 | 18 | String consulAddress = getConsulAddress() 19 | if (consulAddress) { 20 | command.withBackendConfig("address=${consulAddress}") 21 | } 22 | } 23 | 24 | public String getBackendPath(String environment) { 25 | Closure backendPathPattern = pathPattern 26 | 27 | if (backendPathPattern == null) { 28 | String repoSlug = Jenkinsfile.instance.getStandardizedRepoSlug() 29 | backendPathPattern = { String env -> "terraform/${repoSlug}_${env}" } 30 | } 31 | 32 | return backendPathPattern.call(environment) 33 | } 34 | 35 | public String getConsulAddress() { 36 | return defaultAddress ?: Jenkinsfile.instance.getEnv().DEFAULT_CONSUL_ADDRESS 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/CredentialsPlugin.groovy: -------------------------------------------------------------------------------- 1 | class CredentialsPlugin implements BuildStagePlugin, RegressionStagePlugin, TerraformEnvironmentStagePlugin, TerraformValidateStagePlugin, Resettable { 2 | private static bindings = [] 3 | 4 | public static void init() { 5 | def plugin = new CredentialsPlugin() 6 | 7 | BuildStage.addPlugin(plugin) 8 | RegressionStage.addPlugin(plugin) 9 | TerraformEnvironmentStage.addPlugin(plugin) 10 | TerraformValidateStage.addPlugin(plugin) 11 | } 12 | 13 | public static withBinding(Closure binding) { 14 | bindings << binding 15 | return this 16 | } 17 | 18 | // Deprecated: Remove this with Issue #404 and the next major release 19 | public static withBuildCredentials(Map options = [:], String credentialsId) { 20 | Map optionsWithDefaults = populateDefaults(options, credentialsId) 21 | bindings << { usernamePassword(optionsWithDefaults) } 22 | return this 23 | } 24 | 25 | @Override 26 | public void apply(BuildStage buildStage) { 27 | buildStage.decorate(addBuildCredentials()) 28 | } 29 | 30 | @Override 31 | public void apply(RegressionStage regressionStage) { 32 | regressionStage.decorate(addBuildCredentials()) 33 | } 34 | 35 | @Override 36 | public void apply(TerraformEnvironmentStage environmentStage) { 37 | environmentStage.decorate(addBuildCredentials()) 38 | } 39 | 40 | @Override 41 | public void apply(TerraformValidateStage validateStage) { 42 | validateStage.decorate(addBuildCredentials()) 43 | } 44 | 45 | private addBuildCredentials() { 46 | return { innerClosure -> 47 | def workflowScript = delegate 48 | def appliedBindings = getBindings().collect { it -> it.delegate = workflowScript; it() } 49 | 50 | withCredentials(appliedBindings, innerClosure) 51 | } 52 | } 53 | 54 | public static Map populateDefaults(Map options = [:], String credentialsId) { 55 | def credentialsOptions = options.clone() 56 | credentialsOptions['credentialsId'] = credentialsId 57 | credentialsOptions['usernameVariable'] = credentialsOptions['usernameVariable'] ?: "${toEnvironmentVariable(credentialsId)}_USERNAME".toString() 58 | credentialsOptions['passwordVariable'] = credentialsOptions['passwordVariable'] ?: "${toEnvironmentVariable(credentialsId)}_PASSWORD".toString() 59 | 60 | return credentialsOptions 61 | } 62 | 63 | public static String toEnvironmentVariable(String value) { 64 | value.toUpperCase().replaceAll('-', '_') 65 | } 66 | 67 | public static getBindings() { 68 | return bindings 69 | } 70 | 71 | public static void reset() { 72 | bindings = [] 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/DecoratableStage.groovy: -------------------------------------------------------------------------------- 1 | // This is temporary, and should be pushed into Stage interface. See Issue #152 2 | interface DecoratableStage { 3 | public void decorate(Closure closure) 4 | } 5 | -------------------------------------------------------------------------------- /src/DefaultEnvironmentPlugin.groovy: -------------------------------------------------------------------------------- 1 | import static TerraformEnvironmentStage.ALL 2 | 3 | class DefaultEnvironmentPlugin implements TerraformEnvironmentStagePlugin { 4 | 5 | @Override 6 | public void apply(TerraformEnvironmentStage stage) { 7 | String environment = stage.getEnvironment() 8 | 9 | stage.decorate(ALL, addEnvironmentTerraformVariable(environment)) 10 | } 11 | 12 | public static Closure addEnvironmentTerraformVariable(String environment) { 13 | return { closure -> withEnv(["TF_VAR_environment=${environment}"]) { closure() } } 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/DestroyPlugin.groovy: -------------------------------------------------------------------------------- 1 | class DestroyPlugin implements TerraformPlanCommandPlugin, 2 | TerraformApplyCommandPlugin, 3 | Resettable { 4 | 5 | private static arguments = [] 6 | public static DESTROY_CONFIRM_MESSAGE = 'DANGER! Your ${environment} environment will be IMMEDIATELY DESTROYED via "terraform destroy". Review your plan to see all the resources that will be destroyed, and confirm. YOU CANNOT UNDO THIS.' 7 | public static DESTROY_OK_MESSAGE = "Run terraform DESTROY now" 8 | 9 | public static void init() { 10 | DestroyPlugin plugin = new DestroyPlugin() 11 | 12 | def appName = Jenkinsfile.instance.getRepoName() 13 | 14 | ConfirmApplyPlugin.withConfirmMessage(DESTROY_CONFIRM_MESSAGE) 15 | ConfirmApplyPlugin.withOkMessage(DESTROY_OK_MESSAGE) 16 | ConfirmApplyPlugin.withParameter([ 17 | $class: 'hudson.model.StringParameterDefinition', 18 | name: 'CONFIRM_DESTROY', 19 | description: "Type \"destroy ${appName} \${environment}\" to confirm and continue." 20 | ]) 21 | ConfirmApplyPlugin.withConfirmCondition(getConfirmCondition(appName)) 22 | 23 | TerraformEnvironmentStage.withStageNamePattern { options -> "${options['command']}-DESTROY-${options['environment']}" } 24 | 25 | TerraformPlanCommand.addPlugin(plugin) 26 | TerraformApplyCommand.addPlugin(plugin) 27 | } 28 | 29 | public void apply(TerraformPlanCommand command) { 30 | command.withArgument('-destroy') 31 | } 32 | 33 | public void apply(TerraformApplyCommand command) { 34 | command.withCommand('destroy') 35 | for (arg in arguments) { 36 | command.withArgument(arg) 37 | } 38 | } 39 | 40 | public static withArgument(String arg) { 41 | arguments << arg 42 | return this 43 | } 44 | 45 | public static getConfirmCondition(String appName) { 46 | return { options -> 47 | "destroy ${appName} ${options['environment']}".toString() == options['input']['CONFIRM_DESTROY'] 48 | } 49 | } 50 | 51 | public static reset() { 52 | arguments = [] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/EnvironmentVariablePlugin.groovy: -------------------------------------------------------------------------------- 1 | import static TerraformEnvironmentStage.ALL 2 | 3 | class EnvironmentVariablePlugin implements TerraformEnvironmentStagePlugin { 4 | private String key 5 | private String value 6 | 7 | @Override 8 | public void apply(TerraformEnvironmentStage stage) { 9 | stage.decorate(ALL, withEnvClosure(key, value)) 10 | } 11 | 12 | public withEnv(String key, String value) { 13 | this.key = key 14 | this.value = value 15 | } 16 | 17 | private Closure withEnvClosure(String key, String value) { 18 | return { closure -> 19 | withEnv(["${key}=${value}"]) { 20 | closure() 21 | } 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/FileParametersPlugin.groovy: -------------------------------------------------------------------------------- 1 | import static TerraformEnvironmentStage.ALL 2 | 3 | import groovy.text.StreamingTemplateEngine 4 | 5 | class FileParametersPlugin implements TerraformEnvironmentStagePlugin { 6 | public static void init() { 7 | FileParametersPlugin plugin = new FileParametersPlugin() 8 | 9 | TerraformEnvironmentStage.addPlugin(plugin) 10 | } 11 | 12 | @Override 13 | public void apply(TerraformEnvironmentStage stage) { 14 | String environment = stage.getEnvironment() 15 | 16 | stage.decorate(ALL, addEnvironmentSpecificVariables(environment)) 17 | } 18 | 19 | public Closure addEnvironmentSpecificVariables(String environment) { 20 | String environmentFilename = "${environment}.properties" 21 | 22 | return { closure -> 23 | if (fileExists(environmentFilename)) { 24 | echo "Found file: ${environmentFilename} - loading the contents as environment variables." 25 | String fileContent = readFile(environmentFilename) 26 | List variables = getVariables(fileContent) 27 | 28 | withEnv(variables) { closure() } 29 | } else { 30 | echo "No environment properties file found. Create a ${environmentFilename} file to add environment-specific variables to this stage." 31 | closure() 32 | } 33 | } 34 | } 35 | 36 | public List getVariables(String fileContent) { 37 | return fileContent.split('\\r?\\n').collect { String value -> interpolate(value) } 38 | } 39 | 40 | public String interpolate(String value) { 41 | return new StreamingTemplateEngine().createTemplate(value).make([env: getEnv()]).toString() 42 | } 43 | 44 | public getEnv() { 45 | return (Jenkinsfile.instance != null) ? Jenkinsfile.instance.getEnv() : [:] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/FlywayCommand.groovy: -------------------------------------------------------------------------------- 1 | class FlywayCommand implements Resettable { 2 | private String command 3 | private String binary = "flyway" 4 | private static String locations 5 | private static String url 6 | private static String user 7 | private static String password 8 | private static Collection additionalParameters = [] 9 | 10 | public FlywayCommand(String command) { 11 | this.command = command 12 | } 13 | 14 | public String toString() { 15 | def pieces = [] 16 | pieces << binary 17 | pieces << command 18 | 19 | if (locations) { 20 | pieces << "-locations=${locations}" 21 | } 22 | 23 | if (url) { 24 | pieces << "-url=${url}" 25 | } 26 | 27 | if (user) { 28 | pieces << "-user=${user}" 29 | } 30 | 31 | if (password) { 32 | pieces << "-password=${password}" 33 | } 34 | 35 | pieces.addAll(additionalParameters) 36 | 37 | return pieces.join(' ') 38 | } 39 | 40 | public static withUser(String user) { 41 | this.user = user 42 | return this 43 | } 44 | 45 | public static withPassword(String password) { 46 | this.password = password 47 | return this 48 | } 49 | 50 | public static withLocations(String locations) { 51 | this.locations = locations 52 | return this 53 | } 54 | 55 | public static withUrl(String url) { 56 | this.url = url 57 | return this 58 | } 59 | 60 | public static withAdditionalParameter(String parameter) { 61 | this.additionalParameters << parameter 62 | println "withAdditionalParameter: ${parameter}, ${this.additionalParameters}" 63 | return this 64 | } 65 | 66 | public static reset() { 67 | this.locations = null 68 | this.url = null 69 | this.user = null 70 | this.password = null 71 | this.additionalParameters = [] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/HookPoint.groovy: -------------------------------------------------------------------------------- 1 | class HookPoint { 2 | String runBefore = null 3 | String runAfterOnSuccess = null 4 | String runAfterOnFailure = null 5 | String runAfterAlways = null 6 | private String hookName 7 | 8 | HookPoint(String hookName) { 9 | this.hookName = hookName 10 | } 11 | 12 | public String getName() { 13 | return this.hookName 14 | } 15 | 16 | public Boolean isConfigured() { 17 | return ! (this.runBefore == null && this.runAfterOnSuccess == null && this.runAfterOnFailure == null && this.runAfterAlways == null) 18 | } 19 | 20 | public Closure getClosure() { 21 | return { closure -> 22 | try { 23 | if (this.runBefore != null) { sh this.runBefore } 24 | closure() 25 | if (this.runAfterOnSuccess != null) { sh this.runAfterOnSuccess } 26 | } catch (Exception e) { 27 | if (this.runAfterOnFailure != null) { sh this.runAfterOnFailure } 28 | throw e 29 | } finally { 30 | if (this.runAfterAlways != null) { sh this.runAfterAlways } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/ParameterStoreBuildWrapperPlugin.groovy: -------------------------------------------------------------------------------- 1 | import static TerraformValidateStage.ALL 2 | import static TerraformEnvironmentStage.PLAN 3 | import static TerraformEnvironmentStage.APPLY 4 | 5 | class ParameterStoreBuildWrapperPlugin implements TerraformValidateStagePlugin, TerraformEnvironmentStagePlugin, Resettable { 6 | private static globalPathPattern 7 | private static List globalParameterOptions = [] 8 | private static defaultPathPattern = { options -> "/${options['organization']}/${options['repoName']}/${options['environment']}/" } 9 | 10 | public static void init() { 11 | TerraformEnvironmentStage.addPlugin(new ParameterStoreBuildWrapperPlugin()) 12 | TerraformValidateStage.addPlugin(new ParameterStoreBuildWrapperPlugin()) 13 | } 14 | 15 | public static withPathPattern(Closure newPathPattern) { 16 | globalPathPattern = newPathPattern 17 | return this 18 | } 19 | 20 | public static withGlobalParameter(String path, Map options = [:]) { 21 | globalParameterOptions << [path: path] + options 22 | return this 23 | } 24 | 25 | @Override 26 | public void apply(TerraformValidateStage stage) { 27 | globalParameterOptions.each { gp -> 28 | stage.decorate(ALL, addParameterStoreBuildWrapper(gp)) 29 | } 30 | } 31 | 32 | @Override 33 | public void apply(TerraformEnvironmentStage stage) { 34 | def environment = stage.getEnvironment() 35 | List options = getParameterOptions(environment) 36 | 37 | options.each { option -> 38 | stage.decorate(PLAN, addParameterStoreBuildWrapper(option)) 39 | stage.decorate(APPLY, addParameterStoreBuildWrapper(option)) 40 | } 41 | } 42 | 43 | List getParameterOptions(String environment) { 44 | List options = [] 45 | options.add(getEnvironmentParameterOptions(environment)) 46 | options.addAll(getGlobalParameterOptions()) 47 | 48 | return options 49 | } 50 | 51 | List getGlobalParameterOptions() { 52 | return globalParameterOptions 53 | } 54 | 55 | Map getEnvironmentParameterOptions(String environment) { 56 | return [ 57 | path: pathForEnvironment(environment), 58 | credentialsId: "${environment.toUpperCase()}_PARAMETER_STORE_ACCESS" 59 | ] 60 | } 61 | 62 | String pathForEnvironment(String environment) { 63 | String organization = Jenkinsfile.instance.getOrganization() 64 | String repoName = Jenkinsfile.instance.getRepoName() 65 | def patternOptions = [ environment: environment, 66 | repoName: repoName, 67 | organization: organization ] 68 | 69 | def pathPattern = globalPathPattern ?: defaultPathPattern 70 | 71 | return pathPattern(patternOptions) 72 | } 73 | 74 | public Closure addParameterStoreBuildWrapper(Map options = [:]) { 75 | def Map defaultOptions = [ 76 | naming: 'basename' 77 | ] 78 | 79 | def parameterStoreOptions = defaultOptions + options 80 | 81 | return { closure -> 82 | withAWSParameterStore(parameterStoreOptions) { 83 | closure() 84 | } 85 | } 86 | } 87 | 88 | public static reset() { 89 | globalPathPattern = null 90 | globalParameterOptions = [] 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/ParameterStoreExecPlugin.groovy: -------------------------------------------------------------------------------- 1 | import static TerraformEnvironmentStage.PLAN 2 | import static TerraformEnvironmentStage.APPLY 3 | 4 | class ParameterStoreExecPlugin implements TerraformEnvironmentStagePlugin, TerraformPlanCommandPlugin, TerraformApplyCommandPlugin { 5 | public static void init() { 6 | ParameterStoreExecPlugin plugin = new ParameterStoreExecPlugin() 7 | 8 | TerraformEnvironmentStage.addPlugin(plugin) 9 | TerraformPlanCommand.addPlugin(plugin) 10 | TerraformApplyCommand.addPlugin(plugin) 11 | } 12 | 13 | @Override 14 | public void apply(TerraformEnvironmentStage stage) { 15 | def environment = stage.getEnvironment() 16 | def parameterStorePath = pathForEnvironment(environment) 17 | 18 | stage.decorate(PLAN, addEnvVariables(parameterStorePath)) 19 | stage.decorate(APPLY, addEnvVariables(parameterStorePath)) 20 | } 21 | 22 | public String pathForEnvironment(String environment) { 23 | String organization = Jenkinsfile.instance.getOrganization() 24 | String repoName = Jenkinsfile.instance.getRepoName() 25 | 26 | return "/${organization}/${repoName}/${environment}/" 27 | } 28 | 29 | public static Closure addEnvVariables(String path) { 30 | return { closure -> 31 | withEnv(["PARAMETER_STORE_EXEC_PATH=${path}", "PARAMETER_STORE_EXEC_DISABLE_TRANSLATION=true", "AWS_REGION=us-east-1"]) { 32 | closure() 33 | } 34 | } 35 | } 36 | 37 | @Override 38 | public void apply(TerraformPlanCommand command) { 39 | command.withPrefix('parameter-store-exec') 40 | } 41 | 42 | @Override 43 | public void apply(TerraformApplyCommand command) { 44 | command.withPrefix('parameter-store-exec') 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/PassPlanFilePlugin.groovy: -------------------------------------------------------------------------------- 1 | import static TerraformEnvironmentStage.PLAN_COMMAND 2 | import static TerraformEnvironmentStage.APPLY_COMMAND 3 | 4 | class PassPlanFilePlugin implements TerraformPlanCommandPlugin, TerraformApplyCommandPlugin, TerraformEnvironmentStagePlugin, Resettable { 5 | 6 | private static String directory = "./" 7 | 8 | public static void init() { 9 | PassPlanFilePlugin plugin = new PassPlanFilePlugin() 10 | 11 | TerraformEnvironmentStage.addPlugin(plugin) 12 | TerraformPlanCommand.addPlugin(plugin) 13 | TerraformApplyCommand.addPlugin(plugin) 14 | } 15 | 16 | public static withDirectory(String directory) { 17 | PassPlanFilePlugin.directory = directory 18 | return this 19 | } 20 | 21 | @Override 22 | public void apply(TerraformEnvironmentStage stage) { 23 | stage.decorate(PLAN_COMMAND, stashPlan(stage.getEnvironment())) 24 | stage.decorate(APPLY_COMMAND, unstashPlan(stage.getEnvironment())) 25 | } 26 | 27 | @Override 28 | public void apply(TerraformPlanCommand command) { 29 | String env = command.getEnvironment() 30 | command.withArgument("-out=tfplan-" + env) 31 | } 32 | 33 | @Override 34 | public void apply(TerraformApplyCommand command) { 35 | String env = command.getEnvironment() 36 | command.withPlanFile("tfplan-" + env) 37 | } 38 | 39 | public Closure stashPlan(String env) { 40 | return { closure -> 41 | closure() 42 | String planFile = "tfplan-" + env 43 | echo "Stashing ${planFile} file" 44 | dir(directory) { 45 | stash name: planFile, includes: planFile 46 | } 47 | } 48 | } 49 | 50 | public Closure unstashPlan(String env) { 51 | return { closure -> 52 | String planFile = "tfplan-" + env 53 | echo "Unstashing ${planFile} file" 54 | dir(directory) { 55 | unstash planFile 56 | } 57 | closure() 58 | } 59 | } 60 | 61 | public static reset() { 62 | directory = "./" 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/PlanOnlyPlugin.groovy: -------------------------------------------------------------------------------- 1 | import static TerraformEnvironmentStage.APPLY 2 | import static TerraformEnvironmentStage.CONFIRM 3 | 4 | class PlanOnlyPlugin implements TerraformEnvironmentStagePlugin, TerraformPlanCommandPlugin { 5 | 6 | public static void init() { 7 | PlanOnlyPlugin plugin = new PlanOnlyPlugin() 8 | 9 | BuildWithParametersPlugin.withBooleanParameter([ 10 | name: "PLAN_ONLY", 11 | description: 'Run `terraform plan` only, skipping `terraform apply`.' 12 | ]) 13 | 14 | BuildWithParametersPlugin.withBooleanParameter([ 15 | name: "FAIL_PLAN_ON_CHANGES", 16 | description: 'Plan run with -detailed-exitcode; ANY CHANGES will cause failure' 17 | ]) 18 | 19 | TerraformPlanCommand.addPlugin(plugin) 20 | TerraformEnvironmentStage.addPlugin(plugin) 21 | } 22 | 23 | public Closure skipStage(String stageName) { 24 | return { closure -> 25 | echo "Skipping ${stageName} stage. PlanOnlyPlugin is enabled." 26 | } 27 | } 28 | 29 | @Override 30 | public void apply(TerraformEnvironmentStage stage) { 31 | if (Jenkinsfile.instance.getEnv().PLAN_ONLY == 'true') { 32 | stage.decorateAround(CONFIRM, skipStage(CONFIRM)) 33 | stage.decorateAround(APPLY, skipStage(APPLY)) 34 | } 35 | } 36 | 37 | @Override 38 | public void apply(TerraformPlanCommand command) { 39 | if (Jenkinsfile.instance.getEnv().FAIL_PLAN_ON_CHANGES == 'true') { 40 | // set -e: fail on error 41 | // set -o pipefail: return non-zero exit code if any command fails. 42 | // useful when commands are piped together (ie. `terraform plan | landscape`) 43 | command.withPrefix('set -e; set -o pipefail;') 44 | command.withArgument('-detailed-exitcode') 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/RegressionStage.groovy: -------------------------------------------------------------------------------- 1 | class RegressionStage implements Stage, DecoratableStage, Resettable { 2 | 3 | public String testCommand 4 | public List automationRepoList = [] 5 | private String testCommandDirectory 6 | private Jenkinsfile jenkinsfile 7 | 8 | private StageDecorations decorations 9 | 10 | private static plugins = [] 11 | 12 | public RegressionStage() { 13 | this("./bin/test.sh") 14 | } 15 | 16 | public RegressionStage(String testCommand) { 17 | this.testCommand = testCommand 18 | this.jenkinsfile = Jenkinsfile.instance 19 | this.decorations = new StageDecorations() 20 | } 21 | 22 | public RegressionStage withScm(String automationRepo) { 23 | this.automationRepoList.add(automationRepo) 24 | return this 25 | } 26 | 27 | public RegressionStage changeDirectory(String directory) { 28 | this.testCommandDirectory = directory 29 | return this 30 | } 31 | 32 | public Stage then(Stage nextStage) { 33 | return new BuildGraph(this).then(nextStage) 34 | } 35 | 36 | public void build() { 37 | Jenkinsfile.build(pipelineConfiguration()) 38 | } 39 | 40 | private Closure pipelineConfiguration() { 41 | applyPlugins() 42 | 43 | return { 44 | node(jenkinsfile.getNodeName()) { 45 | stage("test") { 46 | decorations.apply { 47 | if (automationRepoList.isEmpty()) { 48 | checkout scm 49 | sh testCommand 50 | } else if (automationRepoList.size() == 1) { 51 | checkout resolveScm(source: [$class: 'GitSCMSource', remote: automationRepoList.first(), traits: [[$class: 'jenkins.plugins.git.traits.BranchDiscoveryTrait'], [$class: 'LocalBranchTrait']]], targets: [BRANCH_NAME, 'main']) 52 | sh testCommand 53 | } else { 54 | for (url in automationRepoList) { 55 | def dirName = url.split('/').last() - '.git' 56 | dir(dirName) { 57 | checkout resolveScm(source: [$class: 'GitSCMSource', remote: url, traits: [[$class: 'jenkins.plugins.git.traits.BranchDiscoveryTrait'], [$class: 'LocalBranchTrait']]], targets: [BRANCH_NAME, 'main']) 58 | } 59 | } 60 | 61 | if (this.testCommandDirectory) { 62 | dir(this.testCommandDirectory) { 63 | sh testCommand 64 | } 65 | } else { 66 | sh testCommand 67 | } 68 | } 69 | } 70 | } 71 | } 72 | } 73 | } 74 | 75 | public void decorate(Closure decoration) { 76 | decorations.add(decoration) 77 | } 78 | 79 | public static getPlugins() { 80 | return plugins 81 | } 82 | 83 | public static void reset() { 84 | this.plugins = [] 85 | } 86 | 87 | public static void addPlugin(plugin) { 88 | plugins << plugin 89 | } 90 | 91 | public void applyPlugins() { 92 | for (plugin in plugins) { 93 | plugin.apply(this) 94 | } 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/RegressionStagePlugin.groovy: -------------------------------------------------------------------------------- 1 | interface RegressionStagePlugin { 2 | public void apply(RegressionStage stage) 3 | } 4 | -------------------------------------------------------------------------------- /src/Resettable.groovy: -------------------------------------------------------------------------------- 1 | public interface Resettable { 2 | } 3 | -------------------------------------------------------------------------------- /src/SemanticVersion.groovy: -------------------------------------------------------------------------------- 1 | class SemanticVersion implements Comparable { 2 | 3 | private String versionString 4 | private List components 5 | 6 | SemanticVersion(String versionString) { 7 | this.versionString = versionString 8 | this.components = versionString.tokenize(/._/) 9 | } 10 | 11 | String getVersion() { 12 | this.versionString 13 | } 14 | 15 | String toString() { 16 | return "SemanticVersion: ${versionString}" 17 | } 18 | 19 | @Override 20 | int compareTo(SemanticVersion other) { 21 | for (i in 0.. 1.0.1 : 1.0.1 <=> 1.0.1-rc1 24 | } else if (i == other.components.size()) { 25 | return this.components[i].isInteger() ? 1 : -1 //1.0.1 <=> 1.0 : 1.0.1-rc1 <=> 1.0.1 26 | } 27 | 28 | if (this.components[i].isInteger() && other.components[i].isInteger()) { //1.0 <=> 1.1 or //1.1 <=> 1.0 29 | int diff = (this.components[i] as int) <=> (other.components[i] as int) 30 | if (diff != 0) { 31 | return diff 32 | } 33 | } else if (this.components[i].isInteger()) { //1.0.3.4 <=> 1.0.3.4b goes to the former. Hmm. 34 | return 1 35 | } else if (other.components[i].isInteger()) { //1.0.3.4-rc3 <=> 1.0.3.4 goes to the later. 36 | return -1 37 | } else { 38 | int diff = this.components[i] <=> other.components[i] //1.0.3.3 <=> 1.0.3.4 works at least. :-/ 39 | if (diff != 0) { 40 | return diff 41 | } 42 | } 43 | } 44 | return 0 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Stage.groovy: -------------------------------------------------------------------------------- 1 | interface Stage { 2 | public Stage then(Stage nextStage) 3 | public void build() 4 | } 5 | -------------------------------------------------------------------------------- /src/StageDecorations.groovy: -------------------------------------------------------------------------------- 1 | class StageDecorations { 2 | private Map decorations = [:] 3 | 4 | public apply(String group = null, Closure innerClosure) { 5 | def currentDecorations = decorations.get(group) ?: { stage -> stage() } 6 | currentDecorations.delegate = innerClosure.owner 7 | currentDecorations(innerClosure) 8 | } 9 | 10 | public add(String group = null, Closure decoration) { 11 | def existingDecoration = decorations.get(group) ?: { stage -> stage() } 12 | def newDecoration = { stage -> 13 | decoration.delegate = delegate 14 | decoration() { 15 | stage.delegate = delegate 16 | existingDecoration.delegate = delegate 17 | existingDecoration(stage) 18 | } 19 | } 20 | 21 | decorations.put(group, newDecoration) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/TagPlugin.groovy: -------------------------------------------------------------------------------- 1 | class TagPlugin implements TerraformPlanCommandPlugin, 2 | TerraformApplyCommandPlugin, 3 | Resettable { 4 | 5 | private static variableName 6 | private static disableOnApply = false 7 | private static writeToFile = false 8 | private static List tagClosures = [] 9 | 10 | public static init() { 11 | def plugin = new TagPlugin() 12 | 13 | TerraformPlanCommand.addPlugin(plugin) 14 | TerraformApplyCommand.addPlugin(plugin) 15 | } 16 | 17 | @Override 18 | public void apply(TerraformPlanCommand command) { 19 | applyToCommand(command) 20 | } 21 | 22 | @Override 23 | public void apply(TerraformApplyCommand command) { 24 | if (disableOnApply) { 25 | return 26 | } 27 | 28 | applyToCommand(command) 29 | } 30 | 31 | private void applyToCommand(command) { 32 | String variableName = getVariableName() 33 | Map tags = getTags(command) 34 | 35 | if (writeToFile) { 36 | command.withVariableFile(variableName, tags) 37 | } else { 38 | command.withVariable(variableName, tags) 39 | } 40 | } 41 | 42 | public static withVariableName(String variableName) { 43 | this.variableName = variableName 44 | } 45 | 46 | public static withEnvironmentTag(String tagKey = 'environment') { 47 | tagClosures << { command -> 48 | Map tag = [:] 49 | tag[tagKey] = command.getEnvironment() 50 | tag 51 | } 52 | return this 53 | } 54 | 55 | public static withTag(String key, String value) { 56 | tagClosures << { command -> 57 | Map tag = [:] 58 | tag[key] = value 59 | tag 60 | } 61 | return this 62 | } 63 | 64 | public static withTagFromFile(String key, String filename) { 65 | tagClosures << { command -> 66 | Map tag = [:] 67 | tag[key] = Jenkinsfile.readFile(filename) 68 | tag 69 | } 70 | return this 71 | } 72 | 73 | public static withTagFromEnvironmentVariable(String key, String variable) { 74 | tagClosures << { command -> 75 | Map tag = [:] 76 | tag[key] = Jenkinsfile.getEnvironmentVariable(variable) 77 | tag 78 | } 79 | return this 80 | } 81 | 82 | public static disableOnApply() { 83 | disableOnApply = true 84 | return this 85 | } 86 | 87 | public static writeToFile() { 88 | writeToFile = true 89 | return this 90 | } 91 | 92 | private static getVariableName() { 93 | return variableName ?: 'tags' 94 | } 95 | 96 | public Map getTags(TerraformCommand command) { 97 | return tagClosures.inject([:]) { memo, tagClosure -> 98 | memo + tagClosure.call(command) 99 | } 100 | } 101 | 102 | public static reset() { 103 | tagClosures = [] 104 | variableName = null 105 | disableOnApply = false 106 | writeToFile = false 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/TargetPlugin.groovy: -------------------------------------------------------------------------------- 1 | class TargetPlugin implements TerraformPlanCommandPlugin, TerraformApplyCommandPlugin { 2 | public static void init() { 3 | TargetPlugin plugin = new TargetPlugin() 4 | 5 | BuildWithParametersPlugin.withStringParameter([ 6 | name: "RESOURCE_TARGETS", 7 | description: 'comma-separated list of resource addresses to pass to plan and apply "-target=" parameters' 8 | ]) 9 | 10 | TerraformPlanCommand.addPlugin(plugin) 11 | TerraformApplyCommand.addPlugin(plugin) 12 | } 13 | 14 | @Override 15 | public void apply(TerraformPlanCommand command) { 16 | def targets = Jenkinsfile.instance.getEnv().RESOURCE_TARGETS ?: '' 17 | targets.split(',') 18 | .collect { item -> item.trim() } 19 | .findAll { item -> item != '' } 20 | .each { item -> command.withArgument("-target ${item}") } 21 | } 22 | 23 | @Override 24 | public void apply(TerraformApplyCommand command) { 25 | def targets = Jenkinsfile.instance.getEnv().RESOURCE_TARGETS ?: '' 26 | targets.split(',') 27 | .collect { item -> item.trim() } 28 | .findAll { item -> item != '' } 29 | .each { item -> command.withArgument("-target ${item}") } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/TerraformApplyCommandPlugin.groovy: -------------------------------------------------------------------------------- 1 | interface TerraformApplyCommandPlugin { 2 | public void apply(TerraformApplyCommand command) 3 | } 4 | -------------------------------------------------------------------------------- /src/TerraformCommand.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * `TerraformCommand` is an interface that can be implemented by a class that wraps a Terraform command. 3 | */ 4 | interface TerraformCommand { 5 | public String getEnvironment() 6 | } 7 | -------------------------------------------------------------------------------- /src/TerraformDirectoryPlugin.groovy: -------------------------------------------------------------------------------- 1 | class TerraformDirectoryPlugin implements TerraformInitCommandPlugin, TerraformValidateCommandPlugin, TerraformPlanCommandPlugin, TerraformApplyCommandPlugin, Resettable { 2 | 3 | private static String directory = "./terraform/" 4 | 5 | public static void init() { 6 | TerraformDirectoryPlugin plugin = new TerraformDirectoryPlugin() 7 | 8 | TerraformInitCommand.addPlugin(plugin) 9 | TerraformValidateCommand.addPlugin(plugin) 10 | TerraformPlanCommand.addPlugin(plugin) 11 | TerraformApplyCommand.addPlugin(plugin) 12 | } 13 | 14 | public static withDirectory(String directory) { 15 | TerraformDirectoryPlugin.directory = directory 16 | return this 17 | } 18 | 19 | @Override 20 | public void apply(TerraformInitCommand command) { 21 | command.withDirectory(directory) 22 | } 23 | 24 | @Override 25 | public void apply(TerraformValidateCommand command) { 26 | command.withDirectory(directory) 27 | } 28 | 29 | @Override 30 | public void apply(TerraformPlanCommand command) { 31 | command.withDirectory(directory) 32 | } 33 | 34 | @Override 35 | public void apply(TerraformApplyCommand command) { 36 | command.withDirectory(directory) 37 | } 38 | 39 | public static reset() { 40 | directory = "./terraform/" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/TerraformEnvironmentStagePlugin.groovy: -------------------------------------------------------------------------------- 1 | interface TerraformEnvironmentStagePlugin { 2 | public void apply(TerraformEnvironmentStage stage) 3 | } 4 | -------------------------------------------------------------------------------- /src/TerraformEnvironmentStageShellHookPlugin.groovy: -------------------------------------------------------------------------------- 1 | import static TerraformEnvironmentStage.ALL 2 | import static TerraformEnvironmentStage.INIT_COMMAND 3 | import static TerraformEnvironmentStage.PLAN 4 | import static TerraformEnvironmentStage.PLAN_COMMAND 5 | import static TerraformEnvironmentStage.APPLY 6 | import static TerraformEnvironmentStage.APPLY_COMMAND 7 | 8 | class TerraformEnvironmentStageShellHookPlugin implements TerraformEnvironmentStagePlugin, Resettable { 9 | static hooks = [ 10 | (ALL): new HookPoint(ALL), 11 | (INIT_COMMAND): new HookPoint(INIT_COMMAND), 12 | (PLAN): new HookPoint(PLAN), 13 | (PLAN_COMMAND): new HookPoint(PLAN_COMMAND), 14 | (APPLY): new HookPoint(APPLY), 15 | (APPLY_COMMAND): new HookPoint(APPLY_COMMAND) 16 | ] 17 | 18 | public static void init() { 19 | TerraformEnvironmentStageShellHookPlugin plugin = new TerraformEnvironmentStageShellHookPlugin() 20 | TerraformEnvironmentStage.addPlugin(plugin) 21 | } 22 | 23 | public static withHook(String hookPoint, String shellCommand, WhenToRun whenToRun = WhenToRun.ON_SUCCESS) { 24 | switch ( whenToRun ) { 25 | case WhenToRun.BEFORE: 26 | hooks[hookPoint].runBefore = shellCommand 27 | break 28 | case WhenToRun.AFTER: 29 | hooks[hookPoint].runAfterAlways = shellCommand 30 | break 31 | case WhenToRun.ON_FAILURE: 32 | hooks[hookPoint].runAfterOnFailure = shellCommand 33 | break 34 | case WhenToRun.ON_SUCCESS: 35 | hooks[hookPoint].runAfterOnSuccess = shellCommand 36 | break 37 | } 38 | return this 39 | } 40 | 41 | @Override 42 | public void apply(TerraformEnvironmentStage stage) { 43 | hooks.each { pointName, hookPoint -> 44 | if (hookPoint.isConfigured()) { 45 | stage.decorate(pointName, hookPoint.getClosure()) 46 | } 47 | } 48 | } 49 | 50 | public static void reset() { 51 | hooks = [ 52 | (ALL): new HookPoint(ALL), 53 | (INIT_COMMAND): new HookPoint(INIT_COMMAND), 54 | (PLAN): new HookPoint(PLAN), 55 | (PLAN_COMMAND): new HookPoint(PLAN_COMMAND), 56 | (APPLY): new HookPoint(APPLY), 57 | (APPLY_COMMAND): new HookPoint(APPLY_COMMAND) 58 | ] 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/TerraformFormatCommand.groovy: -------------------------------------------------------------------------------- 1 | class TerraformFormatCommand implements Resettable { 2 | private static boolean check = false 3 | private static boolean recursive = false 4 | private static boolean diff = false 5 | 6 | private Closure checkOptionPattern 7 | private Closure recursiveOptionPattern 8 | private Closure diffOptionPattern 9 | 10 | private static plugins = [] 11 | private appliedPlugins = [] 12 | 13 | public String toString() { 14 | applyPlugins() 15 | def pattern 16 | def parts = [] 17 | parts << 'terraform fmt' 18 | 19 | pattern = checkOptionPattern ?: { it ? '-check=true' : null } 20 | parts << pattern(check) 21 | 22 | pattern = recursiveOptionPattern ?: { println "recursive is default in Terraform 0.11.x - this is an unsupported option" } 23 | parts << pattern(recursive) 24 | 25 | pattern = diffOptionPattern ?: { it ? '-diff=true' : null } 26 | parts << pattern(diff) 27 | 28 | parts.removeAll { it == null } 29 | return parts.join(' ') 30 | } 31 | 32 | public static withCheck(newValue = true) { 33 | check = newValue 34 | return this 35 | } 36 | 37 | public static boolean isCheckEnabled() { 38 | return check 39 | } 40 | 41 | public static withRecursive(newValue = true) { 42 | recursive = newValue 43 | return this 44 | } 45 | 46 | public static withDiff(newValue = true) { 47 | diff = newValue 48 | return this 49 | } 50 | 51 | public withCheckOptionPattern(Closure pattern) { 52 | checkOptionPattern = pattern 53 | return this 54 | } 55 | 56 | public withRecursiveOptionPattern(Closure pattern) { 57 | recursiveOptionPattern = pattern 58 | return this 59 | } 60 | 61 | public withDiffOptionPattern(Closure pattern) { 62 | diffOptionPattern = pattern 63 | return this 64 | } 65 | 66 | public applyPlugins() { 67 | def remainingPlugins = plugins - appliedPlugins 68 | 69 | for (TerraformFormatCommandPlugin plugin in remainingPlugins) { 70 | plugin.apply(this) 71 | appliedPlugins << plugin 72 | } 73 | } 74 | 75 | public static void addPlugin(TerraformFormatCommandPlugin plugin) { 76 | plugins << plugin 77 | } 78 | 79 | public static void setPlugins(plugins) { 80 | this.plugins = plugins 81 | } 82 | 83 | public static getPlugins() { 84 | return plugins 85 | } 86 | 87 | public static void reset() { 88 | check = false 89 | recursive = false 90 | diff = false 91 | this.plugins = [] 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/TerraformFormatCommandPlugin.groovy: -------------------------------------------------------------------------------- 1 | interface TerraformFormatCommandPlugin { 2 | public void apply(TerraformFormatCommand command) 3 | } 4 | -------------------------------------------------------------------------------- /src/TerraformImportCommand.groovy: -------------------------------------------------------------------------------- 1 | class TerraformImportCommand implements TerraformCommand, Resettable { 2 | private String environment 3 | private static String resource 4 | private static String targetPath 5 | private static plugins = [] 6 | private appliedPlugins = [] 7 | 8 | TerraformImportCommand(String environment) { 9 | this.environment = environment 10 | } 11 | 12 | public String toString() { 13 | applyPlugins() 14 | if (resource) { 15 | if (targetPath) { 16 | def pieces = [] 17 | pieces << 'terraform' 18 | pieces << 'import' 19 | pieces << targetPath 20 | pieces << resource 21 | 22 | return pieces.join(' ') 23 | } 24 | else { 25 | return "echo \"No target path set, skipping 'terraform import'." 26 | } 27 | } 28 | 29 | return "echo \"No resource set, skipping 'terraform import'." 30 | } 31 | 32 | public TerraformImportCommand withResource(String resource) { 33 | this.resource = resource 34 | return this 35 | } 36 | 37 | public String getTargetPath() { 38 | return this.targetPath 39 | } 40 | 41 | public TerraformImportCommand withTargetPath(String targetPath) { 42 | this.targetPath = targetPath 43 | return this 44 | } 45 | 46 | public String getResource() { 47 | return this.resource 48 | } 49 | 50 | public static void reset() { 51 | this.resource = '' 52 | this.targetPath = '' 53 | this.resetPlugins() 54 | } 55 | 56 | /** 57 | * Assures that all plugins are applied, and are applied at most once. It 58 | * can be safely called multiple times. 59 | */ 60 | public applyPlugins() { 61 | def remainingPlugins = plugins - appliedPlugins 62 | 63 | for (TerraformImportCommandPlugin plugin in remainingPlugins) { 64 | plugin.apply(this) 65 | appliedPlugins << plugin 66 | } 67 | } 68 | 69 | /** 70 | * Accepts a plugin of the appropriate type and adds it to the list of plugins. 71 | * 72 | * @param plugin The plugin to add 73 | */ 74 | public static void addPlugin(TerraformImportCommandPlugin plugin) { 75 | plugins << plugin 76 | } 77 | 78 | public static void setPlugins(plugins) { 79 | this.plugins = plugins 80 | } 81 | 82 | public static getPlugins() { 83 | return plugins 84 | } 85 | 86 | /** 87 | * Reset plugins will reset the plugin list to a default set of plugins. 88 | * 89 | * @param defaultPlugins list of plugins to set, default: [] 90 | */ 91 | public static void resetPlugins(defaultPlugins = []) { 92 | this.plugins = defaultPlugins.clone() 93 | } 94 | 95 | public static instanceFor(String environment) { 96 | return new TerraformImportCommand(environment) 97 | } 98 | 99 | public String getEnvironment() { 100 | return this.environment 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/TerraformImportCommandPlugin.groovy: -------------------------------------------------------------------------------- 1 | interface TerraformImportCommandPlugin { 2 | public void apply(TerraformImportCommand command) 3 | } 4 | -------------------------------------------------------------------------------- /src/TerraformImportPlugin.groovy: -------------------------------------------------------------------------------- 1 | import static TerraformEnvironmentStage.PLAN_COMMAND 2 | 3 | class TerraformImportPlugin implements TerraformEnvironmentStagePlugin, TerraformImportCommandPlugin { 4 | 5 | public static void init() { 6 | TerraformImportPlugin plugin = new TerraformImportPlugin() 7 | 8 | BuildWithParametersPlugin.withStringParameter([ 9 | name: "IMPORT_RESOURCE", 10 | description: "Run `terraform import` on the resource specified prior to planning and applying." 11 | ]) 12 | BuildWithParametersPlugin.withStringParameter([ 13 | name: "IMPORT_TARGET_PATH", 14 | description: "The path in the Terraform state to import the spcified resource to." 15 | ]) 16 | BuildWithParametersPlugin.withStringParameter([ 17 | name: "IMPORT_ENVIRONMENT", 18 | description: "The environment in which to run the import." 19 | ]) 20 | 21 | TerraformEnvironmentStage.addPlugin(plugin) 22 | TerraformImportCommand.addPlugin(plugin) 23 | } 24 | 25 | @Override 26 | public void apply(TerraformEnvironmentStage stage) { 27 | def resource = Jenkinsfile.instance.getEnv().IMPORT_RESOURCE 28 | def targetPath = Jenkinsfile.instance.getEnv().IMPORT_TARGET_PATH 29 | def environment = Jenkinsfile.instance.getEnv().IMPORT_ENVIRONMENT 30 | if (resource && targetPath && stage.environment == environment) { 31 | stage.decorate(PLAN_COMMAND, runTerraformImportCommand(stage.environment)) 32 | } 33 | } 34 | 35 | @Override 36 | public void apply(TerraformImportCommand command) { 37 | def resource = Jenkinsfile.instance.getEnv().IMPORT_RESOURCE 38 | def targetPath = Jenkinsfile.instance.getEnv().IMPORT_TARGET_PATH 39 | if (resource && targetPath) { 40 | command.withResource(resource).withTargetPath(targetPath) 41 | } 42 | } 43 | 44 | public Closure runTerraformImportCommand(String environment) { 45 | def importCommand = TerraformImportCommand.instanceFor(environment) 46 | return { closure -> 47 | importCommand.applyPlugins() 48 | if (importCommand.resource) { 49 | echo "Running '${importCommand.toString()}'. TerraformImportPlugin is enabled." 50 | sh importCommand.toString() 51 | } 52 | closure() 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/TerraformInitCommand.groovy: -------------------------------------------------------------------------------- 1 | class TerraformInitCommand implements TerraformCommand, Resettable { 2 | private boolean input = false 3 | private String terraformBinary = "terraform" 4 | private String command = "init" 5 | String environment 6 | private prefixes = [] 7 | private suffixes = [] 8 | private backendConfigs = [] 9 | private boolean doBackend = true 10 | private String directory 11 | private boolean chdir_flag = false 12 | private static plugins = [] 13 | private appliedPlugins = [] 14 | 15 | public TerraformInitCommand(String environment) { 16 | this.environment = environment 17 | } 18 | 19 | public TerraformInitCommand withInput(boolean input) { 20 | this.input = input 21 | return this 22 | } 23 | 24 | public TerraformInitCommand withPrefix(String prefix) { 25 | prefixes << prefix 26 | return this 27 | } 28 | 29 | public TerraformInitCommand withSuffix(String suffix) { 30 | suffixes << suffix 31 | return this 32 | } 33 | 34 | public TerraformInitCommand withDirectory(String directory) { 35 | this.directory = directory 36 | return this 37 | } 38 | 39 | public TerraformInitCommand withChangeDirectoryFlag() { 40 | this.chdir_flag = true 41 | return this 42 | } 43 | 44 | public TerraformInitCommand withBackendConfig(String backendConfig) { 45 | this.backendConfigs << backendConfig 46 | return this 47 | } 48 | 49 | public TerraformInitCommand withoutBackend() { 50 | this.doBackend = false 51 | return this 52 | } 53 | 54 | public String toString() { 55 | applyPlugins() 56 | def pieces = [] 57 | pieces += prefixes 58 | pieces << terraformBinary 59 | if (directory && chdir_flag) { 60 | pieces << "-chdir=${directory}" 61 | } 62 | pieces << command 63 | if (!input) { 64 | pieces << "-input=false" 65 | } 66 | if (doBackend) { 67 | backendConfigs.each { config -> 68 | pieces << "-backend-config=${config}" 69 | } 70 | } else { 71 | pieces << "-backend=false" 72 | } 73 | if (directory && !chdir_flag) { 74 | pieces << directory 75 | } 76 | 77 | pieces += suffixes 78 | 79 | return pieces.join(' ') 80 | } 81 | 82 | public static TerraformInitCommand instanceFor(String environment) { 83 | return new TerraformInitCommand(environment) 84 | } 85 | 86 | public applyPlugins() { 87 | def remainingPlugins = plugins - appliedPlugins 88 | 89 | for (TerraformInitCommandPlugin plugin in remainingPlugins) { 90 | plugin.apply(this) 91 | appliedPlugins << plugin 92 | } 93 | } 94 | 95 | public static void addPlugin(TerraformInitCommandPlugin plugin) { 96 | plugins << plugin 97 | } 98 | 99 | public static void setPlugins(plugins) { 100 | this.plugins = plugins 101 | } 102 | 103 | public static getPlugins() { 104 | return plugins 105 | } 106 | 107 | public static void reset() { 108 | this.plugins = [] 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/TerraformInitCommandPlugin.groovy: -------------------------------------------------------------------------------- 1 | interface TerraformInitCommandPlugin { 2 | public void apply(TerraformInitCommand command) 3 | } 4 | -------------------------------------------------------------------------------- /src/TerraformLandscapePlugin.groovy: -------------------------------------------------------------------------------- 1 | class TerraformLandscapePlugin implements TerraformPlanCommandPlugin { 2 | public static void init() { 3 | TerraformLandscapePlugin plugin = new TerraformLandscapePlugin() 4 | 5 | TerraformPlanCommand.addPlugin(plugin) 6 | } 7 | 8 | @Override 9 | public void apply(TerraformPlanCommand command) { 10 | command.withSuffix("| landscape") 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/TerraformOutputCommand.groovy: -------------------------------------------------------------------------------- 1 | class TerraformOutputCommand implements TerraformCommand, Resettable { 2 | private String command = "output" 3 | private String terraformBinary = "terraform" 4 | String environment 5 | private boolean json = false 6 | private String redirectFile 7 | private String stateFilePath 8 | private static plugins = [] 9 | private appliedPlugins = [] 10 | 11 | public TerraformOutputCommand(String environment) { 12 | this.environment = environment 13 | } 14 | 15 | public TerraformOutputCommand withJson(boolean json) { 16 | this.json = json 17 | return this 18 | } 19 | 20 | public TerraformOutputCommand withRedirectFile(String redirectFile) { 21 | this.redirectFile = redirectFile 22 | return this 23 | } 24 | 25 | public String toString() { 26 | applyPlugins() 27 | def pieces = [] 28 | pieces << terraformBinary 29 | pieces << command 30 | 31 | if (json) { 32 | pieces << "-json" 33 | } 34 | 35 | if (redirectFile) { 36 | pieces << ">${redirectFile}" 37 | } 38 | 39 | return pieces.join(' ') 40 | } 41 | 42 | public static TerraformOutputCommand instanceFor(String environment) { 43 | return new TerraformOutputCommand(environment).withJson(false) 44 | } 45 | 46 | public String getEnvironment() { 47 | return environment 48 | } 49 | 50 | public applyPlugins() { 51 | def remainingPlugins = plugins - appliedPlugins 52 | 53 | for (TerraformOutputCommandPlugin plugin in remainingPlugins) { 54 | plugin.apply(this) 55 | appliedPlugins << plugin 56 | } 57 | } 58 | 59 | public static void addPlugin(TerraformOutputCommandPlugin plugin) { 60 | plugins << plugin 61 | } 62 | 63 | public static void setPlugins(plugins) { 64 | this.plugins = plugins 65 | } 66 | 67 | public static getPlugins() { 68 | return plugins 69 | } 70 | 71 | public static void reset() { 72 | this.plugins = [] 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/TerraformOutputCommandPlugin.groovy: -------------------------------------------------------------------------------- 1 | interface TerraformOutputCommandPlugin { 2 | public void apply(TerraformOutputCommand command) 3 | } 4 | -------------------------------------------------------------------------------- /src/TerraformOutputOnlyPlugin.groovy: -------------------------------------------------------------------------------- 1 | import static TerraformEnvironmentStage.INIT_COMMAND 2 | import static TerraformEnvironmentStage.PLAN_COMMAND 3 | import static TerraformEnvironmentStage.APPLY 4 | import static TerraformEnvironmentStage.CONFIRM 5 | 6 | class TerraformOutputOnlyPlugin implements TerraformEnvironmentStagePlugin, TerraformOutputCommandPlugin { 7 | 8 | public static void init() { 9 | TerraformOutputOnlyPlugin plugin = new TerraformOutputOnlyPlugin() 10 | 11 | BuildWithParametersPlugin.withBooleanParameter([ 12 | name: "SHOW_OUTPUTS_ONLY", 13 | description: "Only run 'terraform output' to show outputs, skipping plan and apply." 14 | ]) 15 | BuildWithParametersPlugin.withBooleanParameter([ 16 | name: "JSON_FORMAT_OUTPUTS", 17 | description: "Render 'terraform output' results as JSON. Only applies if SHOW_OUTPUTS_ONLY is selected." 18 | ]) 19 | BuildWithParametersPlugin.withStringParameter([ 20 | name: "REDIRECT_OUTPUTS_TO_FILE", 21 | description: "Filename relative to the current workspace to redirect output to. Only applies if 'SHOW_OUTPUTS_ONLY' is selected." 22 | ]) 23 | 24 | TerraformEnvironmentStage.addPlugin(plugin) 25 | TerraformOutputCommand.addPlugin(plugin) 26 | } 27 | 28 | public Closure skipStage(String stageName) { 29 | return { closure -> 30 | echo "Skipping ${stageName} stage. TerraformOutputOnlyPlugin is enabled." 31 | } 32 | } 33 | 34 | public Closure runTerraformOutputCommand(String environment) { 35 | def outputCommand = TerraformOutputCommand.instanceFor(environment) 36 | return { closure -> 37 | closure() 38 | echo "Running 'terraform output'. TerraformOutputOnlyPlugin is enabled." 39 | sh outputCommand.toString() 40 | } 41 | } 42 | 43 | @Override 44 | public void apply(TerraformEnvironmentStage stage) { 45 | if (Jenkinsfile.instance.getEnv().SHOW_OUTPUTS_ONLY == 'true') { 46 | stage.decorate(INIT_COMMAND, runTerraformOutputCommand(stage.getEnvironment())) 47 | stage.decorate(PLAN_COMMAND, skipStage(PLAN_COMMAND)) 48 | stage.decorateAround(CONFIRM, skipStage(CONFIRM)) 49 | stage.decorateAround(APPLY, skipStage(APPLY)) 50 | } 51 | } 52 | 53 | @Override 54 | public void apply(TerraformOutputCommand command) { 55 | if (Jenkinsfile.instance.getEnv().JSON_FORMAT_OUTPUTS == 'true') { 56 | command.withJson(true) 57 | } 58 | if (Jenkinsfile.instance.getEnv().REDIRECT_OUTPUTS_TO_FILE) { 59 | command.withRedirectFile(Jenkinsfile.instance.getEnv().REDIRECT_OUTPUTS_TO_FILE) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/TerraformPlanCommandPlugin.groovy: -------------------------------------------------------------------------------- 1 | interface TerraformPlanCommandPlugin { 2 | public void apply(TerraformPlanCommand command) 3 | } 4 | -------------------------------------------------------------------------------- /src/TerraformPluginVersion.groovy: -------------------------------------------------------------------------------- 1 | abstract class TerraformPluginVersion implements TerraformValidateStagePlugin, 2 | TerraformValidateCommandPlugin, 3 | TerraformInitCommandPlugin, 4 | TerraformPlanCommandPlugin, 5 | TerraformApplyCommandPlugin, 6 | TerraformFormatCommandPlugin { 7 | @Override 8 | public void apply(TerraformValidateStage validateStage) { 9 | } 10 | 11 | @Override 12 | public void apply(TerraformValidateCommand command) { 13 | } 14 | 15 | @Override 16 | public void apply(TerraformInitCommand command) { 17 | } 18 | 19 | @Override 20 | public void apply(TerraformPlanCommand command) { 21 | } 22 | 23 | @Override 24 | public void apply(TerraformApplyCommand command) { 25 | } 26 | 27 | @Override 28 | public void apply(TerraformFormatCommand command) { 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/TerraformPluginVersion11.groovy: -------------------------------------------------------------------------------- 1 | class TerraformPluginVersion11 extends TerraformPluginVersion { 2 | 3 | @Override 4 | public void apply(TerraformValidateCommand command) { 5 | command.withArgument('-check-variables=false') 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/TerraformPluginVersion12.groovy: -------------------------------------------------------------------------------- 1 | class TerraformPluginVersion12 extends TerraformPluginVersion { 2 | 3 | @Override 4 | public void apply(TerraformValidateStage validateStage) { 5 | validateStage.decorate(TerraformValidateStage.VALIDATE, addInitBefore()) 6 | } 7 | 8 | public Closure addInitBefore() { 9 | return { closure -> 10 | def initCommand = initCommandForValidate() 11 | sh initCommand.toString() 12 | 13 | closure() 14 | } 15 | } 16 | 17 | public static TerraformInitCommand initCommandForValidate() { 18 | return TerraformInitCommand.instanceFor('validate').withoutBackend() 19 | } 20 | 21 | public void apply(TerraformPlanCommand command) { 22 | command.withVariablePattern { key, value -> "-var='${key}=${value}'" } 23 | .withMapPattern { map -> 24 | def result = map.collect { key, value -> "\"${key}\":\"${value}\"" }.join(',') 25 | return "{${result}}" 26 | } 27 | } 28 | 29 | public void apply(TerraformApplyCommand command) { 30 | command.withVariablePattern { key, value -> "-var='${key}=${value}'" } 31 | .withMapPattern { map -> 32 | def result = map.collect { key, value -> "\"${key}\":\"${value}\"" }.join(',') 33 | return "{${result}}" 34 | } 35 | } 36 | 37 | public void apply(TerraformFormatCommand command) { 38 | command.withCheckOptionPattern { it ? '-check' : null } 39 | command.withRecursiveOptionPattern { it ? '-recursive' : null } 40 | command.withDiffOptionPattern { it ? '-diff' : null } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/TerraformPluginVersion15.groovy: -------------------------------------------------------------------------------- 1 | class TerraformPluginVersion15 extends TerraformPluginVersion12 { 2 | 3 | public void apply(TerraformValidateCommand command) { 4 | super.apply(command.withChangeDirectoryFlag()) 5 | } 6 | 7 | public void apply(TerraformInitCommand command) { 8 | super.apply(command.withChangeDirectoryFlag()) 9 | } 10 | 11 | public void apply(TerraformPlanCommand command) { 12 | super.apply(command.withChangeDirectoryFlag()) 13 | } 14 | 15 | public void apply(TerraformApplyCommand command) { 16 | super.apply(command.withChangeDirectoryFlag()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/TerraformStartDirectoryPlugin.groovy: -------------------------------------------------------------------------------- 1 | import static TerraformValidateStage.ALL 2 | import static TerraformEnvironmentStage.ALL 3 | 4 | public class TerraformStartDirectoryPlugin implements TerraformValidateStagePlugin, TerraformEnvironmentStagePlugin, Resettable { 5 | private static String directory = "./terraform/" 6 | 7 | public static void init() { 8 | def plugin = new TerraformStartDirectoryPlugin() 9 | 10 | TerraformValidateStage.addPlugin(plugin) 11 | TerraformEnvironmentStage.addPlugin(plugin) 12 | } 13 | 14 | public static withDirectory(String directory) { 15 | this.directory = directory 16 | return this 17 | } 18 | 19 | @Override 20 | public void apply(TerraformValidateStage stage) { 21 | stage.decorate(TerraformValidateStage.ALL, addDirectory()) 22 | } 23 | 24 | @Override 25 | public void apply(TerraformEnvironmentStage stage) { 26 | stage.decorate(TerraformEnvironmentStage.ALL, addDirectory()) 27 | } 28 | 29 | public Closure addDirectory() { 30 | return { closure -> 31 | dir(this.directory) { 32 | closure() 33 | } 34 | } 35 | } 36 | 37 | public static reset() { 38 | directory = "./terraform/" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/TerraformTaintCommand.groovy: -------------------------------------------------------------------------------- 1 | class TerraformTaintCommand implements TerraformCommand, Resettable { 2 | private String command = "taint" 3 | private String resource 4 | private String environment 5 | private static plugins = [] 6 | private appliedPlugins = [] 7 | 8 | public TerraformTaintCommand(String environment) { 9 | this.environment = environment 10 | } 11 | 12 | public TerraformTaintCommand withResource(String resource) { 13 | this.resource = resource 14 | return this 15 | } 16 | 17 | public String toString() { 18 | applyPlugins() 19 | def parts = [] 20 | parts << 'terraform' 21 | parts << command 22 | parts << resource 23 | 24 | parts.removeAll { it == null } 25 | return parts.join(' ') 26 | } 27 | 28 | public String getResource() { 29 | return this.resource 30 | } 31 | 32 | public static TerraformTaintCommand instanceFor(String environment) { 33 | return new TerraformTaintCommand(environment) 34 | } 35 | 36 | public String getEnvironment() { 37 | return this.environment 38 | } 39 | 40 | public applyPlugins() { 41 | def remainingPlugins = plugins - appliedPlugins 42 | 43 | for (TerraformTaintCommandPlugin plugin in remainingPlugins) { 44 | plugin.apply(this) 45 | appliedPlugins << plugin 46 | } 47 | } 48 | 49 | public static void addPlugin(TerraformTaintCommandPlugin plugin) { 50 | plugins << plugin 51 | } 52 | 53 | public static void setPlugins(plugins) { 54 | this.plugins = plugins 55 | } 56 | 57 | public static getPlugins() { 58 | return plugins 59 | } 60 | 61 | public static void reset() { 62 | this.plugins = [] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/TerraformTaintCommandPlugin.groovy: -------------------------------------------------------------------------------- 1 | interface TerraformTaintCommandPlugin { 2 | public void apply(TerraformTaintCommand command) 3 | } 4 | -------------------------------------------------------------------------------- /src/TerraformTaintPlugin.groovy: -------------------------------------------------------------------------------- 1 | import static TerraformEnvironmentStage.PLAN_COMMAND 2 | 3 | class TerraformTaintPlugin implements TerraformEnvironmentStagePlugin, TerraformTaintCommandPlugin, TerraformUntaintCommandPlugin, Resettable { 4 | private static DEFAULT_BRANCHES = ['main', 'master'] 5 | private static branches = DEFAULT_BRANCHES 6 | 7 | public static void init() { 8 | TerraformTaintPlugin plugin = new TerraformTaintPlugin() 9 | 10 | BuildWithParametersPlugin.withStringParameter([ 11 | name: "TAINT_RESOURCE", 12 | description: 'Run `terraform taint` on the resource specified prior to planning and applying.' 13 | ]) 14 | 15 | BuildWithParametersPlugin.withStringParameter([ 16 | name: "UNTAINT_RESOURCE", 17 | description: 'Run `terraform untaint` on the resource specified prior to planning and applying.' 18 | ]) 19 | 20 | TerraformEnvironmentStage.addPlugin(plugin) 21 | TerraformTaintCommand.addPlugin(plugin) 22 | TerraformUntaintCommand.addPlugin(plugin) 23 | } 24 | 25 | public static onBranch(String branchName) { 26 | this.branches << branchName 27 | return this 28 | } 29 | 30 | public void apply(TerraformEnvironmentStage stage) { 31 | stage.decorate(PLAN_COMMAND, runTerraformUntaintCommand(stage.getEnvironment())) 32 | stage.decorate(PLAN_COMMAND, runTerraformTaintCommand(stage.getEnvironment())) 33 | } 34 | 35 | public void apply(TerraformTaintCommand command) { 36 | def resource = Jenkinsfile.instance.getEnv().TAINT_RESOURCE 37 | if (resource) { 38 | command.withResource(resource) 39 | } 40 | } 41 | 42 | public void apply(TerraformUntaintCommand command) { 43 | def resource = Jenkinsfile.instance.getEnv().UNTAINT_RESOURCE 44 | if (resource) { 45 | command.withResource(resource) 46 | } 47 | } 48 | 49 | public boolean shouldApply() { 50 | // Check branches 51 | if (branches.contains(Jenkinsfile.instance.getEnv().BRANCH_NAME)) { 52 | return true 53 | } else if (null == Jenkinsfile.instance.getEnv().BRANCH_NAME) { 54 | return true 55 | } 56 | 57 | return false 58 | } 59 | 60 | public Closure runTerraformTaintCommand(String environment) { 61 | def taintCommand = TerraformTaintCommand.instanceFor(environment) 62 | return { closure -> 63 | if (shouldApply() && Jenkinsfile.instance.getEnv().TAINT_RESOURCE) { 64 | echo "Running '${taintCommand.toString()}'. TerraformTaintPlugin is enabled." 65 | sh taintCommand.toString() 66 | } 67 | closure() 68 | } 69 | } 70 | 71 | public Closure runTerraformUntaintCommand(String environment) { 72 | def untaintCommand = TerraformUntaintCommand.instanceFor(environment) 73 | return { closure -> 74 | if (shouldApply() && Jenkinsfile.instance.getEnv().UNTAINT_RESOURCE) { 75 | echo "Running '${untaintCommand.toString()}'. TerraformTaintPlugin is enabled." 76 | sh untaintCommand.toString() 77 | } 78 | closure() 79 | } 80 | } 81 | 82 | public static reset() { 83 | this.branches = DEFAULT_BRANCHES.clone() 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/TerraformUntaintCommand.groovy: -------------------------------------------------------------------------------- 1 | class TerraformUntaintCommand implements TerraformCommand, Resettable { 2 | private String command = "untaint" 3 | private String resource 4 | private String environment 5 | private static plugins = [] 6 | private appliedPlugins = [] 7 | 8 | public TerraformUntaintCommand(String environment) { 9 | this.environment = environment 10 | } 11 | 12 | public TerraformUntaintCommand withResource(String resource) { 13 | this.resource = resource 14 | return this 15 | } 16 | 17 | public String toString() { 18 | applyPlugins() 19 | def parts = [] 20 | parts << 'terraform' 21 | parts << command 22 | parts << resource 23 | 24 | parts.removeAll { it == null } 25 | return parts.join(' ') 26 | } 27 | 28 | public String getResource() { 29 | return this.resource 30 | } 31 | 32 | public static TerraformUntaintCommand instanceFor(String environment) { 33 | return new TerraformUntaintCommand(environment) 34 | } 35 | 36 | public String getEnvironment() { 37 | return this.environment 38 | } 39 | 40 | public applyPlugins() { 41 | def remainingPlugins = plugins - appliedPlugins 42 | 43 | for (TerraformUntaintCommandPlugin plugin in remainingPlugins) { 44 | plugin.apply(this) 45 | appliedPlugins << plugin 46 | } 47 | } 48 | 49 | public static void addPlugin(TerraformUntaintCommandPlugin plugin) { 50 | plugins << plugin 51 | } 52 | 53 | public static void setPlugins(plugins) { 54 | this.plugins = plugins 55 | } 56 | 57 | public static getPlugins() { 58 | return plugins 59 | } 60 | 61 | public static void reset() { 62 | this.plugins = [] 63 | } 64 | } 65 | 66 | -------------------------------------------------------------------------------- /src/TerraformUntaintCommandPlugin.groovy: -------------------------------------------------------------------------------- 1 | interface TerraformUntaintCommandPlugin { 2 | public void apply(TerraformUntaintCommand command) 3 | } 4 | -------------------------------------------------------------------------------- /src/TerraformValidateCommand.groovy: -------------------------------------------------------------------------------- 1 | class TerraformValidateCommand implements Resettable { 2 | private String terraformBinary = "terraform" 3 | private String command = "validate" 4 | private arguments = [] 5 | private prefixes = [] 6 | private suffixes = [] 7 | private String directory 8 | private boolean chdir_flag = false 9 | private static plugins = [] 10 | private appliedPlugins = [] 11 | 12 | public TerraformValidateCommand() { 13 | } 14 | 15 | public TerraformValidateCommand withArgument(String argument) { 16 | this.arguments << argument 17 | return this 18 | } 19 | 20 | public TerraformValidateCommand withPrefix(String prefix) { 21 | prefixes << prefix 22 | return this 23 | } 24 | 25 | public TerraformValidateCommand withSuffix(String suffix) { 26 | suffixes << suffix 27 | return this 28 | } 29 | 30 | public TerraformValidateCommand withDirectory(String directory) { 31 | this.directory = directory 32 | return this 33 | } 34 | 35 | public TerraformValidateCommand withChangeDirectoryFlag() { 36 | this.chdir_flag = true 37 | return this 38 | } 39 | 40 | public String toString() { 41 | applyPlugins() 42 | def pieces = [] 43 | pieces = pieces + prefixes 44 | pieces << terraformBinary 45 | if (directory && chdir_flag) { 46 | pieces << "-chdir=${directory}" 47 | } 48 | pieces << command 49 | for (String argument in arguments) { 50 | pieces << argument 51 | } 52 | if (directory && !chdir_flag) { 53 | pieces << directory 54 | } 55 | pieces += suffixes 56 | 57 | return pieces.join(' ') 58 | } 59 | 60 | public static TerraformValidateCommand instance() { 61 | return new TerraformValidateCommand() 62 | } 63 | 64 | public applyPlugins() { 65 | def remainingPlugins = plugins - appliedPlugins 66 | 67 | for (TerraformValidateCommandPlugin plugin in remainingPlugins) { 68 | plugin.apply(this) 69 | appliedPlugins << plugin 70 | } 71 | } 72 | 73 | public static void addPlugin(TerraformValidateCommandPlugin plugin) { 74 | plugins << plugin 75 | } 76 | 77 | public static void setPlugins(plugins) { 78 | this.plugins = plugins 79 | } 80 | 81 | public static getPlugins() { 82 | return plugins 83 | } 84 | 85 | public static void reset() { 86 | this.plugins = [] 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/TerraformValidateCommandPlugin.groovy: -------------------------------------------------------------------------------- 1 | interface TerraformValidateCommandPlugin { 2 | public void apply(TerraformValidateCommand command) 3 | } 4 | -------------------------------------------------------------------------------- /src/TerraformValidateStage.groovy: -------------------------------------------------------------------------------- 1 | class TerraformValidateStage implements Stage, DecoratableStage, Resettable { 2 | private Jenkinsfile jenkinsfile 3 | private StageDecorations decorations 4 | 5 | private static globalPlugins = [] 6 | 7 | public static final String ALL = 'all' 8 | public static final String VALIDATE = 'validate' 9 | 10 | public TerraformValidateStage() { 11 | this.jenkinsfile = Jenkinsfile.instance 12 | this.decorations = new StageDecorations() 13 | } 14 | 15 | public Stage then(Stage nextStage) { 16 | return new BuildGraph(this).then(nextStage) 17 | } 18 | 19 | public void build() { 20 | Jenkinsfile.build(pipelineConfiguration()) 21 | } 22 | 23 | public Closure pipelineConfiguration() { 24 | applyPlugins() 25 | 26 | def validateCommand = TerraformValidateCommand.instance() 27 | 28 | return { 29 | node(jenkinsfile.getNodeName()) { 30 | deleteDir() 31 | checkout(scm) 32 | 33 | decorations.apply(ALL) { 34 | stage("validate") { 35 | decorations.apply(VALIDATE) { 36 | sh validateCommand.toString() 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | 44 | public void decorate(Closure decoration) { 45 | decorations.add(ALL, decoration) 46 | } 47 | 48 | public decorate(String stageName, Closure decoration) { 49 | decorations.add(stageName, decoration) 50 | } 51 | 52 | public static addPlugin(plugin) { 53 | this.globalPlugins << plugin 54 | } 55 | 56 | public void applyPlugins() { 57 | for (plugin in globalPlugins) { 58 | plugin.apply(this) 59 | } 60 | } 61 | 62 | public static getPlugins() { 63 | return this.globalPlugins 64 | } 65 | 66 | public static void reset() { 67 | this.globalPlugins = [] 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/TerraformValidateStagePlugin.groovy: -------------------------------------------------------------------------------- 1 | interface TerraformValidateStagePlugin { 2 | public void apply(TerraformValidateStage stage) 3 | } 4 | -------------------------------------------------------------------------------- /src/TfvarsFilesPlugin.groovy: -------------------------------------------------------------------------------- 1 | class TfvarsFilesPlugin implements TerraformPlanCommandPlugin, TerraformApplyCommandPlugin, Resettable { 2 | 3 | static String directory = "." 4 | static List globalFiles = [] 5 | 6 | static withDirectory(String directory) { 7 | TfvarsFilesPlugin.directory = directory 8 | return this 9 | } 10 | 11 | static withGlobalVarFile(String fileName) { 12 | TfvarsFilesPlugin.globalFiles << fileName 13 | return this 14 | } 15 | 16 | static void init() { 17 | def plugin = new TfvarsFilesPlugin() 18 | 19 | TerraformPlanCommand.addPlugin(plugin) 20 | TerraformApplyCommand.addPlugin(plugin) 21 | } 22 | 23 | private originalContext = Jenkinsfile.instance.original 24 | 25 | @Override 26 | void apply(TerraformApplyCommand command) { 27 | applyToCommand(command) 28 | } 29 | 30 | @Override 31 | void apply(TerraformPlanCommand command) { 32 | applyToCommand(command) 33 | } 34 | 35 | void applyToCommand(command) { 36 | def files = globalFiles.collect { file -> 37 | return "${directory}/${file}" 38 | } + "${directory}/${command.environment}.tfvars" 39 | 40 | files.each { file -> 41 | if (originalContext.fileExists(file)) { 42 | command.withArgument("-var-file=${file}") 43 | } else { 44 | originalContext.echo "${file} does not exist." 45 | } 46 | } 47 | } 48 | 49 | public static void reset() { 50 | directory = "." 51 | globalFiles = [] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/ValidateFormatPlugin.groovy: -------------------------------------------------------------------------------- 1 | class ValidateFormatPlugin implements TerraformValidateStagePlugin { 2 | public static init() { 3 | TerraformValidateStage.addPlugin(new ValidateFormatPlugin()) 4 | TerraformFormatCommand.withCheck() 5 | } 6 | 7 | public void apply(TerraformValidateStage stage) { 8 | stage.decorate(TerraformValidateStage.VALIDATE, formatClosure()) 9 | } 10 | 11 | public Closure formatClosure() { 12 | return { closure -> 13 | closure() 14 | def formatCommand = new TerraformFormatCommand() 15 | sh formatCommand.toString() 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/WhenToRun.groovy: -------------------------------------------------------------------------------- 1 | public final enum WhenToRun { 2 | BEFORE, ON_SUCCESS, ON_FAILURE, AFTER 3 | } 4 | -------------------------------------------------------------------------------- /src/WithAwsPlugin.groovy: -------------------------------------------------------------------------------- 1 | import static TerraformEnvironmentStage.ALL 2 | 3 | class WithAwsPlugin implements TerraformEnvironmentStagePlugin, Resettable { 4 | private static role 5 | private static duration 6 | 7 | public static void init() { 8 | WithAwsPlugin plugin = new WithAwsPlugin() 9 | 10 | TerraformEnvironmentStage.addPlugin(plugin) 11 | } 12 | 13 | @Override 14 | public void apply(TerraformEnvironmentStage stage) { 15 | String environment = stage.getEnvironment() 16 | 17 | stage.decorate(ALL, addWithAwsRole(environment)) 18 | } 19 | 20 | public Closure addWithAwsRole(String environment) { 21 | return { closure -> 22 | String iamRole = getRole(environment) 23 | Integer sessionDuration = getDuration() 24 | 25 | if (iamRole != null) { 26 | withAWS(role: iamRole, duration: sessionDuration) { 27 | sh "echo Running AWS commands under the role: ${iamRole}" 28 | closure() 29 | } 30 | } else { 31 | sh "echo no role found. Skipping withAWS" 32 | closure() 33 | } 34 | } 35 | } 36 | 37 | public static withRole(String role = null) { 38 | this.role = role 39 | 40 | return this 41 | } 42 | 43 | public static withDuration(Integer duration = 3600) { 44 | this.duration = duration 45 | 46 | return this 47 | } 48 | 49 | public String getRole(String environment) { 50 | def tempRole = this.role 51 | 52 | if (tempRole == null) { 53 | tempRole = Jenkinsfile.instance.getEnv()['AWS_ROLE_ARN'] 54 | } 55 | 56 | if (tempRole == null) { 57 | tempRole = Jenkinsfile.instance.getEnv()["${environment.toUpperCase()}_AWS_ROLE_ARN"] 58 | } 59 | 60 | if (tempRole == null) { 61 | tempRole = Jenkinsfile.instance.getEnv()["${environment}_AWS_ROLE_ARN"] 62 | } 63 | 64 | return tempRole 65 | } 66 | 67 | public Integer getDuration() { 68 | def tempDuration = this.@duration 69 | 70 | if (tempDuration == null) { 71 | tempDuration = 3600 72 | } 73 | 74 | return tempDuration 75 | } 76 | 77 | public static void reset() { 78 | this.role = null 79 | this.duration = 3600 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test/AnsiColorPluginTest.groovy: -------------------------------------------------------------------------------- 1 | import static org.hamcrest.MatcherAssert.assertThat 2 | import static org.hamcrest.Matchers.equalTo 3 | import static org.hamcrest.Matchers.hasItem 4 | import static org.hamcrest.Matchers.instanceOf 5 | import static org.mockito.Mockito.any; 6 | import static org.mockito.Mockito.doReturn; 7 | import static org.mockito.Mockito.eq; 8 | import static org.mockito.Mockito.mock; 9 | import static org.mockito.Mockito.spy; 10 | import static org.mockito.Mockito.verify; 11 | 12 | import org.junit.jupiter.api.Nested 13 | import org.junit.jupiter.api.Test 14 | import org.junit.jupiter.api.extension.ExtendWith 15 | 16 | @ExtendWith(ResetStaticStateExtension.class) 17 | class AnsiColorPluginTest { 18 | @Nested 19 | public class Init { 20 | @Test 21 | void modifiesTerraformValidateStage() { 22 | AnsiColorPlugin.init() 23 | 24 | Collection actualPlugins = TerraformValidateStage.getPlugins() 25 | assertThat(actualPlugins, hasItem(instanceOf(AnsiColorPlugin.class))) 26 | } 27 | 28 | @Test 29 | void modifiesTerraformEnvironmentStage() { 30 | AnsiColorPlugin.init() 31 | 32 | Collection actualPlugins = TerraformEnvironmentStage.getPlugins() 33 | assertThat(actualPlugins, hasItem(instanceOf(AnsiColorPlugin.class))) 34 | } 35 | } 36 | 37 | @Nested 38 | public class ApplyTerraformValidateStage { 39 | @Test 40 | void decoratesTheStage() { 41 | def stage = mock(TerraformValidateStage.class) 42 | def plugin = spy(new AnsiColorPlugin()) 43 | def expectedClosure = { -> } 44 | doReturn(expectedClosure).when(plugin).addColor() 45 | 46 | plugin.apply(stage) 47 | 48 | verify(stage).decorate(expectedClosure) 49 | } 50 | } 51 | 52 | @Nested 53 | public class ApplyTerraformEnvironmentStage { 54 | @Test 55 | void decoratesThePlanStep() { 56 | def stage = mock(TerraformEnvironmentStage.class) 57 | def plugin = spy(new AnsiColorPlugin()) 58 | def expectedClosure = { -> } 59 | doReturn(expectedClosure).when(plugin).addColor() 60 | 61 | plugin.apply(stage) 62 | 63 | verify(stage).decorate(TerraformEnvironmentStage.PLAN, expectedClosure) 64 | } 65 | 66 | @Test 67 | void decoratesTheApplyStep() { 68 | def stage = mock(TerraformEnvironmentStage.class) 69 | def plugin = spy(new AnsiColorPlugin()) 70 | def expectedClosure = { -> } 71 | doReturn(expectedClosure).when(plugin).addColor() 72 | 73 | plugin.apply(stage) 74 | 75 | verify(stage).decorate(TerraformEnvironmentStage.APPLY, expectedClosure) 76 | } 77 | } 78 | 79 | @Nested 80 | public class AddColor { 81 | @Test 82 | void executesTheInnerClosure() { 83 | def wasCalled = false 84 | def mockWorkflowScript = new MockWorkflowScript() 85 | def innerClosure = { -> wasCalled = true } 86 | def plugin = new AnsiColorPlugin() 87 | 88 | def colorClosure = plugin.addColor() 89 | colorClosure.delegate = mockWorkflowScript 90 | colorClosure(innerClosure) 91 | 92 | assertThat(wasCalled, equalTo(true)) 93 | } 94 | 95 | @Test 96 | void callsAnsiColorWithXTermArgument() { 97 | def mockWorkflowScript = spy(new MockWorkflowScript()) 98 | def plugin = new AnsiColorPlugin() 99 | 100 | def colorClosure = plugin.addColor() 101 | colorClosure.delegate = mockWorkflowScript 102 | colorClosure() { -> } 103 | 104 | verify(mockWorkflowScript).ansiColor(eq('xterm'), any(Closure.class)) 105 | } 106 | } 107 | } 108 | 109 | -------------------------------------------------------------------------------- /test/BuildGraphTest.groovy: -------------------------------------------------------------------------------- 1 | import static org.mockito.Mockito.inOrder 2 | import static org.mockito.Mockito.mock 3 | import static org.mockito.Mockito.times 4 | import static org.mockito.Mockito.verify 5 | 6 | import org.mockito.InOrder 7 | import org.junit.jupiter.api.Nested 8 | import org.junit.jupiter.api.Test 9 | 10 | class BuildGraphTest { 11 | @Nested 12 | public class WithASingleStage { 13 | @Test 14 | void buildsTheStageThatItWasCreatedWith() { 15 | Stage startStage = mock(Stage.class) 16 | BuildGraph graph = new BuildGraph(startStage) 17 | 18 | graph.build() 19 | 20 | verify(startStage, times(1)).build() 21 | } 22 | } 23 | 24 | @Nested 25 | public class WithMultipleStages { 26 | @Test 27 | void buildsTheStagesInOrder() { 28 | Stage stage1 = mock(Stage.class) 29 | Stage stage2 = mock(Stage.class) 30 | Stage stage3 = mock(Stage.class) 31 | Stage stage4 = mock(Stage.class) 32 | 33 | BuildGraph graph = new BuildGraph(stage1).then(stage2) 34 | .then(stage3) 35 | .then(stage4) 36 | .build() 37 | 38 | InOrder inOrder = inOrder(stage1, stage2, stage3, stage4); 39 | inOrder.verify(stage1, times(1)).build() 40 | inOrder.verify(stage2, times(1)).build() 41 | inOrder.verify(stage3, times(1)).build() 42 | inOrder.verify(stage4, times(1)).build() 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/BuildStageTest.groovy: -------------------------------------------------------------------------------- 1 | import static org.mockito.Mockito.spy 2 | import static org.mockito.Mockito.doReturn 3 | 4 | import org.junit.jupiter.api.Nested 5 | import org.junit.jupiter.api.Test 6 | 7 | class BuildStageTest { 8 | 9 | @Nested 10 | public class Build { 11 | @Test 12 | void buildsWithoutError() { 13 | def expectedPipelineConfig = { -> } 14 | MockJenkinsfile.withMockedOriginal() 15 | 16 | BuildStage stage = spy(new BuildStage()) 17 | doReturn(expectedPipelineConfig).when(stage).pipelineConfiguration() 18 | 19 | stage.build() 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/DefaultEnvironmentPluginTest.groovy: -------------------------------------------------------------------------------- 1 | import static org.hamcrest.Matchers.hasItem 2 | import static org.hamcrest.Matchers.instanceOf 3 | import static org.hamcrest.MatcherAssert.assertThat 4 | 5 | import org.junit.jupiter.api.Nested 6 | import org.junit.jupiter.api.Test 7 | 8 | class DefaultEnvironmentPluginTest { 9 | @Nested 10 | public class Init { 11 | @Test 12 | void modifiesTerraformEnvironmentStageByDefault() { 13 | Collection actualPlugins = TerraformEnvironmentStage.getPlugins() 14 | 15 | assertThat(actualPlugins, hasItem(instanceOf(DefaultEnvironmentPlugin.class))) 16 | } 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /test/FileParametersPluginTest.groovy: -------------------------------------------------------------------------------- 1 | import static org.hamcrest.MatcherAssert.assertThat 2 | import static org.hamcrest.Matchers.hasItem 3 | import static org.hamcrest.Matchers.instanceOf 4 | import static org.junit.jupiter.api.Assertions.assertEquals 5 | 6 | import org.junit.jupiter.api.BeforeEach 7 | import org.junit.jupiter.api.Nested 8 | import org.junit.jupiter.api.Test 9 | import org.junit.jupiter.api.extension.ExtendWith 10 | 11 | @ExtendWith(ResetStaticStateExtension.class) 12 | class FileParametersPluginTest { 13 | @Nested 14 | public class Init { 15 | @Test 16 | void modifiesTerraformEnvironmentStage() { 17 | FileParametersPlugin.init() 18 | 19 | Collection actualPlugins = TerraformEnvironmentStage.getPlugins() 20 | assertThat(actualPlugins, hasItem(instanceOf(FileParametersPlugin.class))) 21 | } 22 | } 23 | 24 | @Nested 25 | public class GetVariables { 26 | @BeforeEach 27 | void setupJenkinsfile() { 28 | MockJenkinsfile.withEnv() 29 | } 30 | 31 | @Test 32 | void returnsAValueForEachLine() { 33 | List expectedValues = [ "VAR1=VALUE1", "VAR2=VALUE2" ] 34 | String fileContents = expectedValues.join('\n') 35 | 36 | FileParametersPlugin plugin = new FileParametersPlugin() 37 | List actualValues = plugin.getVariables(fileContents) 38 | 39 | assertEquals(expectedValues, actualValues) 40 | } 41 | 42 | @Test 43 | void ignoresTrailingNewline() { 44 | List expectedValues = [ "VAR1=VALUE1", "VAR2=VALUE2" ] 45 | String fileContents = expectedValues.join('\n') + '\n\n' 46 | 47 | FileParametersPlugin plugin = new FileParametersPlugin() 48 | List actualValues = plugin.getVariables(fileContents) 49 | 50 | assertEquals(expectedValues, actualValues) 51 | } 52 | 53 | @Test 54 | void handlesCarriageReturnCharacters() { 55 | List expectedValues = [ "VAR1=VALUE1", "VAR2=VALUE2" ] 56 | String fileContents = expectedValues.join('\r\n') 57 | 58 | FileParametersPlugin plugin = new FileParametersPlugin() 59 | List actualValues = plugin.getVariables(fileContents) 60 | 61 | assertEquals(expectedValues, actualValues) 62 | } 63 | 64 | @Test 65 | void interpolatesReferencesToOtherEnvironmentVariables() { 66 | String fileContents = 'SOME_VARIABLE=${env.OTHER_VARIABLE}' 67 | MockJenkinsfile.withEnv(OTHER_VARIABLE: 'VALUE1') 68 | FileParametersPlugin plugin = new FileParametersPlugin() 69 | 70 | List actualValues = plugin.getVariables(fileContents) 71 | 72 | assertEquals(["SOME_VARIABLE=VALUE1"], actualValues) 73 | } 74 | } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /test/MockJenkinsfile.groovy: -------------------------------------------------------------------------------- 1 | import static org.mockito.Mockito.doReturn 2 | import static org.mockito.Mockito.mock 3 | import static org.mockito.Mockito.mockingDetails 4 | import static org.mockito.Mockito.spy 5 | import static org.mockito.Mockito.when 6 | 7 | public class MockJenkinsfile { 8 | 9 | public static withEnv(Map env = [:]) { 10 | if (needsJenkinsfileInstanceMock()) { 11 | mockJenkinsfileInstance() 12 | } 13 | 14 | when(Jenkinsfile.instance.getEnv()).thenReturn(env) 15 | return this 16 | } 17 | 18 | public static withMockedOriginal() { 19 | Jenkinsfile.original = new MockWorkflowScript() 20 | return this 21 | } 22 | 23 | public static withStandardizedRepoSlug(String slug) { 24 | if (needsJenkinsfileInstanceMock()) { 25 | mockJenkinsfileInstance() 26 | } 27 | 28 | when(Jenkinsfile.instance.getStandardizedRepoSlug()).thenReturn(slug) 29 | return this 30 | } 31 | 32 | public static withRepoName(String repoName) { 33 | if (needsJenkinsfileInstanceMock()) { 34 | mockJenkinsfileInstance() 35 | } 36 | 37 | when(Jenkinsfile.instance.getRepoName()).thenReturn(repoName) 38 | return this 39 | } 40 | 41 | public static withOrganization(String organization) { 42 | if (needsJenkinsfileInstanceMock()) { 43 | mockJenkinsfileInstance() 44 | } 45 | 46 | when(Jenkinsfile.instance.getOrganization()).thenReturn(organization) 47 | return this 48 | } 49 | 50 | public static withParsedScmUrl(Map results) { 51 | if (needsJenkinsfileInstanceMock()) { 52 | mockJenkinsfileInstance() 53 | } 54 | 55 | when(Jenkinsfile.instance.getParsedScmUrl()).thenReturn(results) 56 | return this 57 | } 58 | 59 | public static withFile(String filePath, String fileContent = '') { 60 | def original = spy(new MockWorkflowScript()) 61 | doReturn(true).when(original).fileExists(filePath) 62 | doReturn(fileContent).when(original).readFile(filePath) 63 | 64 | Jenkinsfile.original = original 65 | return this 66 | } 67 | 68 | public static needsJenkinsfileInstanceMock() { 69 | return Jenkinsfile.instance == null || !mockingDetails(Jenkinsfile.instance).isMock() 70 | } 71 | 72 | public static mockJenkinsfileInstance() { 73 | Jenkinsfile.instance = mock(Jenkinsfile.class) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /test/MockWorkflowScript.groovy: -------------------------------------------------------------------------------- 1 | class MockWorkflowScript { 2 | public docker 3 | public scm 4 | public env = [:] 5 | public static BRANCH_NAME 6 | 7 | public MockWorkflowScript() { 8 | docker = this 9 | } 10 | 11 | public docker(Closure closure = null) { 12 | return this 13 | } 14 | 15 | public image(String dockerImage) { return this } 16 | public inside(String dockerImage, Closure closure) { 17 | closure() 18 | return this 19 | } 20 | public build(String image, String options) { return this } 21 | public echo(String message) { println "MockWorkflowScript.echo ${message}" } 22 | public pwd(Map options) { return '/MockWorkflowScript/currentDir' } 23 | public writeFile(Map options) { println "MockWorkflowScript.writeFile: ${options.toString()}" } 24 | public writeFile(String filename, String content) { println "MockWorkflowScript.writeFile: ${filename} : ${content}" } 25 | public readFile(String filename) { return "MockWorkflowScript.readFile(${filename}): some content" } 26 | public fileExists(String filename) { return false } 27 | public sh(String command) { println "MockWorkflowScript.sh: ${command.toString()}"; return 'MockWorkflowScript.sh output' } 28 | public sh(Map options) { println "MockWorkflowScript.sh: ${options.toString()}"; return 'MockWorkflowScript.sh output' } 29 | public string(Map options) { println "MockWorkflowScript.string: ${options.toString()}" } 30 | public parameters(params) { println "MockWorkflowScript.parameters: ${params.toString()}" } 31 | public properties(props) { println "MockWorkflowScript.properties: ${props.toString()}" } 32 | public node(String nodeName, Closure closure) { 33 | println "MockWorkflowScript.node(${nodeName})" 34 | closure() 35 | } 36 | public node(Closure closure) { 37 | println "MockWorkflowScript.node { }" 38 | closure() 39 | } 40 | public deleteDir() { println "MockWorkflowScript.deleteDir" } 41 | public checkout(scm) { println "MockWorkflowScript.checkout(${scm})" } 42 | public withEnv(Collection env, Closure closure) { 43 | println "MockWorkflowScript.withEnv(${env})" 44 | closure() 45 | } 46 | public stage(String stageName, Closure closure) { 47 | println "MockWorkflowScript.stage(${stageName})" 48 | closure() 49 | } 50 | public timeout(options, Closure closure) { 51 | println "MockWorkflowScript.timeout(${options})" 52 | closure() 53 | } 54 | public input(options) { 55 | println "MockWorkflowScript.input(${options})" 56 | } 57 | public ApplyJenkinsfileClosure(Closure closure) { 58 | println "MockWorkflowScript.ApplyJenkinsfileClosure" 59 | closure.delegate = this 60 | closure() 61 | } 62 | public stash(args) { 63 | println "MockWorkflowScript.stash(${args})" 64 | } 65 | public unstash(args) { 66 | println "MockWorkflowScript.unstash(${args})" 67 | } 68 | 69 | public dir(String directory, Closure closure) { 70 | println "MockWorkflowScript.dir(${directory})" 71 | closure.delegate = this 72 | closure() 73 | } 74 | 75 | public resolveScm(Map args) { 76 | println "MockWorkflowScript.resolveScm(${args})" 77 | } 78 | 79 | public ansiColor(String arg, Closure closure) { 80 | print "MockWorkflowScript.ansiColor(${arg})" 81 | closure.delegate = this 82 | closure() 83 | } 84 | 85 | public withCredentials(bindings, Closure closure) { 86 | print "MockworkflowScript.withCredentials(${bindings})" 87 | closure.delegate = this 88 | closure() 89 | } 90 | } 91 | 92 | -------------------------------------------------------------------------------- /test/ParameterStoreExecPluginTest.groovy: -------------------------------------------------------------------------------- 1 | import static org.hamcrest.Matchers.containsString 2 | import static org.hamcrest.Matchers.equalTo 3 | import static org.hamcrest.Matchers.hasItem 4 | import static org.hamcrest.Matchers.instanceOf 5 | import static org.hamcrest.MatcherAssert.assertThat 6 | 7 | import org.junit.jupiter.api.Nested 8 | import org.junit.jupiter.api.Test 9 | import org.junit.jupiter.api.extension.ExtendWith 10 | 11 | @ExtendWith(ResetStaticStateExtension.class) 12 | class ParameterStoreExecPluginTest { 13 | @Nested 14 | public class Init { 15 | @Test 16 | void modifiesTerraformEnvironmentStage() { 17 | ParameterStoreExecPlugin.init() 18 | 19 | Collection actualPlugins = TerraformEnvironmentStage.getPlugins() 20 | assertThat(actualPlugins, hasItem(instanceOf(ParameterStoreExecPlugin.class))) 21 | } 22 | 23 | @Test 24 | void modifiesTerraformPlanCommand() { 25 | ParameterStoreExecPlugin.init() 26 | 27 | Collection actualPlugins = TerraformPlanCommand.getPlugins() 28 | assertThat(actualPlugins, hasItem(instanceOf(ParameterStoreExecPlugin.class))) 29 | } 30 | 31 | @Test 32 | void modifiesTerraformApplyCommand() { 33 | ParameterStoreExecPlugin.init() 34 | 35 | Collection actualPlugins = TerraformApplyCommand.getPlugins() 36 | assertThat(actualPlugins, hasItem(instanceOf(ParameterStoreExecPlugin.class))) 37 | } 38 | } 39 | 40 | @Nested 41 | public class Apply { 42 | @Test 43 | void addsParameterStorePrefixToTerraformPlan() { 44 | ParameterStoreExecPlugin plugin = new ParameterStoreExecPlugin() 45 | TerraformPlanCommand command = new TerraformPlanCommand() 46 | 47 | plugin.apply(command) 48 | 49 | String result = command.toString() 50 | assertThat(result, containsString("parameter-store-exec terraform plan")) 51 | } 52 | 53 | @Test 54 | void addsParameterStorePrefixToTerraformApply() { 55 | ParameterStoreExecPlugin plugin = new ParameterStoreExecPlugin() 56 | TerraformApplyCommand command = new TerraformApplyCommand() 57 | 58 | plugin.apply(command) 59 | 60 | String result = command.toString() 61 | assertThat(result, containsString("parameter-store-exec terraform apply")) 62 | } 63 | } 64 | 65 | @Nested 66 | public class PathForEnvironment { 67 | @Test 68 | void constructPathUsingOrgRepoAndEnvironment() { 69 | String organization = 'SomeOrg' 70 | String repoName = 'SomeRepo' 71 | String environment = "qa" 72 | 73 | MockJenkinsfile.withOrganization(organization).withRepoName(repoName) 74 | ParameterStoreExecPlugin plugin = new ParameterStoreExecPlugin() 75 | 76 | String actual = plugin.pathForEnvironment(environment) 77 | assertThat(actual, equalTo("/${organization}/${repoName}/${environment}/".toString())) 78 | } 79 | } 80 | 81 | } 82 | 83 | -------------------------------------------------------------------------------- /test/RegressionStageTest.groovy: -------------------------------------------------------------------------------- 1 | import static org.mockito.Mockito.mock 2 | import static org.mockito.Mockito.verify 3 | 4 | import org.junit.jupiter.api.BeforeEach 5 | import org.junit.jupiter.api.Nested 6 | import org.junit.jupiter.api.Test 7 | import org.junit.jupiter.api.extension.ExtendWith 8 | 9 | @ExtendWith(ResetStaticStateExtension.class) 10 | class RegressionStageTest { 11 | 12 | @Nested 13 | public class AutomationRepo { 14 | @BeforeEach 15 | void mockJenkinsfileOriginal() { 16 | MockJenkinsfile.withMockedOriginal() 17 | } 18 | 19 | @Test 20 | void automationRepoSpecifiedSuccessfullyCallApply() { 21 | RegressionStagePlugin fakePlugin = mock(RegressionStagePlugin.class) 22 | RegressionStage.addPlugin(fakePlugin) 23 | 24 | RegressionStage stage = new RegressionStage().withScm("git:someHost:someUser/someRepo.git") 25 | stage.build() 26 | 27 | verify(fakePlugin).apply(stage) 28 | } 29 | 30 | @Test 31 | void automationRepoAndAppRepoSpecifiedSuccessfullyCallApply() { 32 | RegressionStagePlugin fakePlugin = mock(RegressionStagePlugin.class) 33 | RegressionStage.addPlugin(fakePlugin) 34 | 35 | RegressionStage stage = new RegressionStage().withScm("git:someHost:someUser/someRepo.git") 36 | .withScm("git:someHost:someUser/someOtherRepo.git") 37 | stage.build() 38 | 39 | verify(fakePlugin).apply(stage) 40 | } 41 | 42 | @Test 43 | void automationRepoAndAppRepoWithChangeDirectorySpecifiedSuccessfullyCallApply() { 44 | RegressionStagePlugin fakePlugin = mock(RegressionStagePlugin.class) 45 | RegressionStage.addPlugin(fakePlugin) 46 | 47 | RegressionStage stage = new RegressionStage().withScm("git:someHost:someUser/someRepo.git") 48 | .withScm("git:someHost:someUser/someOtherRepo.git") 49 | .changeDirectory("someDir") 50 | stage.build() 51 | 52 | verify(fakePlugin).apply(stage) 53 | } 54 | 55 | @Test 56 | void noAutomationRepoSpecifiedSuccessfullyCallApply() { 57 | RegressionStagePlugin fakePlugin = mock(RegressionStagePlugin.class) 58 | RegressionStage.addPlugin(fakePlugin) 59 | 60 | RegressionStage stage = new RegressionStage() 61 | stage.build() 62 | 63 | verify(fakePlugin).apply(stage) 64 | } 65 | } 66 | 67 | @Nested 68 | public class AddedPlugins { 69 | @Test 70 | void willHaveApplyCalled() { 71 | RegressionStagePlugin fakePlugin = mock(RegressionStagePlugin.class) 72 | RegressionStage.addPlugin(fakePlugin) 73 | 74 | RegressionStage stage = new RegressionStage() 75 | stage.applyPlugins() 76 | 77 | verify(fakePlugin).apply(stage) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/ResetStaticStateExtension.groovy: -------------------------------------------------------------------------------- 1 | import org.codehaus.groovy.transform.trait.Traits 2 | import org.junit.jupiter.api.extension.AfterEachCallback 3 | import org.junit.jupiter.api.extension.BeforeEachCallback 4 | import org.junit.jupiter.api.extension.ExtensionContext 5 | import org.reflections.Reflections 6 | import groovy.transform.Memoized 7 | 8 | public class ResetStaticStateExtension implements BeforeEachCallback, 9 | AfterEachCallback { 10 | 11 | @Override 12 | public void beforeEach(ExtensionContext context) { 13 | reset() 14 | } 15 | 16 | @Override 17 | public void afterEach(ExtensionContext context) { 18 | reset() 19 | } 20 | 21 | public reset() { 22 | def resetList = findAllResettableClasses() 23 | 24 | resetList.each { needsReset -> 25 | needsReset.reset() 26 | } 27 | } 28 | 29 | @Memoized 30 | public findAllResettableClasses() { 31 | return new Reflections(Resettable.getPackage().getName()).getSubTypesOf( Resettable.class ).findAll { !Traits.isTrait(it) } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/SemanticVersionTest.groovy: -------------------------------------------------------------------------------- 1 | import static org.hamcrest.MatcherAssert.assertThat; 2 | import static org.hamcrest.Matchers.equalTo 3 | 4 | import org.junit.jupiter.api.Test 5 | 6 | class SemanticVersionTest { 7 | 8 | @Test 9 | void sortsCorrectly() { 10 | 100.times { 11 | List versions = SORTED.clone().collect { v -> new SemanticVersion(v) } 12 | // Randomize 13 | Collections.shuffle(versions) 14 | // Then sort 15 | versions.sort() 16 | 17 | def result = versions.collect { sv -> sv.version } 18 | assertThat(result, equalTo(SORTED)) 19 | } 20 | } 21 | 22 | static SORTED = [ 23 | 'fixme', 24 | '0.2-beta', 25 | '0.1', 26 | '0.1.0.2', 27 | '0.1.0.2', 28 | '0.1.0.10', 29 | '0.1.0.23', 30 | '0.2', 31 | '0.2.0.3.1', 32 | '0.2.0.4', 33 | '1.0RC1', 34 | '1.0RC2', 35 | '1.0', 36 | '1.0.1.2', 37 | '1.0.2', 38 | '1.2.2.3', 39 | '1.2.3', 40 | '1.5.2_04', 41 | '1.5.2_05', 42 | '1.5.2_10', 43 | '1.6.0_01', 44 | '2.0', 45 | '2.0.0_02', 46 | '3.1' 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /test/TerraformImportCommandTest.groovy: -------------------------------------------------------------------------------- 1 | import static org.hamcrest.Matchers.containsString 2 | import static org.hamcrest.Matchers.blankString 3 | import static org.hamcrest.MatcherAssert.assertThat 4 | import static org.mockito.Mockito.mock 5 | import static org.mockito.Mockito.times 6 | import static org.mockito.Mockito.verify 7 | 8 | import org.junit.jupiter.api.Nested 9 | import org.junit.jupiter.api.Test 10 | import org.junit.jupiter.api.extension.ExtendWith 11 | 12 | @ExtendWith(ResetStaticStateExtension.class) 13 | class TerraformImportCommandTest { 14 | @Nested 15 | public class WithResource { 16 | @Test 17 | void defaultsToEmpty() { 18 | def command = new TerraformImportCommand() 19 | 20 | def actualCommand = command.toString() 21 | assertThat(actualCommand, containsString("No resource set, skipping 'terraform import'.")) 22 | } 23 | 24 | @Test 25 | void doesntRunWithoutTargetPath() { 26 | def command = new TerraformImportCommand().withResource("foo") 27 | 28 | def actualCommand = command.toString() 29 | assertThat(actualCommand, containsString("No target path set, skipping 'terraform import'.")) 30 | } 31 | 32 | @Test 33 | void runsCommandWhenTargetIsAlsoSet() { 34 | def command = new TerraformImportCommand().withResource("foo").withTargetPath("target.foo") 35 | 36 | def actualCommand = command.toString() 37 | assertThat(actualCommand, containsString("terraform import target.foo foo")) 38 | } 39 | } 40 | 41 | @Nested 42 | public class WithTargetPath { 43 | @Test 44 | void defaultsToEmpty() { 45 | def command = new TerraformImportCommand() 46 | 47 | assertThat(command.targetPath, blankString()) 48 | } 49 | 50 | @Test 51 | void doesntRunWithoutResource() { 52 | def command = new TerraformImportCommand().withTargetPath("target.foo") 53 | 54 | def actualCommand = command.toString() 55 | assertThat(actualCommand, containsString("No resource set, skipping 'terraform import'.")) 56 | } 57 | } 58 | 59 | @Nested 60 | public class Plugins { 61 | @Test 62 | void areAppliedToTheCommand() { 63 | TerraformImportCommandPlugin plugin = mock(TerraformImportCommandPlugin.class) 64 | TerraformImportCommand.addPlugin(plugin) 65 | 66 | TerraformImportCommand command = TerraformImportCommand.instanceFor("env") 67 | command.toString() 68 | 69 | verify(plugin).apply(command) 70 | } 71 | 72 | @Test 73 | void areAppliedExactlyOnce() { 74 | TerraformImportCommandPlugin plugin = mock(TerraformImportCommandPlugin.class) 75 | TerraformImportCommand.addPlugin(plugin) 76 | 77 | TerraformImportCommand command = TerraformImportCommand.instanceFor("env") 78 | 79 | String firstCommand = command.toString() 80 | String secondCommand = command.toString() 81 | 82 | verify(plugin, times(1)).apply(command) 83 | } 84 | 85 | @Test 86 | void areAppliedEvenAfterCommandAlreadyInstantiated() { 87 | TerraformImportCommandPlugin firstPlugin = mock(TerraformImportCommandPlugin.class) 88 | TerraformImportCommandPlugin secondPlugin = mock(TerraformImportCommandPlugin.class) 89 | 90 | TerraformImportCommand.addPlugin(firstPlugin) 91 | TerraformImportCommand command = TerraformImportCommand.instanceFor("env") 92 | 93 | TerraformImportCommand.addPlugin(secondPlugin) 94 | 95 | command.toString() 96 | 97 | verify(secondPlugin, times(1)).apply(command) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /test/TerraformLandscapePluginTest.groovy: -------------------------------------------------------------------------------- 1 | import static org.hamcrest.Matchers.containsString 2 | import static org.hamcrest.Matchers.hasItem 3 | import static org.hamcrest.Matchers.instanceOf 4 | import static org.hamcrest.MatcherAssert.assertThat 5 | 6 | import org.junit.jupiter.api.Nested 7 | import org.junit.jupiter.api.Test 8 | import org.junit.jupiter.api.extension.ExtendWith 9 | 10 | @ExtendWith(ResetStaticStateExtension.class) 11 | class TerraformLandscapePluginTest { 12 | @Nested 13 | public class Init { 14 | @Test 15 | void modifiesTerraformPlanCommand() { 16 | TerraformLandscapePlugin.init() 17 | 18 | Collection actualPlugins = TerraformPlanCommand.getPlugins() 19 | assertThat(actualPlugins, hasItem(instanceOf(TerraformLandscapePlugin.class))) 20 | } 21 | } 22 | 23 | @Nested 24 | public class Apply { 25 | @Test 26 | void addsLandscapeArgumentToTerraformPlan() { 27 | TerraformLandscapePlugin plugin = new TerraformLandscapePlugin() 28 | TerraformPlanCommand command = new TerraformPlanCommand() 29 | 30 | plugin.apply(command) 31 | 32 | String result = command.toString() 33 | assertThat(result, containsString("| landscape")) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/TerraformOutputCommandTest.groovy: -------------------------------------------------------------------------------- 1 | import static org.hamcrest.Matchers.containsString 2 | import static org.hamcrest.Matchers.not 3 | import static org.hamcrest.MatcherAssert.assertThat 4 | import static org.mockito.Mockito.mock 5 | import static org.mockito.Mockito.times 6 | import static org.mockito.Mockito.verify 7 | 8 | import org.junit.jupiter.api.Nested 9 | import org.junit.jupiter.api.Test 10 | import org.junit.jupiter.api.extension.ExtendWith 11 | 12 | @ExtendWith(ResetStaticStateExtension.class) 13 | class TerraformOutputCommandTest { 14 | @Nested 15 | public class WithJson { 16 | @Test 17 | void defaultsToFalse() { 18 | def command = new TerraformOutputCommand() 19 | 20 | def actualCommand = command.toString() 21 | assertThat(actualCommand, not(containsString("-json"))) 22 | } 23 | 24 | @Test 25 | void skipsJsonFlagWhenFalse() { 26 | def command = new TerraformOutputCommand().withJson(false) 27 | 28 | def actualCommand = command.toString() 29 | assertThat(actualCommand, not(containsString(" -json"))) 30 | } 31 | 32 | @Test 33 | void addsJsonFlagWhenTrue() { 34 | def command = new TerraformOutputCommand().withJson(true) 35 | 36 | def actualCommand = command.toString() 37 | assertThat(actualCommand, containsString(" -json")) 38 | } 39 | } 40 | 41 | @Nested 42 | public class WithRedirectFile { 43 | @Test 44 | void defaultsToEmpty() { 45 | def command = new TerraformOutputCommand() 46 | 47 | def actualCommand = command.toString() 48 | assertThat(actualCommand, not(containsString(">"))) 49 | } 50 | 51 | @Test 52 | void addsRedirectWhenSet() { 53 | def command = new TerraformOutputCommand().withRedirectFile("foo") 54 | 55 | def actualCommand = command.toString() 56 | assertThat(actualCommand, containsString(">foo")) 57 | } 58 | } 59 | 60 | @Nested 61 | public class Plugins { 62 | @Test 63 | void areAppliedToTheCommand() { 64 | TerraformOutputCommandPlugin plugin = mock(TerraformOutputCommandPlugin.class) 65 | TerraformOutputCommand.addPlugin(plugin) 66 | 67 | TerraformOutputCommand command = TerraformOutputCommand.instanceFor("env") 68 | command.toString() 69 | 70 | verify(plugin).apply(command) 71 | } 72 | 73 | @Test 74 | void areAppliedExactlyOnce() { 75 | TerraformOutputCommandPlugin plugin = mock(TerraformOutputCommandPlugin.class) 76 | TerraformOutputCommand.addPlugin(plugin) 77 | 78 | TerraformOutputCommand command = TerraformOutputCommand.instanceFor("env") 79 | 80 | String firstCommand = command.toString() 81 | String secondCommand = command.toString() 82 | 83 | verify(plugin, times(1)).apply(command) 84 | } 85 | 86 | @Test 87 | void areAppliedEvenAfterCommandAlreadyInstantiated() { 88 | TerraformOutputCommandPlugin firstPlugin = mock(TerraformOutputCommandPlugin.class) 89 | TerraformOutputCommandPlugin secondPlugin = mock(TerraformOutputCommandPlugin.class) 90 | 91 | TerraformOutputCommand.addPlugin(firstPlugin) 92 | TerraformOutputCommand command = TerraformOutputCommand.instanceFor("env") 93 | 94 | TerraformOutputCommand.addPlugin(secondPlugin) 95 | 96 | command.toString() 97 | 98 | verify(secondPlugin, times(1)).apply(command) 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /test/TerraformPluginVersion11Test.groovy: -------------------------------------------------------------------------------- 1 | import static org.hamcrest.Matchers.containsString 2 | import static org.hamcrest.MatcherAssert.assertThat 3 | import static org.mockito.Mockito.spy 4 | import static org.mockito.Mockito.verify; 5 | 6 | import org.junit.jupiter.api.Nested 7 | import org.junit.jupiter.api.Test 8 | import org.junit.jupiter.api.extension.ExtendWith 9 | 10 | @ExtendWith(ResetStaticStateExtension.class) 11 | class TerraformPluginVersion11Test { 12 | @Nested 13 | class ModifiesTerraformValidateCommand { 14 | @Test 15 | void addsCheckVariablesFalseToValidateCommand() { 16 | def validateCommand = spy(new TerraformValidateCommand()) 17 | def version11 = new TerraformPluginVersion11() 18 | 19 | version11.apply((TerraformValidateCommand)validateCommand) 20 | 21 | verify(validateCommand).withArgument('-check-variables=false') 22 | } 23 | } 24 | 25 | @Nested 26 | class ModifiesTerraformPlanCommand { 27 | @Test 28 | void toPreserveTerraform11CliSyntaxForVariables() { 29 | def plan = new TerraformPlanCommand() 30 | def version11 = new TerraformPluginVersion11() 31 | 32 | version11.apply(plan) 33 | plan.withVariable('key', 'value') 34 | def result = plan.toString() 35 | 36 | assertThat(result, containsString("-var 'key=value'")) 37 | } 38 | 39 | @Test 40 | void toPreserveTerraform11CliFormatForMapVariables() { 41 | def plan = new TerraformPlanCommand() 42 | def version11 = new TerraformPluginVersion11() 43 | 44 | version11.apply(plan) 45 | plan.withVariable('myMap', [key1:'value1', key2: 'value2']) 46 | def result = plan.toString() 47 | 48 | assertThat(result, containsString("-var 'myMap={key1=\"value1\",key2=\"value2\"}'")) 49 | } 50 | } 51 | 52 | @Nested 53 | class ModifiesTerraformApplyCommand { 54 | @Test 55 | void toPreserveTerraform11CliSyntaxForVariables() { 56 | def applyCommand = new TerraformApplyCommand() 57 | def version11 = new TerraformPluginVersion11() 58 | 59 | version11.apply(applyCommand) 60 | applyCommand.withVariable('key', 'value') 61 | def result = applyCommand.toString() 62 | 63 | assertThat(result, containsString("-var 'key=value'")) 64 | } 65 | 66 | @Test 67 | void toPreserveTerraform11CliFormatForMapVariables() { 68 | def applyCommand = new TerraformPlanCommand() 69 | def version11 = new TerraformPluginVersion11() 70 | 71 | version11.apply(applyCommand) 72 | applyCommand.withVariable('myMap', [key1:'value1', key2: 'value2']) 73 | def result = applyCommand.toString() 74 | 75 | assertThat(result, containsString("-var 'myMap={key1=\"value1\",key2=\"value2\"}'")) 76 | } 77 | } 78 | } 79 | 80 | -------------------------------------------------------------------------------- /test/TerraformPluginVersion15Test.groovy: -------------------------------------------------------------------------------- 1 | import static org.hamcrest.Matchers.containsString 2 | import static org.hamcrest.MatcherAssert.assertThat 3 | import org.junit.jupiter.api.Nested 4 | import org.junit.jupiter.api.Test 5 | import org.junit.jupiter.api.extension.ExtendWith 6 | 7 | @ExtendWith(ResetStaticStateExtension.class) 8 | class TerraformPluginVersion15Test { 9 | @Nested 10 | class ModifiesTerraformValidateCommand { 11 | @Test 12 | void toUseTerraform15CliSyntaxForDirectory() { 13 | def validate = new TerraformValidateCommand() 14 | def version15 = new TerraformPluginVersion15() 15 | 16 | version15.apply(validate) 17 | validate.withDirectory('foobar') 18 | def result = validate.toString() 19 | 20 | assertThat(result, containsString(" -chdir=foobar")) 21 | } 22 | } 23 | 24 | @Nested 25 | class ModifiesTerraformInitCommand { 26 | @Test 27 | void toUseTerraform15CliSyntaxForDirectory() { 28 | def init = new TerraformInitCommand() 29 | def version15 = new TerraformPluginVersion15() 30 | 31 | version15.apply(init) 32 | init.withDirectory('foobar') 33 | def result = init.toString() 34 | 35 | assertThat(result, containsString(" -chdir=foobar")) 36 | } 37 | } 38 | 39 | @Nested 40 | class ModifiesTerraformPlanCommand { 41 | @Test 42 | void toUseTerraform15CliSyntaxForDirectory() { 43 | def plan = new TerraformPlanCommand() 44 | def version15 = new TerraformPluginVersion15() 45 | 46 | version15.apply(plan) 47 | plan.withDirectory('foobar') 48 | def result = plan.toString() 49 | 50 | assertThat(result, containsString(" -chdir=foobar")) 51 | } 52 | } 53 | 54 | @Nested 55 | class ModifiesTerraformApplyCommand { 56 | @Test 57 | void toUseTerraform15CliSyntaxForDirectory() { 58 | def apply = new TerraformApplyCommand() 59 | def version15 = new TerraformPluginVersion15() 60 | 61 | version15.apply(apply) 62 | apply.withDirectory('foobar') 63 | def result = apply.toString() 64 | 65 | assertThat(result, containsString(" -chdir=foobar")) 66 | } 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /test/TerraformTaintCommandTest.groovy: -------------------------------------------------------------------------------- 1 | import static org.hamcrest.Matchers.equalTo 2 | import static org.hamcrest.MatcherAssert.assertThat 3 | import static org.mockito.Mockito.mock 4 | import static org.mockito.Mockito.times 5 | import static org.mockito.Mockito.verify 6 | 7 | import org.junit.jupiter.api.Nested 8 | import org.junit.jupiter.api.Test 9 | import org.junit.jupiter.api.extension.ExtendWith 10 | 11 | @ExtendWith(ResetStaticStateExtension.class) 12 | class TerraformTaintCommandTest { 13 | @Nested 14 | public class WithResource { 15 | @Test 16 | void defaultsToEmpty() { 17 | def command = new TerraformTaintCommand() 18 | 19 | def actualCommand = command.toString() 20 | assertThat(actualCommand, equalTo("terraform taint")) 21 | } 22 | 23 | @Test 24 | void addsResourceWhenSet() { 25 | def command = new TerraformTaintCommand().withResource("foo") 26 | 27 | def actualCommand = command.toString() 28 | assertThat(actualCommand, equalTo("terraform taint foo")) 29 | } 30 | } 31 | 32 | @Nested 33 | public class Plugins { 34 | @Test 35 | void areAppliedToTheCommand() { 36 | TerraformTaintCommandPlugin plugin = mock(TerraformTaintCommandPlugin.class) 37 | TerraformTaintCommand.addPlugin(plugin) 38 | 39 | TerraformTaintCommand command = new TerraformTaintCommand() 40 | command.environment = "env" 41 | command.toString() 42 | 43 | verify(plugin).apply(command) 44 | } 45 | 46 | @Test 47 | void areAppliedExactlyOnce() { 48 | TerraformTaintCommandPlugin plugin = mock(TerraformTaintCommandPlugin.class) 49 | TerraformTaintCommand.addPlugin(plugin) 50 | 51 | TerraformTaintCommand command = new TerraformTaintCommand() 52 | command.environment = "env" 53 | 54 | String firstCommand = command.toString() 55 | String secondCommand = command.toString() 56 | 57 | verify(plugin, times(1)).apply(command) 58 | } 59 | 60 | @Test 61 | void areAppliedEvenAfterCommandAlreadyInstantiated() { 62 | TerraformTaintCommandPlugin firstPlugin = mock(TerraformTaintCommandPlugin.class) 63 | TerraformTaintCommandPlugin secondPlugin = mock(TerraformTaintCommandPlugin.class) 64 | 65 | TerraformTaintCommand.addPlugin(firstPlugin) 66 | TerraformTaintCommand command = new TerraformTaintCommand() 67 | command.environment = "env" 68 | 69 | TerraformTaintCommand.addPlugin(secondPlugin) 70 | 71 | command.toString() 72 | 73 | verify(firstPlugin, times(1)).apply(command) 74 | verify(secondPlugin, times(1)).apply(command) 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /test/TerraformUntaintCommandTest.groovy: -------------------------------------------------------------------------------- 1 | import static org.hamcrest.Matchers.equalTo 2 | import static org.hamcrest.MatcherAssert.assertThat 3 | import static org.mockito.Mockito.mock 4 | import static org.mockito.Mockito.times 5 | import static org.mockito.Mockito.verify 6 | 7 | import org.junit.jupiter.api.Nested 8 | import org.junit.jupiter.api.Test 9 | import org.junit.jupiter.api.extension.ExtendWith 10 | 11 | @ExtendWith(ResetStaticStateExtension.class) 12 | class TerraformUntaintCommandTest { 13 | @Nested 14 | public class WithResource { 15 | @Test 16 | void defaultsToEmpty() { 17 | def command = new TerraformUntaintCommand() 18 | 19 | def actualCommand = command.toString() 20 | assertThat(actualCommand, equalTo("terraform untaint")) 21 | } 22 | 23 | @Test 24 | void addsResourceWhenSet() { 25 | def command = new TerraformUntaintCommand().withResource("foo") 26 | 27 | def actualCommand = command.toString() 28 | assertThat(actualCommand, equalTo("terraform untaint foo")) 29 | } 30 | } 31 | 32 | @Nested 33 | public class Plugins { 34 | @Test 35 | void areAppliedToTheCommand() { 36 | TerraformUntaintCommandPlugin plugin = mock(TerraformUntaintCommandPlugin.class) 37 | TerraformUntaintCommand.addPlugin(plugin) 38 | 39 | TerraformUntaintCommand command = new TerraformUntaintCommand() 40 | command.environment = "env" 41 | command.toString() 42 | 43 | verify(plugin).apply(command) 44 | } 45 | 46 | @Test 47 | void areAppliedExactlyOnce() { 48 | TerraformUntaintCommandPlugin plugin = mock(TerraformUntaintCommandPlugin.class) 49 | TerraformUntaintCommand.addPlugin(plugin) 50 | 51 | TerraformUntaintCommand command = new TerraformUntaintCommand() 52 | command.environment = "env" 53 | 54 | String firstCommand = command.toString() 55 | String secondCommand = command.toString() 56 | 57 | verify(plugin, times(1)).apply(command) 58 | } 59 | 60 | @Test 61 | void areAppliedEvenAfterCommandAlreadyInstantiated() { 62 | TerraformUntaintCommandPlugin firstPlugin = mock(TerraformUntaintCommandPlugin.class) 63 | TerraformUntaintCommandPlugin secondPlugin = mock(TerraformUntaintCommandPlugin.class) 64 | 65 | TerraformUntaintCommand.addPlugin(firstPlugin) 66 | TerraformUntaintCommand command = new TerraformUntaintCommand() 67 | command.environment = "env" 68 | 69 | TerraformUntaintCommand.addPlugin(secondPlugin) 70 | 71 | command.toString() 72 | 73 | verify(firstPlugin, times(1)).apply(command) 74 | verify(secondPlugin, times(1)).apply(command) 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /test/TerraformValidateStageTest.groovy: -------------------------------------------------------------------------------- 1 | import static org.mockito.Mockito.mock 2 | import static org.mockito.Mockito.spy 3 | import static org.hamcrest.MatcherAssert.assertThat 4 | import static org.hamcrest.CoreMatchers.is 5 | import static org.hamcrest.CoreMatchers.instanceOf 6 | 7 | import org.junit.jupiter.api.Nested 8 | import org.junit.jupiter.api.Test 9 | import org.junit.jupiter.api.extension.ExtendWith 10 | 11 | @ExtendWith(ResetStaticStateExtension.class) 12 | class TerraformValidateStageTest { 13 | @Nested 14 | public class PipelineConfiguration { 15 | @Test 16 | void returnsAJobDslClosure() { 17 | def validateStage = new TerraformValidateStage() 18 | 19 | def result = validateStage.pipelineConfiguration() 20 | 21 | assertThat(result, is(instanceOf(Closure.class))) 22 | } 23 | 24 | // This should be split into separate tests, and assert behavior 25 | @Test 26 | void justExerciseClosureNoAssertions() { 27 | Jenkinsfile.instance = spy(new Jenkinsfile()) 28 | Jenkinsfile.original = new MockWorkflowScript() 29 | def validateStage = new TerraformValidateStage() 30 | 31 | def closure = validateStage.pipelineConfiguration() 32 | closure.delegate = Jenkinsfile.original 33 | closure() 34 | } 35 | } 36 | 37 | @Nested 38 | public class Then { 39 | @Test 40 | void nextStageisCalled() { 41 | def stage = new TerraformValidateStage() 42 | def stage2 = mock(Stage.class) 43 | 44 | def result = stage.then(stage2) 45 | 46 | assertThat(result, is(instanceOf(BuildGraph.class))) 47 | } 48 | } 49 | 50 | @Nested 51 | public class Build { 52 | @Test 53 | void justExerciseNoAssertions() { 54 | def stage = new TerraformValidateStage() 55 | MockJenkinsfile.withMockedOriginal() 56 | 57 | stage.build() 58 | } 59 | } 60 | 61 | @Nested 62 | public class Decorate { 63 | @Test 64 | void justExerciseNoAssertions() { 65 | def stage = new TerraformValidateStage() 66 | 67 | stage.decorate { } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /test/ValidateFormatPluginTest.groovy: -------------------------------------------------------------------------------- 1 | import static org.hamcrest.Matchers.hasItem 2 | import static org.hamcrest.Matchers.instanceOf 3 | import static org.hamcrest.MatcherAssert.assertThat 4 | import static org.junit.jupiter.api.Assertions.assertTrue 5 | import static org.mockito.Mockito.doReturn; 6 | import static org.mockito.Mockito.spy; 7 | import static org.mockito.Mockito.verify; 8 | 9 | import org.junit.jupiter.api.Nested 10 | import org.junit.jupiter.api.Test 11 | import org.junit.jupiter.api.extension.ExtendWith 12 | 13 | @ExtendWith(ResetStaticStateExtension.class) 14 | class ValidateFormatPluginTest { 15 | public class Init { 16 | @Test 17 | void modifiesTerraformValidateStage() { 18 | ValidateFormatPlugin.init() 19 | 20 | Collection actualPlugins = TerraformValidateStage.getPlugins() 21 | assertThat(actualPlugins, hasItem(instanceOf(ValidateFormatPlugin.class))) 22 | } 23 | 24 | @Test 25 | void enablesCheckOnTerraformFormat() { 26 | ValidateFormatPlugin.init() 27 | 28 | assertTrue(TerraformFormatCommand.isCheckEnabled()) 29 | } 30 | } 31 | 32 | @Nested 33 | public class ApplyForValidateStage { 34 | @Test 35 | void addsClosureToRunTerraformFormat() { 36 | def expectedClosure = { -> } 37 | def validateStage = spy(new TerraformValidateStage()) 38 | def plugin = spy(new ValidateFormatPlugin()) 39 | doReturn(expectedClosure).when(plugin).formatClosure() 40 | 41 | plugin.apply(validateStage) 42 | 43 | verify(validateStage).decorate(TerraformValidateStage.VALIDATE, expectedClosure) 44 | } 45 | } 46 | 47 | @Nested 48 | public class FormatClosure { 49 | @Test 50 | void runsTheGivenInnerClosure() { 51 | def wasRun = false 52 | def innerClosure = { -> wasRun = true } 53 | def original = spy(new MockWorkflowScript()) 54 | def plugin = new ValidateFormatPlugin() 55 | 56 | def formatClosure = plugin.formatClosure() 57 | formatClosure.delegate = original 58 | formatClosure.call(innerClosure) 59 | 60 | assertTrue(wasRun) 61 | } 62 | 63 | @Test 64 | void runsTerraformFormatCommandInAShell() { 65 | def expectedFormatCommand = 'terraform fmt' 66 | def original = spy(new MockWorkflowScript()) 67 | def plugin = new ValidateFormatPlugin() 68 | 69 | def formatClosure = plugin.formatClosure() 70 | formatClosure.delegate = original 71 | formatClosure.call { -> } 72 | 73 | verify(original).sh(expectedFormatCommand) 74 | } 75 | } 76 | } 77 | 78 | -------------------------------------------------------------------------------- /test/WithAwsPluginTest.groovy: -------------------------------------------------------------------------------- 1 | import static org.hamcrest.MatcherAssert.assertThat 2 | import static org.hamcrest.Matchers.equalTo 3 | import static org.hamcrest.Matchers.hasItem 4 | import static org.hamcrest.Matchers.instanceOf 5 | import static org.hamcrest.Matchers.is 6 | 7 | import org.junit.jupiter.api.Nested 8 | import org.junit.jupiter.api.Test 9 | import org.junit.jupiter.api.extension.ExtendWith 10 | 11 | @ExtendWith(ResetStaticStateExtension.class) 12 | class WithAwsPluginTest { 13 | @Nested 14 | public class Init { 15 | @Test 16 | void modifiesTerraformEnvironmentStage() { 17 | WithAwsPlugin.init() 18 | 19 | Collection actualPlugins = TerraformEnvironmentStage.getPlugins() 20 | assertThat(actualPlugins, hasItem(instanceOf(WithAwsPlugin.class))) 21 | } 22 | } 23 | 24 | public class WithRole { 25 | @Test 26 | void isFluentAndReturnsThePluginClass() { 27 | def result = WithAwsPlugin.withRole() 28 | 29 | assertThat(result, equalTo(WithAwsPlugin)) 30 | } 31 | } 32 | 33 | @Nested 34 | public class WithImplicitRole { 35 | @Test 36 | void returnsGenericRoleIfPresent() { 37 | def expectedRole = "myRole" 38 | def plugin = new WithAwsPlugin() 39 | MockJenkinsfile.withEnv(AWS_ROLE_ARN: expectedRole) 40 | 41 | plugin.withRole() 42 | 43 | def actualRole = plugin.getRole() 44 | assertThat(actualRole, is(expectedRole)) 45 | } 46 | 47 | @Test 48 | void returnsEnvironmentSpecificRoleIfPresent() { 49 | def expectedRole = "myRole" 50 | def plugin = new WithAwsPlugin() 51 | MockJenkinsfile.withEnv(QA_AWS_ROLE_ARN: expectedRole) 52 | 53 | plugin.withRole() 54 | 55 | def actualRole = plugin.getRole('qa') 56 | assertThat(actualRole, is(expectedRole)) 57 | } 58 | 59 | @Test 60 | void returnsCaseInsensitiveEnvironmentSpecificRoleIfPresent() { 61 | def expectedRole = "myRole" 62 | def plugin = new WithAwsPlugin() 63 | MockJenkinsfile.withEnv(qa_AWS_ROLE_ARN: expectedRole) 64 | 65 | plugin.withRole() 66 | 67 | def actualRole = plugin.getRole('qa') 68 | assertThat(actualRole, is(expectedRole)) 69 | } 70 | 71 | @Test 72 | void prefersGenericRoleOverEnvironmentRole() { 73 | def expectedRole = "correctRole" 74 | def plugin = new WithAwsPlugin() 75 | MockJenkinsfile.withEnv([ 76 | AWS_ROLE_ARN: expectedRole, 77 | QA_AWS_ROLE_ARN: 'incorrectRole' 78 | ]) 79 | 80 | plugin.withRole() 81 | 82 | def actualRole = plugin.getRole('qa') 83 | assertThat(actualRole, is(expectedRole)) 84 | } 85 | } 86 | 87 | @Nested 88 | public class WithDefaultDuration { 89 | @Test 90 | void returnsDefaultDuration() { 91 | def expectedDuration = 3600 92 | def plugin = new WithAwsPlugin() 93 | MockJenkinsfile.withEnv(AWS_ROLE_ARN: 'foo') 94 | 95 | def actualDuration = plugin.getDuration() 96 | assertThat(actualDuration, is(expectedDuration)) 97 | } 98 | } 99 | 100 | @Nested 101 | public class WithExplicitDuration { 102 | @Test 103 | void returnsExplicitDuration() { 104 | def expectedDuration = 43200 105 | def plugin = new WithAwsPlugin() 106 | 107 | plugin.withDuration(expectedDuration) 108 | 109 | def actualDuration = plugin.getDuration() 110 | 111 | assertThat(actualDuration, is(expectedDuration)) 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /vars/ApplyJenkinsfileClosure.groovy: -------------------------------------------------------------------------------- 1 | def call(closure) { 2 | closure.delegate = this 3 | closure.call() 4 | } 5 | -------------------------------------------------------------------------------- /vars/Pipeline2Stage.groovy: -------------------------------------------------------------------------------- 1 | def call(args) { 2 | pipeline { 3 | agent none 4 | options { preserveStashes() } 5 | 6 | stages { 7 | stage('1') { 8 | steps { 9 | script { 10 | ((Stage)args.getAt(0)).build() 11 | } 12 | } 13 | } 14 | 15 | stage('2') { 16 | steps { 17 | script { 18 | ((Stage)args.getAt(1)).build() 19 | } 20 | } 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /vars/Pipeline3Stage.groovy: -------------------------------------------------------------------------------- 1 | def call(args) { 2 | pipeline { 3 | agent none 4 | options { preserveStashes() } 5 | 6 | stages { 7 | stage('1') { 8 | steps { 9 | script { 10 | ((Stage)args.getAt(0)).build() 11 | } 12 | } 13 | } 14 | 15 | stage('2') { 16 | steps { 17 | script { 18 | ((Stage)args.getAt(1)).build() 19 | } 20 | } 21 | } 22 | 23 | stage('3') { 24 | steps { 25 | script { 26 | ((Stage)args.getAt(2)).build() 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /vars/Pipeline4Stage.groovy: -------------------------------------------------------------------------------- 1 | def call(args) { 2 | pipeline { 3 | agent none 4 | options { preserveStashes() } 5 | 6 | stages { 7 | stage('1') { 8 | steps { 9 | script { 10 | ((Stage)args.getAt(0)).build() 11 | } 12 | } 13 | } 14 | 15 | stage('2') { 16 | steps { 17 | script { 18 | ((Stage)args.getAt(1)).build() 19 | } 20 | } 21 | } 22 | 23 | stage('3') { 24 | steps { 25 | script { 26 | ((Stage)args.getAt(2)).build() 27 | } 28 | } 29 | } 30 | 31 | stage('4') { 32 | steps { 33 | script { 34 | ((Stage)args.getAt(3)).build() 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /vars/Pipeline5Stage.groovy: -------------------------------------------------------------------------------- 1 | def call(args) { 2 | pipeline { 3 | agent none 4 | options { preserveStashes() } 5 | 6 | stages { 7 | stage('1') { 8 | steps { 9 | script { 10 | ((Stage)args.getAt(0)).build() 11 | } 12 | } 13 | } 14 | 15 | stage('2') { 16 | steps { 17 | script { 18 | ((Stage)args.getAt(1)).build() 19 | } 20 | } 21 | } 22 | 23 | stage('3') { 24 | steps { 25 | script { 26 | ((Stage)args.getAt(2)).build() 27 | } 28 | } 29 | } 30 | 31 | stage('4') { 32 | steps { 33 | script { 34 | ((Stage)args.getAt(3)).build() 35 | } 36 | } 37 | } 38 | 39 | stage('5') { 40 | steps { 41 | script { 42 | ((Stage)args.getAt(4)).build() 43 | } 44 | } 45 | } 46 | } 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /vars/Pipeline6Stage.groovy: -------------------------------------------------------------------------------- 1 | def call(args) { 2 | pipeline { 3 | agent none 4 | options { preserveStashes() } 5 | 6 | stages { 7 | stage('1') { 8 | steps { 9 | script { 10 | ((Stage)args.getAt(0)).build() 11 | } 12 | } 13 | } 14 | 15 | stage('2') { 16 | steps { 17 | script { 18 | ((Stage)args.getAt(1)).build() 19 | } 20 | } 21 | } 22 | 23 | stage('3') { 24 | steps { 25 | script { 26 | ((Stage)args.getAt(2)).build() 27 | } 28 | } 29 | } 30 | 31 | stage('4') { 32 | steps { 33 | script { 34 | ((Stage)args.getAt(3)).build() 35 | } 36 | } 37 | } 38 | 39 | stage('5') { 40 | steps { 41 | script { 42 | ((Stage)args.getAt(4)).build() 43 | } 44 | } 45 | } 46 | 47 | stage('6') { 48 | steps { 49 | script { 50 | ((Stage)args.getAt(5)).build() 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /vars/Pipeline7Stage.groovy: -------------------------------------------------------------------------------- 1 | def call(args) { 2 | pipeline { 3 | agent none 4 | options { preserveStashes() } 5 | 6 | stages { 7 | stage('1') { 8 | steps { 9 | script { 10 | ((Stage)args.getAt(0)).build() 11 | } 12 | } 13 | } 14 | 15 | stage('2') { 16 | steps { 17 | script { 18 | ((Stage)args.getAt(1)).build() 19 | } 20 | } 21 | } 22 | 23 | stage('3') { 24 | steps { 25 | script { 26 | ((Stage)args.getAt(2)).build() 27 | } 28 | } 29 | } 30 | 31 | stage('4') { 32 | steps { 33 | script { 34 | ((Stage)args.getAt(3)).build() 35 | } 36 | } 37 | } 38 | 39 | stage('5') { 40 | steps { 41 | script { 42 | ((Stage)args.getAt(4)).build() 43 | } 44 | } 45 | } 46 | 47 | stage('6') { 48 | steps { 49 | script { 50 | ((Stage)args.getAt(5)).build() 51 | } 52 | } 53 | } 54 | 55 | stage('7') { 56 | steps { 57 | script { 58 | ((Stage)args.getAt(6)).build() 59 | } 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | --------------------------------------------------------------------------------