Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add export to azure app configuration #106

Merged
merged 1 commit into from
Dec 1, 2024
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
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
Loading