diff --git a/src/Aspire.Hosting.Azure.AppContainers/Aspire.Hosting.Azure.AppContainers.csproj b/src/Aspire.Hosting.Azure.AppContainers/Aspire.Hosting.Azure.AppContainers.csproj
index 2aeb93f852a..6ff4d136e47 100644
--- a/src/Aspire.Hosting.Azure.AppContainers/Aspire.Hosting.Azure.AppContainers.csproj
+++ b/src/Aspire.Hosting.Azure.AppContainers/Aspire.Hosting.Azure.AppContainers.csproj
@@ -9,6 +9,7 @@
+
diff --git a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppResource.cs b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppResource.cs
index 97de4cb0ee9..df349430b23 100644
--- a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppResource.cs
+++ b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppResource.cs
@@ -48,18 +48,22 @@ public AzureContainerAppResource(string name, Action 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"],
diff --git a/src/Aspire.Hosting.Azure.AppContainers/ContainerAppUrls.cs b/src/Aspire.Hosting.Azure.AppContainers/ContainerAppUrls.cs
new file mode 100644
index 00000000000..1184755dbfe
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.AppContainers/ContainerAppUrls.cs
@@ -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 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);
+ }
+}
diff --git a/src/Aspire.Hosting.Azure.AppService/AppSvcUrls.cs b/src/Aspire.Hosting.Azure.AppService/AppSvcUrls.cs
new file mode 100644
index 00000000000..2c2bd01f218
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.AppService/AppSvcUrls.cs
@@ -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 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);
+ }
+}
diff --git a/src/Aspire.Hosting.Azure.AppService/Aspire.Hosting.Azure.AppService.csproj b/src/Aspire.Hosting.Azure.AppService/Aspire.Hosting.Azure.AppService.csproj
index 0bf2b531164..4f61d9ccd63 100644
--- a/src/Aspire.Hosting.Azure.AppService/Aspire.Hosting.Azure.AppService.csproj
+++ b/src/Aspire.Hosting.Azure.AppService/Aspire.Hosting.Azure.AppService.csproj
@@ -11,6 +11,7 @@
+
diff --git a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceWebSiteResource.cs b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceWebSiteResource.cs
index 022ed9cc912..dc927a415d1 100644
--- a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceWebSiteResource.cs
+++ b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceWebSiteResource.cs
@@ -55,10 +55,14 @@ public AzureAppServiceWebSiteResource(string name, Action
- /// 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.
///
/// The pipeline step context.
- /// The optional deployment slot name to append to the website name.
/// A task that represents the asynchronous operation. The task result contains the website name.
- private async Task GetAppServiceWebsiteNameAsync(PipelineStepContext context, string? deploymentSlot = null)
+ private async Task 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)
diff --git a/src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj b/src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj
index bc11518e795..87a93b17cf2 100644
--- a/src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj
+++ b/src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj
@@ -13,6 +13,7 @@
+
@@ -40,7 +41,6 @@
-
diff --git a/src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs b/src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs
index 169f271df5d..62006e71eee 100644
--- a/src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs
+++ b/src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs
@@ -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);
}
diff --git a/src/Aspire.Hosting.Azure/AzurePortalUrls.cs b/src/Aspire.Hosting.Azure/AzurePortalUrls.cs
deleted file mode 100644
index 28c95acb5b0..00000000000
--- a/src/Aspire.Hosting.Azure/AzurePortalUrls.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace Aspire.Hosting.Azure;
-
-///
-/// Helpers for generating Azure portal URLs.
-///
-internal static class AzurePortalUrls
-{
- private const string PortalDeploymentOverviewUrl = "https://portal.azure.com/#view/HubsExtension/DeploymentDetailsBlade/~/overview/id";
-
- ///
- /// Gets the Azure portal URL for a resource group overview page.
- ///
- internal static string GetResourceGroupUrl(string subscriptionId, string resourceGroupName, Guid? tenantId = null)
- {
- var tenantSegment = tenantId.HasValue ? $"#@{tenantId.Value}" : "#";
- return $"https://portal.azure.com/{tenantSegment}/resource/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/overview";
- }
-
- ///
- /// Gets the Azure portal URL for a deployment details page.
- ///
- 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}";
- }
-
- ///
- /// Gets the Azure portal URL for a deployment details page using a full deployment resource ID.
- ///
- internal static string GetDeploymentUrl(global::Azure.Core.ResourceIdentifier deploymentId)
- {
- return $"{PortalDeploymentOverviewUrl}/{Uri.EscapeDataString(deploymentId.ToString())}";
- }
-}
diff --git a/src/Shared/AzurePortalUrls.cs b/src/Shared/AzurePortalUrls.cs
new file mode 100644
index 00000000000..36a074dcc84
--- /dev/null
+++ b/src/Shared/AzurePortalUrls.cs
@@ -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;
+
+///
+/// Helpers for generating Azure portal URLs.
+///
+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";
+
+ ///
+ /// Gets the Azure portal URL for a resource group overview page.
+ ///
+ internal static string GetResourceGroupUrl(string subscriptionId, string resourceGroupName, Guid? tenantId = null)
+ {
+ return $"{PortalRootUrl}/{GetTenantSegment(tenantId)}/resource/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/overview";
+ }
+
+ ///
+ /// Gets a markdown link to a resource group overview page in the Azure portal.
+ ///
+ internal static MarkdownString GetResourceGroupLink(string subscriptionId, string resourceGroupName, Guid? tenantId = null)
+ {
+ return GetMarkdownLink(resourceGroupName, GetResourceGroupUrl(subscriptionId, resourceGroupName, tenantId));
+ }
+
+ ///
+ /// Gets the Azure portal URL for a resource group's deployments page.
+ ///
+ internal static string GetResourceGroupDeploymentsUrl(string subscriptionId, string resourceGroupName, Guid? tenantId = null)
+ {
+ return $"{PortalRootUrl}/{GetTenantSegment(tenantId)}/resource/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/deployments";
+ }
+
+ ///
+ /// Gets a markdown link to a resource group's deployments page in the Azure portal.
+ ///
+ internal static MarkdownString GetResourceGroupDeploymentsLink(string subscriptionId, string resourceGroupName, Guid? tenantId = null)
+ {
+ return GetMarkdownLink(AzurePortalLinkText, GetResourceGroupDeploymentsUrl(subscriptionId, resourceGroupName, tenantId));
+ }
+
+ ///
+ /// Gets the ARM resource ID for a resource group.
+ ///
+ internal static string GetResourceGroupResourceId(string subscriptionId, string resourceGroupName)
+ {
+ return $"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}";
+ }
+
+ ///
+ /// Gets the Azure portal URL for a resource overview page.
+ ///
+ internal static string GetResourceUrl(string resourceId, Guid? tenantId = null)
+ {
+ return $"{PortalRootUrl}/{GetTenantSegment(tenantId)}/resource{resourceId}/overview";
+ }
+
+ ///
+ /// Gets the Azure portal URL for a resource overview page using a full resource ID.
+ ///
+ internal static string GetResourceUrl(ResourceIdentifier resourceId, Guid? tenantId = null)
+ {
+ return GetResourceUrl(resourceId.ToString(), tenantId);
+ }
+
+ ///
+ /// Gets a markdown link to a resource overview page in the Azure portal.
+ ///
+ internal static MarkdownString GetResourceLink(string resourceId, string linkText = AzurePortalLinkText, Guid? tenantId = null)
+ {
+ return GetMarkdownLink(linkText, GetResourceUrl(resourceId, tenantId));
+ }
+
+ ///
+ /// Gets a markdown link to a resource overview page in the Azure portal using a full resource ID.
+ ///
+ internal static MarkdownString GetResourceLink(ResourceIdentifier resourceId, string linkText = AzurePortalLinkText, Guid? tenantId = null)
+ {
+ return GetMarkdownLink(linkText, GetResourceUrl(resourceId, tenantId));
+ }
+
+ ///
+ /// Gets the Azure portal URL for a deployment details page.
+ ///
+ 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}";
+ }
+
+ ///
+ /// Gets the Azure portal URL for a deployment details page using a full deployment resource ID.
+ ///
+ 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})");
+ }
+}
diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs
index 9226e6f0ade..393dcb82da3 100644
--- a/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs
+++ b/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs
@@ -11,6 +11,7 @@
#pragma warning disable ASPIRECONTAINERRUNTIME001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
using System.Text.Json.Nodes;
+using Azure.Core;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Azure.Provisioning;
using Aspire.Hosting.Azure.Provisioning.Internal;
@@ -30,6 +31,12 @@ namespace Aspire.Hosting.Azure.Tests;
public class AzureDeployerTests(ITestOutputHelper testOutputHelper)
{
+ private const string TestSubscriptionId = "12345678-1234-1234-1234-123456789012";
+ private const string TestResourceGroupName = "test-rg";
+ private static readonly Guid s_testTenantId = Guid.Parse("87654321-4321-4321-4321-210987654321");
+
+ private static string GetTestResourceId(string resourcePath) => $"/subscriptions/{TestSubscriptionId}/resourceGroups/{TestResourceGroupName}{resourcePath}";
+
[Fact]
public async Task DeployAsync_PromptsViaInteractionService()
{
@@ -143,9 +150,9 @@ public async Task DeployAsync_WithBuildOnlyContainers()
{
["AZURE_CONTAINER_REGISTRY_NAME"] = new { type = "String", value = "testregistry" },
["AZURE_CONTAINER_REGISTRY_ENDPOINT"] = new { type = "String", value = "testregistry.azurecr.io" },
- ["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity" },
+ ["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"] = new { type = "String", value = GetTestResourceId("/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity") },
["AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN"] = new { type = "String", value = "test.westus.azurecontainerapps.io" },
- ["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.App/managedEnvironments/testenv" }
+ ["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"] = new { type = "String", value = $"/subscriptions/{TestSubscriptionId}/resourceGroups/{TestResourceGroupName}/providers/Microsoft.App/managedEnvironments/testenv" }
},
_ => []
};
@@ -205,9 +212,9 @@ public async Task DeployAsync_WithAzureStorageResourcesWorks()
{
["AZURE_CONTAINER_REGISTRY_NAME"] = new { type = "String", value = "testregistry" },
["AZURE_CONTAINER_REGISTRY_ENDPOINT"] = new { type = "String", value = "testregistry.azurecr.io" },
- ["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity" },
+ ["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"] = new { type = "String", value = GetTestResourceId("/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity") },
["AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN"] = new { type = "String", value = "test.westus.azurecontainerapps.io" },
- ["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.App/managedEnvironments/testenv" }
+ ["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"] = new { type = "String", value = $"/subscriptions/{TestSubscriptionId}/resourceGroups/{TestResourceGroupName}/providers/Microsoft.App/managedEnvironments/testenv" }
},
_ => []
};
@@ -264,9 +271,9 @@ public async Task DeployAsync_WithContainer_Works()
{
["AZURE_CONTAINER_REGISTRY_NAME"] = new { type = "String", value = "testregistry" },
["AZURE_CONTAINER_REGISTRY_ENDPOINT"] = new { type = "String", value = "testregistry.azurecr.io" },
- ["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity" },
+ ["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"] = new { type = "String", value = GetTestResourceId("/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity") },
["AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN"] = new { type = "String", value = "test.westus.azurecontainerapps.io" },
- ["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.App/managedEnvironments/testenv" }
+ ["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"] = new { type = "String", value = $"/subscriptions/{TestSubscriptionId}/resourceGroups/{TestResourceGroupName}/providers/Microsoft.App/managedEnvironments/testenv" }
},
_ => []
};
@@ -289,9 +296,9 @@ public async Task DeployAsync_WithContainer_Works()
// hoisted up for the container resource
Assert.Equal("testregistry", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_NAME"]);
Assert.Equal("testregistry.azurecr.io", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_ENDPOINT"]);
- Assert.Equal("/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"]);
+ Assert.Equal(GetTestResourceId("/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity"), containerAppEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"]);
Assert.Equal("test.westus.azurecontainerapps.io", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN"]);
- Assert.Equal("/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.App/managedEnvironments/testenv", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"]);
+ Assert.Equal($"/subscriptions/{TestSubscriptionId}/resourceGroups/{TestResourceGroupName}/providers/Microsoft.App/managedEnvironments/testenv", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"]);
// Assert - Verify ACR login was not called since no image was pushed
Assert.False(fakeContainerRuntime.WasLoginToRegistryCalled);
@@ -324,9 +331,9 @@ public async Task DeployAsync_WithDockerfile_Works()
{
["AZURE_CONTAINER_REGISTRY_NAME"] = new { type = "String", value = "testregistry" },
["AZURE_CONTAINER_REGISTRY_ENDPOINT"] = new { type = "String", value = "testregistry.azurecr.io" },
- ["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity" },
+ ["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"] = new { type = "String", value = GetTestResourceId("/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity") },
["AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN"] = new { type = "String", value = "test.westus.azurecontainerapps.io" },
- ["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.App/managedEnvironments/testenv" }
+ ["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"] = new { type = "String", value = $"/subscriptions/{TestSubscriptionId}/resourceGroups/{TestResourceGroupName}/providers/Microsoft.App/managedEnvironments/testenv" }
},
_ => []
};
@@ -349,9 +356,9 @@ public async Task DeployAsync_WithDockerfile_Works()
// hoisted up for the container resource
Assert.Equal("testregistry", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_NAME"]);
Assert.Equal("testregistry.azurecr.io", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_ENDPOINT"]);
- Assert.Equal("/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"]);
+ Assert.Equal(GetTestResourceId("/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity"), containerAppEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"]);
Assert.Equal("test.westus.azurecontainerapps.io", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN"]);
- Assert.Equal("/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.App/managedEnvironments/testenv", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"]);
+ Assert.Equal($"/subscriptions/{TestSubscriptionId}/resourceGroups/{TestResourceGroupName}/providers/Microsoft.App/managedEnvironments/testenv", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"]);
// Assert - Verify ACR login was called using IContainerRuntime
Assert.True(fakeContainerRuntime.WasLoginToRegistryCalled);
@@ -390,9 +397,9 @@ public async Task DeployAsync_WithProjectResource_Works()
{
["AZURE_CONTAINER_REGISTRY_NAME"] = new { type = "String", value = "testregistry" },
["AZURE_CONTAINER_REGISTRY_ENDPOINT"] = new { type = "String", value = "testregistry.azurecr.io" },
- ["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity" },
+ ["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"] = new { type = "String", value = GetTestResourceId("/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity") },
["AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN"] = new { type = "String", value = "test.westus.azurecontainerapps.io" },
- ["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.App/managedEnvironments/testenv" }
+ ["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"] = new { type = "String", value = $"/subscriptions/{TestSubscriptionId}/resourceGroups/{TestResourceGroupName}/providers/Microsoft.App/managedEnvironments/testenv" }
},
_ => []
};
@@ -415,9 +422,9 @@ public async Task DeployAsync_WithProjectResource_Works()
// hoisted up for the container resource
Assert.Equal("testregistry", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_NAME"]);
Assert.Equal("testregistry.azurecr.io", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_ENDPOINT"]);
- Assert.Equal("/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"]);
+ Assert.Equal(GetTestResourceId("/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity"), containerAppEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"]);
Assert.Equal("test.westus.azurecontainerapps.io", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN"]);
- Assert.Equal("/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.App/managedEnvironments/testenv", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"]);
+ Assert.Equal($"/subscriptions/{TestSubscriptionId}/resourceGroups/{TestResourceGroupName}/providers/Microsoft.App/managedEnvironments/testenv", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"]);
// Assert - Verify ACR login was called using IContainerRuntime
Assert.True(fakeContainerRuntime.WasLoginToRegistryCalled);
@@ -435,6 +442,123 @@ public async Task DeployAsync_WithProjectResource_Works()
resource.Name == "api");
}
+ [Fact]
+ public async Task DeployAsync_WithContainerAppExternalEndpoint_IncludesPortalLinksInSummary()
+ {
+ // Arrange
+ var mockProcessRunner = new MockProcessRunner();
+ var fakeContainerRuntime = new FakeContainerRuntime();
+ var mockActivityReporter = new TestPipelineActivityReporter(testOutputHelper);
+ using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, step: WellKnownPipelineSteps.Deploy);
+ var armClientProvider = new TestArmClientProvider(deploymentName =>
+ {
+ return deploymentName switch
+ {
+ string name when name.StartsWith("env-acr") => new Dictionary
+ {
+ ["name"] = new { type = "String", value = "testregistry" },
+ ["loginServer"] = new { type = "String", value = "testregistry.azurecr.io" }
+ },
+ string name when name.StartsWith("env") => new Dictionary
+ {
+ ["AZURE_CONTAINER_REGISTRY_NAME"] = new { type = "String", value = "testregistry" },
+ ["AZURE_CONTAINER_REGISTRY_ENDPOINT"] = new { type = "String", value = "testregistry.azurecr.io" },
+ ["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"] = new { type = "String", value = GetTestResourceId("/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity") },
+ ["AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN"] = new { type = "String", value = "test.westus.azurecontainerapps.io" },
+ ["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"] = new { type = "String", value = $"/subscriptions/{TestSubscriptionId}/resourceGroups/{TestResourceGroupName}/providers/Microsoft.App/managedEnvironments/testenv" }
+ },
+ _ => []
+ };
+ });
+ ConfigureTestServices(builder, armClientProvider: armClientProvider, processRunner: mockProcessRunner, activityReporter: mockActivityReporter, containerRuntime: fakeContainerRuntime);
+
+ var containerAppEnv = builder.AddAzureContainerAppEnvironment("env");
+ builder.AddProject("api", launchProfileName: null)
+ .WithHttpEndpoint()
+ .WithExternalHttpEndpoints()
+ .WithComputeEnvironment(containerAppEnv);
+
+ // Act
+ using var app = builder.Build();
+ await app.StartAsync();
+ await app.WaitForShutdownAsync();
+
+ // Assert
+ Assert.NotEqual(CompletionState.CompletedWithError, mockActivityReporter.ResultCompletionState);
+
+ var summary = mockActivityReporter.PipelineSummary;
+ Assert.NotNull(summary);
+ var (containerAppSubscriptionId, containerAppResourceGroupName) = GetSubscriptionAndResourceGroup((string)containerAppEnv.Resource.Outputs["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"]!);
+
+ AssertSummaryItem(summary, "📦 Resource Group", $"[{TestResourceGroupName}]({AzurePortalUrls.GetResourceGroupUrl(TestSubscriptionId, TestResourceGroupName, s_testTenantId)})");
+ AssertSummaryItem(summary, "📜 Deployments", $"[Azure Portal]({AzurePortalUrls.GetResourceGroupDeploymentsUrl(TestSubscriptionId, TestResourceGroupName, s_testTenantId)})");
+ AssertSummaryItem(
+ summary,
+ "api",
+ $"[https://api.test.westus.azurecontainerapps.io](https://api.test.westus.azurecontainerapps.io) ([Azure Portal]({AzurePortalUrls.GetResourceUrl($"/subscriptions/{containerAppSubscriptionId}/resourceGroups/{containerAppResourceGroupName}/providers/Microsoft.App/containerApps/api")}))");
+ }
+
+ [Fact]
+ public async Task DeployAsync_WithAppServiceExternalEndpoint_IncludesPortalLinksInSummary()
+ {
+ // Arrange
+ var mockProcessRunner = new MockProcessRunner();
+ var fakeContainerRuntime = new FakeContainerRuntime();
+ var mockActivityReporter = new TestPipelineActivityReporter(testOutputHelper);
+ using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, step: WellKnownPipelineSteps.Deploy);
+ var armClientProvider = new TestArmClientProvider(deploymentName =>
+ {
+ return deploymentName switch
+ {
+ string name when name.StartsWith("env-acr") => new Dictionary
+ {
+ ["name"] = new { type = "String", value = "testregistry" },
+ ["loginServer"] = new { type = "String", value = "testregistry.azurecr.io" }
+ },
+ string name when name.StartsWith("env") => new Dictionary
+ {
+ ["AZURE_CONTAINER_REGISTRY_NAME"] = new { type = "String", value = "testregistry" },
+ ["AZURE_CONTAINER_REGISTRY_ENDPOINT"] = new { type = "String", value = "testregistry.azurecr.io" },
+ ["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"] = new { type = "String", value = GetTestResourceId("/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity") },
+ ["AZURE_WEBSITE_CONTRIBUTOR_MANAGED_IDENTITY_ID"] = new { type = "String", value = $"/subscriptions/{TestSubscriptionId}/resourceGroups/{TestResourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-website-identity" },
+ ["AZURE_WEBSITE_CONTRIBUTOR_MANAGED_IDENTITY_PRINCIPAL_ID"] = new { type = "String", value = "test-website-principal-id" },
+ ["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_CLIENT_ID"] = new { type = "String", value = "test-client-id" },
+ ["planId"] = new { type = "String", value = $"/subscriptions/{TestSubscriptionId}/resourceGroups/{TestResourceGroupName}/providers/Microsoft.Web/serverfarms/testplan" },
+ ["webSiteSuffix"] = new { type = "String", value = "website123" },
+ ["AZURE_APP_SERVICE_DASHBOARD_URI"] = new { type = "String", value = "https://infra-aspiredashboard-test.azurewebsites.net" }
+ },
+ _ => []
+ };
+ });
+ ConfigureTestServices(builder, armClientProvider: armClientProvider, processRunner: mockProcessRunner, activityReporter: mockActivityReporter, containerRuntime: fakeContainerRuntime);
+
+ var appServiceEnv = builder.AddAzureAppServiceEnvironment("env");
+ builder.AddProject("api", launchProfileName: null)
+ .WithHttpEndpoint()
+ .WithExternalHttpEndpoints()
+ .WithComputeEnvironment(appServiceEnv);
+
+ // Act
+ using var app = builder.Build();
+ await app.StartAsync();
+ await app.WaitForShutdownAsync();
+
+ // Assert
+ Assert.NotEqual(CompletionState.CompletedWithError, mockActivityReporter.ResultCompletionState);
+
+ var summary = mockActivityReporter.PipelineSummary;
+ Assert.NotNull(summary);
+ var webSiteSuffix = Assert.IsType(appServiceEnv.Resource.Outputs["webSiteSuffix"]);
+ var websiteName = $"api-{webSiteSuffix}";
+ var (planSubscriptionId, planResourceGroupName) = GetSubscriptionAndResourceGroup((string)appServiceEnv.Resource.Outputs["planId"]!);
+
+ AssertSummaryItem(summary, "📜 Deployments", $"[Azure Portal]({AzurePortalUrls.GetResourceGroupDeploymentsUrl(TestSubscriptionId, TestResourceGroupName, s_testTenantId)})");
+ AssertSummaryItem(
+ summary,
+ "api",
+ $"[https://{websiteName}.azurewebsites.net](https://{websiteName}.azurewebsites.net) ([Azure Portal]({AzurePortalUrls.GetResourceUrl($"/subscriptions/{planSubscriptionId}/resourceGroups/{planResourceGroupName}/providers/Microsoft.Web/sites/{websiteName}")}))");
+ }
+
[Theory]
[InlineData("deploy")]
[InlineData("diagnostics")]
@@ -458,10 +582,10 @@ public async Task DeployAsync_WithMultipleComputeEnvironments_Works(string step)
{
["AZURE_CONTAINER_REGISTRY_NAME"] = new { type = "String", value = "acaregistry" },
["AZURE_CONTAINER_REGISTRY_ENDPOINT"] = new { type = "String", value = "acaregistry.azurecr.io" },
- ["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aca-identity" },
+ ["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"] = new { type = "String", value = GetTestResourceId("/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aca-identity") },
["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_CLIENT_ID"] = new { type = "String", value = "aca-client-id" },
["AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN"] = new { type = "String", value = "aca.westus.azurecontainerapps.io" },
- ["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.App/managedEnvironments/acaenv" }
+ ["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"] = new { type = "String", value = GetTestResourceId("/providers/Microsoft.App/managedEnvironments/acaenv") }
},
string name when name.StartsWith("aas-env-acr") => new Dictionary
{
@@ -472,12 +596,12 @@ public async Task DeployAsync_WithMultipleComputeEnvironments_Works(string step)
{
["AZURE_CONTAINER_REGISTRY_NAME"] = new { type = "String", value = "aasregistry" },
["AZURE_CONTAINER_REGISTRY_ENDPOINT"] = new { type = "String", value = "aasregistry.azurecr.io" },
- ["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aas-identity" },
+ ["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"] = new { type = "String", value = GetTestResourceId("/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aas-identity") },
["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_CLIENT_ID"] = new { type = "String", value = "aas-client-id" },
- ["AZURE_WEBSITE_CONTRIBUTOR_MANAGED_IDENTITY_ID"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aas-website-identity" },
+ ["AZURE_WEBSITE_CONTRIBUTOR_MANAGED_IDENTITY_ID"] = new { type = "String", value = GetTestResourceId("/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aas-website-identity") },
["AZURE_WEBSITE_CONTRIBUTOR_MANAGED_IDENTITY_PRINCIPAL_ID"] = new { type = "String", value = "aas-website-principal-id" },
["webSiteSuffix"] = new { type = "String", value = ".azurewebsites.net" },
- ["planId"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.Web/serverfarms/aasplan" },
+ ["planId"] = new { type = "String", value = GetTestResourceId("/providers/Microsoft.Web/serverfarms/aasplan") },
["AZURE_APP_SERVICE_DASHBOARD_URI"] = new { type = "String", value = "https://infra-aspiredashboard-test.azurewebsites.net" }
},
_ => []
@@ -521,15 +645,15 @@ public async Task DeployAsync_WithMultipleComputeEnvironments_Works(string step)
// Assert ACA environment outputs are properly set
Assert.Equal("acaregistry", acaEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_NAME"]);
Assert.Equal("acaregistry.azurecr.io", acaEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_ENDPOINT"]);
- Assert.Equal("/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aca-identity", acaEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"]);
+ Assert.Equal(GetTestResourceId("/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aca-identity"), acaEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"]);
Assert.Equal("aca.westus.azurecontainerapps.io", acaEnv.Resource.Outputs["AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN"]);
- Assert.Equal("/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.App/managedEnvironments/acaenv", acaEnv.Resource.Outputs["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"]);
+ Assert.Equal(GetTestResourceId("/providers/Microsoft.App/managedEnvironments/acaenv"), acaEnv.Resource.Outputs["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"]);
// Assert AAS environment outputs are properly set
Assert.Equal("aasregistry", aasEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_NAME"]);
Assert.Equal("aasregistry.azurecr.io", aasEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_ENDPOINT"]);
- Assert.Equal("/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aas-identity", aasEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"]);
- Assert.Equal("/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.Web/serverfarms/aasplan", aasEnv.Resource.Outputs["planId"]);
+ Assert.Equal(GetTestResourceId("/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aas-identity"), aasEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"]);
+ Assert.Equal(GetTestResourceId("/providers/Microsoft.Web/serverfarms/aasplan"), aasEnv.Resource.Outputs["planId"]);
Assert.Equal("aas-client-id", aasEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_CLIENT_ID"]);
// Assert - Verify ACR login was called using IContainerRuntime for both registries
@@ -692,9 +816,9 @@ public async Task DeployAsync_WithSingleRedisCache_CallsDeployingComputeResource
{
["AZURE_CONTAINER_REGISTRY_NAME"] = new { type = "String", value = "testregistry" },
["AZURE_CONTAINER_REGISTRY_ENDPOINT"] = new { type = "String", value = "testregistry.azurecr.io" },
- ["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity" },
+ ["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"] = new { type = "String", value = GetTestResourceId("/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity") },
["AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN"] = new { type = "String", value = "test.westus.azurecontainerapps.io" },
- ["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.App/managedEnvironments/testenv" }
+ ["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"] = new { type = "String", value = $"/subscriptions/{TestSubscriptionId}/resourceGroups/{TestResourceGroupName}/providers/Microsoft.App/managedEnvironments/testenv" }
},
_ => []
};
@@ -718,9 +842,9 @@ public async Task DeployAsync_WithSingleRedisCache_CallsDeployingComputeResource
// Assert that container environment outputs are propagated
Assert.Equal("testregistry", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_NAME"]);
Assert.Equal("testregistry.azurecr.io", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_ENDPOINT"]);
- Assert.Equal("/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"]);
+ Assert.Equal(GetTestResourceId("/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity"), containerAppEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"]);
Assert.Equal("test.westus.azurecontainerapps.io", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN"]);
- Assert.Equal("/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.App/managedEnvironments/testenv", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"]);
+ Assert.Equal($"/subscriptions/{TestSubscriptionId}/resourceGroups/{TestResourceGroupName}/providers/Microsoft.App/managedEnvironments/testenv", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"]);
// Assert that compute resources deployment logic was triggered (Redis doesn't require image build/push)
var mockImageBuilder = app.Services.GetRequiredService() as MockImageBuilder;
@@ -755,7 +879,7 @@ public async Task DeployAsync_WithOnlyAzureResources_PrintsDashboardUrl()
string name when name.StartsWith("env") => new Dictionary
{
["AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN"] = new { type = "String", value = "test.westus.azurecontainerapps.io" },
- ["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.App/managedEnvironments/testenv" }
+ ["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"] = new { type = "String", value = GetTestResourceId("/providers/Microsoft.App/managedEnvironments/testenv") }
},
_ => []
};
@@ -779,7 +903,7 @@ public async Task DeployAsync_WithOnlyAzureResources_PrintsDashboardUrl()
// Assert that container environment outputs are propagated
Assert.Equal("test.westus.azurecontainerapps.io", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN"]);
- Assert.Equal("/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.App/managedEnvironments/testenv", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"]);
+ Assert.Equal(GetTestResourceId("/providers/Microsoft.App/managedEnvironments/testenv"), containerAppEnv.Resource.Outputs["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"]);
// Assert that no compute resources were deployed (no image build/push)
var mockImageBuilder = app.Services.GetRequiredService() as MockImageBuilder;
@@ -923,9 +1047,9 @@ public async Task DeployAsync_WithAzureFunctionsProject_Works()
["name"] = new { type = "String", value = "env" },
["AZURE_CONTAINER_REGISTRY_NAME"] = new { type = "String", value = "testregistry" },
["AZURE_CONTAINER_REGISTRY_ENDPOINT"] = new { type = "String", value = "testregistry.azurecr.io" },
- ["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity" },
+ ["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"] = new { type = "String", value = GetTestResourceId("/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity") },
["AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN"] = new { type = "String", value = "test.westus.azurecontainerapps.io" },
- ["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.App/managedEnvironments/testenv" }
+ ["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"] = new { type = "String", value = $"/subscriptions/{TestSubscriptionId}/resourceGroups/{TestResourceGroupName}/providers/Microsoft.App/managedEnvironments/testenv" }
},
string name when name.StartsWith("funcstorage") => new Dictionary
{
@@ -945,20 +1069,20 @@ public async Task DeployAsync_WithAzureFunctionsProject_Works()
},
string name when name.StartsWith("funcapp-identity") => new Dictionary
{
- ["id"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/funcapp-identity" },
+ ["id"] = new { type = "String", value = GetTestResourceId("/providers/Microsoft.ManagedIdentity/userAssignedIdentities/funcapp-identity") },
["clientId"] = new { type = "String", value = "funcapp-client-id" },
- ["principalId"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity" },
+ ["principalId"] = new { type = "String", value = GetTestResourceId("/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity") },
},
string name when name.StartsWith("funcapp-containerapp") => new Dictionary
{
- ["id"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.App/containerApps/funcapp" },
+ ["id"] = new { type = "String", value = GetTestResourceId("/providers/Microsoft.App/containerApps/funcapp") },
["name"] = new { type = "String", value = "funcapp" },
["fqdn"] = new { type = "String", value = "funcapp.azurecontainerapps.io" },
- ["environmentId"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.App/managedEnvironments/env" }
+ ["environmentId"] = new { type = "String", value = GetTestResourceId("/providers/Microsoft.App/managedEnvironments/env") }
},
string name when name.StartsWith("funcapp") => new Dictionary
{
- ["identity_id"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity" },
+ ["identity_id"] = new { type = "String", value = GetTestResourceId("/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity") },
["identity_clientId"] = new { type = "String", value = "test-client-id" }
},
_ => []
@@ -990,9 +1114,9 @@ public async Task DeployAsync_WithAzureFunctionsProject_Works()
// Assert that container environment outputs are propagated
Assert.Equal("testregistry", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_NAME"]);
Assert.Equal("testregistry.azurecr.io", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_ENDPOINT"]);
- Assert.Equal("/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"]);
+ Assert.Equal(GetTestResourceId("/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity"), containerAppEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"]);
Assert.Equal("test.westus.azurecontainerapps.io", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN"]);
- Assert.Equal("/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.App/managedEnvironments/testenv", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"]);
+ Assert.Equal($"/subscriptions/{TestSubscriptionId}/resourceGroups/{TestResourceGroupName}/providers/Microsoft.App/managedEnvironments/testenv", containerAppEnv.Resource.Outputs["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"]);
// Assert that funcapp outputs are propagated
var funcAppDeployment = Assert.IsAssignableFrom(funcApp.Resource.GetDeploymentTargetAnnotation()?.DeploymentTarget);
@@ -1037,8 +1161,8 @@ public async Task DeployAsync_WithAzureResourceDependencies_DoesNotHang(string s
{
["AZURE_CONTAINER_REGISTRY_NAME"] = new { type = "String", value = "testregistry" },
["AZURE_CONTAINER_REGISTRY_ENDPOINT"] = new { type = "String", value = "testregistry.azurecr.io" },
- ["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity" },
- ["planId"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.Web/serverfarms/testplan" },
+ ["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"] = new { type = "String", value = GetTestResourceId("/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity") },
+ ["planId"] = new { type = "String", value = GetTestResourceId("/providers/Microsoft.Web/serverfarms/testplan") },
["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_CLIENT_ID"] = new { type = "String", value = "test-client-id" }
},
string name when name.StartsWith("kv") => new Dictionary
@@ -1253,6 +1377,26 @@ public async Task DeployAsync_WithRequestFailedException_DoesNotIncludeVerboseHt
await Verify(allMessages);
}
+ private static void AssertSummaryItem(IReadOnlyList summary, string key, string value)
+ {
+ var item = Assert.Single(summary, item => item.Key == key);
+ Assert.Equal(value, item.Value);
+ Assert.True(item.EnableMarkdown);
+ }
+
+ private static (string SubscriptionId, string ResourceGroupName) GetSubscriptionAndResourceGroup(string resourceId)
+ {
+ var resourceIdentifier = new ResourceIdentifier(resourceId);
+ var subscriptionId = resourceIdentifier.SubscriptionId;
+ var resourceGroupName = resourceIdentifier.ResourceGroupName;
+
+ return (subscriptionId, resourceGroupName) switch
+ {
+ ({ Length: > 0 }, { Length: > 0 }) => (subscriptionId, resourceGroupName),
+ _ => throw new InvalidOperationException($"Resource id '{resourceId}' does not contain both subscription and resource group segments.")
+ };
+ }
+
private void ConfigureTestServices(IDistributedApplicationTestingBuilder builder,
IInteractionService? interactionService = null,
IBicepProvisioner? bicepProvisioner = null,