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
121 changes: 116 additions & 5 deletions DataGateway.Config/RuntimeConfigPath.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using Azure.DataGateway.Service.Exceptions;

namespace Azure.DataGateway.Config
{
/// <summary>
Expand Down Expand Up @@ -29,7 +34,7 @@ public class RuntimeConfigPath
{
if (File.Exists(ConfigFileName))
{
runtimeConfigJson = File.ReadAllText(ConfigFileName);
runtimeConfigJson = ParseConfigJsonAndReplaceEnvVariables(File.ReadAllText(ConfigFileName));
}
else
{
Expand All @@ -53,6 +58,112 @@ public class RuntimeConfigPath
return null;
}

/// <summary>
/// Parse Json and replace @env('ENVIRONMENT_VARIABLE_NAME') with
/// the environment variable's value that corresponds to ENVIRONMENT_VARIABLE_NAME.
/// If no environment variable is found with that name, throw exception.
/// </summary>
/// <param name="json">Json string representing the runtime config file.</param>
/// <returns>Parsed json string.</returns>
public static string? ParseConfigJsonAndReplaceEnvVariables(string json)
{
Utf8JsonReader reader = new(jsonData: Encoding.UTF8.GetBytes(json),
isFinalBlock: true,
state: new());
MemoryStream stream = new();
Utf8JsonWriter writer = new(stream, options: new() { Indented = true });

// @env\(' : match @env('
// .*? : lazy match any character except newline 0 or more times
Comment thread
aaronburtle marked this conversation as resolved.
// (?='\)) : look ahead for ') which will combine with our lazy match
// ie: in @env('hello')goodbye') we match @env('hello')
// '\) : consume the ') into the match (look ahead doesn't capture)
// This pattern lazy matches any string that starts with @env(' and ends with ')
// ie: fooBAR@env('hello-world')bash)FOO') match: @env('hello-world')
Comment thread
aaronburtle marked this conversation as resolved.
// This matching pattern allows for the @env('<match>') to be safely nested
// within strings that contain ') after our match.
// ie: if the environment variable "Baz" has the value of "Bar"
// fooBarBaz: "('foo@env('Baz')Baz')" would parse into
// fooBarBaz: "('fooBarBaz')"
// Note that there is no escape character currently for ') to exist
// within the name of the environment variable, but that ') is not
// a valid environment variable name in certain shells.
string envPattern = @"@env\('.*?(?='\))'\)";

// The approach for parsing is to re-write the Json to a new string
// as we read, using regex.replace for the matches we get from our
// pattern. We call a helper function for each match that handles
// getting the environment variable for replacement.
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.PropertyName:
writer.WritePropertyName(reader.GetString()!);
break;
case JsonTokenType.String:
string valueToWrite = Regex.Replace(reader.GetString()!, envPattern, new MatchEvaluator(ReplaceMatchWithEnvVariable));
writer.WriteStringValue(valueToWrite);
break;
case JsonTokenType.Number:
writer.WriteNumberValue(reader.GetDecimal());
break;
case JsonTokenType.True:
case JsonTokenType.False:
writer.WriteBooleanValue(reader.GetBoolean());
break;
case JsonTokenType.StartObject:
writer.WriteStartObject();
break;
case JsonTokenType.StartArray:
writer.WriteStartArray();
break;
case JsonTokenType.EndArray:
writer.WriteEndArray();
break;
case JsonTokenType.EndObject:
writer.WriteEndObject();
break;
// ie: "path" : null
case JsonTokenType.Null:
writer.WriteNullValue();
break;
default:
writer.WriteRawValue(reader.GetString()!);
break;
}
}

writer.Flush();
return Encoding.UTF8.GetString(stream.ToArray());
}

/// <summary>
/// Retrieves the name of the environment variable
/// and then returns the environment variable value associated
/// with that name, throwing an exception if none is found.
/// </summary>
/// <param name="match">The match holding the environment variable name.</param>
/// <returns>The environment variable value associated with the provided name.</returns>
/// <exception cref="DataGatewayException"></exception>
private static string ReplaceMatchWithEnvVariable(Match match)
{
// [^@env\(] : any substring that is not @env(
// .* : any char except newline any number of times
// (?=\)) : look ahead for end char of )
// This pattern greedy matches all characters that are not a part of @env()
// ie: @env('hello@env('goodbye')world') match: 'hello@env('goodbye')world'
string innerPattern = @"[^@env\(].*(?=\))";

// strip's first and last characters, ie: '''hello'' --> ''hello'
string envName = Regex.Match(match.Value, innerPattern).Value[1..^1];
string? envValue = Environment.GetEnvironmentVariable(envName);
Comment thread
aaronburtle marked this conversation as resolved.
return envValue is not null ? envValue :
throw new DataGatewayException(message: $"Environmental Variable, {envName}, not found.",
statusCode: System.Net.HttpStatusCode.ServiceUnavailable,
subStatusCode: DataGatewayException.SubStatusCodes.ErrorInInitialization);
}

/// <summary>
/// Precedence of environments is
/// 1) Value of HAWAII_ENVIRONMENT.
Expand Down Expand Up @@ -81,10 +192,10 @@ public static string GetFileNameForEnvironment(string? hostingEnvironmentName)
index++)
{
if (!string.IsNullOrWhiteSpace(environmentPrecedence[index])
// The last index is for the default case - the last fallback option
// where environmentPrecedence[index] is string.Empty
// for that case, we still need to get the file name considering overrides
// so need to do an OR on the last index here
// The last index is for the default case - the last fallback option
// where environmentPrecedence[index] is string.Empty
// for that case, we still need to get the file name considering overrides
// so need to do an OR on the last index here
Comment thread
aaronburtle marked this conversation as resolved.
|| index == environmentPrecedence.Length - 1)
{
configFileNameWithExtension =
Expand Down
Loading