Skip to content

Commit

Permalink
Merge pull request #19 from drwatson1/feature/issue16
Browse files Browse the repository at this point in the history
Support of AzureSql integrated security
  • Loading branch information
drwatson1 authored Feb 6, 2022
2 parents 2a19115 + 9c4f1c4 commit 768afaa
Show file tree
Hide file tree
Showing 8 changed files with 318 additions and 65 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,16 @@ The tool has almost all the features the DbUp has, but without a single line of
## Supported Databases

* MS SQL Server
* AzureSQL
* PostgreSQL
* MySQL

## Release Notes

|Date|Version|Description|
|-|-|-|
|2022-02-06|1.6.4|Support of drop and ensure for Azure SQL
|2022-02-02|1.6.3|Support of AzureSQL integrated sequrity
|2022-01-30|1.6.2|PostgreSQL SCRAM authentication support interim fix
|2022-01-29|1.6.1|BUGFIX: 'version' and '--version' should return exit code 0
|2021-10-03|1.6.0|Add a 'journalTo' option to dbup.yml
Expand Down
3 changes: 2 additions & 1 deletion src/dbup-cli/ConfigFile/Provider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ public enum Provider
UnsupportedProfider,
SqlServer,
PostgreSQL,
MySQL
MySQL,
AzureSql
}
}
33 changes: 33 additions & 0 deletions src/dbup-cli/ConfigurationHelper.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using DbUp.Builder;
using DbUp.Cli.CommandLineOptions;
using DbUp.Cli.DbUpCustomization;
using DbUp.Engine.Output;
using DbUp.Engine.Transactions;
using DbUp.Helpers;
Expand All @@ -12,6 +13,14 @@ namespace DbUp.Cli
{
public static class ConfigurationHelper
{
private static bool UseAzureSqlIntegratedSecurity(string connectionString)
{
// Use IndexOf to make the code compatible with .NetFramework 4.6
return !(connectionString.IndexOf("Password", StringComparison.InvariantCultureIgnoreCase) >= 0 ||
connectionString.IndexOf("Integrated Security", StringComparison.InvariantCultureIgnoreCase) >= 0 ||
connectionString.IndexOf("Trusted_Connection", StringComparison.InvariantCultureIgnoreCase) >= 0);
}

public static Option<UpgradeEngineBuilder, Error> SelectDbProvider(Provider provider, string connectionString, int connectionTimeoutSec)
{
var timeout = TimeSpan.FromSeconds(connectionTimeoutSec);
Expand All @@ -22,6 +31,10 @@ public static Option<UpgradeEngineBuilder, Error> SelectDbProvider(Provider prov
return DeployChanges.To.SqlDatabase(connectionString)
.WithExecutionTimeout(timeout)
.Some<UpgradeEngineBuilder, Error>();
case Provider.AzureSql:
return DeployChanges.To.SqlDatabase(connectionString, null, UseAzureSqlIntegratedSecurity(connectionString))
.WithExecutionTimeout(timeout)
.Some<UpgradeEngineBuilder, Error>();
case Provider.PostgreSQL:
return DeployChanges.To.PostgresqlDatabase(connectionString)
.WithExecutionTimeout(timeout)
Expand All @@ -44,6 +57,16 @@ public static Option<bool, Error> EnsureDb(IUpgradeLog logger, Provider provider
case Provider.SqlServer:
EnsureDatabase.For.SqlDatabase(connectionString, logger, connectionTimeoutSec);
return true.Some<bool, Error>();
case Provider.AzureSql:
if (UseAzureSqlIntegratedSecurity(connectionString))
{
EnsureDatabase.For.AzureSqlDatabase(connectionString, logger, connectionTimeoutSec);
}
else
{
EnsureDatabase.For.SqlDatabase(connectionString, logger, connectionTimeoutSec);
}
return true.Some<bool, Error>();
case Provider.PostgreSQL:
EnsureDatabase.For.PostgresqlDatabase(connectionString, logger); // Postgres provider does not support timeout...
return true.Some<bool, Error>();
Expand All @@ -69,6 +92,16 @@ public static Option<bool, Error> DropDb(IUpgradeLog logger, Provider provider,
case Provider.SqlServer:
DropDatabase.For.SqlDatabase(connectionString, logger, connectionTimeoutSec);
return true.Some<bool, Error>();
case Provider.AzureSql:
if (UseAzureSqlIntegratedSecurity(connectionString))
{
DropDatabase.For.AzureSqlDatabase(connectionString, logger, connectionTimeoutSec);
}
else
{
DropDatabase.For.SqlDatabase(connectionString, logger, connectionTimeoutSec);
}
return true.Some<bool, Error>();
case Provider.PostgreSQL:
return Option.None<bool, Error>(Error.Create("PostgreSQL database provider does not support 'drop' command for now"));
case Provider.MySQL:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
using DbUp.Engine.Output;
using DbUp.SqlServer;
using Microsoft.Azure.Services.AppAuthentication;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Text;

namespace DbUp.Cli.DbUpCustomization
{
static class AzureSqlDatabaseWithIntegratedSecurity
{
/*
* CAUTION!!! This code is copied from original file https://github.com/DbUp/DbUp/blob/master/src/dbup-sqlserver/SqlServerExtensions.cs
* The reason is that the DbUp does not fully support AzureSQL.
* More discussions see in https://github.com/drwatson1/dbup-cli/issues/16
*/

/// <summary>
/// Ensures that the database specified in the connection string exists.
/// </summary>
/// <param name="supported">Fluent helper type.</param>
/// <param name="connectionString">The connection string.</param>
/// <param name="logger">The <see cref="DbUp.Engine.Output.IUpgradeLog"/> used to record actions.</param>
/// <param name="timeout">Use this to set the command time out for creating a database in case you're encountering a time out in this operation.</param>
/// <param name="azureDatabaseEdition">Use to indicate that the SQL server database is in Azure</param>
/// <param name="collation">The collation name to set during database creation</param>
/// <returns></returns>
public static void AzureSqlDatabase(
this SupportedDatabasesForEnsureDatabase supported,
string connectionString,
IUpgradeLog logger,
int timeout = -1,
AzureDatabaseEdition azureDatabaseEdition = AzureDatabaseEdition.None,
string collation = null)
{
GetMasterConnectionStringBuilder(connectionString, logger, out var masterConnectionString, out var databaseName);

using (var connection = new SqlConnection(masterConnectionString))
{
connection.AccessToken = GetAccessToken();
try
{
connection.Open();
}
catch (SqlException)
{
// Failed to connect to master, lets try direct
if (DatabaseExistsIfConnectedToDirectly(logger, connectionString, databaseName))
return;

throw;
}

if (DatabaseExists(connection, databaseName))
return;

var collationString = string.IsNullOrEmpty(collation) ? "" : $@" COLLATE {collation}";
var sqlCommandText = $@"create database [{databaseName}]{collationString}";

switch (azureDatabaseEdition)
{
case AzureDatabaseEdition.None:
sqlCommandText += ";";
break;
case AzureDatabaseEdition.Basic:
sqlCommandText += " ( EDITION = ''basic'' );";
break;
case AzureDatabaseEdition.Standard:
sqlCommandText += " ( EDITION = ''standard'' );";
break;
case AzureDatabaseEdition.Premium:
sqlCommandText += " ( EDITION = ''premium'' );";
break;
}

// Create the database...
using (var command = new SqlCommand(sqlCommandText, connection)
{
CommandType = CommandType.Text
})
{
if (timeout >= 0)
{
command.CommandTimeout = timeout;
}

command.ExecuteNonQuery();
}

logger.WriteInformation(@"Created database {0}", databaseName);
}
}

/// <summary>
/// Drop the database specified in the connection string.
/// </summary>
/// <param name="supported">Fluent helper type.</param>
/// <param name="connectionString">The connection string.</param>
/// <param name="logger">The <see cref="DbUp.Engine.Output.IUpgradeLog"/> used to record actions.</param>
/// <param name="timeout">Use this to set the command time out for dropping a database in case you're encountering a time out in this operation.</param>
/// <returns></returns>
public static void AzureSqlDatabase(this SupportedDatabasesForDropDatabase supported, string connectionString, IUpgradeLog logger, int timeout = -1)
{
GetMasterConnectionStringBuilder(connectionString, logger, out var masterConnectionString, out var databaseName);

using (var connection = new SqlConnection(masterConnectionString))
{
connection.AccessToken = GetAccessToken();

connection.Open();
if (!DatabaseExists(connection, databaseName))
return;

// Actually we should call ALTER DATABASE [{databaseName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
// before DROP as for the SQL Server,
// but it does not work with the following error message:
//
// ODBC error: State: 42000: Error: 1468 Message:'[Microsoft][ODBC Driver 17 for SQL Server][SQL Server]The operation cannot be performed on database "MYNEWDB" because it is involved in a database mirroring session or an availability group. Some operations are not allowed on a database that is participating in a database mirroring session or in an availability group.'.
// ALTER DATABASE statement failed.
//
// Experiment shows that DROP works fine even the other user is connected.
// So single user mode is not necessary for Azure SQL
var dropDatabaseCommand = new SqlCommand($"DROP DATABASE [{databaseName}];", connection) { CommandType = CommandType.Text };
using (var command = dropDatabaseCommand)
{
command.ExecuteNonQuery();
}

logger.WriteInformation("Dropped database {0}", databaseName);
}
}

static void GetMasterConnectionStringBuilder(string connectionString, IUpgradeLog logger, out string masterConnectionString, out string databaseName)
{
if (string.IsNullOrEmpty(connectionString) || connectionString.Trim() == string.Empty)
throw new ArgumentNullException("connectionString");

if (logger == null)
throw new ArgumentNullException("logger");

var masterConnectionStringBuilder = new SqlConnectionStringBuilder(connectionString);
databaseName = masterConnectionStringBuilder.InitialCatalog;

if (string.IsNullOrEmpty(databaseName) || databaseName.Trim() == string.Empty)
throw new InvalidOperationException("The connection string does not specify a database name.");

masterConnectionStringBuilder.InitialCatalog = "master";
var logMasterConnectionStringBuilder = new SqlConnectionStringBuilder(masterConnectionStringBuilder.ConnectionString)
{
Password = string.Empty.PadRight(masterConnectionStringBuilder.Password.Length, '*')
};

logger.WriteInformation("Master ConnectionString => {0}", logMasterConnectionStringBuilder.ConnectionString);
masterConnectionString = masterConnectionStringBuilder.ConnectionString;
}

static bool DatabaseExists(SqlConnection connection, string databaseName)
{
var sqlCommandText = string.Format
(
@"SELECT TOP 1 case WHEN dbid IS NOT NULL THEN 1 ELSE 0 end FROM sys.sysdatabases WHERE name = '{0}';",
databaseName
);

// check to see if the database already exists..
using (var command = new SqlCommand(sqlCommandText, connection)
{
CommandType = CommandType.Text
})

{
var results = (int?)command.ExecuteScalar();

if (results.HasValue && results.Value == 1)
return true;
else
return false;
}
}

static bool DatabaseExistsIfConnectedToDirectly(IUpgradeLog logger, string connectionString, string databaseName)
{
try
{
using (var connection = new SqlConnection(connectionString))
{
connection.AccessToken = GetAccessToken();

connection.Open();
return DatabaseExists(connection, databaseName);
}
}
catch
{
logger.WriteInformation("Could not connect to the database directly");
return false;
}
}

static string GetAccessToken(string resource = "https://database.windows.net/", string tenantId = null, string azureAdInstance = "https://login.microsoftonline.com/")
{
return new AzureServiceTokenProvider(azureAdInstance: azureAdInstance).GetAccessTokenAsync(resource, tenantId)
.ConfigureAwait(false)
.GetAwaiter()
.GetResult();
}
}
}
2 changes: 1 addition & 1 deletion src/dbup-cli/DefaultOptions/dbup.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
dbUp:
version: 1 # should be 1
provider: sqlserver # DB provider: sqlserver
provider: sqlserver # DB provider: sqlserver, postgresql, mysql, azuresql
connectionString: $CONNSTR$ # Connection string to DB. For example, "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=dbup;Integrated Security=True" for sqlserver
connectionTimeoutSec: 30 # Connection timeout in seconds
transaction: None # Single / PerScript / None (default)
Expand Down
4 changes: 2 additions & 2 deletions src/dbup-cli/How-to-add-a-new-db.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

1. Add a corresponding DbUp NuGet-package. Typically they are named as `dbup-<db-name>`, for example `dbup-mysql`
1. Add a new provider name to the Provider enum in the `ConfigFile/Provider` file
1. Update methods `SelectDbProvider`, `EnsureDb`, `DropDb`
1. Update methods `SelectDbProvider`, `EnsureDb`, `DropDb` in `ConfigurationHelper.cs` file
1. Create a new integration test in the `dbup-cli.integration-tests` project. The easiest way to do so is to copy one of the tests, already there.
- Under the `Scripts` folder create a new folder for database scripts for tests. You can copy it from another folder. I don't recommend using one of the existing script folder.
- Change a provider name in `dbup.yml` files
- Change SQL in `Timeout` folder because different databases have different syntax for sleep or delay execution
- Anjust connection strings
- Adjust connection strings
- Adjust `TestInitialize` method in according to documenation
- Add corresponding NuGet-package to the `dbup-cli.integration-tests` project
- Replace connection and command classes all over the test
12 changes: 9 additions & 3 deletions src/dbup-cli/How-to-create-a-new-release.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
# How to create a new release

1. Open Project Properties and update version, release notes, tags and so on
1. Build a NuGet-package as described in the [README](./README.md) file.
1. Update `/build/PackDbUp.cmd` and build standalone utility as described in this [README](../../build/README.md).
1. Open Project Properties and update a version, release notes, tags and so on
1. Build a NuGet-package
* Open a console
* Go to the `src\dbup-cli` folder
* Run `dotnet pack -c Release`
1. Build a .NetFramework 4.6 standalone utility
* Run `dotnet build -c Release -p:GlobalTool=false`
* Update `/build/PackDbUp.cmd` if needed
* Pack the utility to a single exe-file: go to the `build` folder and run `PackDbUp.cmd`. See the `build/readme.md` for additional instructions
1. Update Release Notes on the project main [README](https://github.com/drwatson1/dbup-cli/blob/master/README.md) page.
1. Update Wiki-pages if needed
1. Publish NuGet-package. Dont't remember to add additional documentation from the main [README](https://github.com/drwatson1/dbup-cli/blob/master/README.md) page.
Expand Down
Loading

0 comments on commit 768afaa

Please sign in to comment.