Skip to content

Commit 9507784

Browse files
authored
Merge pull request #371 from kmanning/issue_368
Issue 368: Prompt the user *again* if a pending migration is detected. Optionally disable.
2 parents 7c4dbaf + e42fe51 commit 9507784

File tree

4 files changed

+255
-11
lines changed

4 files changed

+255
-11
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* [Issue #363](https://github.com/manheim/terraform-pipeline/issues/363) Feature: Optionally disable echo on flyway CLI commands
1010
* [Issue #365](https://github.com/manheim/terraform-pipeline/issues/365) Feature: Optionally configure flyway username/password through CLI options
1111
* [Issue #362](https://github.com/manheim/terraform-pipeline/issues/362) Bug Fix: Apply AnsiColorPlugin on `terraform validate` and `terraform init`
12+
* [Issue #368](https://github.com/manheim/terraform-pipeline/issues/368) Feature: FlywayMigrationPlugin - prompt user *again* before applying migration. Optionally disable prompt.
1213

1314
# v5.15
1415

docs/FlywayMigrationPlugin.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
## [FlywayMigrationPlugin](../src/FlywayMigrationPlugin.groovy)
22

3-
Enable this plugin to run automated database migrations with [Flyway](https://flywaydb.org/). Flyway can be configured through standard configuration files, or through environment variables.
3+
Enable this plugin to run automated database migrations with [Flyway](https://flywaydb.org/). Flyway can be configured through standard configuration files, or through environment variables. Since migrations may happen less frequently than terrform changes, by default, if a pending migration is detected the pipeline will prompt you *again* to confirm that you want to proceed with the migration.
44

55
```
66
@Library(['terraform-pipeline']) _
@@ -73,3 +73,29 @@ validate.then(deployQa)
7373
.build()
7474
```
7575

76+
If you don't want to be prompted a second time when migrations are detected, you can disable the migration confirmation with `FlywayMigrationPlugin.confirmBeforeApplyingMigration(false)`.
77+
78+
```
79+
@Library(['terraform-pipeline']) _
80+
Jenkinsfile.init(this, Customizations)
81+
// Disable second confirmation when pending migration is detected.
82+
FlywayMigrationPlugin.confirmBeforeApplyingMigration(false)
83+
.init()
84+
85+
Jenkinsfile.init(this, Customizations)
86+
Jenkinsfile.defaultNodeName = 'docker'
87+
ConditionalApplyPlugin.withApplyOnEnvironment('qa')
88+
89+
AgentNodePlugin.withAgentDockerImage("custom-terraform-fakedb")
90+
.withAgentDockerfile()
91+
.withAgentDockerImageOptions("--entrypoint=''")
92+
.init()
93+
94+
def validate = new TerraformValidateStage()
95+
def deployQa = new TerraformEnvironmentStage('qa')
96+
def deployUat = new TerraformEnvironmentStage('uat')
97+
98+
validate.then(deployQa)
99+
.then(deployUat)
100+
.build()
101+
```

src/FlywayMigrationPlugin.groovy

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
class FlywayMigrationPlugin implements TerraformEnvironmentStagePlugin, Resettable {
22
public static Map<String,String> variableMap = [:]
33
public static boolean echoEnabled = false
4+
public static boolean confirmBeforeApply = true
45

56
public static void init() {
67
TerraformEnvironmentStage.addPlugin(new FlywayMigrationPlugin())
@@ -23,8 +24,36 @@ class FlywayMigrationPlugin implements TerraformEnvironmentStagePlugin, Resettab
2324
}
2425
}
2526

27+
public boolean hasPendingMigration(workflowScript) {
28+
def closure = {
29+
def resultString = sh (
30+
script: 'set +e; grep Pending flyway_output.txt > /dev/null; if [ $? -eq 0 ]; then echo true; else echo false; fi',
31+
returnStdout: true
32+
).trim()
33+
return new Boolean(resultString)
34+
}
35+
36+
closure.delegate = workflowScript
37+
return closure()
38+
}
39+
40+
public void confirmMigration(workflowScript) {
41+
def closure = {
42+
timeout(time: 1, unit: 'MINUTES') {
43+
input("One or more pending migrations will be applied immediately if you continue - please review the flyway info output. Are you sure you want to continue?")
44+
}
45+
}
46+
47+
closure.delegate = workflowScript
48+
closure()
49+
}
50+
2651
public Closure flywayMigrateClosure() {
2752
return { innerClosure ->
53+
if (confirmBeforeApply && hasPendingMigration(delegate)) {
54+
confirmMigration(delegate)
55+
}
56+
2857
innerClosure()
2958

3059
def environmentVariables = buildEnvironmentVariableList(env)
@@ -47,7 +76,18 @@ class FlywayMigrationPlugin implements TerraformEnvironmentStagePlugin, Resettab
4776
if (!echoEnabled) {
4877
pieces << 'set +x'
4978
}
50-
pieces << command.toString()
79+
80+
if (confirmBeforeApply) {
81+
pieces << 'set -o pipefail'
82+
}
83+
84+
def commandString = command.toString()
85+
if (confirmBeforeApply) {
86+
commandString += "| tee flyway_output.txt"
87+
}
88+
89+
pieces << commandString
90+
5191
if (!echoEnabled) {
5292
pieces << 'set -x'
5393
}
@@ -65,8 +105,14 @@ class FlywayMigrationPlugin implements TerraformEnvironmentStagePlugin, Resettab
65105
return this
66106
}
67107

108+
public static confirmBeforeApplyingMigration(boolean trueOrFalse = true) {
109+
this.confirmBeforeApply = trueOrFalse
110+
return this
111+
}
112+
68113
public static reset() {
69114
variableMap = [:]
70115
echoEnabled = false
116+
confirmBeforeApply = true
71117
}
72118
}

test/FlywayMigrationPluginTest.groovy

Lines changed: 180 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1+
import static org.hamcrest.Matchers.containsString
2+
import static org.hamcrest.Matchers.endsWith
13
import static org.hamcrest.Matchers.equalTo
24
import static org.hamcrest.Matchers.hasItem
35
import static org.hamcrest.Matchers.instanceOf
6+
import static org.hamcrest.Matchers.not
7+
import static org.hamcrest.Matchers.startsWith
48
import static org.hamcrest.MatcherAssert.assertThat
59
import static org.mockito.Mockito.any
6-
import static org.mockito.Mockito.doReturn;
10+
import static org.mockito.Mockito.doReturn
711
import static org.mockito.Mockito.eq
8-
import static org.mockito.Mockito.mock;
9-
import static org.mockito.Mockito.spy;
10-
import static org.mockito.Mockito.verify;
12+
import static org.mockito.Mockito.mock
13+
import static org.mockito.Mockito.spy
14+
import static org.mockito.Mockito.times
15+
import static org.mockito.Mockito.verify
1116

1217
import org.junit.jupiter.api.Nested
1318
import org.junit.jupiter.api.Test
@@ -131,6 +136,81 @@ class FlywayMigrationPluginTest {
131136

132137
verify(mockWorkflowScript).withEnv(eq(expectedList), any(Closure.class))
133138
}
139+
140+
@Test
141+
void runsConfirmMigrationIfConfirmBeforeApplyAndHasPendingMigration() {
142+
def plugin = spy(new FlywayMigrationPlugin())
143+
doReturn(true).when(plugin).hasPendingMigration(any(Object.class))
144+
FlywayMigrationPlugin.confirmBeforeApplyingMigration(true)
145+
146+
def flywayClosure = plugin.flywayMigrateClosure()
147+
flywayClosure.delegate = new MockWorkflowScript()
148+
flywayClosure { -> }
149+
150+
verify(plugin).confirmMigration(any(Object.class))
151+
}
152+
153+
@Test
154+
void doesNotRunConfirmMigrationIfNotConfirmBeforeApplyAndHasPendingMigration() {
155+
def plugin = spy(new FlywayMigrationPlugin())
156+
doReturn(true).when(plugin).hasPendingMigration(any(Object.class))
157+
FlywayMigrationPlugin.confirmBeforeApplyingMigration(false)
158+
159+
def flywayClosure = plugin.flywayMigrateClosure()
160+
flywayClosure.delegate = new MockWorkflowScript()
161+
flywayClosure { -> }
162+
163+
verify(plugin, times(0)).confirmMigration(any(Object.class))
164+
}
165+
166+
@Test
167+
void doesNotRunConfirmMigrationIfConfirmBeforeApplyAndDoesNotHavePendingMigration() {
168+
def plugin = spy(new FlywayMigrationPlugin())
169+
doReturn(false).when(plugin).hasPendingMigration(any(Object.class))
170+
FlywayMigrationPlugin.confirmBeforeApplyingMigration(true)
171+
172+
def flywayClosure = plugin.flywayMigrateClosure()
173+
flywayClosure.delegate = new MockWorkflowScript()
174+
flywayClosure { -> }
175+
176+
verify(plugin, times(0)).confirmMigration(any(Object.class))
177+
}
178+
}
179+
180+
@Nested
181+
public class HasPendingMigration {
182+
@Test
183+
void returnsTrueWhenShellReturnsTrueString() {
184+
def plugin = new FlywayMigrationPlugin()
185+
def workflowScript = spy(new MockWorkflowScript())
186+
doReturn('true').when(workflowScript).sh(any(Map.class))
187+
188+
def result = plugin.hasPendingMigration(workflowScript)
189+
190+
assertThat(result, equalTo(true))
191+
}
192+
193+
@Test
194+
void returnsFalseWhenShellReturnsFalseString() {
195+
def plugin = new FlywayMigrationPlugin()
196+
def workflowScript = spy(new MockWorkflowScript())
197+
doReturn('false').when(workflowScript).sh(any(Map.class))
198+
199+
def result = plugin.hasPendingMigration(workflowScript)
200+
201+
assertThat(result, equalTo(false))
202+
}
203+
204+
@Test
205+
void returnsFalseWhenShellReturnsAnyOtherString() {
206+
def plugin = new FlywayMigrationPlugin()
207+
def workflowScript = spy(new MockWorkflowScript())
208+
doReturn('blahblah').when(workflowScript).sh(any(Map.class))
209+
210+
def result = plugin.hasPendingMigration(workflowScript)
211+
212+
assertThat(result, equalTo(false))
213+
}
134214
}
135215

136216
@Nested
@@ -162,28 +242,119 @@ class FlywayMigrationPluginTest {
162242
@Nested
163243
public class BuildFlywayCommand {
164244
@Test
165-
void disablesEchoBeforeFlywayAndEnablesEchoAfterByDefault() {
245+
void constructsTheFlywayCommand() {
246+
def flywayCommand = 'flyway foo'
247+
def command = mock(FlywayCommand.class)
248+
doReturn(flywayCommand).when(command).toString()
249+
def plugin = new FlywayMigrationPlugin()
250+
251+
def result = plugin.buildFlywayCommand(command)
252+
253+
assertThat(result, containsString(flywayCommand))
254+
}
255+
256+
@Test
257+
void disablesEchoBeforeFlywayByDefault() {
166258
def flywayCommand = 'flyway foo'
167259
def command = mock(FlywayCommand.class)
168260
doReturn(flywayCommand).when(command).toString()
169261
def plugin = new FlywayMigrationPlugin()
170262

171263
def result = plugin.buildFlywayCommand(command)
172264

173-
assertThat(result, equalTo("set +x\n${flywayCommand}\nset -x".toString()))
265+
assertThat(result, startsWith("set +x"))
174266
}
175267

176268
@Test
177-
void returnsTheCommandIfEchoEnabled() {
269+
void disablesEchoAfterFlywayByDefault() {
178270
def flywayCommand = 'flyway foo'
179271
def command = mock(FlywayCommand.class)
180272
doReturn(flywayCommand).when(command).toString()
181273
def plugin = new FlywayMigrationPlugin()
182-
FlywayMigrationPlugin.withEchoEnabled()
183274

184275
def result = plugin.buildFlywayCommand(command)
185276

186-
assertThat(result, equalTo(flywayCommand))
277+
assertThat(result, endsWith("set -x"))
278+
}
279+
280+
@Test
281+
void prefixesFlywayCommandWithPipelineFail() {
282+
def plugin = spy(new FlywayMigrationPlugin())
283+
284+
def result = plugin.buildFlywayCommand(mock(FlywayCommand.class))
285+
286+
assertThat(result, containsString("set -o pipefail"))
287+
}
288+
289+
@Test
290+
void pipesFlywayCommandToFileWithTee() {
291+
def plugin = spy(new FlywayMigrationPlugin())
292+
293+
def result = plugin.buildFlywayCommand(mock(FlywayCommand.class))
294+
295+
assertThat(result, containsString("| tee flyway_output.txt"))
296+
}
297+
298+
@Nested
299+
public class WithEchoEnabled {
300+
@Test
301+
void doesNotDisbleEchoBeforeFlyway() {
302+
def flywayCommand = 'flyway foo'
303+
def command = mock(FlywayCommand.class)
304+
doReturn(flywayCommand).when(command).toString()
305+
def plugin = new FlywayMigrationPlugin()
306+
307+
FlywayMigrationPlugin.withEchoEnabled()
308+
def result = plugin.buildFlywayCommand(command)
309+
310+
assertThat(result, not(startsWith("set +x")))
311+
}
312+
313+
@Test
314+
void doesNotReenbleEchoAfterFlyway() {
315+
def flywayCommand = 'flyway foo'
316+
def command = mock(FlywayCommand.class)
317+
doReturn(flywayCommand).when(command).toString()
318+
def plugin = new FlywayMigrationPlugin()
319+
320+
FlywayMigrationPlugin.withEchoEnabled()
321+
def result = plugin.buildFlywayCommand(command)
322+
323+
assertThat(result, not(endsWith("set -x")))
324+
}
325+
}
326+
327+
@Nested
328+
public class WithConfirmBeforeApplyingMigrationDisabled {
329+
@Test
330+
void doesNotPrefixFlywayCommandWithPipelineFail() {
331+
def plugin = spy(new FlywayMigrationPlugin())
332+
FlywayMigrationPlugin.confirmBeforeApplyingMigration(false)
333+
334+
def result = plugin.buildFlywayCommand(mock(FlywayCommand.class))
335+
336+
assertThat(result, not(containsString("set -o pipefail")))
337+
}
338+
339+
@Test
340+
void doesNotPipeFlywayCommandToFileWthTee() {
341+
def plugin = spy(new FlywayMigrationPlugin())
342+
FlywayMigrationPlugin.confirmBeforeApplyingMigration(false)
343+
344+
def result = plugin.buildFlywayCommand(mock(FlywayCommand.class))
345+
346+
assertThat(result, not(containsString("| tee flyway_output.txt")))
347+
}
348+
}
349+
}
350+
351+
@Nested
352+
public class ConfirmBeforeApplyingMigration {
353+
@Test
354+
void isFluent() {
355+
def result = FlywayMigrationPlugin.confirmBeforeApplyingMigration()
356+
357+
assertThat(result, equalTo(FlywayMigrationPlugin.class))
187358
}
188359
}
189360
}

0 commit comments

Comments
 (0)