Skip to content

Conversation

@aaronburtle
Copy link
Contributor

Why make this change?

Adds AKV variable replacement and expands our design for doing variable replacements to be more extensible when new variable replacement logic is added.

Closes #2708
Closes #2748
Related to #2863

What is this change?

Change the way that variable replacement is handled to instead of simply using a bool to indicate that we want env variable replacement, we add a class which holds all of the replacement settings. This will hold whether or not we will do replacement for each kind of variable that we will handle replacement for during deserialization. We also include the replacement failure mode, and put the logic for handling the replacements into a strategy dictionary which pairs the replacement variable type with the strategy for doing that replacement.

Because Azure Key Vault secret replacement requires having the retry and connection settings in order to do the AKV replacement, we must do a first pass where we only do non-AKV replacement and get the required settings so that if AKV replacement is used we have the required settings to do that replacement.

We also have to keep in mind that the legacy of the Configuration Controller will ignore all variable replacement, so we construct the replacement settings for this code path to not use any variable replacement at all.

How was this tested?

We have updated the logic for the tests to use the new system, however manual testing using an actual AKV is still required.

Sample Request(s)

  • Example REST and/or GraphQL request to demonstrate modifications
  • Example of CLI usage to demonstrate modifications

Copy link
Contributor

@RubenCerna2079 RubenCerna2079 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still need to get rid of envVar in some places and resolve missing logic inside of an if statement.

Copy link
Contributor

@souvikghosh04 souvikghosh04 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added comments

Copy link
Collaborator

@Aniruddh25 Aniruddh25 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes look good to me. This is a good refactor. However, unit testing still remains and so does the test in AKV environment.

{
Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(
GetModifiedJsonString(repValues, @"""postgresql"""), out expectedConfig, replaceEnvVar: replaceEnvVar),
GetModifiedJsonString(repValues, @"""postgresql"""), out expectedConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAKVVar: true)),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add similar tests for AKV replacement checks

@aaronburtle aaronburtle marked this pull request as ready for review October 31, 2025 16:48
Copilot AI review requested due to automatic review settings October 31, 2025 16:48
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR refactors the configuration deserialization system to introduce Azure Key Vault (AKV) variable replacement support alongside environment variable replacement. The key change is replacing individual boolean parameters (replaceEnvVar, replacementFailureMode) with a unified DeserializationVariableReplacementSettings object that encapsulates all variable replacement configuration, including the new AKV functionality.

  • Introduces DeserializationVariableReplacementSettings class to centralize variable replacement configuration
  • Adds Azure Key Vault secret resolution support with configurable retry policies
  • Updates all JSON converters and configuration loading methods to use the new settings object

Reviewed Changes

Copilot reviewed 33 out of 33 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
src/Config/DeserializationVariableReplacementSettings.cs New class that encapsulates variable replacement logic for both environment variables and Azure Key Vault secrets
src/Config/RuntimeConfigLoader.cs Updated to use new settings object and extract AKV options with two-pass parsing
src/Config/Converters/StringJsonConverterFactory.cs Refactored to use pluggable replacement strategies from settings object
src/Config/Converters/*ConverterFactory.cs Updated multiple converter factories to accept DeserializationVariableReplacementSettings instead of boolean parameters
src/Config/ObjectModel/AzureKeyVaultOptions.cs Added constructor and flags to track user-provided properties
src/Directory.Packages.props Added Azure.Security.KeyVault.Secrets package dependency
src/Service/Controllers/ConfigurationController.cs Updated Initialize call with new settings object
Test files Updated test code to use new settings object instead of boolean parameters

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

aaronburtle and others added 17 commits October 31, 2025 10:12
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
ExportGraphQL(options, runtimeConfig, fileSystem, loader, logger).Wait();
isSuccess = true;
break;
if (runtimeConfig is not null)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change? The fact that TryLoadConfig returned true means runtimeConfig is going to be not null. Why do we need to check again here?


SqlConnectionStringBuilder builder = new(runtimeConfig.DataSource.ConnectionString);
Assert.AreEqual(ProductInfo.GetDataApiBuilderUserAgent(), builder.ApplicationName);
if (runtimeConfig is not null)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, we are already Assert for true above. If the assert fails, it will error out. so runtimeConfig can never be null here.

Copy link
Collaborator

@Aniruddh25 Aniruddh25 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to carefully look at what the default value of replaceEnvVar property should be when replaceMentSettings is null or new(). And ensure no regressions


using System.Text.Json;
using System.Text.Json.Serialization;
using Azure.DataApiBuilder.Config.ObjectModel;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be an unnecessary using statement

// value or not while deserializing.
private readonly DeserializationVariableReplacementSettings? _replacementSettings;

/// <param name="replaceEnvVar">Whether to replace environment variable with its
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// <param name="replaceEnvVar">Whether to replace environment variable with its
/// <param name="replacementSettings">Whether to replace environment variable with its

case "retry-policy":
if (reader.TokenType is JsonTokenType.StartObject)
{
// Pass the replaceEnvVar setting to the retry policy converter
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how is this setting passed to the retry policy converter?

// .* : 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'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the match here, the substring @env( is a substring but the pattern [^@env\(] expects any substring that is not @env(. so how did it match?

{
// Need to remove the dependencies in startup on the RuntimeConfigProvider
// before we can have an ILogger here.
// before we can have anILogger here.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
// before we can have anILogger here.
// before we can have an ILogger here.


if (!string.IsNullOrEmpty(json) && TryParseConfig(json, out RuntimeConfig, connectionString: _connectionString, replaceEnvVar: replaceEnvVar))
// Use default replacement settings if none provided
replacementSettings ??= new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default for this function was to keep replaceEnvVar to false. But, here I see it is true, why is this change needed and if needed, did we ensure all the callers are updated accordingly?

_dataSourceNameToDataSource = _dataSourceNameToDataSource.Concat(config._dataSourceNameToDataSource).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
_entityNameToDataSourceName = _entityNameToDataSourceName.Concat(config._entityNameToDataSourceName).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
allEntities = allEntities.Concat(config.Entities.AsEnumerable());
if (config is not null)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need to check for config is not null when TryLoadConfig is true?

{
string configJson = TestHelper.AddPropertiesToJson(TestHelper.BASE_CONFIG, entityJson);
RuntimeConfigLoader.TryParseConfig(configJson, out RuntimeConfig deserializedConfig, logger: null, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL));
RuntimeConfigLoader.TryParseConfig(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for TryParseConfig, the default value of replaceEnvVar was false. But with the addition of the replacementSettings, the default value for this call ends up being true. This will break the previous assumptions. In case of refactors, the best idea is to maintain previous behavior to avoid regressions.

@Aniruddh25
Copy link
Collaborator

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 6 pipeline(s).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support .akv files just like .env files. [Enhancement]: Support AKV

5 participants