Provisioning Clones for Flyway Projects
This article demonstrates how to use Redgate Test Data Manager to automate the delivery and teardown of data containers (clones) on four different RDBMS platforms, SQL Server, PostgreSQL, Oracle and MySQL, for use in Flyway development projects.
Follow the series
What are Clones? – ▶ Get Started with Clones – Development and Testing with Clones
My previous article, Getting Started with Cloning in Redgate Test Data Manager, covers the basic of creating and maintaining the data images and data containers (clones). Here, we’ll demonstrate how to automate clone delivery, so that developers and testers can then use them directly in branch-based Flyway development projects, and can reset and recreate them at will. We’ll provision an empty copy of the same database on each data container and then use Flyway to connect to each one in turn and migrate the database to the latest development version.
How it Works (overview)
We use the cloning CLI in Test Data Manager (rgclone
) to create four data images, all for the same, empty Northwind
database but on four different RDBMSs. From each image, we’ll create a data container (clone) as a working database instance. I provide PowerShell routines that will automatically create and teardown all these images and containers, as needed.
Next, we create four Flyway projects, one for each RDBMS. With the connection details and credentials passed back by rgclone
, we will then create extra Flyway configuration files, stored securely in the user area, which will be passed to Flyway as parameters when it is executed, to provide it with the necessary credentials.
Finally, we then execute a single PowerShell process that connects each Flyway project to its database, in turn, and runs a single script, in the appropriate dialect, to migrate each database from empty to the required version. The following diagram shows it connecting to and migrating the PostgreSQL data container.
Trying it out
You’ll already need to have installed and set up the Cloning CLI (rgclone
), provided the API endpoint for your Clone server, and authenticated to the server. All of this is explained in my previous article, Getting Started with Cloning in Redgate Test Data Manager, along with basic details of creating images and containers.
You can then download the full source code from my Flyway Teamwork project on GitHub. You don’t need to be using my Teamwork framework for this article, as it doesn’t rely on any of the extra functionality that it provides. You can find the migration scripts for the four different version of the Northwind database here:
- Northwind – the SQL Server version
- NorthwindMySQL – the MySQL version
- NorthwindOracle – the Oracle version
- NorthwindPG – the PostgreSQL version
All you need to do then is to execute the Flyway app to execute the migrations.
Setup routine
The following PowerShell routine sets a few variables that keep things neat and simple for our subsequent scripting and automation tasks involving rgclone
. It sets up an RGClone directory, within the user profile directory, where we’ll securely store the image definition (YAML) files along with JSON files containing information about the containers we create and their connection details. This setup routine also sets the correct file format and interprets the exit codes from rgclone
, making it easier to understand and handle its outputs and errors.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
# Set the location where we store the details, including logins. This must be in the user area # and preferably encrypted. $WorkDirectory="$env:USERPROFILE\RGClone" if (!(Test-Path $WorkDirectory -PathType Container)) { $null=New-Item -ItemType Directory -Force -Path $WorkDirectory } <# Set Powershell to save files in UTF-8. This keeps you out of trouble when writing files that are then read by Flyway. #> $PSDefaultParameterValues['Out-File:Encoding'] = 'utf8' <# exit codes for RGClone. We need these not just to find out what went wrong but also because RGClone produces an error for authentication, but there is currently no way of querying as to whether the user is still authenticated before you do an operation. #> $RGCloneExitCodes= @( @{'ExitCode'=0;'Reason'='Success'; 'Explanation'='rgclone command completed successfully'} @{'ExitCode'=1;'Reason'='Unexpected Error'; 'Explanation'='Used when no other error code is suitable. It''s the default error code.'} @{'ExitCode'=2;'Reason'='Resource Not Found'; 'Explanation'='a resource (e.g. data image or data container) could not be found. Can be an error (e.g. you try to get a non-existent data image or container – e.g. rgclone get dc 17) or not (rgclone get di will return 2 when no data images exist).'} @{'ExitCode'=3;'Reason'='Unrecognised Command'; 'Explanation'='a requested command is not recognised (e.g. rgclone idonotexist).'} @{'ExitCode'=4;'Reason'='Operation Failed'; 'Explanation'='The rgclone operation failed (usually server-side).'} @{'ExitCode'=5;'Reason'='Output Error'; 'Explanation'='an error occurred while writing to the output stream.'} @{'ExitCode'=6;'Reason'='Resource Not Specified'; 'Explanation'='a resource (data image or data container) was not specified for an operation.'} @{'ExitCode'=7;'Reason'='Authentication Failed'; 'Explanation'='An authentication operation failed (in any rgclone command, not only auth).'} @{'ExitCode'=8;'Reason'='Ctl Error'; 'Explanation'='An error occurred while configuring the rgclone tool itself. This is typically an internal only error and in that case indicates a bug in the product.'} @{'ExitCode'=9;'Reason'='Forbidden Error'; 'Explanation'='a user does not have access to the requested resource (e.g. data image or data container) or operation (e.g. delete).'} @{'ExitCode'=10;'Reason'='Update Check Error'; 'Explanation'='the rgclone check for update has failed (typically when communications with the server are not successful).'} ) |
Teardown routine
If you are using this sample project like I did, which is to try out and get used to how cloning works in Test Data Manager, then you’ll want to build the data images and containers you need, migrate them to the required version, and then tear them down again, repeatedly.
Before we get to the “building” part, we’ll start with the teardown routine that destroys all surviving data images and data containers from previous experiments. Please note that this routine, as provided, is a mass-extinction event for all existing data containers and data images, even those unrelated to the current Northwind project. However, it is very easy to add a WHERE
clause to the routine to exclude or include just the containers or images you want to remove.
Containers are more ‘volatile’ than images so you can easily execute the first part without the second. However, I wouldn’t want to delete anything without preserving the file used to specify how to create the image (which is the reason for the JSON file created in the previous setup routine).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<# This is a tear-down. This deletes all the current containers and images. We delete all our containers for this demonstration. We must do this before the images #> $Containerlist=@() #We delete all our containers first. We must do this before the images $Containerlist=rgclone get data-containers --output json|Convertfrom-json|foreach{$_.id} $Result=$RGCloneExitCodes|where {$_.ExitCode -eq $LastExitCode} if ($result.ExitCode -notin (0,2)) #no trauma if there aren't any {Throw "$($result.Reason)`n $($result.Explanation)"} if ($result.ExitCode -ne 2) { #no resources found RGClone delete data-containers $Containerlist } $Imagelist=@() #We delete all our Images. We must do this after the containers $Imagelist=rgclone get data-images --output json|Convertfrom-json|foreach{$_.id} $Result=$RGCloneExitCodes|where {$_.ExitCode -eq $LastExitCode} if ($result.ExitCode -notin (0,2)) #no trauma if there aren't any {Throw "$($result.Reason)`n $($result.Explanation)"} if ($result.ExitCode -ne 2) { #no resources found RGclone delete data-images $Imagelist } |
If all goes well, this will give something like:
Data container '26' deleted! Data container '27' deleted! Data container '28' deleted! Data image '31' deleted! Data image '32' deleted! Data image '33' deleted!
Building data images and data containers
Now for the ‘build’ part and the first stage is to create the data images, and after which we can create data containers, from each image. These routines assume you run the teardown process, so no images, or data containers containing “Northwind” in their names, currently exist.
Create the data images
When creating a batch of images, in this case one data image per database engine, I like to create a list containing hashtables, where each hashtable defines the specifications for a different database image. We write the specification for each data image to a data image template (YAML) file, named using a name–engine–project convention, and store them in the RGClone directory. Once you have the YAML files on disk, you can do subsequent mass-creation of images from them.
For the SQL Server image, we will specify the image source as an empty backup, and for the others just create an empty image with a CREATE
DATABASE
NorthWind
‘prescript’ (see my previous article for details).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
<# Ensure that we have a work directory. Call it what you will #> $WorkDirectory="$env:USERPROFILE\RGClone" if (!(Test-Path $WorkDirectory -PathType Container)) { $null=New-Item -ItemType Directory -Force -Path $WorkDirectory } <#-----------We now create all our Data Images (templates)---------------#> <# Here we create our images. We do it from a hashtable so we can then choose whether to store this image info on disk as JSON, Yaml, TOML or PSON. We only need this for reference as we can use 'RGClone get data-images' more conveniently For this example, we are creating databases for northwind in four different RDBMSs that that we will then build using Flyway. #> $ProjectName = 'northwind' #the name of our development $DatabaseName = 'Northwind' #The name of the actual database @( @{ 'name' = 'Sneezy'; 'engine' = 'mssql'; 'version' = '2019'; 'Backups' = @('SQLBackups/Northwind.bak'); 'tags' = @('V1.1.1', 'Develop', 'clone'); }, @{ 'name' = 'Bashful'; 'engine' = 'postgresql'; 'version' = '15'; 'Backups' = @(); 'tags' = @() 'PreScript'="CREATE DATABASE $DatabaseName";'PostScript'='' }, @{ 'name' = 'Grumpy'; 'engine' = 'mysql'; 'version' = '8'; 'Backups' = @(); 'tags' = @() 'PreScript'="CREATE DATABASE $DatabaseName";'PostScript'='' }, @{ 'name' = 'Dozy'; 'engine' = 'oracle'; 'version' = '19-se'; 'Backups' = @(); 'tags' = @() 'PreScript'="CREATE DATABASE $($DatabaseName.ToUpper())";'PostScript'='' } ) | foreach { #write out the following YAML file $ImageSpecFilename = "$WorkDirectory\$($_.'name')-$($_.'engine')-$ProjectName.yaml" If ($_.tags.Count -eq 0) { $Taglist = '' } else { $Taglist = "`ntags:`n - $($_.tags.ToLower() -join "`n - ")" } If ($_.backups.Count -eq 0) { $BackupList = ''; $SourceType = 'empty' } else { $BackupList = "`nbackups:`n - path: $($_.backups -join "`n - ")"; $SourceType = 'backup' } If ([string]::IsNullOrEmpty($_.preScript)) { $prescript = '' } else { $preScript = "`npreScript: |`n $($_.preScript) "; } If ([string]::IsNullOrEmpty($_.postScript)) { $postscript = '' } else { $postScript = "`npostScript: |`n $($_.postScript) "; } # and now insert the values into the YAML @" sourceType: $SourceType name: $($_.Name)-$ProjectName-$($_.Engine) engine: $($_.engine) version: $($_.version)$TagList$Backuplist$preScript$postscript "@ >$ImageSpecFilename #we've saved the yaml file so we can re-create the image if necessary $output = rgclone create data-image -f $ImageSpecFilename $Result=$RGCloneExitCodes|where {$_.ExitCode -eq $LastExitCode} if ($result.ExitCode -ne 0) {write-error "$($result.Reason)`n $($result.Explanation)"} if (!($?)) { Throw "That didn't end well" } Else { write-output $output } } |
We end up with four data images, named according to a name–project–engine convention:
Data image 'Sneezy-northwind-mssql' (35) created! Data image 'Bashful-northwind-postgresql' (36) created! Data image 'Grumpy-northwind-mysql' (37) created! Data image 'Dozy-northwind-oracle' (38) created!
Create the data containers
Now that we have all the data images, the next stage is to create data containers (clones) from each image. We’ll end up with four data containers, each one a working database instance running on a different RDBMS, and each instance containing an empty copy of the Northwind database, ready for Flyway.
We define the branch of the project that the clone will support (in this case, Main
) and we also give each container an identifying name. This isn’t strictly necessary, but without it, I got a bit confused as to which was which! We end up with data container named according to the convention ImageName–Branch–ContainerName.
For the sake of simplicity in this demo, I use --lifetime 0
to ensure the data containers never ‘disappear’ until you tear them down. Generally, though, clones are designed to be ephemeral.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
<#-----------We now create all our containers---------------#> <# here we create the containers. We will save the details and credentials so that we can reference them later. For Flyway, later we will generate a .conf file for the connection string and credentials. Third-party tools are more of a problem #> $ProjectName='Northwind'; #all our databases are in the one project $Branch='Main'; # we are only executing the main branch $ContainerNames=@( #Just so we can identify them easily - I then make fewer mistakes! 'Euphrosyne', # mirth 'Aglaia', # elegance 'Thalia', # youth and beauty. 'Sabrina' # patience ) # get the data images and create one container from each data image $CurrentImages=RGClone get data-images --output json |convertFrom-json $Result=$RGCloneExitCodes|where {$_.ExitCode -eq $LastExitCode} if ($result.ExitCode -ne 0) {write-error "$($result.Reason)`n $($result.Explanation)"} $CurrentImages |where {$_.name -like '*northwind*'} | foreach -begin {$ii=0}{ $ContainerName="$($_.Name)-$Branch-$($ContainerNames[$ii])" rgclone create data-container --image $_.Name --name $ContainerName --lifetime 0 --output json > "$WorkDirectory\Container_$ContainerName_($(Get-Date -UFormat "%A-%d-%b-%Y")).json" $Result=$RGCloneExitCodes|where {$_.ExitCode -eq $LastExitCode} if ($result.ExitCode -ne 0) {write-error "$($result.Reason)`n $($result.Explanation)"} $ii++ } |
And here are our four clones being created:
Data container 'Sneezy-northwind-mssql-Main-Euphrosyne' (29) created! Data container 'Bashful-northwind-postgresql-Main-Aglaia' (30) created! Data container 'Grumpy-northwind-mysql-Main-Thalia' (31) created! Data container 'Dozy-northwind-oracle-Main-Sabrina' (32) created!
Save connection details for each data container in a Flyway config file
Now we have new fresh data containers, but to connect to them, Flyway needs to know the user, port address and password. These change each time we create or reset the containers.
Fortunately, Flyway will take credentials and connection strings from a Flyway configuration (.conf) file, so we’ll grab the details from rgclone
and store them in config files in the user area. This will protect these ‘secrets’ from casual access, but in actual use we’d want to encrypt/decrypt the file too (encryption at rest) or obtain credentials direct from rgclone
at the point of executing our favorite build/migration tool.
The routines above and below can be changed to allow the encryption of the credentials with GPG or other utilities (I use Yubikey).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<#------------Update Flyway Config 'connection' files--------------#> <# We now update or create all our Flyway conf files that deal with connection and credentials. We can generate or over-write the .conf files for each project containing the connection strings that Flyway can use. #> # first get the information about all the current containers $ContainerInfo=rgclone get data-containers --output json|Convertfrom-json $Result=$RGCloneExitCodes|where {$_.ExitCode -eq $LastExitCode} if ($result.ExitCode -ne 0) #Make sure that it worked {write-error "$($result.Reason)`n $($result.Explanation)"} $ContainerInfo|foreach{ $Tail= "$(if ($_.engine -like 'MSSQL') {";databaseName=$DatabaseName;trustServerCertificate=true;"} elseif ($_.engine -like 'postgresql') {"/$($DatabaseName.ToLower())"} else {"/$DatabaseName"})" if ($_.engine -notlike 'oracle') {$JDBCURL=(($_.jdbcConnectionString -ireplace '[;?]user=.{1,200}', '') -ireplace '(?<!/)/[^/].{1,100}', '') +$tail} else {$JDBCURL=$_.jdbcConnectionString} $Names=$_.name -split '-' $ConfName="$($Names[1])_$($Names[2])_$($Names[3])_RGClone" @" # connection details for $($_.name) flyway.url=$JDBCURL flyway.user=$($_.user) flyway.password=$($_.password) "@> "$env:USERPROFILE\$ConfName.conf" } |
Enter Flyway
So, we now have four containerized, empty Northwind
databases running on four different RDBMSs (MSSQL, PostgreSQL, MySQL, and Oracle). We can now take Flyway out of its box and use it to apply migrations and bring these databases the latest development version.
For each RDBMS-version of Northwind, we prepare a Flyway project. Here, I’ve kept each Flyway project in a different directory, for simplicity. Each directory has a config file that defines the locations where migrations will be found. In this case, there is only one migration file for each project, appropriate to the RDBMS being used. The placeholders in each config file aren’t required at this point.
The following code automates the process of cleaning and migrating each of the four databases, in turn. It reads the necessary configuration for that database (which we stored in the config files in the user area), constructs the Flyway command-line parameters, and executes the commands.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
<# At this point, we have created a database in four different RDBMSs but there is nothing in them. We could have used a backup but instead we will build the database to the version and branch we want, using Flyway. #> $env:FlywayWorkPath = "C:\Path\To\Your\Flyway\Work\Directory" @( @{ 'Comment' = 'Northwind Main for MSSQL '; 'secrets' = "Northwind_MSSQL_Main_RGClone.conf"; 'Directory' = "$env:FlywayWorkPath\Northwind" }, @{ 'Comment' = 'Northwind Main for PostgreSQL'; 'secrets' = "Northwind_PostgreSQL_Main_RGClone.conf"; 'Directory' = "$env:FlywayWorkPath\NorthwindPG" }, @{ 'Comment' = 'Northwind Main for MySQL'; 'secrets' = "Northwind_MySQL_Main_RGClone.conf"; 'Directory' = "$env:FlywayWorkPath\NorthwindMySQL" }, @{ 'Comment' = 'Northwind Main for Oracle'; 'secrets' = "Northwind_oracle_Main_RGClone.conf"; 'Directory' = "$env:FlywayWorkPath\NorthwindOracle" } ) | foreach { cd $_.Directory; $Secrets = $_.Secrets; $Extraparameters = @(); if (-not (Test-Path $Secrets)) { $Secrets = "$($env:USERPROFILE)\$Secrets" } get-content $Secrets | foreach { $_.Split("`n") | where { ($_ -notlike '#*') -and ("$($_)".Trim() -notlike '') } | foreach{ $Extraparameters += (($_ -replace '\Aflyway\.', '-')) } } #pass these config lines as parameters $Extraparameters += "-placeholders.ParameterConfigItem=$Secrets"; Try { flyway $Extraparameters clean; flyway $Extraparameters migrate } catch { $Extraparameters } if (!($?)) {Write-output "$($Extraparameters -join ' ')"} } |
I’ve kept this demonstration code free of functions just to make it clear what the process is doing. Although this is fine for demonstration work, you’d want to automate this for routine work. Here is a cmdlet, Run-FlywayCommand that does this, taken from the Flyway Teamwork framework.
Here is an example of its use:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
Run-FlywayCommand @(@{ 'Comment' = 'Northwind Main for Oracle'; 'secrets' = "Northwind_oracle_Main_RGClone.conf"; 'Directory' = "$env:FlywayWorkPath\NorthwindOracle" } ) @('clean','migrate') function Run-FlywayCommand { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] $TheProjects, [Parameter(Mandatory = $false)] $commands = @('clean', 'migrate') ) $TheProjects | foreach { write-verbose "running $($_.Comment) in $($_.Directory)"; cd $_.Directory; $Secrets = $_.Secrets; $Extraparameters = @(); if (-not (Test-Path $Secrets)) { $Secrets = "$($env:USERPROFILE)\$Secrets" } get-content $Secrets | foreach { $_.Split("`n") | where { ($_ -notlike '#*') -and ("$($_)".Trim() -notlike '') } | foreach{ $Extraparameters += (($_ -replace '\Aflyway\.', '-')) } } #pass these config lines as parameters $Extraparameters += "-placeholders.ParameterConfigItem=$Secrets"; Try { $commands | foreach{ flyway $Extraparameters $_ | where { $_ -notin @('Table altered.','1 row created.') } } } catch { $Extraparameters } if (!($?)) { Write-output "$($Extraparameters -join ' ')" } } } |
Conclusions
Cloning in Redgate Test Data Manager, using the rgclone
CLI, is easy to automate. This article is about demonstrating how easy it is to use, and how it might be used, more than it is about the need to create and develop the Northwind
database on four different RDBMS.
In this demonstration, the clones have been made to last. This treats them like traditional connections. This was not necessary for this demonstration, because Flyway works well with very temporary, volatile connections. However, the more traditional tools such as SSMS and most other IDEs are poorly set up to cope with databases that change their port, user and password, so that clones that last are handy for the traditional IDE. However, to get the most from working with clones, we need to get over this hurdle, and we’ll tackle that problem in the next article.