Skip to content

Commit 768afaa

Browse files
authored
Merge pull request #19 from drwatson1/feature/issue16
Support of AzureSql integrated security
2 parents 2a19115 + 9c4f1c4 commit 768afaa

File tree

8 files changed

+318
-65
lines changed

8 files changed

+318
-65
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,16 @@ The tool has almost all the features the DbUp has, but without a single line of
5050
## Supported Databases
5151

5252
* MS SQL Server
53+
* AzureSQL
5354
* PostgreSQL
5455
* MySQL
5556

5657
## Release Notes
5758

5859
|Date|Version|Description|
5960
|-|-|-|
61+
|2022-02-06|1.6.4|Support of drop and ensure for Azure SQL
62+
|2022-02-02|1.6.3|Support of AzureSQL integrated sequrity
6063
|2022-01-30|1.6.2|PostgreSQL SCRAM authentication support interim fix
6164
|2022-01-29|1.6.1|BUGFIX: 'version' and '--version' should return exit code 0
6265
|2021-10-03|1.6.0|Add a 'journalTo' option to dbup.yml

src/dbup-cli/ConfigFile/Provider.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ public enum Provider
55
UnsupportedProfider,
66
SqlServer,
77
PostgreSQL,
8-
MySQL
8+
MySQL,
9+
AzureSql
910
}
1011
}

src/dbup-cli/ConfigurationHelper.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using DbUp.Builder;
22
using DbUp.Cli.CommandLineOptions;
3+
using DbUp.Cli.DbUpCustomization;
34
using DbUp.Engine.Output;
45
using DbUp.Engine.Transactions;
56
using DbUp.Helpers;
@@ -12,6 +13,14 @@ namespace DbUp.Cli
1213
{
1314
public static class ConfigurationHelper
1415
{
16+
private static bool UseAzureSqlIntegratedSecurity(string connectionString)
17+
{
18+
// Use IndexOf to make the code compatible with .NetFramework 4.6
19+
return !(connectionString.IndexOf("Password", StringComparison.InvariantCultureIgnoreCase) >= 0 ||
20+
connectionString.IndexOf("Integrated Security", StringComparison.InvariantCultureIgnoreCase) >= 0 ||
21+
connectionString.IndexOf("Trusted_Connection", StringComparison.InvariantCultureIgnoreCase) >= 0);
22+
}
23+
1524
public static Option<UpgradeEngineBuilder, Error> SelectDbProvider(Provider provider, string connectionString, int connectionTimeoutSec)
1625
{
1726
var timeout = TimeSpan.FromSeconds(connectionTimeoutSec);
@@ -22,6 +31,10 @@ public static Option<UpgradeEngineBuilder, Error> SelectDbProvider(Provider prov
2231
return DeployChanges.To.SqlDatabase(connectionString)
2332
.WithExecutionTimeout(timeout)
2433
.Some<UpgradeEngineBuilder, Error>();
34+
case Provider.AzureSql:
35+
return DeployChanges.To.SqlDatabase(connectionString, null, UseAzureSqlIntegratedSecurity(connectionString))
36+
.WithExecutionTimeout(timeout)
37+
.Some<UpgradeEngineBuilder, Error>();
2538
case Provider.PostgreSQL:
2639
return DeployChanges.To.PostgresqlDatabase(connectionString)
2740
.WithExecutionTimeout(timeout)
@@ -44,6 +57,16 @@ public static Option<bool, Error> EnsureDb(IUpgradeLog logger, Provider provider
4457
case Provider.SqlServer:
4558
EnsureDatabase.For.SqlDatabase(connectionString, logger, connectionTimeoutSec);
4659
return true.Some<bool, Error>();
60+
case Provider.AzureSql:
61+
if (UseAzureSqlIntegratedSecurity(connectionString))
62+
{
63+
EnsureDatabase.For.AzureSqlDatabase(connectionString, logger, connectionTimeoutSec);
64+
}
65+
else
66+
{
67+
EnsureDatabase.For.SqlDatabase(connectionString, logger, connectionTimeoutSec);
68+
}
69+
return true.Some<bool, Error>();
4770
case Provider.PostgreSQL:
4871
EnsureDatabase.For.PostgresqlDatabase(connectionString, logger); // Postgres provider does not support timeout...
4972
return true.Some<bool, Error>();
@@ -69,6 +92,16 @@ public static Option<bool, Error> DropDb(IUpgradeLog logger, Provider provider,
6992
case Provider.SqlServer:
7093
DropDatabase.For.SqlDatabase(connectionString, logger, connectionTimeoutSec);
7194
return true.Some<bool, Error>();
95+
case Provider.AzureSql:
96+
if (UseAzureSqlIntegratedSecurity(connectionString))
97+
{
98+
DropDatabase.For.AzureSqlDatabase(connectionString, logger, connectionTimeoutSec);
99+
}
100+
else
101+
{
102+
DropDatabase.For.SqlDatabase(connectionString, logger, connectionTimeoutSec);
103+
}
104+
return true.Some<bool, Error>();
72105
case Provider.PostgreSQL:
73106
return Option.None<bool, Error>(Error.Create("PostgreSQL database provider does not support 'drop' command for now"));
74107
case Provider.MySQL:
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
using DbUp.Engine.Output;
2+
using DbUp.SqlServer;
3+
using Microsoft.Azure.Services.AppAuthentication;
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Data;
7+
using System.Data.SqlClient;
8+
using System.Text;
9+
10+
namespace DbUp.Cli.DbUpCustomization
11+
{
12+
static class AzureSqlDatabaseWithIntegratedSecurity
13+
{
14+
/*
15+
* CAUTION!!! This code is copied from original file https://github.com/DbUp/DbUp/blob/master/src/dbup-sqlserver/SqlServerExtensions.cs
16+
* The reason is that the DbUp does not fully support AzureSQL.
17+
* More discussions see in https://github.com/drwatson1/dbup-cli/issues/16
18+
*/
19+
20+
/// <summary>
21+
/// Ensures that the database specified in the connection string exists.
22+
/// </summary>
23+
/// <param name="supported">Fluent helper type.</param>
24+
/// <param name="connectionString">The connection string.</param>
25+
/// <param name="logger">The <see cref="DbUp.Engine.Output.IUpgradeLog"/> used to record actions.</param>
26+
/// <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>
27+
/// <param name="azureDatabaseEdition">Use to indicate that the SQL server database is in Azure</param>
28+
/// <param name="collation">The collation name to set during database creation</param>
29+
/// <returns></returns>
30+
public static void AzureSqlDatabase(
31+
this SupportedDatabasesForEnsureDatabase supported,
32+
string connectionString,
33+
IUpgradeLog logger,
34+
int timeout = -1,
35+
AzureDatabaseEdition azureDatabaseEdition = AzureDatabaseEdition.None,
36+
string collation = null)
37+
{
38+
GetMasterConnectionStringBuilder(connectionString, logger, out var masterConnectionString, out var databaseName);
39+
40+
using (var connection = new SqlConnection(masterConnectionString))
41+
{
42+
connection.AccessToken = GetAccessToken();
43+
try
44+
{
45+
connection.Open();
46+
}
47+
catch (SqlException)
48+
{
49+
// Failed to connect to master, lets try direct
50+
if (DatabaseExistsIfConnectedToDirectly(logger, connectionString, databaseName))
51+
return;
52+
53+
throw;
54+
}
55+
56+
if (DatabaseExists(connection, databaseName))
57+
return;
58+
59+
var collationString = string.IsNullOrEmpty(collation) ? "" : $@" COLLATE {collation}";
60+
var sqlCommandText = $@"create database [{databaseName}]{collationString}";
61+
62+
switch (azureDatabaseEdition)
63+
{
64+
case AzureDatabaseEdition.None:
65+
sqlCommandText += ";";
66+
break;
67+
case AzureDatabaseEdition.Basic:
68+
sqlCommandText += " ( EDITION = ''basic'' );";
69+
break;
70+
case AzureDatabaseEdition.Standard:
71+
sqlCommandText += " ( EDITION = ''standard'' );";
72+
break;
73+
case AzureDatabaseEdition.Premium:
74+
sqlCommandText += " ( EDITION = ''premium'' );";
75+
break;
76+
}
77+
78+
// Create the database...
79+
using (var command = new SqlCommand(sqlCommandText, connection)
80+
{
81+
CommandType = CommandType.Text
82+
})
83+
{
84+
if (timeout >= 0)
85+
{
86+
command.CommandTimeout = timeout;
87+
}
88+
89+
command.ExecuteNonQuery();
90+
}
91+
92+
logger.WriteInformation(@"Created database {0}", databaseName);
93+
}
94+
}
95+
96+
/// <summary>
97+
/// Drop the database specified in the connection string.
98+
/// </summary>
99+
/// <param name="supported">Fluent helper type.</param>
100+
/// <param name="connectionString">The connection string.</param>
101+
/// <param name="logger">The <see cref="DbUp.Engine.Output.IUpgradeLog"/> used to record actions.</param>
102+
/// <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>
103+
/// <returns></returns>
104+
public static void AzureSqlDatabase(this SupportedDatabasesForDropDatabase supported, string connectionString, IUpgradeLog logger, int timeout = -1)
105+
{
106+
GetMasterConnectionStringBuilder(connectionString, logger, out var masterConnectionString, out var databaseName);
107+
108+
using (var connection = new SqlConnection(masterConnectionString))
109+
{
110+
connection.AccessToken = GetAccessToken();
111+
112+
connection.Open();
113+
if (!DatabaseExists(connection, databaseName))
114+
return;
115+
116+
// Actually we should call ALTER DATABASE [{databaseName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
117+
// before DROP as for the SQL Server,
118+
// but it does not work with the following error message:
119+
//
120+
// 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.'.
121+
// ALTER DATABASE statement failed.
122+
//
123+
// Experiment shows that DROP works fine even the other user is connected.
124+
// So single user mode is not necessary for Azure SQL
125+
var dropDatabaseCommand = new SqlCommand($"DROP DATABASE [{databaseName}];", connection) { CommandType = CommandType.Text };
126+
using (var command = dropDatabaseCommand)
127+
{
128+
command.ExecuteNonQuery();
129+
}
130+
131+
logger.WriteInformation("Dropped database {0}", databaseName);
132+
}
133+
}
134+
135+
static void GetMasterConnectionStringBuilder(string connectionString, IUpgradeLog logger, out string masterConnectionString, out string databaseName)
136+
{
137+
if (string.IsNullOrEmpty(connectionString) || connectionString.Trim() == string.Empty)
138+
throw new ArgumentNullException("connectionString");
139+
140+
if (logger == null)
141+
throw new ArgumentNullException("logger");
142+
143+
var masterConnectionStringBuilder = new SqlConnectionStringBuilder(connectionString);
144+
databaseName = masterConnectionStringBuilder.InitialCatalog;
145+
146+
if (string.IsNullOrEmpty(databaseName) || databaseName.Trim() == string.Empty)
147+
throw new InvalidOperationException("The connection string does not specify a database name.");
148+
149+
masterConnectionStringBuilder.InitialCatalog = "master";
150+
var logMasterConnectionStringBuilder = new SqlConnectionStringBuilder(masterConnectionStringBuilder.ConnectionString)
151+
{
152+
Password = string.Empty.PadRight(masterConnectionStringBuilder.Password.Length, '*')
153+
};
154+
155+
logger.WriteInformation("Master ConnectionString => {0}", logMasterConnectionStringBuilder.ConnectionString);
156+
masterConnectionString = masterConnectionStringBuilder.ConnectionString;
157+
}
158+
159+
static bool DatabaseExists(SqlConnection connection, string databaseName)
160+
{
161+
var sqlCommandText = string.Format
162+
(
163+
@"SELECT TOP 1 case WHEN dbid IS NOT NULL THEN 1 ELSE 0 end FROM sys.sysdatabases WHERE name = '{0}';",
164+
databaseName
165+
);
166+
167+
// check to see if the database already exists..
168+
using (var command = new SqlCommand(sqlCommandText, connection)
169+
{
170+
CommandType = CommandType.Text
171+
})
172+
173+
{
174+
var results = (int?)command.ExecuteScalar();
175+
176+
if (results.HasValue && results.Value == 1)
177+
return true;
178+
else
179+
return false;
180+
}
181+
}
182+
183+
static bool DatabaseExistsIfConnectedToDirectly(IUpgradeLog logger, string connectionString, string databaseName)
184+
{
185+
try
186+
{
187+
using (var connection = new SqlConnection(connectionString))
188+
{
189+
connection.AccessToken = GetAccessToken();
190+
191+
connection.Open();
192+
return DatabaseExists(connection, databaseName);
193+
}
194+
}
195+
catch
196+
{
197+
logger.WriteInformation("Could not connect to the database directly");
198+
return false;
199+
}
200+
}
201+
202+
static string GetAccessToken(string resource = "https://database.windows.net/", string tenantId = null, string azureAdInstance = "https://login.microsoftonline.com/")
203+
{
204+
return new AzureServiceTokenProvider(azureAdInstance: azureAdInstance).GetAccessTokenAsync(resource, tenantId)
205+
.ConfigureAwait(false)
206+
.GetAwaiter()
207+
.GetResult();
208+
}
209+
}
210+
}

src/dbup-cli/DefaultOptions/dbup.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
dbUp:
22
version: 1 # should be 1
3-
provider: sqlserver # DB provider: sqlserver
3+
provider: sqlserver # DB provider: sqlserver, postgresql, mysql, azuresql
44
connectionString: $CONNSTR$ # Connection string to DB. For example, "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=dbup;Integrated Security=True" for sqlserver
55
connectionTimeoutSec: 30 # Connection timeout in seconds
66
transaction: None # Single / PerScript / None (default)

src/dbup-cli/How-to-add-a-new-db.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
1. Add a corresponding DbUp NuGet-package. Typically they are named as `dbup-<db-name>`, for example `dbup-mysql`
44
1. Add a new provider name to the Provider enum in the `ConfigFile/Provider` file
5-
1. Update methods `SelectDbProvider`, `EnsureDb`, `DropDb`
5+
1. Update methods `SelectDbProvider`, `EnsureDb`, `DropDb` in `ConfigurationHelper.cs` file
66
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.
77
- 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.
88
- Change a provider name in `dbup.yml` files
99
- Change SQL in `Timeout` folder because different databases have different syntax for sleep or delay execution
10-
- Anjust connection strings
10+
- Adjust connection strings
1111
- Adjust `TestInitialize` method in according to documenation
1212
- Add corresponding NuGet-package to the `dbup-cli.integration-tests` project
1313
- Replace connection and command classes all over the test

src/dbup-cli/How-to-create-a-new-release.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
# How to create a new release
22

3-
1. Open Project Properties and update version, release notes, tags and so on
4-
1. Build a NuGet-package as described in the [README](./README.md) file.
5-
1. Update `/build/PackDbUp.cmd` and build standalone utility as described in this [README](../../build/README.md).
3+
1. Open Project Properties and update a version, release notes, tags and so on
4+
1. Build a NuGet-package
5+
* Open a console
6+
* Go to the `src\dbup-cli` folder
7+
* Run `dotnet pack -c Release`
8+
1. Build a .NetFramework 4.6 standalone utility
9+
* Run `dotnet build -c Release -p:GlobalTool=false`
10+
* Update `/build/PackDbUp.cmd` if needed
11+
* 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
612
1. Update Release Notes on the project main [README](https://github.com/drwatson1/dbup-cli/blob/master/README.md) page.
713
1. Update Wiki-pages if needed
814
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.

0 commit comments

Comments
 (0)