diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e9ef3e0..017676bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ # Unpublished changes * [Issue #193](https://github.com/manheim/terraform-pipeline/issues/193) Support passing branch plans to Github PRs +* [Issue #102](https://github.com/manheim/terraform-pipeline/issues/102) Support target when running terraform plan and apply # v5.7 diff --git a/README.md b/README.md index baa20e5b..4e6c7850 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ The example above gives you a bare-bones pipeline, and there may be Jenkinsfile * [TerraformDirectoryPlugin](./docs/TerraformDirectoryPlugin.md): Change the default directory containing your terraform code. * [TerraformLandscapePlugin](./docs/TerraformLandscapePlugin.md): Enable terraform-landscape plan output. * [TerraformPlanResultsPR](./docs/TerraformPlanResultsPR.md): Use this to post Terraform plan results in the comments of a PR. +* [TargetPlugin](./docs/TargetPlugin.md): set `-target` parameter for terraform plan and apply. ## Write your own Plugin diff --git a/docs/TargetPlugin.md b/docs/TargetPlugin.md new file mode 100644 index 00000000..1650499d --- /dev/null +++ b/docs/TargetPlugin.md @@ -0,0 +1,27 @@ +## [TargetPlugin](../src/TargetPlugin.groovy) + +Enable this plugin to run plan/apply on selective resource targets + +``` +// Jenkinsfile +@Library(['terraform-pipeline']) _ + +Jenkinsfile.init(this, env) +TargetPlugin.init() // Optionally limit plan/apply to specific targets + + +def validate = new TerraformValidateStage() + +def deployQa = new TerraformEnvironmentStage('qa') +def deployUat = new TerraformEnvironmentStage('uat') +def deployProd = new TerraformEnvironmentStage('prod') + + +// Pipeline can now be built with "Build with Parameters" +// New 'target' option in parameter list can limit plan/apply to specific targets +// By default, builds should skip the 'target' option, and build as-normal +validate.then(deployQa) + .then(deployUat) + .then(deployProd) + .build() +``` diff --git a/src/TargetPlugin.groovy b/src/TargetPlugin.groovy new file mode 100644 index 00000000..48684003 --- /dev/null +++ b/src/TargetPlugin.groovy @@ -0,0 +1,43 @@ +import static TerraformEnvironmentStage.ALL + +class TargetPlugin implements TerraformPlanCommandPlugin, TerraformApplyCommandPlugin, TerraformEnvironmentStagePlugin { + public static void init() { + TargetPlugin plugin = new TargetPlugin() + + TerraformPlanCommand.addPlugin(plugin) + TerraformApplyCommand.addPlugin(plugin) + TerraformEnvironmentStage.addPlugin(plugin) + } + + @Override + public void apply(TerraformPlanCommand command) { + def targets = Jenkinsfile.instance.getEnv().RESOURCE_TARGETS ?: '' + targets.split(',').each { item -> command.withArgument("-target ${item}") } + } + + @Override + public void apply(TerraformApplyCommand command) { + def targets = Jenkinsfile.instance.getEnv().RESOURCE_TARGETS ?: '' + targets.split(',').each { item -> command.withArgument("-target ${item}") } + } + + @Override + public void apply(TerraformEnvironmentStage stage) { + stage.decorate(ALL, addBuildParams()) + } + + public static Closure addBuildParams() { + return { closure -> + def params = [ + string(name: 'RESOURCE_TARGETS', defaultValue: '', description: 'comma-separated list of resource addresses to pass to plan and apply "-target=" parameters'), + ] + def props = [ + parameters(params) + ] + properties(props) + + closure() + } + } + +} diff --git a/test/TargetPluginTest.groovy b/test/TargetPluginTest.groovy new file mode 100644 index 00000000..4564ac8f --- /dev/null +++ b/test/TargetPluginTest.groovy @@ -0,0 +1,109 @@ +import static org.hamcrest.Matchers.containsString +import static org.hamcrest.Matchers.hasItem +import static org.hamcrest.Matchers.instanceOf +import static org.junit.Assert.assertThat +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.times; +import static TerraformEnvironmentStage.ALL; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.mock; + +import org.junit.Test +import org.junit.Before +import org.junit.After +import org.junit.runner.RunWith +import de.bechte.junit.runners.context.HierarchicalContextRunner + +@RunWith(HierarchicalContextRunner.class) +class TargetPluginTest { + @Before + void resetJenkinsEnv() { + Jenkinsfile.instance = mock(Jenkinsfile.class) + when(Jenkinsfile.instance.getEnv()).thenReturn([:]) + } + + private configureJenkins(Map config = [:]) { + Jenkinsfile.instance = mock(Jenkinsfile.class) + when(Jenkinsfile.instance.getEnv()).thenReturn(config.env ?: [:]) + } + + public class Init { + @After + void resetPlugins() { + TerraformPlanCommand.resetPlugins() + TerraformApplyCommand.resetPlugins() + TerraformEnvironmentStage.resetPlugins() + } + + @Test + void modifiesTerraformPlanCommand() { + TargetPlugin.init() + + Collection actualPlugins = TerraformPlanCommand.getPlugins() + assertThat(actualPlugins, hasItem(instanceOf(TargetPlugin.class))) + } + + @Test + void modifiesTerraformApplyCommand() { + TargetPlugin.init() + + Collection actualPlugins = TerraformApplyCommand.getPlugins() + assertThat(actualPlugins, hasItem(instanceOf(TargetPlugin.class))) + } + + @Test + void modifiesTerraformEnvironmentStageCommand() { + TargetPlugin.init() + + Collection actualPlugins = TerraformEnvironmentStage.getPlugins() + assertThat(actualPlugins, hasItem(instanceOf(TargetPlugin.class))) + } + } + + public class Apply { + + @Test + void addsTargetArgumentToTerraformPlan() { + TargetPlugin plugin = new TargetPlugin() + TerraformPlanCommand command = new TerraformPlanCommand() + configureJenkins(env: [ + 'RESOURCE_TARGETS': 'aws_dynamodb_table.test-table-2,aws_dynamodb_table.test-table-3' + ]) + + plugin.apply(command) + + String result = command.toString() + assertThat(result, containsString(" -target aws_dynamodb_table.test-table-2 -target aws_dynamodb_table.test-table-3")) + } + + @Test + void addsTargetArgumentToTerraformApply() { + TargetPlugin plugin = new TargetPlugin() + TerraformApplyCommand command = new TerraformApplyCommand() + configureJenkins(env: [ + 'RESOURCE_TARGETS': 'aws_dynamodb_table.test-table-2,aws_dynamodb_table.test-table-3' + ]) + + plugin.apply(command) + + String result = command.toString() + assertThat(result, containsString(" -target aws_dynamodb_table.test-table-2 -target aws_dynamodb_table.test-table-3")) + } + + @Test + void decoratesTheTerraformEnvironmentStage() { + TargetPlugin plugin = new TargetPlugin() + def environment = spy(new TerraformEnvironmentStage()) + configureJenkins(env: [ + 'RESOURCE_TARGETS': 'aws_dynamodb_table.test-table-2,aws_dynamodb_table.test-table-3' + ]) + + plugin.apply(environment) + + verify(environment, times(1)).decorate(eq(TerraformEnvironmentStage.ALL), any(Closure.class)) + } + } +}