Skip to content

Commit 248692f

Browse files
authored
Merge pull request #355 from duckpuppy/add_terraform_import
[Issue #280] Add TerraformImportCommand and TerraformImportPlugin
2 parents 1c9074e + dcfcf08 commit 248692f

7 files changed

+705
-153
lines changed

CHANGELOG.md

Lines changed: 182 additions & 153 deletions
Large diffs are not rendered by default.

docs/TerraformImportPlugin.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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(['[email protected]']) _
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+
```

src/TerraformImportCommand.groovy

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
interface TerraformImportCommandPlugin {
2+
public void apply(TerraformImportCommand command)
3+
}

src/TerraformImportPlugin.groovy

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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+
}

0 commit comments

Comments
 (0)