Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 #354](https://github.com/manheim/terraform-pipeline/issues/354) Feature: Add TerraformTaintPlugin - Allows performing `terraform taint` or `terraform untaint` prior to the plan phase.
* [Issue #21](https://github.com/manheim/terraform-pipeline/issues/21) Feature: Plugin to run database migration scripts

# v5.15

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ The example above gives you a bare-bones pipeline, and there may be Jenkinsfile
### Terraform Backend Management
* [ConsulBackendPlugin](./docs/ConsulBackendPlugin.md): Use Consul backend to manage terraform state.
* [S3BackendPlugin](./docs/S3BackendPlugin.md): Use S3 backend to manage terraform state.
### Database Migrations
* [FlywayMigrationPlugin](./docs/FlywayMigrationPlugin.md): Use Flyway to automate your database migrations.
### Other
* [AgentNodePlugin](./docs/AgentNodePlugin.md): Run your pipeline on agents that are configured with Docker.
* [AnsiColorPlugin](./docs/AnsiColorPlugin.md): Enable ansi-color output.
Expand Down
52 changes: 52 additions & 0 deletions docs/FlywayMigrationPlugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
## [FlywayMigrationPlugin](../src/FlywayMigrationPlugin.groovy)

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.

```
@Library(['terraform-pipeline']) _
Jenkinsfile.init(this, Customizations)
FlywayMigrationPlugin.init()

def validate = new TerraformValidateStage()
// For each environment
// Run the `flyway info` command after a successful `terraform plan`
// Run the `flyway migrate` command after a successful `terraform apply`
def deployQa = new TerraformEnvironmentStage('qa')
def deployUat = new TerraformEnvironmentStage('uat')
def deployProd = new TerraformEnvironmentStage('prod')

validate.then(deployQa)
.then(deployUat)
.then(deployProd)
.build()
```

If needed, flyway configuration can be set through the Plugin, or FlywayCommand.

```
@Library(['terraform-pipeline']) _
Jenkinsfile.init(this, Customizations)
// Use the FlywayCommand to modify specific options like `locations` and `url`
FlywayCommand.withLocations("filesystem:`pwd`/migrations")
.withUrl("`terraform output jdbc_url`")
// Flyway automatically picks up specific enviornment variables (FLYWAY_USER, FLYWAY_PASSWORD)
// Sets FLYWAY_USER to the value of $TF_VAR_MIGRATION_USER
// Sets FLYWAY_PASSWORD to the value of $TF_VAR_MIGRATION_PASSWORD
FlywayMigrationPlugin.withUserVariable('TF_VAR_MIGRATION_USER')
.withPasswordVariable('TF_VAR_MIGRATION_PASSWORD')
.init()

def validate = new TerraformValidateStage()
// For each environment
// Run the `flyway info` command after a successful `terraform plan`
// Run the `flyway migrate` command after a successful `terraform apply`
def deployQa = new TerraformEnvironmentStage('qa')
def deployUat = new TerraformEnvironmentStage('uat')
def deployProd = new TerraformEnvironmentStage('prod')

validate.then(deployQa)
.then(deployUat)
.then(deployProd)
.build()
```

41 changes: 41 additions & 0 deletions src/FlywayCommand.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
class FlywayCommand implements Resettable {
private String command
private String binary = "flyway"
private static String locations
private static String url

public FlywayCommand(String command) {
this.command = command
}

public String toString() {
def pieces = []
pieces << binary
pieces << command

if (locations) {
pieces << "-locations=${locations}"
}

if (url) {
pieces << "-url=${url}"
}

return pieces.join(' ')
}

public static withLocations(String locations) {
this.locations = locations
return this
}

public static withUrl(String url) {
this.url = url
return this
}

public static reset() {
this.locations = null
this.url = null
}
}
65 changes: 65 additions & 0 deletions src/FlywayMigrationPlugin.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
class FlywayMigrationPlugin implements TerraformEnvironmentStagePlugin, Resettable {
public static String passwordVariable
public static String userVariable

public static void init() {
TerraformEnvironmentStage.addPlugin(new FlywayMigrationPlugin())
}

public void apply(TerraformEnvironmentStage stage) {
stage.decorate(TerraformEnvironmentStage.PLAN, flywayInfoClosure())
stage.decorate(TerraformEnvironmentStage.APPLY, flywayMigrateClosure())
}

public Closure flywayInfoClosure() {
return { innerClosure ->
innerClosure()

def environmentVariables = buildEnvironmentVariableList(env)
withEnv(environmentVariables) {
def command = new FlywayCommand('info')
sh command.toString()
}
}
}

public Closure flywayMigrateClosure() {
return { innerClosure ->
innerClosure()

def environmentVariables = buildEnvironmentVariableList(env)
withEnv(environmentVariables) {
def command = new FlywayCommand('migrate')
sh command.toString()
}
}
}

public Collection buildEnvironmentVariableList(env) {
def list = []
if (passwordVariable) {
list << "FLYWAY_PASSWORD=${env[passwordVariable]}"
}

if (userVariable) {
list << "FLYWAY_USER=${env[userVariable]}"
}

return list
}

public static withPasswordFromEnvironmentVariable(String passwordVariable) {
this.passwordVariable = passwordVariable
return this
}

public static withUserFromEnvironmentVariable(String userVariable) {
this.userVariable = userVariable
return this
}

public static reset() {
passwordVariable = null
userVariable = null
}
}
64 changes: 64 additions & 0 deletions test/FlywayCommandTest.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import static org.hamcrest.Matchers.containsString
import static org.hamcrest.Matchers.equalTo
import static org.hamcrest.MatcherAssert.assertThat

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

@ExtendWith(ResetStaticStateExtension.class)
class FlywayCommandTest {
@Nested
public class ToString {
@Test
void constructsTheCommand() {
def command = new FlywayCommand("info")

def result = command.toString()

assertThat(result, equalTo("flyway info"))
}

@Nested
public class WithLocations {
@Test
void isFluent() {
def result = FlywayCommand.withLocations('someLocations')

assertThat(result, equalTo(FlywayCommand.class))
}

@Test
void includesTheLocationsParameter() {
def expectedLocations = 'filesystem:/some/dir'
FlywayCommand.withLocations(expectedLocations)
def command = new FlywayCommand('blah')

def result = command.toString()

assertThat(result, containsString("-locations=${expectedLocations}"))
}
}

@Nested
public class WithUrl {
@Test
void isFluent() {
def result = FlywayCommand.withUrl('someUrl')

assertThat(result, equalTo(FlywayCommand.class))
}

@Test
void includesTheUrlParameter() {
def expectedUrl = 'jdbc:mysql://someurl'
FlywayCommand.withUrl(expectedUrl)
def command = new FlywayCommand('blah')

def result = command.toString()

assertThat(result, containsString("-url=${expectedUrl}"))
}
}
}
}
Loading