-
Notifications
You must be signed in to change notification settings - Fork 53
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #353 from duckpuppy/add_taint_untaint_command
Issue #354: Add taint and untaint command
- Loading branch information
Showing
10 changed files
with
516 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
interface TerraformTaintCommandPlugin { | ||
public void apply(TerraformTaintCommand command) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
interface TerraformUntaintCommandPlugin { | ||
public void apply(TerraformUntaintCommand command) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |
Oops, something went wrong.