Skip to content
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 @@ -9,6 +9,7 @@
</PropertyGroup>

<ItemGroup>
<Compile Include="$(SharedDir)AzurePortalUrls.cs" Link="AzurePortalUrls.cs" />
<Compile Include="$(SharedDir)BicepFormattingHelpers.cs" LinkBase="Shared\BicepFormattingHelpers.cs" />
<Compile Include="$(SharedDir)ResourceNameComparer.cs" LinkBase="Shared\ResourceNameComparer.cs" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,22 @@ public AzureContainerAppResource(string name, Action<AzureResourceInfrastructure
var containerAppEnv = (AzureContainerAppEnvironmentResource)deploymentTargetAnnotation.ComputeEnvironment!;

var domainValue = await containerAppEnv.ContainerAppDomain.GetValueAsync(ctx.CancellationToken).ConfigureAwait(false);
var portalLink = await ContainerAppUrls.GetPortalLinkAsync(containerAppEnv, targetResource.Name.ToLowerInvariant(), ctx.CancellationToken).ConfigureAwait(false);

if (targetResource.TryGetEndpoints(out var endpoints) && endpoints.Any(e => e.IsExternal))
{
var endpoint = $"https://{targetResource.Name.ToLowerInvariant()}.{domainValue}";
var summaryValue = $"[{endpoint}]({endpoint}) ({portalLink})";

ctx.ReportingStep.Log(LogLevel.Information, new MarkdownString($"Successfully deployed **{targetResource.Name}** to [{endpoint}]({endpoint})"));
ctx.Summary.Add(targetResource.Name, new MarkdownString($"[{endpoint}]({endpoint})"));
ctx.ReportingStep.Log(LogLevel.Information, new MarkdownString($"Successfully deployed **{targetResource.Name}** to {summaryValue}"));
ctx.Summary.Add(targetResource.Name, new MarkdownString(summaryValue));
}
else
{
ctx.ReportingStep.Log(LogLevel.Information, new MarkdownString($"Successfully deployed **{targetResource.Name}** to Azure Container Apps environment **{containerAppEnv.Name}**. No public endpoints were configured."));
ctx.Summary.Add(targetResource.Name, "No public endpoints");
var summaryValue = $"No public endpoints ({portalLink})";

ctx.ReportingStep.Log(LogLevel.Information, new MarkdownString($"Successfully deployed **{targetResource.Name}** to Azure Container Apps environment **{containerAppEnv.Name}**. {summaryValue}"));
ctx.Summary.Add(targetResource.Name, new MarkdownString(summaryValue));
}
},
Tags = ["print-summary"],
Expand Down
33 changes: 33 additions & 0 deletions src/Aspire.Hosting.Azure.AppContainers/ContainerAppUrls.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#pragma warning disable ASPIREPIPELINES001

using Azure.Core;
using Aspire.Hosting.Pipelines;

namespace Aspire.Hosting.Azure.AppContainers;

internal static class ContainerAppUrls
{
private const string ContainerAppResourceType = "/providers/Microsoft.App/containerApps/";

internal static async Task<MarkdownString> GetPortalLinkAsync(AzureContainerAppEnvironmentResource containerAppEnv, string containerAppName, CancellationToken cancellationToken)
{
var environmentIdValue = await containerAppEnv.ContainerAppEnvironmentId.GetValueAsync(cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException($"Missing container app environment id output for '{containerAppEnv.Name}'.");
var (subscriptionId, resourceGroupName) = GetSubscriptionAndResourceGroup(environmentIdValue);
var resourceId = $"{AzurePortalUrls.GetResourceGroupResourceId(subscriptionId, resourceGroupName)}{ContainerAppResourceType}{containerAppName}";

return AzurePortalUrls.GetResourceLink(resourceId);
}

private static (string SubscriptionId, string ResourceGroupName) GetSubscriptionAndResourceGroup(string containerAppEnvironmentId)
{
var environmentId = new ResourceIdentifier(containerAppEnvironmentId);
var subscriptionId = environmentId.SubscriptionId ?? throw new InvalidOperationException($"Container app environment id '{containerAppEnvironmentId}' does not contain a subscription id.");
var resourceGroupName = environmentId.ResourceGroupName ?? throw new InvalidOperationException($"Container app environment id '{containerAppEnvironmentId}' does not contain a resource group name.");

return (subscriptionId, resourceGroupName);
}
}
39 changes: 39 additions & 0 deletions src/Aspire.Hosting.Azure.AppService/AppSvcUrls.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#pragma warning disable ASPIREPIPELINES001

using Azure.Core;
using Aspire.Hosting.Pipelines;

namespace Aspire.Hosting.Azure;

internal static class AppSvcUrls
{
private const string SiteResourceType = "/providers/Microsoft.Web/sites/";
private const string SlotPathPrefix = "/slots/";

internal static async Task<MarkdownString> GetPortalLinkAsync(AzureAppServiceEnvironmentResource computerEnv, string siteName, string? deploymentSlot, CancellationToken cancellationToken)
{
var planIdValue = await computerEnv.PlanIdOutputReference.GetValueAsync(cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException($"Missing app service plan id output for '{computerEnv.Name}'.");
var (subscriptionId, resourceGroupName) = GetSubscriptionAndResourceGroup(planIdValue);
var resourceId = $"{AzurePortalUrls.GetResourceGroupResourceId(subscriptionId, resourceGroupName)}{SiteResourceType}{siteName}";

if (!string.IsNullOrWhiteSpace(deploymentSlot))
{
resourceId += $"{SlotPathPrefix}{deploymentSlot}";
}

return AzurePortalUrls.GetResourceLink(resourceId);
}

private static (string SubscriptionId, string ResourceGroupName) GetSubscriptionAndResourceGroup(string appServicePlanId)
{
var planId = new ResourceIdentifier(appServicePlanId);
var subscriptionId = planId.SubscriptionId ?? throw new InvalidOperationException($"App Service plan id '{appServicePlanId}' does not contain a subscription id.");
var resourceGroupName = planId.ResourceGroupName ?? throw new InvalidOperationException($"App Service plan id '{appServicePlanId}' does not contain a resource group name.");

return (subscriptionId, resourceGroupName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
</PropertyGroup>

<ItemGroup>
<Compile Include="$(SharedDir)AzurePortalUrls.cs" Link="AzurePortalUrls.cs" />
<Compile Include="$(SharedDir)BicepFormattingHelpers.cs" LinkBase="Shared\BicepFormattingHelpers.cs" />
<Compile Include="$(SharedDir)DashboardConfigNames.cs" LinkBase="Shared\DashboardConfigNames.cs" />
<Compile Include="$(SharedDir)KnownConfigNames.cs" LinkBase="Shared\KnownConfigNames.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,14 @@ public AzureAppServiceWebSiteResource(string name, Action<AzureResourceInfrastru
await computerEnv.DeploymentSlotParameter.GetValueAsync(ctx.CancellationToken).ConfigureAwait(false);
}

var hostName = await GetAppServiceWebsiteNameAsync(ctx, deploymentSlot).ConfigureAwait(false);
var websiteName = await GetAppServiceWebsiteBaseNameAsync(ctx).ConfigureAwait(false);
var hostName = GetAppServiceWebsiteName(websiteName, deploymentSlot);
var endpoint = $"https://{hostName}.azurewebsites.net";
ctx.ReportingStep.Log(LogLevel.Information, new MarkdownString($"Successfully deployed **{targetResource.Name}** to [{endpoint}]({endpoint})"));
ctx.Summary.Add(targetResource.Name, new MarkdownString($"[{endpoint}]({endpoint})"));
var portalLink = await AppSvcUrls.GetPortalLinkAsync(computerEnv, websiteName, deploymentSlot, ctx.CancellationToken).ConfigureAwait(false);
var summaryValue = $"[{endpoint}]({endpoint}) ({portalLink})";

ctx.ReportingStep.Log(LogLevel.Information, new MarkdownString($"Successfully deployed **{targetResource.Name}** to {summaryValue}"));
ctx.Summary.Add(targetResource.Name, new MarkdownString(summaryValue));
},
Tags = ["print-summary"],
RequiredBySteps = [WellKnownPipelineSteps.Deploy]
Expand Down Expand Up @@ -100,26 +104,28 @@ public AzureAppServiceWebSiteResource(string name, Action<AzureResourceInfrastru
public IResource TargetResource { get; }

/// <summary>
/// Gets the Azure App Service website name, optionally including the deployment slot suffix.
/// Gets the base Azure App Service website name without any deployment slot suffix.
/// </summary>
/// <param name="context">The pipeline step context.</param>
/// <param name="deploymentSlot">The optional deployment slot name to append to the website name.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the website name.</returns>
private async Task<string> GetAppServiceWebsiteNameAsync(PipelineStepContext context, string? deploymentSlot = null)
private async Task<string> GetAppServiceWebsiteBaseNameAsync(PipelineStepContext context)
{
var computerEnv = (AzureAppServiceEnvironmentResource)TargetResource.GetDeploymentTargetAnnotation()!.ComputeEnvironment!;
var websiteSuffix = await computerEnv.WebSiteSuffix.GetValueAsync(context.CancellationToken).ConfigureAwait(false);
var websiteName = $"{TargetResource.Name.ToLowerInvariant()}-{websiteSuffix}";
return TruncateToMaxLength($"{TargetResource.Name.ToLowerInvariant()}-{websiteSuffix}", 60);
}

private static string GetAppServiceWebsiteName(string websiteName, string? deploymentSlot = null)
{
if (string.IsNullOrWhiteSpace(deploymentSlot))
{
return TruncateToMaxLength(websiteName, 60);
return websiteName;
}

websiteName = TruncateToMaxLength(websiteName, MaxWebSiteNamePrefixLengthWithSlot);
websiteName += $"-{deploymentSlot}";
var slotHostName = TruncateToMaxLength(websiteName, MaxWebSiteNamePrefixLengthWithSlot);
slotHostName += $"-{deploymentSlot}";

return TruncateToMaxLength(websiteName, MaxHostPrefixLengthWithSlot);
return TruncateToMaxLength(slotHostName, MaxHostPrefixLengthWithSlot);
}

private static string TruncateToMaxLength(string value, int maxLength)
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<Compile Include="..\Aspire.Hosting\Dcp\Process\ProcessResult.cs" Link="Process\ProcessResult.cs" />
<Compile Include="..\Aspire.Hosting\Dcp\Process\ProcessSpec.cs" Link="Process\ProcessSpec.cs" />
<Compile Include="..\Aspire.Hosting\Dcp\Process\ProcessUtil.cs" Link="Process\ProcessUtil.cs" />
<Compile Include="$(SharedDir)AzurePortalUrls.cs" Link="AzurePortalUrls.cs" />
<Compile Include="$(SharedDir)CustomResourceSnapshotExtensions.cs" Link="Utils\CustomResourceSnapshotExtensions.cs" />
<Compile Include="$(SharedDir)StringComparers.cs" Link="Utils\StringComparers.cs" />
<Compile Include="$(SharedDir)Model\KnownRelationshipTypes.cs" />
Expand Down Expand Up @@ -40,7 +41,6 @@
<ItemGroup>
<InternalsVisibleTo Include="Aspire.Hosting.Azure.Tests" />
<InternalsVisibleTo Include="Aspire.Hosting.Azure.ContainerRegistry" />
<InternalsVisibleTo Include="Aspire.Hosting.Azure.AppService" />
</ItemGroup>

</Project>
5 changes: 2 additions & 3 deletions src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,10 @@ private static void AddToPipelineSummary(PipelineStepContext ctx, ProvisioningCo
var location = provisioningContext.Location.Name;

var tenantId = provisioningContext.Tenant.TenantId;
var portalUrl = AzurePortalUrls.GetResourceGroupUrl(subscriptionId, resourceGroupName, tenantId);
var resourceGroupValue = $"[{resourceGroupName}]({portalUrl})";

ctx.Summary.Add("☁️ Target", "Azure");
ctx.Summary.Add("📦 Resource Group", new MarkdownString(resourceGroupValue));
ctx.Summary.Add("📦 Resource Group", AzurePortalUrls.GetResourceGroupLink(subscriptionId, resourceGroupName, tenantId));
ctx.Summary.Add("📜 Deployments", AzurePortalUrls.GetResourceGroupDeploymentsLink(subscriptionId, resourceGroupName, tenantId));
ctx.Summary.Add("🔑 Subscription", subscriptionId);
ctx.Summary.Add("🌐 Location", location);
}
Expand Down
39 changes: 0 additions & 39 deletions src/Aspire.Hosting.Azure/AzurePortalUrls.cs

This file was deleted.

119 changes: 119 additions & 0 deletions src/Shared/AzurePortalUrls.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#pragma warning disable ASPIREPIPELINES001

using Azure.Core;
using Aspire.Hosting.Pipelines;

namespace Aspire.Hosting.Azure;

/// <summary>
/// Helpers for generating Azure portal URLs.
/// </summary>
internal static class AzurePortalUrls
{
private const string PortalRootUrl = "https://portal.azure.com";
private const string PortalDeploymentOverviewUrl = "https://portal.azure.com/#view/HubsExtension/DeploymentDetailsBlade/~/overview/id";
private const string AzurePortalLinkText = "Azure Portal";

/// <summary>
/// Gets the Azure portal URL for a resource group overview page.
/// </summary>
internal static string GetResourceGroupUrl(string subscriptionId, string resourceGroupName, Guid? tenantId = null)
{
return $"{PortalRootUrl}/{GetTenantSegment(tenantId)}/resource/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/overview";
}

/// <summary>
/// Gets a markdown link to a resource group overview page in the Azure portal.
/// </summary>
internal static MarkdownString GetResourceGroupLink(string subscriptionId, string resourceGroupName, Guid? tenantId = null)
{
return GetMarkdownLink(resourceGroupName, GetResourceGroupUrl(subscriptionId, resourceGroupName, tenantId));
}

/// <summary>
/// Gets the Azure portal URL for a resource group's deployments page.
/// </summary>
internal static string GetResourceGroupDeploymentsUrl(string subscriptionId, string resourceGroupName, Guid? tenantId = null)
{
return $"{PortalRootUrl}/{GetTenantSegment(tenantId)}/resource/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/deployments";
}

/// <summary>
/// Gets a markdown link to a resource group's deployments page in the Azure portal.
/// </summary>
internal static MarkdownString GetResourceGroupDeploymentsLink(string subscriptionId, string resourceGroupName, Guid? tenantId = null)
{
return GetMarkdownLink(AzurePortalLinkText, GetResourceGroupDeploymentsUrl(subscriptionId, resourceGroupName, tenantId));
}

/// <summary>
/// Gets the ARM resource ID for a resource group.
/// </summary>
internal static string GetResourceGroupResourceId(string subscriptionId, string resourceGroupName)
{
return $"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}";
}

/// <summary>
/// Gets the Azure portal URL for a resource overview page.
/// </summary>
internal static string GetResourceUrl(string resourceId, Guid? tenantId = null)
{
return $"{PortalRootUrl}/{GetTenantSegment(tenantId)}/resource{resourceId}/overview";
}

/// <summary>
/// Gets the Azure portal URL for a resource overview page using a full resource ID.
/// </summary>
internal static string GetResourceUrl(ResourceIdentifier resourceId, Guid? tenantId = null)
{
return GetResourceUrl(resourceId.ToString(), tenantId);
}

/// <summary>
/// Gets a markdown link to a resource overview page in the Azure portal.
/// </summary>
internal static MarkdownString GetResourceLink(string resourceId, string linkText = AzurePortalLinkText, Guid? tenantId = null)
{
return GetMarkdownLink(linkText, GetResourceUrl(resourceId, tenantId));
}

/// <summary>
/// Gets a markdown link to a resource overview page in the Azure portal using a full resource ID.
/// </summary>
internal static MarkdownString GetResourceLink(ResourceIdentifier resourceId, string linkText = AzurePortalLinkText, Guid? tenantId = null)
{
return GetMarkdownLink(linkText, GetResourceUrl(resourceId, tenantId));
}

/// <summary>
/// Gets the Azure portal URL for a deployment details page.
/// </summary>
internal static string GetDeploymentUrl(string subscriptionResourceId, string resourceGroupName, string deploymentName)
{
var path = $"{subscriptionResourceId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Resources/deployments/{deploymentName}";
var encodedPath = Uri.EscapeDataString(path);
return $"{PortalDeploymentOverviewUrl}/{encodedPath}";
}

/// <summary>
/// Gets the Azure portal URL for a deployment details page using a full deployment resource ID.
/// </summary>
internal static string GetDeploymentUrl(ResourceIdentifier deploymentId)
{
return $"{PortalDeploymentOverviewUrl}/{Uri.EscapeDataString(deploymentId.ToString())}";
}

private static string GetTenantSegment(Guid? tenantId)
{
return tenantId.HasValue ? $"#@{tenantId.Value}" : "#";
}

private static MarkdownString GetMarkdownLink(string linkText, string url)
{
return new($"[{linkText}]({url})");
}
}
Loading
Loading