Skip to content

Commit

Permalink
[release/8.0] Generate and store random RG name when provisioning (#3508
Browse files Browse the repository at this point in the history
)

* Generate and store random RG name when provisioning

* Update src/Aspire.Hosting.Azure/Provisioning/Provisioners/AzureProvisioner.cs

Co-authored-by: Eric Erhardt <eric.erhardt@microsoft.com>

* Noarmlize generated RG names

* PR feedback

* Move helpers to Aspite.Hosting.Azure

* Remove unused code

---------

Co-authored-by: Sebastien Ros <sebastienros@gmail.com>
Co-authored-by: Eric Erhardt <eric.erhardt@microsoft.com>
  • Loading branch information
3 people authored Apr 10, 2024
1 parent fa6d363 commit 8cac9f3
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 8 deletions.
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>
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);
}
}

0 comments on commit 8cac9f3

Please sign in to comment.