Skip to content

Commit

Permalink
feat: add export to azure app configuration (#106)
Browse files Browse the repository at this point in the history
  • Loading branch information
tnc1997 authored Dec 1, 2024
1 parent 62ede9b commit e146e2b
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ private static async Task<string> ComputeContentHash(
if (request.Content is not null)
{
await request.Content.CopyToAsync(stream, cancellationToken);

stream.Position = 0;
}

using var sha256 = SHA256.Create();
Expand Down
20 changes: 20 additions & 0 deletions src/AzureAppConfigurationEmulator/Common/ConfigurationClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,24 @@ public async IAsyncEnumerable<string> GetKeys(
}
} while (link is { Next: not null });
}

public async Task SetConfigurationSetting(
ConfigurationSetting setting,
CancellationToken cancellationToken = default)
{
using var activity = Telemetry.ActivitySource.StartActivity($"{nameof(ConfigurationClient)}.{nameof(SetConfigurationSetting)}");

var uri = new Uri($"/kv/{Uri.EscapeDataString(setting.Key)}?label={Uri.EscapeDataString(setting.Label ?? LabelFilter.Null)}&api-version=1.0", UriKind.Relative);

using var request = new HttpRequestMessage(HttpMethod.Put, uri);
request.Content = JsonContent.Create(new
{
value = setting.Value,
content_type = setting.ContentType,
tags = setting.Tags
});

using var response = await httpClient.SendAsync(request, cancellationToken);
response.EnsureSuccessStatusCode();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@ public IAsyncEnumerable<string> GetKeys(

public IAsyncEnumerable<string?> GetLabels(
CancellationToken cancellationToken = default);

public Task SetConfigurationSetting(
ConfigurationSetting setting,
CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<AzureInputSelect AdditionalAttributes="@AdditionalAttributes" TValue="@string" Value="@Value" ValueChanged="@HandleValueChanged" ValueExpression="@ValueExpression">
<option checked="@(Value is null)" hidden value="">Please select a target service</option>
<option checked="@(Value is TargetType.AzureAppConfiguration)" disabled value="@TargetType.AzureAppConfiguration">App Configuration</option>
<option checked="@(Value is TargetType.AzureAppConfiguration)" value="@TargetType.AzureAppConfiguration">App Configuration</option>
<option checked="@(Value is TargetType.AzureAppService)" disabled value="@TargetType.AzureAppService">App Service</option>
<option checked="@(Value is TargetType.ConfigurationFile)" value="@TargetType.ConfigurationFile">Configuration file</option>
</AzureInputSelect>
Expand Down
122 changes: 116 additions & 6 deletions src/AzureAppConfigurationEmulator/Components/Pages/ImportExport.razor
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
{
case ImportExportOperationInputRadioGroup.Operation.Export:
<div class="flex flex-col gap-3">
<ImportExportOperationInputRadioGroup @bind-Value="@Model.Operation" name="@nameof(Model.Operation)"/>
<ImportExportOperationInputRadioGroup name="@nameof(Model.Operation)" Value="@Model.Operation" ValueChanged="@HandleOperationChanged" ValueExpression="@(() => Model.Operation)"/>

<label class="flex flex-row items-center">
<div class="w-[200px]">Target type</div>
Expand All @@ -39,6 +39,62 @@

switch (Model.TargetType)
{
case ImportExportTargetTypeInputSelect.TargetType.AzureAppConfiguration:
<div class="flex flex-col gap-3">
<div class="font-bold text-lg">Select key-values</div>

<label class="flex flex-row items-center">
<div class="w-[200px]">Key filter</div>
<div class="flex-1 max-w-[600px]">
<AzureInputText @bind-Value="@Model.KeyFilter" name="@nameof(Model.KeyFilter)" placeholder="abc | abc,xyz,..."/>
</div>
</label>

<label class="flex flex-row items-center">
<div class="w-[200px]">At a specific time</div>
<div class="flex-1 max-w-[600px]">
<AzureInputDate @bind-Value="@Model.Moment" name="@nameof(Model.Moment)" Type="@InputDateType.DateTimeLocal"/>
</div>
</label>

<label class="flex flex-row items-center">
<div class="w-[200px]">From label</div>
<div class="flex-1 max-w-[600px]">
<ImportExportLabelFilterInputSelect @bind-Value="@Model.LabelFilter" Labels="@Labels" name="@nameof(Model.LabelFilter)"/>
</div>
</label>
</div>

<div class="flex flex-col gap-3">
<div class="font-bold text-lg">Select target</div>

<label class="flex flex-row items-center">
<div class="w-[200px]">Connection string <span class="text-alizarin-crimson">*</span></div>
<div class="flex-1 max-w-[600px]">
<AzureInputText @bind-Value="@Model.TargetConnectionString" name="@nameof(Model.TargetConnectionString)" required/>
</div>
</label>
</div>

<div class="flex flex-col gap-3">
<div class="font-bold text-lg">Apply changes to key-values</div>

<label class="flex flex-row items-center">
<div class="w-[200px]">Remove prefix</div>
<div class="flex-1 max-w-[600px]">
<AzureInputText @bind-Value="@Model.Prefix" name="@nameof(Model.Prefix)"/>
</div>
</label>
</div>

if (!string.IsNullOrEmpty(Model.TargetConnectionString))
{
<div>
<AzureButton Appearance="AzureButton.AzureAppearance.Primary" type="submit">Export</AzureButton>
</div>
}

break;
case ImportExportTargetTypeInputSelect.TargetType.ConfigurationFile:
<div class="flex flex-col gap-3">
<div class="font-bold text-lg">Export options</div>
Expand Down Expand Up @@ -82,7 +138,7 @@
break;
case ImportExportOperationInputRadioGroup.Operation.Import:
<div class="flex flex-col gap-3">
<ImportExportOperationInputRadioGroup @bind-Value="@Model.Operation" name="@nameof(Model.Operation)"/>
<ImportExportOperationInputRadioGroup name="@nameof(Model.Operation)" Value="@Model.Operation" ValueChanged="@HandleOperationChanged" ValueExpression="@(() => Model.Operation)"/>

<label class="flex flex-row items-center">
<div class="w-[200px]">Source type</div>
Expand All @@ -99,9 +155,9 @@
<div class="font-bold text-lg">Select source</div>

<label class="flex flex-row items-center">
<div class="w-[200px]">Connection string</div>
<div class="w-[200px]">Connection string <span class="text-alizarin-crimson">*</span></div>
<div class="flex-1 max-w-[600px]">
<AzureInputText name="@nameof(Model.SourceConnectionString)" Value="@Model.SourceConnectionString" ValueChanged="@HandleSourceConnectionStringChange" ValueExpression="@(() => Model.SourceConnectionString)"/>
<AzureInputText name="@nameof(Model.SourceConnectionString)" Value="@Model.SourceConnectionString" ValueChanged="@HandleSourceConnectionStringChanged" ValueExpression="@(() => Model.SourceConnectionString)" required/>
</div>
</label>
</div>
Expand Down Expand Up @@ -248,9 +304,32 @@
Model ??= new InputModel();
}

private async Task HandleSourceConnectionStringChange(string? connectionString)
private async Task HandleOperationChanged(string operation)
{
using var activity = Telemetry.ActivitySource.StartActivity($"{nameof(ImportExport)}.{nameof(HandleSourceConnectionStringChange)}");
using var activity = Telemetry.ActivitySource.StartActivity($"{nameof(ImportExport)}.{nameof(HandleOperationChanged)}");

if (Model is not null)
{
Model.Operation = operation;
StateHasChanged();

Labels.Clear();
StateHasChanged();

if (operation is ImportExportOperationInputRadioGroup.Operation.Export)
{
await foreach (var label in ConfigurationSettingRepository.Get().Select(setting => setting.Label))
{
Labels.Add(label);
StateHasChanged();
}
}
}
}

private async Task HandleSourceConnectionStringChanged(string? connectionString)
{
using var activity = Telemetry.ActivitySource.StartActivity($"{nameof(ImportExport)}.{nameof(HandleSourceConnectionStringChanged)}");

if (Model is not null)
{
Expand Down Expand Up @@ -299,6 +378,35 @@
{
switch (Model?.TargetType)
{
case ImportExportTargetTypeInputSelect.TargetType.AzureAppConfiguration:
{
var dictionary = Model.TargetConnectionString!.Split(';').Select(s => s.Split('=', 2)).ToDictionary(s => s[0], s => s[1]);
var endpoint = dictionary["Endpoint"];
var credential = dictionary["Id"];
var secret = dictionary["Secret"];

using var httpMessageHandler = new HmacAuthenticatingHttpMessageHandler(credential, secret);
httpMessageHandler.InnerHandler = new HttpClientHandler();

using var httpClient = new HttpClient(httpMessageHandler);
httpClient.BaseAddress = new Uri(endpoint, UriKind.Absolute);

var configurationClient = new ConfigurationClient(httpClient, ConfigurationSettingFactory);

await foreach (var sourceSetting in ConfigurationSettingRepository.Get(Model.KeyFilter ?? KeyFilter.Any, LabelFilter.Any, Model.Moment))
{
if (!Model.LabelFilter.Contains(sourceSetting.Label))
{
continue;
}

await configurationClient.SetConfigurationSetting(sourceSetting);
}

Model = new InputModel();

break;
}
case ImportExportTargetTypeInputSelect.TargetType.ConfigurationFile:
{
using var document = KeyValuePairJsonEncoder.Encode(await ConfigurationSettingRepository.Get().Where(setting => setting is not FeatureFlagConfigurationSetting).ToDictionaryAsync(setting => setting.Key, setting => setting.Value), Model.Prefix, Model.Separator);
Expand Down Expand Up @@ -433,6 +541,8 @@

public string? SourceType { get; set; }

public string? TargetConnectionString { get; set; }

public string? TargetType { get; set; }
}

Expand Down

0 comments on commit e146e2b

Please sign in to comment.