Skip to content

Commit dcea0f3

Browse files
authored
Merge pull request #294 from jleopold28/issue_175
Issue 175: Pass terraform plan file to apply
2 parents 83486b5 + 712ab07 commit dcea0f3

File tree

6 files changed

+220
-0
lines changed

6 files changed

+220
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Unreleased
22

33
* [Issue #293](https://github.com/manheim/terraform-pipeline/issues/293) withEnv & withGlobalEnv docs
4+
* [Issue #175](https://github.com/manheim/terraform-pipeline/issues/175) Pass terraform plan output to apply
45

56
# v5.10
67

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ The example above gives you a bare-bones pipeline, and there may be Jenkinsfile
117117
* [CrqPlugin](./docs/CrqPlugin.md): Use the manheim_remedier gem to open automated Change Requests.
118118
* [DestroyPlugin](./docs/DestroyPlugin.md): Use this to change the pipeline functionality to `terraform destroy`. (Requires manual confirmation)
119119
* [GithubPRPlanPlugin](./docs/GithubPRPlanPlugin.md): Use this to post Terraform plan results in the comments of a Github PullRequest.
120+
* [PassPlanFilePlugin](./docs/PassPlanFilePlugin.md): Pass the plan file into apply stage
120121
* [PlanOnlyPlugin](./docs/PlanOnlyPlugin.md): Use this to change the pipeline functionality to `terraform plan` only.
121122
* [TargetPlugin](./docs/TargetPlugin.md): set `-target` parameter for terraform plan and apply.
122123
* [TerraformDirectoryPlugin](./docs/TerraformDirectoryPlugin.md): Change the default directory containing your terraform code.

docs/PassPlanFilePlugin.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
9+
```
10+
// Jenkinsfile
11+
@Library(['terraform-pipeline@v3.10']) _
12+
13+
Jenkinsfile.init(this, env)
14+
15+
// Pass the plan file to 'terraform apply'
16+
PassPlanFilePlugin.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+
```

src/PassPlanFilePlugin.groovy

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import static TerraformEnvironmentStage.PLAN
2+
import static TerraformEnvironmentStage.APPLY
3+
4+
class PassPlanFilePlugin implements TerraformPlanCommandPlugin, TerraformApplyCommandPlugin, TerraformEnvironmentStagePlugin {
5+
6+
public static void init() {
7+
PassPlanFilePlugin plugin = new PassPlanFilePlugin()
8+
9+
TerraformEnvironmentStage.addPlugin(plugin)
10+
TerraformPlanCommand.addPlugin(plugin)
11+
TerraformApplyCommand.addPlugin(plugin)
12+
}
13+
14+
@Override
15+
public void apply(TerraformEnvironmentStage stage) {
16+
stage.decorate(PLAN, stashPlan(stage.getEnvironment()))
17+
stage.decorate(APPLY, unstashPlan(stage.getEnvironment()))
18+
}
19+
20+
@Override
21+
public void apply(TerraformPlanCommand command) {
22+
String env = command.getEnvironment()
23+
command.withArgument("-out=tfplan-" + env)
24+
}
25+
26+
@Override
27+
public void apply(TerraformApplyCommand command) {
28+
String env = command.getEnvironment()
29+
command.withDirectory("tfplan-" + env)
30+
}
31+
32+
public Closure stashPlan(String env) {
33+
return { closure ->
34+
closure()
35+
String planFile = "tfplan-" + env
36+
echo "Stashing ${planFile} file"
37+
stash name: planFile, includes: planFile
38+
}
39+
}
40+
41+
public Closure unstashPlan(String env) {
42+
return { closure ->
43+
String planFile = "tfplan-" + env
44+
echo "Unstashing ${planFile} file"
45+
unstash planFile
46+
closure()
47+
}
48+
}
49+
50+
}

test/DummyJenkinsfile.groovy

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,10 @@ class DummyJenkinsfile {
5757
closure.delegate = this
5858
closure()
5959
}
60+
public stash(args) {
61+
println "DummyJenkinsfile.stash(${args})"
62+
}
63+
public unstash(args) {
64+
println "DummyJenkinsfile.unstash(${args})"
65+
}
6066
}

test/PassPlanFilePluginTest.groovy

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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.junit.Assert.assertThat
5+
import static org.junit.Assert.assertTrue
6+
import static org.mockito.Mockito.spy;
7+
import static org.mockito.Mockito.verify;
8+
import static org.mockito.Mockito.any;
9+
import static org.mockito.Mockito.eq;
10+
import static org.mockito.Mockito.times;
11+
import static org.mockito.Mockito.when;
12+
import static org.mockito.Mockito.mock;
13+
import org.junit.Test
14+
import org.junit.Before
15+
import org.junit.After
16+
import org.junit.runner.RunWith
17+
import de.bechte.junit.runners.context.HierarchicalContextRunner
18+
19+
@RunWith(HierarchicalContextRunner.class)
20+
class PassPlanFilePluginTest {
21+
@Before
22+
void resetJenkinsEnv() {
23+
Jenkinsfile.instance = mock(Jenkinsfile.class)
24+
when(Jenkinsfile.instance.getEnv()).thenReturn([:])
25+
}
26+
27+
private configureJenkins(Map config = [:]) {
28+
Jenkinsfile.instance = mock(Jenkinsfile.class)
29+
when(Jenkinsfile.instance.getEnv()).thenReturn(config.env ?: [:])
30+
}
31+
32+
public class Init {
33+
@After
34+
void resetPlugins() {
35+
TerraformPlanCommand.resetPlugins()
36+
TerraformApplyCommand.resetPlugins()
37+
TerraformEnvironmentStage.reset()
38+
}
39+
40+
@Test
41+
void modifiesTerraformEnvironmentStageCommand() {
42+
PassPlanFilePlugin.init()
43+
44+
Collection actualPlugins = TerraformEnvironmentStage.getPlugins()
45+
assertThat(actualPlugins, hasItem(instanceOf(PassPlanFilePlugin.class)))
46+
}
47+
48+
@Test
49+
void modifiesTerraformPlanCommand() {
50+
PassPlanFilePlugin.init()
51+
52+
Collection actualPlugins = TerraformPlanCommand.getPlugins()
53+
assertThat(actualPlugins, hasItem(instanceOf(PassPlanFilePlugin.class)))
54+
}
55+
56+
@Test
57+
void modifiesTerraformApplyCommand() {
58+
PassPlanFilePlugin.init()
59+
60+
Collection actualPlugins = TerraformApplyCommand.getPlugins()
61+
assertThat(actualPlugins, hasItem(instanceOf(PassPlanFilePlugin.class)))
62+
}
63+
64+
}
65+
66+
public class Apply {
67+
68+
@Test
69+
void decoratesTheTerraformEnvironmentStage() {
70+
PassPlanFilePlugin plugin = new PassPlanFilePlugin()
71+
def environment = spy(new TerraformEnvironmentStage())
72+
plugin.apply(environment)
73+
74+
verify(environment, times(1)).decorate(eq(TerraformEnvironmentStage.PLAN), any(Closure.class))
75+
verify(environment, times(1)).decorate(eq(TerraformEnvironmentStage.APPLY), any(Closure.class))
76+
}
77+
78+
@Test
79+
void addsArgumentToTerraformPlan() {
80+
PassPlanFilePlugin plugin = new PassPlanFilePlugin()
81+
TerraformPlanCommand command = new TerraformPlanCommand("dev")
82+
plugin.apply(command)
83+
84+
String result = command.toString()
85+
assertThat(result, containsString("-out=tfplan-dev"))
86+
}
87+
88+
@Test
89+
void addsArgumentToTerraformApply() {
90+
PassPlanFilePlugin plugin = new PassPlanFilePlugin()
91+
TerraformApplyCommand command = new TerraformApplyCommand("dev")
92+
plugin.apply(command)
93+
94+
String result = command.toString()
95+
assertThat(result, containsString("tfplan-dev"))
96+
}
97+
98+
}
99+
100+
public class StashPlan {
101+
102+
@Test
103+
void runsStashPlan() {
104+
def wasCalled = false
105+
def passedClosure = { -> wasCalled = true }
106+
def plugin = new PassPlanFilePlugin()
107+
108+
def stashClosure = plugin.stashPlan('dev')
109+
stashClosure.delegate = new DummyJenkinsfile()
110+
stashClosure.call(passedClosure)
111+
112+
assertTrue(wasCalled)
113+
}
114+
115+
}
116+
117+
public class UnstashPlan {
118+
119+
@Test
120+
void runsUnstashPlan() {
121+
def wasCalled = false
122+
def passedClosure = { -> wasCalled = true }
123+
def plugin = new PassPlanFilePlugin()
124+
125+
def unstashClosure = plugin.unstashPlan('dev')
126+
unstashClosure.delegate = new DummyJenkinsfile()
127+
unstashClosure.call(passedClosure)
128+
129+
assertTrue(wasCalled)
130+
}
131+
132+
}
133+
134+
}

0 commit comments

Comments
 (0)