Skip to content

Commit

Permalink
Merge pull request #353 from duckpuppy/add_taint_untaint_command
Browse files Browse the repository at this point in the history
Issue #354: Add taint and untaint command
  • Loading branch information
duckpuppy committed Mar 31, 2021
2 parents 4a56d34 + a7ad263 commit 70d493a
Show file tree
Hide file tree
Showing 10 changed files with 516 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* [Issue #347](https://github.com/manheim/terraform-pipeline/pull/348) Feature: TerraformOutputOnlyPlugin - can restrict a pipeline run to displaying the current state outputs only via new job parameters.
* [Issue #318](https://github.com/manheim/terraform-pipeline/issues/318) Feature: Show environment on confirm command page
* [Issue #349](https://github.com/manheim/terraform-pipeline/pull/349) Extract plugin handling to a `Pluggable` trait to DRY up the existing TerraformCommand classes.
* [Issue #354](https://github.com/manheim/terraform-pipeline/issues/354) Feature: Add TerraformTaintPlugin - Allows performing `terraform taint` or `terraform untaint` prior to the plan phase.

# v5.15

Expand Down
37 changes: 37 additions & 0 deletions docs/TerraformTaintPlugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## [TerraformTaintPlugin](../src/TerraformTaintPlugin.groovy)

Enable this plugin to add `TAINT_RESOURCE` and `UNTAINT_RESOURCE` parameters
to the build. If a resource path is provided via one of those parameters, then
the `terraform plan` command will be preceded by the corresponding Terraform
command to taint or untaint the appropriate resources.

Note that the untaint command will take precedence, so if for some reason the
same resource is placed in both parameters, it will be tainted and immediately
untainted, resulting in no change.

There are several ways to customize where and when the taint/untaint can run:

* `onBranch()`: This takes in a branch name as a parameter. This adds the
branch to the list of approved branches.

```
// Jenkinsfile
@Library(['terraform-pipeline@v3.10']) _
Jenkinsfile.init(this, env)
// This enables the "taint" and "untaint" functionality
// It will only apply to the master branch (default behavior)
TerraformTaintPlugin.init()
def validate = new TerraformValidateStage()
def destroyQa = new TerraformEnvironmentStage('qa')
def destroyUat = new TerraformEnvironmentStage('uat')
def destroyProd = new TerraformEnvironmentStage('prod')
validate.then(destroyQa)
.then(destroyUat)
.then(destroyProd)
.build()
```
37 changes: 37 additions & 0 deletions src/TerraformTaintCommand.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
class TerraformTaintCommand implements TerraformCommand, Pluggable<TerraformTaintCommandPlugin> {
private String command = "taint"
private String resource
private String environment

public TerraformTaintCommand(String environment) {
this.environment = environment
}

public TerraformTaintCommand withResource(String resource) {
this.resource = resource
return this
}

public String toString() {
applyPlugins()
def parts = []
parts << 'terraform'
parts << command
parts << resource

parts.removeAll { it == null }
return parts.join(' ')
}

public String getResource() {
return this.resource
}

public static TerraformTaintCommand instanceFor(String environment) {
return new TerraformTaintCommand(environment)
}

public String getEnvironment() {
return this.environment
}
}
3 changes: 3 additions & 0 deletions src/TerraformTaintCommandPlugin.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
interface TerraformTaintCommandPlugin {
public void apply(TerraformTaintCommand command)
}
85 changes: 85 additions & 0 deletions src/TerraformTaintPlugin.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import static TerraformEnvironmentStage.PLAN_COMMAND

class TerraformTaintPlugin implements TerraformEnvironmentStagePlugin, TerraformTaintCommandPlugin, TerraformUntaintCommandPlugin, Resettable {
private static DEFAULT_BRANCHES = ['master']
private static branches = DEFAULT_BRANCHES

public static void init() {
TerraformTaintPlugin plugin = new TerraformTaintPlugin()

BuildWithParametersPlugin.withStringParameter([
name: "TAINT_RESOURCE",
description: 'Run `terraform taint` on the resource specified prior to planning and applying.'
])

BuildWithParametersPlugin.withStringParameter([
name: "UNTAINT_RESOURCE",
description: 'Run `terraform untaint` on the resource specified prior to planning and applying.'
])

TerraformEnvironmentStage.addPlugin(plugin)
TerraformTaintCommand.addPlugin(plugin)
TerraformUntaintCommand.addPlugin(plugin)
}

public static onBranch(String branchName) {
this.branches << branchName
return this
}

public void apply(TerraformEnvironmentStage stage) {
stage.decorate(PLAN_COMMAND, runTerraformTaintCommand(stage.getEnvironment()))
stage.decorate(PLAN_COMMAND, runTerraformUntaintCommand(stage.getEnvironment()))
}

public void apply(TerraformTaintCommand command) {
def resource = Jenkinsfile.instance.getEnv().TAINT_RESOURCE
if (resource) {
command.withResource(resource)
}
}

public void apply(TerraformUntaintCommand command) {
def resource = Jenkinsfile.instance.getEnv().UNTAINT_RESOURCE
if (resource) {
command.withResource(resource)
}
}

public boolean shouldApply() {
// Check branches
if (branches.contains(Jenkinsfile.instance.getEnv().BRANCH_NAME)) {
return true
} else if (null == Jenkinsfile.instance.getEnv().BRANCH_NAME) {
return true
}

return false
}

public Closure runTerraformTaintCommand(String environment) {
def taintCommand = TerraformTaintCommand.instanceFor(environment)
return { closure ->
if (shouldApply()) {
echo "Running '${taintCommand.toString()}'. TerraformTaintPlugin is enabled."
sh taintCommand.toString()
}
closure()
}
}

public Closure runTerraformUntaintCommand(String environment) {
def untaintCommand = TerraformUntaintCommand.instanceFor(environment)
return { closure ->
if (shouldApply()) {
echo "Running '${untaintCommand.toString()}'. TerraformTaintPlugin is enabled."
sh untaintCommand.toString()
}
closure()
}
}

public static reset() {
this.branches = DEFAULT_BRANCHES.clone()
}
}
38 changes: 38 additions & 0 deletions src/TerraformUntaintCommand.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
class TerraformUntaintCommand implements TerraformCommand, Pluggable<TerraformUntaintCommandPlugin> {
private String command = "untaint"
private String resource
private String environment

public TerraformUntaintCommand(String environment) {
this.environment = environment
}

public TerraformUntaintCommand withResource(String resource) {
this.resource = resource
return this
}

public String toString() {
applyPlugins()
def parts = []
parts << 'terraform'
parts << command
parts << resource

parts.removeAll { it == null }
return parts.join(' ')
}

public String getResource() {
return this.resource
}

public static TerraformUntaintCommand instanceFor(String environment) {
return new TerraformUntaintCommand(environment)
}

public String getEnvironment() {
return this.environment
}
}

3 changes: 3 additions & 0 deletions src/TerraformUntaintCommandPlugin.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
interface TerraformUntaintCommandPlugin {
public void apply(TerraformUntaintCommand command)
}
77 changes: 77 additions & 0 deletions test/TerraformTaintCommandTest.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import static org.hamcrest.Matchers.equalTo
import static org.hamcrest.MatcherAssert.assertThat
import static org.mockito.Mockito.mock
import static org.mockito.Mockito.times
import static org.mockito.Mockito.verify

import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(ResetStaticStateExtension.class)
class TerraformTaintCommandTest {
@Nested
public class WithResource {
@Test
void defaultsToEmpty() {
def command = new TerraformTaintCommand()

def actualCommand = command.toString()
assertThat(actualCommand, equalTo("terraform taint"))
}

@Test
void addsResourceWhenSet() {
def command = new TerraformTaintCommand().withResource("foo")

def actualCommand = command.toString()
assertThat(actualCommand, equalTo("terraform taint foo"))
}
}

@Nested
public class Plugins {
@Test
void areAppliedToTheCommand() {
TerraformTaintCommandPlugin plugin = mock(TerraformTaintCommandPlugin.class)
TerraformTaintCommand.addPlugin(plugin)

TerraformTaintCommand command = new TerraformTaintCommand()
command.environment = "env"
command.toString()

verify(plugin).apply(command)
}

@Test
void areAppliedExactlyOnce() {
TerraformTaintCommandPlugin plugin = mock(TerraformTaintCommandPlugin.class)
TerraformTaintCommand.addPlugin(plugin)

TerraformTaintCommand command = new TerraformTaintCommand()
command.environment = "env"

String firstCommand = command.toString()
String secondCommand = command.toString()

verify(plugin, times(1)).apply(command)
}

@Test
void areAppliedEvenAfterCommandAlreadyInstantiated() {
TerraformTaintCommandPlugin firstPlugin = mock(TerraformTaintCommandPlugin.class)
TerraformTaintCommandPlugin secondPlugin = mock(TerraformTaintCommandPlugin.class)

TerraformTaintCommand.addPlugin(firstPlugin)
TerraformTaintCommand command = new TerraformTaintCommand()
command.environment = "env"

TerraformTaintCommand.addPlugin(secondPlugin)

command.toString()

verify(firstPlugin, times(1)).apply(command)
verify(secondPlugin, times(1)).apply(command)
}
}
}
Loading

0 comments on commit 70d493a

Please sign in to comment.