Skip to content

Commit 0b3e3ab

Browse files
Add support for .env file (#1497)
## Why make this change? - Closes #1374 - To allow users maintain a local `.env` to manage environment variables during local development. ## What is this change? - Using the package [DotNetEnv](https://www.nuget.org/packages/DotNetEnv/) - Now, users can maintain a local file `.env` to manage environment variables instead of setting environment variables in the system. - If `.env` file is used, it will take precedence over system environment variables. ## How was this tested? - [x] Unit Tests ## NOTE: - This feature is to enhance local development experience, and it works when used via DAB CLI. ## QnA 1. Where should the `.env` file be created? -> For `.env file` to be considered, it should be present in the same directory where the dab commands are executed. 2. Does it overwrite the existing environment variables? -> No, it doesn't update any existing variables. It just gives precedence to the variable defined in the file over the system defined variable in case it is present in both places. --------- Co-authored-by: Aaron Powell <[email protected]>
1 parent 4717b1b commit 0b3e3ab

File tree

6 files changed

+193
-5
lines changed

6 files changed

+193
-5
lines changed

src/Cli.Tests/EnvironmentTests.cs

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace Cli.Tests;
5+
6+
/// <summary>
7+
/// Contains test involving environment variables.
8+
/// </summary>
9+
[TestClass]
10+
public class EnvironmentTests
11+
{
12+
public const string TEST_ENV_VARIABLE = "DAB_TEST_ENVIRONMENT";
13+
/// <summary>
14+
/// Test to verify that environment variable setup in the system is picked up correctly
15+
/// when no .env file is present.
16+
/// </summary>
17+
[TestMethod]
18+
public void TestEnvironmentVariableIsConsumedCorrectly()
19+
{
20+
string jsonWithEnvVariable = @"{""envValue"": ""@env('DAB_TEST_ENVIRONMENT')""}";
21+
22+
// No environment File, No environment variable set in the system
23+
Assert.AreEqual(null, Environment.GetEnvironmentVariable(TEST_ENV_VARIABLE));
24+
25+
// Configuring environment variable in the system
26+
Environment.SetEnvironmentVariable(TEST_ENV_VARIABLE, "TEST");
27+
28+
// Test environment variable is correctly resolved in the config file
29+
string? resolvedJson = RuntimeConfigPath.ParseConfigJsonAndReplaceEnvVariables(jsonWithEnvVariable);
30+
Assert.IsNotNull(resolvedJson);
31+
Assert.IsTrue(JToken.DeepEquals(
32+
JObject.Parse(@"{""envValue"": ""TEST""}"),
33+
JObject.Parse(resolvedJson)), "JSON resolved with environment variable correctly");
34+
35+
// removing Environment variable from the System
36+
Environment.SetEnvironmentVariable(TEST_ENV_VARIABLE, null);
37+
Assert.ThrowsException<DataApiBuilderException>(() =>
38+
RuntimeConfigPath.ParseConfigJsonAndReplaceEnvVariables(jsonWithEnvVariable),
39+
$"Environmental Variable, {TEST_ENV_VARIABLE}, not found.");
40+
}
41+
42+
/// <summary>
43+
/// This test creates a .env file and adds a variable and verifies that the variable is
44+
/// correctly consumed.
45+
/// For this test there were no existing environment variables. The values are picked up
46+
/// directly from the `.env` file.
47+
/// </summary>
48+
[TestMethod]
49+
public void TestEnvironmentFileIsConsumedCorrectly()
50+
{
51+
string jsonWithEnvVariable = @"{""envValue"": ""@env('DAB_TEST_ENVIRONMENT')""}";
52+
53+
// No environment File, no environment variable set in the system
54+
Assert.IsNull(Environment.GetEnvironmentVariable(TEST_ENV_VARIABLE));
55+
56+
// Creating environment variable file
57+
File.Create(".env").Close();
58+
File.WriteAllText(".env", $"{TEST_ENV_VARIABLE}=DEVELOPMENT");
59+
DotNetEnv.Env.Load();
60+
61+
// Test environment variable is picked up from the .env file and is correctly resolved in the config file.
62+
Assert.AreEqual("DEVELOPMENT", Environment.GetEnvironmentVariable(TEST_ENV_VARIABLE));
63+
string? resolvedJson = RuntimeConfigPath.ParseConfigJsonAndReplaceEnvVariables(jsonWithEnvVariable);
64+
Assert.IsNotNull(resolvedJson);
65+
Assert.IsTrue(JToken.DeepEquals(
66+
JObject.Parse(@"{""envValue"": ""DEVELOPMENT""}"),
67+
JObject.Parse(resolvedJson)), "JSON resolved with environment variable correctly");
68+
}
69+
70+
/// <summary>
71+
/// This test setups an environment variable in the system and also creates a .env file containing
72+
/// the same variable with different value. In such a case, the value stored for variable in .env file is given
73+
/// precedence over the value specified in the system.
74+
/// </summary>
75+
[TestMethod]
76+
public void TestPrecedenceOfEnvironmentFileOverExistingVariables()
77+
{
78+
// The variable set in the .env file takes precedence over the environment value set in the system.
79+
Environment.SetEnvironmentVariable(TEST_ENV_VARIABLE, "TEST");
80+
81+
// Creating environment variable file
82+
File.Create(".env").Close();
83+
File.WriteAllText(".env", $"{TEST_ENV_VARIABLE}=DEVELOPMENT");
84+
DotNetEnv.Env.Load(); // It contains value DEVELOPMENT
85+
Assert.AreEqual("DEVELOPMENT", Environment.GetEnvironmentVariable(TEST_ENV_VARIABLE));
86+
87+
// If a variable is not present in the .env file then the system defined variable would be used if defined.
88+
Environment.SetEnvironmentVariable("HOSTING_TEST_ENVIRONMENT", "PHOENIX_TEST");
89+
string? resolvedJson = RuntimeConfigPath.ParseConfigJsonAndReplaceEnvVariables(
90+
@"{
91+
""envValue"": ""@env('DAB_TEST_ENVIRONMENT')"",
92+
""hostingEnvValue"": ""@env('HOSTING_TEST_ENVIRONMENT')""
93+
}"
94+
);
95+
Assert.IsNotNull(resolvedJson);
96+
Assert.IsTrue(JToken.DeepEquals(
97+
JObject.Parse(
98+
@"{
99+
""envValue"": ""DEVELOPMENT"",
100+
""hostingEnvValue"": ""PHOENIX_TEST""
101+
}"),
102+
JObject.Parse(resolvedJson)), "JSON resolved with environment variable correctly");
103+
}
104+
105+
/// <summary>
106+
/// Test to verify that no error is thrown if .env file is not present, and existing system variables are used.
107+
/// </summary>
108+
[TestMethod]
109+
public void TestSystemEnvironmentVariableIsUsedInAbsenceOfEnvironmentFile()
110+
{
111+
Environment.SetEnvironmentVariable(TEST_ENV_VARIABLE, "TEST");
112+
Assert.IsFalse(File.Exists(".env"));
113+
DotNetEnv.Env.Load(); // No error is thrown
114+
Assert.AreEqual("TEST", Environment.GetEnvironmentVariable(TEST_ENV_VARIABLE));
115+
}
116+
117+
/// <summary>
118+
/// Test to verify that if the environment variables are not resolved correctly, the runtime engine will not start.
119+
/// Here, in the first scenario, engine fails to start because the variable defined in the environment file
120+
/// is typed incorrectly and does not match the one present in the config.
121+
/// </summary>
122+
[DataRow("COMM_STRINX=test_connection_string", true, DisplayName = "Incorrect Variable name used in the environment file.")]
123+
[DataRow("CONN_STRING=test_connection_string", false, DisplayName = "Correct Variable name used in the environment file.")]
124+
[DataTestMethod]
125+
public void TestFailureToStartWithUnresolvedJsonConfig(
126+
string environmentFileContent,
127+
bool isFailure
128+
)
129+
{
130+
// Creating environment variable file
131+
File.Create(".env").Close();
132+
File.WriteAllText(".env", environmentFileContent);
133+
if (File.Exists(TEST_RUNTIME_CONFIG_FILE))
134+
{
135+
File.Delete(TEST_RUNTIME_CONFIG_FILE);
136+
}
137+
138+
string[] initArgs = { "init", "-c", TEST_RUNTIME_CONFIG_FILE, "--database-type", "mssql", "--connection-string", "@env('CONN_STRING')" };
139+
Program.Main(initArgs);
140+
141+
// Trying to start the runtime engine
142+
using Process process = ExecuteDabCommand(
143+
"start",
144+
$"-c {TEST_RUNTIME_CONFIG_FILE}"
145+
);
146+
147+
string? output = process.StandardOutput.ReadToEnd();
148+
Assert.IsNotNull(output);
149+
150+
if (isFailure)
151+
{
152+
// Failed to resolve the environment variables in the config.
153+
Assert.IsFalse(output.Contains("Starting the runtime engine..."));
154+
Assert.IsTrue(output.Contains("Error: Failed due to: Environmental Variable, CONN_STRING, not found."));
155+
}
156+
else
157+
{
158+
// config resolved correctly.
159+
Assert.IsTrue(output.Contains("Starting the runtime engine..."));
160+
Assert.IsFalse(output.Contains("Error: Failed due to: Environmental Variable, CONN_STRING, not found."));
161+
}
162+
}
163+
164+
[TestCleanup]
165+
public void CleanUp()
166+
{
167+
if (File.Exists(".env"))
168+
{
169+
File.Delete(".env");
170+
}
171+
}
172+
}

src/Cli.Tests/Usings.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
global using System.Diagnostics;
55
global using System.Text.Json;
66
global using Azure.DataApiBuilder.Config;
7+
global using Azure.DataApiBuilder.Service.Exceptions;
78
global using Microsoft.Extensions.Logging;
89
global using Microsoft.VisualStudio.TestTools.UnitTesting;
910
global using Moq;

src/Cli/Program.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ public static int Main(string[] args)
2929
}
3030
);
3131

32+
// Load environment variables from .env file if present.
33+
DotNetEnv.Env.Load();
34+
3235
// Setting up Logger for CLI.
3336
ILoggerFactory loggerFactory = new LoggerFactory();
3437
loggerFactory.AddProvider(new CustomLoggerProvider());

src/Cli/Utils.cs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using static Azure.DataApiBuilder.Config.AuthenticationConfig;
1313
using static Azure.DataApiBuilder.Config.MergeJsonProvider;
1414
using static Azure.DataApiBuilder.Config.RuntimeConfigPath;
15+
using static Azure.DataApiBuilder.Service.Configurations.RuntimeConfigProvider;
1516
using static Azure.DataApiBuilder.Service.Configurations.RuntimeConfigValidator;
1617
using PermissionOperation = Azure.DataApiBuilder.Config.PermissionOperation;
1718

@@ -582,6 +583,7 @@ public static bool TryGetConfigFileBasedOnCliPrecedence(
582583
else
583584
{
584585
_logger.LogInformation("Config not provided. Trying to get default config based on DAB_ENVIRONMENT...");
586+
_logger.LogInformation("Environment variable DAB_ENVIRONMENT is {value}", Environment.GetEnvironmentVariable("DAB_ENVIRONMENT"));
585587
/// Need to reset to true explicitly so any that any re-invocations of this function
586588
/// get simulated as being called for the first time specifically useful for tests.
587589
RuntimeConfigPath.CheckPrecedenceForConfigInEngine = true;
@@ -597,7 +599,7 @@ public static bool TryGetConfigFileBasedOnCliPrecedence(
597599
}
598600

599601
/// <summary>
600-
/// Checks if config can be correctly parsed by deserializing the
602+
/// Checks if config can be correctly resolved and parsed by deserializing the
601603
/// json config into runtime config object.
602604
/// Also checks that connection-string is not null or empty whitespace.
603605
/// If parsing is successful and the config has valid connection-string, it
@@ -608,18 +610,25 @@ public static bool CanParseConfigCorrectly(
608610
[NotNullWhen(true)] out RuntimeConfig? deserializedRuntimeConfig)
609611
{
610612
deserializedRuntimeConfig = null;
611-
if (!TryReadRuntimeConfig(configFile, out string runtimeConfigJson))
613+
string? runtimeConfigJson;
614+
615+
try
616+
{
617+
// Tries to read the config and resolve environment variables.
618+
runtimeConfigJson = GetRuntimeConfigJsonString(configFile);
619+
}
620+
catch (Exception e)
612621
{
613-
_logger.LogError($"Failed to read the config file: {configFile}.");
622+
_logger.LogError("Failed due to: {exceptionMessage}", e.Message);
614623
return false;
615624
}
616625

617-
if (!RuntimeConfig.TryGetDeserializedRuntimeConfig(
626+
if (string.IsNullOrEmpty(runtimeConfigJson) || !RuntimeConfig.TryGetDeserializedRuntimeConfig(
618627
runtimeConfigJson,
619628
out deserializedRuntimeConfig,
620629
logger: null))
621630
{
622-
_logger.LogError($"Failed to parse the config file: {configFile}.");
631+
_logger.LogError("Failed to parse the config file: {configFile}.", configFile);
623632
return false;
624633
}
625634

@@ -906,6 +915,7 @@ public static bool TryMergeConfigsIfAvailable([NotNullWhen(true)] out string? me
906915
string baseConfigJson = File.ReadAllText(baseConfigFile);
907916
string overrideConfigJson = File.ReadAllText(environmentBasedConfigFile);
908917
string currentDir = Directory.GetCurrentDirectory();
918+
_logger.LogInformation("Using DAB_ENVIRONMENT = {value}", environmentValue);
909919
_logger.LogInformation($"Merging {Path.Combine(currentDir, baseConfigFile)}"
910920
+ $" and {Path.Combine(currentDir, environmentBasedConfigFile)}");
911921
string mergedConfigJson = Merge(baseConfigJson, overrideConfigJson);

src/Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<PackageVersion Include="HotChocolate.AspNetCore.Authorization" Version="12.18.0" />
1212
<PackageVersion Include="Humanizer" Version="2.14.1" />
1313
<PackageVersion Include="Humanizer.Core" Version="2.14.1" />
14+
<PackageVersion Include="DotNetEnv" Version="2.5.0" />
1415
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="6.0.14" />
1516
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.14" />
1617
<PackageVersion Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />

src/Service/Azure.DataApiBuilder.Service.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
<PackageReference Include="Newtonsoft.Json" />
6565
<PackageReference Include="Npgsql" />
6666
<PackageReference Include="Polly" />
67+
<PackageReference Include="DotNetEnv" />
6768
<PackageReference Include="StyleCop.Analyzers">
6869
<PrivateAssets>all</PrivateAssets>
6970
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

0 commit comments

Comments
 (0)