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

[release/8.0] Generate and store random RG name when provisioning #3508

Merged
merged 6 commits into from
Apr 10, 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 @@ -11,6 +11,11 @@ internal sealed class AzureProvisionerOptions

public string? ResourceGroup { get; set; }

/// <summary>
/// Gets or sets a prefix used in resource groups names created.
/// </summary>
public string? ResourceGroupPrefix { get; set; }

public bool? AllowResourceGroupCreation { get; set; }

public string? Location { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Reflection;
using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Nodes;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Azure.Provisioning;
using Aspire.Hosting.Azure.Utils;
using Aspire.Hosting.Lifecycle;
using Azure;
using Azure.Core;
Expand Down Expand Up @@ -362,14 +364,44 @@ private async Task<ProvisioningContext> GetProvisioningContextAsync(Lazy<Task<Js
throw new MissingConfigurationException("An azure location/region is required. Set the Azure:Location configuration value.");
}

var unique = $"{Environment.MachineName.ToLowerInvariant()}-{environment.ApplicationName.ToLowerInvariant()}";
var userSecrets = await userSecretsLazy.Value.ConfigureAwait(false);

string resourceGroupName;
bool createIfAbsent;

// Name of the resource group to create based on the machine name and application name
var (resourceGroupName, createIfAbsent) = _options.ResourceGroup switch
if (string.IsNullOrEmpty(_options.ResourceGroup))
{
null or { Length: 0 } => ($"rg-aspire-{unique}", true),
string rg => (rg, _options.AllowResourceGroupCreation ?? false)
};
// Generate an resource group name since none was provided

var prefix = "rg-aspire";

if (!string.IsNullOrWhiteSpace(_options.ResourceGroupPrefix))
{
prefix = _options.ResourceGroupPrefix;
}

var suffix = RandomNumberGenerator.GetHexString(8, lowercase: true);

var maxApplicationNameSize = ResourceGroupNameHelpers.MaxResourceGroupNameLength - prefix.Length - suffix.Length - 2; // extra '-'s

var normalizedApplicationName = ResourceGroupNameHelpers.NormalizeResourceGroupName(environment.ApplicationName.ToLowerInvariant());
if (normalizedApplicationName.Length > maxApplicationNameSize)
{
normalizedApplicationName = normalizedApplicationName[..maxApplicationNameSize];
}

// Create a unique resource group name and save it in user secrets
resourceGroupName = $"{prefix}-{normalizedApplicationName}-{suffix}";

createIfAbsent = true;

userSecrets.Prop("Azure")["ResourceGroup"] = resourceGroupName;
}
else
{
resourceGroupName = _options.ResourceGroup;
createIfAbsent = _options.AllowResourceGroupCreation ?? false;
}

var resourceGroups = subscriptionResource.GetResourceGroups();

Expand Down Expand Up @@ -406,8 +438,6 @@ private async Task<ProvisioningContext> GetProvisioningContextAsync(Lazy<Task<Js

var resourceMap = new Dictionary<string, ArmResource>();

var userSecrets = await userSecretsLazy.Value.ConfigureAwait(false);

return new ProvisioningContext(
credential,
armClient,
Expand Down
58 changes: 58 additions & 0 deletions src/Aspire.Hosting.Azure/Utils/ResourceGroupNameHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Globalization;
using System.Text;

namespace Aspire.Hosting.Azure.Utils;

internal static class ResourceGroupNameHelpers
{
public static int MaxResourceGroupNameLength = 90;

/// <summary>
/// Converts or excludes any characters which are not valid resource group name components.
/// </summary>
/// <param name="resourceGroupName">The text to normalize.</param>
/// <returns>The normalized resource group name or an empty string if no characters were valid.</returns>
danmoseley marked this conversation as resolved.
Show resolved Hide resolved
public static string NormalizeResourceGroupName(string resourceGroupName)
{
resourceGroupName = RemoveDiacritics(resourceGroupName);

var stringBuilder = new StringBuilder(capacity: resourceGroupName.Length);

for (var i = 0; i < resourceGroupName.Length; i++)
{
var c = resourceGroupName[i];

if (!char.IsAsciiLetterOrDigit(c) && c != '-' && c != '_')
{
continue;
}

stringBuilder.Append(c);
}

return stringBuilder.ToString();
}

private static string RemoveDiacritics(string text)
{
var normalizedString = text.Normalize(NormalizationForm.FormD);
var stringBuilder = new StringBuilder(capacity: normalizedString.Length);

for (var i = 0; i < normalizedString.Length; i++)
{
var c = normalizedString[i];
var unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c);
if (unicodeCategory != UnicodeCategory.NonSpacingMark)
{
stringBuilder.Append(c);
}
}

return stringBuilder
.ToString()
.Normalize(NormalizationForm.FormC);
}
}
25 changes: 25 additions & 0 deletions tests/Aspire.Hosting.Tests/Azure/ResourceGroupNameHelpersTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Hosting.Azure.Utils;
using Xunit;

namespace Aspire.Hosting.Tests.Azure;

public class ResourceGroupNameHelpersTests
{
[Theory]
[InlineData("äæǽåàçéïôùÀÇÉÏÔÙ", "aaaceiouACEIOU")]
[InlineData("🔥🤔😅🤘", "")]
[InlineData("こんにちは", "")]
[InlineData("", "")]
[InlineData(" ", "")]
[InlineData("-.()_", "-_")]
[InlineData("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_")]
public void ShouldCreateAzdCompatibleResourceGroupNames(string input, string expected)
{
var result = ResourceGroupNameHelpers.NormalizeResourceGroupName(input);

Assert.Equal(expected, result);
}
}