Skip to content

Commit 5c34ed5

Browse files
yogivkAniruddh25
authored andcommitted
Add application name for PGSQL connections (#2208)
## Why make this change? Closes #1635 ## What is this change? Checking if Application Name is already present in the PgSql connection string. I f yes, we append DAB APP NAME with ',' as separator else, we add an Application Name property to connection string. ## How was this tested? - [ ] Unit Tests ## Sample Request(s) If Application Name is specified by the developer, then DAB will append to it, else will add Application Name property --------- Co-authored-by: Aniruddh Munde <[email protected]>
1 parent df426a4 commit 5c34ed5

File tree

7 files changed

+134
-24
lines changed

7 files changed

+134
-24
lines changed

src/Cli.Tests/EndToEndTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ public void TestInitializingRestAndGraphQLGlobalSettings()
178178
[DataRow(CliBool.None, "cosmosdb_nosql", DatabaseType.CosmosDB_NoSQL, DisplayName = "Init command without '--graphql.multiple-create.enabled' option for cosmosdb_nosql database type")]
179179
public void TestEnablingMultipleCreateOperation(CliBool isMultipleCreateEnabled, string dbType, DatabaseType expectedDbType)
180180
{
181-
List<string> args = new() { "init", "-c", TEST_RUNTIME_CONFIG_FILE, "--connection-string", SAMPLE_TEST_CONN_STRING, "--database-type", dbType };
181+
List<string> args = new() { "init", "-c", TEST_RUNTIME_CONFIG_FILE, "--connection-string", dbType == "postgresql" ? SAMPLE_TEST_PGSQL_CONN_STRING : SAMPLE_TEST_CONN_STRING, "--database-type", dbType };
182182

183183
if (string.Equals("cosmosdb_nosql", dbType, StringComparison.OrdinalIgnoreCase))
184184
{

src/Cli.Tests/TestHelper.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ public static class TestHelper
1313

1414
public const string SAMPLE_TEST_CONN_STRING = "Data Source=<>;Initial Catalog=<>;User ID=<>;Password=<>;";
1515

16+
public const string SAMPLE_TEST_PGSQL_CONN_STRING = "Host=<>;Database=<>;username=<>;password=<>";
17+
1618
// test schema for cosmosDB
1719
public const string TEST_SCHEMA_FILE = "test-schema.gql";
1820
public const string DAB_DRAFT_SCHEMA_TEST_PATH = "https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch/dab.draft.schema.json";

src/Config/Azure.DataApiBuilder.Config.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
</PackageReference>
2828
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
2929
<PackageReference Include="Humanizer" />
30+
<PackageReference Include="Npgsql" />
3031
</ItemGroup>
3132

3233
<ItemGroup>

src/Config/RuntimeConfigLoader.cs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
using Azure.DataApiBuilder.Service.Exceptions;
1515
using Microsoft.Data.SqlClient;
1616
using Microsoft.Extensions.Logging;
17+
using Microsoft.IdentityModel.Tokens;
18+
using Npgsql;
1719

1820
[assembly: InternalsVisibleTo("Azure.DataApiBuilder.Service.Tests")]
1921
namespace Azure.DataApiBuilder.Config;
@@ -105,11 +107,15 @@ public static bool TryParseConfig(string json,
105107

106108
DataSource ds = config.GetDataSourceFromDataSourceName(dataSourceKey);
107109

108-
// Add Application Name for telemetry for MsSQL
110+
// Add Application Name for telemetry for MsSQL or PgSql
109111
if (ds.DatabaseType is DatabaseType.MSSQL && replaceEnvVar)
110112
{
111113
updatedConnection = GetConnectionStringWithApplicationName(connectionValue);
112114
}
115+
else if (ds.DatabaseType is DatabaseType.PostgreSQL && replaceEnvVar)
116+
{
117+
updatedConnection = GetPgSqlConnectionStringWithApplicationName(connectionValue);
118+
}
113119

114120
ds = ds with { ConnectionString = updatedConnection };
115121
config.UpdateDataSourceNameToDataSource(dataSourceName, ds);
@@ -235,4 +241,52 @@ internal static string GetConnectionStringWithApplicationName(string connectionS
235241
// Return the updated connection string.
236242
return connectionStringBuilder.ConnectionString;
237243
}
244+
245+
/// <summary>
246+
/// It adds or replaces a property in the connection string with `Application Name` property.
247+
/// If the connection string already contains the property, it appends the property `Application Name` to the connection string,
248+
/// else add the Application Name property with DataApiBuilder Application Name based on hosted/oss platform.
249+
/// </summary>
250+
/// <param name="connectionString">Connection string for connecting to database.</param>
251+
/// <returns>Updated connection string with `Application Name` property.</returns>
252+
internal static string GetPgSqlConnectionStringWithApplicationName(string connectionString)
253+
{
254+
// If the connection string is null, empty, or whitespace, return it as is.
255+
if (string.IsNullOrWhiteSpace(connectionString))
256+
{
257+
return connectionString;
258+
}
259+
260+
string applicationName = ProductInfo.GetDataApiBuilderUserAgent();
261+
262+
// Create a StringBuilder from the connection string.
263+
NpgsqlConnectionStringBuilder connectionStringBuilder;
264+
try
265+
{
266+
connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString);
267+
}
268+
catch (Exception ex)
269+
{
270+
throw new DataApiBuilderException(
271+
message: DataApiBuilderException.CONNECTION_STRING_ERROR_MESSAGE,
272+
statusCode: HttpStatusCode.ServiceUnavailable,
273+
subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization,
274+
innerException: ex);
275+
}
276+
277+
// If the connection string does not contain the `Application Name` property, add it.
278+
// or if the connection string contains the `Application Name` property, replace it with the DataApiBuilder Application Name.
279+
if (connectionStringBuilder.ApplicationName.IsNullOrEmpty())
280+
{
281+
connectionStringBuilder.ApplicationName = applicationName;
282+
}
283+
else
284+
{
285+
// If the connection string contains the `ApplicationName` property with a value, update the value by adding the DataApiBuilder Application Name.
286+
connectionStringBuilder.ApplicationName += $",{applicationName}";
287+
}
288+
289+
// Return the updated connection string.
290+
return connectionStringBuilder.ConnectionString;
291+
}
238292
}

src/Service.Tests/Configuration/ConfigurationTests.cs

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -652,11 +652,64 @@ public void MsSqlConnStringSupplementedWithAppNameProperty(
652652
message: "DAB did not properly set the 'Application Name' connection string property.");
653653
}
654654

655+
/// <summary>
656+
/// Validates that DAB supplements the PgSQL database connection strings with the property "ApplicationName" and
657+
/// 1. Adds the property/value "Application Name=dab_oss_Major.Minor.Patch" when the env var DAB_APP_NAME_ENV is not set.
658+
/// 2. Adds the property/value "Application Name=dab_hosted_Major.Minor.Patch" when the env var DAB_APP_NAME_ENV is set to "dab_hosted".
659+
/// (DAB_APP_NAME_ENV is set in hosted scenario or when user sets the value.)
660+
/// NOTE: "#pragma warning disable format" is used here to avoid removing intentional, readability promoting spacing in DataRow display names.
661+
/// </summary>
662+
/// <param name="configProvidedConnString">connection string provided in the config.</param>
663+
/// <param name="expectedDabModifiedConnString">Updated connection string with Application Name.</param>
664+
/// <param name="dabEnvOverride">Whether DAB_APP_NAME_ENV is set in environment. (Always present in hosted scenario or if user supplies value.)</param>
665+
[DataTestMethod]
666+
[DataRow("Host=foo;Username=testuser;", "Host=foo;Username=testuser;Application Name=", false, DisplayName = "[PGSQL]:DAB adds version 'dab_oss_major_minor_patch' to non-provided connection string property 'ApplicationName']")]
667+
[DataRow("Host=foo;Username=testuser;", "Host=foo;Username=testuser;Application Name=", true, DisplayName = "[PGSQL]:DAB adds DAB_APP_NAME_ENV value 'dab_hosted' and version suffix '_major_minor_patch' to non-provided connection string property 'ApplicationName'.]")]
668+
[DataRow("Host=foo;Username=testuser;Application Name=UserAppName", "Host=foo;Username=testuser;Application Name=UserAppName,", false, DisplayName = "[PGSQL]:DAB appends version 'dab_oss_major_minor_patch' to user supplied 'Application Name' property.]")]
669+
[DataRow("Host=foo;Username=testuser;Application Name=UserAppName", "Host=foo;Username=testuser;Application Name=UserAppName,", true, DisplayName = "[PGSQL]:DAB appends version string 'dab_hosted' and version suffix '_major_minor_patch' to user supplied 'ApplicationName' property.]")]
670+
public void PgSqlConnStringSupplementedWithAppNameProperty(
671+
string configProvidedConnString,
672+
string expectedDabModifiedConnString,
673+
bool dabEnvOverride)
674+
{
675+
// Explicitly set the DAB_APP_NAME_ENV to null to ensure that the DAB_APP_NAME_ENV is not set.
676+
if (dabEnvOverride)
677+
{
678+
Environment.SetEnvironmentVariable(ProductInfo.DAB_APP_NAME_ENV, "dab_hosted");
679+
}
680+
else
681+
{
682+
Environment.SetEnvironmentVariable(ProductInfo.DAB_APP_NAME_ENV, null);
683+
}
684+
685+
// Resolve assembly version. Not possible to do in DataRow as DataRows expect compile-time constants.
686+
string resolvedAssemblyVersion = ProductInfo.GetDataApiBuilderUserAgent();
687+
expectedDabModifiedConnString += resolvedAssemblyVersion;
688+
689+
RuntimeConfig runtimeConfig = CreateBasicRuntimeConfigWithNoEntity(DatabaseType.PostgreSQL, configProvidedConnString);
690+
691+
// Act
692+
bool configParsed = RuntimeConfigLoader.TryParseConfig(
693+
runtimeConfig.ToJson(),
694+
out RuntimeConfig updatedRuntimeConfig,
695+
replaceEnvVar: true);
696+
697+
// Assert
698+
Assert.AreEqual(
699+
expected: true,
700+
actual: configParsed,
701+
message: "Runtime config unexpectedly failed parsing.");
702+
Assert.AreEqual(
703+
expected: expectedDabModifiedConnString,
704+
actual: updatedRuntimeConfig.DataSource.ConnectionString,
705+
message: "DAB did not properly set the 'Application Name' connection string property.");
706+
}
707+
655708
/// <summary>
656709
/// Validates that DAB doesn't append nor modify
657710
/// - the 'Application Name' or 'App' properties in MySQL database connection strings.
658711
/// - the 'Application Name' property in
659-
/// PostgreSQL, CosmosDB_PostgreSQl, CosmosDB_NoSQL database connection strings.
712+
/// CosmosDB_PostgreSQL, CosmosDB_NoSQL database connection strings.
660713
/// This test validates that this behavior holds true when the DAB_APP_NAME_ENV environment variable
661714
/// - is set (dabEnvOverride==true) -> (DAB hosted)
662715
/// - is not set (dabEnvOverride==false) -> (DAB OSS).
@@ -673,10 +726,6 @@ public void MsSqlConnStringSupplementedWithAppNameProperty(
673726
[DataRow(DatabaseType.MySQL, "Something;" , "Something;" , true , DisplayName = "[MYSQL|DAB hosted]:No addition of 'Application Name' or 'App' property to connection string.")]
674727
[DataRow(DatabaseType.MySQL, "Something;Application Name=CustAppName;" , "Something;Application Name=CustAppName;" , true , DisplayName = "[MYSQL|DAB hosted]:No modification of customer overridden 'Application Name' property.")]
675728
[DataRow(DatabaseType.MySQL, "Something1;App=CustAppName;Something2;" , "Something1;App=CustAppName;Something2;" , true, DisplayName = "[MySQL|DAB hosted]:No modification of customer overridden 'App' property.")]
676-
[DataRow(DatabaseType.PostgreSQL, "Something;" , "Something;" , false, DisplayName = "[PGSQL|DAB OSS]:No addition of 'Application Name' property to connection string.]")]
677-
[DataRow(DatabaseType.PostgreSQL, "Something;Application Name=CustAppName;", "Something;Application Name=CustAppName;", false, DisplayName = "[PGSQL|DAB OSS]:No modification of customer overridden 'Application Name' property.")]
678-
[DataRow(DatabaseType.PostgreSQL, "Something;" , "Something;" , true , DisplayName = "[PGSQL|DAB hosted]:No addition of 'Application Name' property to connection string.")]
679-
[DataRow(DatabaseType.PostgreSQL, "Something;Application Name=CustAppName;", "Something;Application Name=CustAppName;", true , DisplayName = "[PGSQL|DAB hosted]:No modification of customer overridden 'Application Name' property.")]
680729
[DataRow(DatabaseType.CosmosDB_NoSQL, "Something;" , "Something;" , false, DisplayName = "[COSMOSDB_NOSQL|DAB OSS]:No addition of 'Application Name' property to connection string.")]
681730
[DataRow(DatabaseType.CosmosDB_NoSQL, "Something;Application Name=CustAppName;", "Something;Application Name=CustAppName;", false, DisplayName = "[COSMOSDB_NOSQL|DAB OSS]:No modification of customer overridden 'Application Name' property.")]
682731
[DataRow(DatabaseType.CosmosDB_NoSQL, "Something;" , "Something;" , true , DisplayName = "[COSMOSDB_NOSQL|DAB hosted]:No addition of 'Application Name' property to connection string.")]

src/Service.Tests/Unittests/RuntimeConfigLoaderJsonDeserializerTests.cs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,6 @@ public class RuntimeConfigLoaderJsonDeserializerTests
4242
/// <param name="repKeys">Replacement used as key to get environment variable.</param>
4343
/// <param name="repValues">Replacement value.</param>
4444
[DataTestMethod]
45-
[DataRow(
46-
new string[] { "@env(')", "@env()", "@env(')'@env('()", "@env('@env()'", "@@eennvv((''''))" },
47-
new string[] { "@env(')", "@env()", "@env(')'@env('()", "@env('@env()'", "@@eennvv((''''))" },
48-
true,
49-
true,
50-
DisplayName = "Replacement strings that won't match.")]
5145
[DataRow(
5246
new string[] { "@env('envVarName')", "@env(@env('envVarName'))", "@en@env('envVarName')", "@env'()@env'@env('envVarName')')')" },
5347
new string[] { "envVarValue", "@env(envVarValue)", "@enenvVarValue", "@env'()@env'envVarValue')')" },
@@ -539,6 +533,7 @@ private static string GetDataSourceConfigForGivenDatabase(string databaseType)
539533
{
540534
string options = "";
541535
string databaseTypeEnvVariable = "";
536+
string connectionStringEnvVarName = "DATABASE_CONNECTION_STRING";
542537

543538
switch (databaseType)
544539
{
@@ -561,6 +556,7 @@ private static string GetDataSourceConfigForGivenDatabase(string databaseType)
561556
break;
562557
case "postgresql":
563558
databaseTypeEnvVariable = $"@env('POSTGRESQL_DB_TYPE')";
559+
connectionStringEnvVarName = "DATABASE_CONNECTION_STRING_PGSQL";
564560
options = @",""options"": null";
565561
break;
566562
case "dwsql":
@@ -572,7 +568,7 @@ private static string GetDataSourceConfigForGivenDatabase(string databaseType)
572568
return $@"
573569
{{
574570
""database-type"": ""{databaseTypeEnvVariable}"",
575-
""connection-string"": ""@env('DATABASE_CONNECTION_STRING')""
571+
""connection-string"": ""@env('{connectionStringEnvVarName}')""
576572
{options}
577573
}}";
578574
}
@@ -621,7 +617,8 @@ private static void ClearEnvironmentVariablesFromDictionary(Dictionary<string, s
621617
{ "DATABASE_CONTAINER", "xyz"},
622618
{ "DATABASE_NAME", "planet" },
623619
{ "GRAPHQL_SCHEMA_PATH", "gql-schema.gql" },
624-
{ "DATABASE_CONNECTION_STRING", "Data Source=<>;Initial Catalog=<>;User ID=<>;Password=<>;" }
620+
{ "DATABASE_CONNECTION_STRING", "Data Source=<>;Initial Catalog=<>;User ID=<>;Password=<>;" },
621+
{ "DATABASE_CONNECTION_STRING_PGSQL", "Host=<>;Database=<>;username=<>;password=<>" }
625622
};
626623

627624
/// <summary>

src/Service.Tests/Unittests/SqlMetadataProviderUnitTests.cs

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,14 @@ public async Task CheckExceptionForBadConnectionStringForMySql(string connection
166166
[DataRow("")]
167167
public async Task CheckExceptionForBadConnectionStringForPgSql(string connectionString)
168168
{
169+
170+
// For strings that are an invalid format for the connection string builder, need to
171+
// redirect std error to a string writer for comparison to expected error messaging later.
172+
StringWriter sw = new();
173+
Console.SetError(sw);
174+
169175
DatabaseEngine = TestCategory.POSTGRESQL;
170-
await CheckExceptionForBadConnectionStringHelperAsync(DatabaseEngine, connectionString);
176+
await CheckExceptionForBadConnectionStringHelperAsync(DatabaseEngine, connectionString, sw);
171177
}
172178

173179
/// <summary>
@@ -192,6 +198,10 @@ private static async Task CheckExceptionForBadConnectionStringHelperAsync(string
192198
{
193199
_queryBuilder = new MySqlQueryBuilder();
194200
}
201+
else if (string.Equals(databaseType, TestCategory.POSTGRESQL))
202+
{
203+
_queryBuilder = new PostgresQueryBuilder();
204+
}
195205

196206
try
197207
{
@@ -213,16 +223,13 @@ private static async Task CheckExceptionForBadConnectionStringHelperAsync(string
213223
}
214224
catch (DataApiBuilderException ex)
215225
{
216-
// use contains to correctly cover db/user unique error messaging
217-
// if sw is not null it holds the error messaging
218-
string error = sw is null ? ex.Message : sw.ToString();
219-
Assert.IsTrue(error.Contains(DataApiBuilderException.CONNECTION_STRING_ERROR_MESSAGE));
226+
// Combine both the console and exception messages because they both
227+
// may contain the connection string errors this function expects to exist.
228+
string consoleMessages = sw is not null ? sw.ToString() : string.Empty;
229+
string allErrorMessages = ex.Message + " " + consoleMessages;
230+
Assert.IsTrue(allErrorMessages.Contains(DataApiBuilderException.CONNECTION_STRING_ERROR_MESSAGE));
220231
Assert.AreEqual(DataApiBuilderException.SubStatusCodes.ErrorInInitialization, ex.SubStatusCode);
221232
Assert.AreEqual(HttpStatusCode.ServiceUnavailable, ex.StatusCode);
222-
if (sw is not null)
223-
{
224-
Assert.IsTrue(error.StartsWith("Deserialization of the configuration file failed during a post-processing step."));
225-
}
226233
}
227234

228235
TestHelper.UnsetAllDABEnvironmentVariables();

0 commit comments

Comments
 (0)