Skip to content
Merged
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
43cbdd1
Add AKS starter deployment E2E test (Phase 1)
Feb 5, 2026
6ef79de
Fix AKS test: register required resource providers
Feb 5, 2026
7b754c1
Fix AKS test: use Standard_B2s_v2 VM size
Feb 5, 2026
886b180
Fix AKS test: use Standard_D2s_v3 VM size
Feb 5, 2026
4318b51
Add Phase 2 & 3: Aspire project creation, Helm chart generation, and …
Feb 5, 2026
d38acb6
Fix Kubernetes deployment: Add container build/push step
Feb 5, 2026
f46b53e
Fix duplicate Service ports in Kubernetes publisher
Feb 5, 2026
f175bca
Add explicit AKS-ACR attachment verification step
Feb 5, 2026
3ab65aa
Fix AKS image pull: correct Helm value paths and add ACR check
Feb 6, 2026
da42bb7
Fix duplicate Service/container ports: compare underlying values not …
Feb 6, 2026
1057b42
Re-enable AppService deployment tests
Feb 6, 2026
45adcbb
Add endpoint verification via kubectl port-forward to AKS test
Feb 6, 2026
c2aa4d7
Wait for pods to be ready before port-forward verification
Feb 6, 2026
41dd194
Use retry loop for health endpoint verification and log HTTP status c…
Feb 6, 2026
d1d6551
Use real app endpoints: /weatherforecast and / instead of /health
Feb 6, 2026
1934af2
Improve comments explaining duplicate port dedup rationale
Feb 6, 2026
a9bbfb9
Refactor cleanup to async pattern matching other deployment tests
Feb 6, 2026
f3aed68
Fix duplicate K8s ports: skip DefaultHttpsEndpoint in ProcessEndpoints
Feb 6, 2026
5fa81a7
Add AKS + Redis E2E deployment test
Feb 6, 2026
7d3be48
Fix ACR name collision between parallel AKS tests
Feb 6, 2026
dd62bbf
Fix Redis Helm deployment: provide missing cross-resource secret value
Feb 6, 2026
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
220 changes: 220 additions & 0 deletions tests/Aspire.Deployment.EndToEnd.Tests/AksStarterDeploymentTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Cli.Tests.Utils;
using Aspire.Deployment.EndToEnd.Tests.Helpers;
using Hex1b;
using Hex1b.Automation;
using Xunit;

namespace Aspire.Deployment.EndToEnd.Tests;

/// <summary>
/// End-to-end tests for deploying Aspire applications to Azure Kubernetes Service (AKS).
/// </summary>
public sealed class AksStarterDeploymentTests(ITestOutputHelper output)
{
// Timeout set to 45 minutes to allow for AKS provisioning (~10-15 min) plus deployment.

Copilot AI Feb 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timeout comment mentions "plus deployment" but Phase 1 (current implementation) only performs infrastructure provisioning and verification, not deployment. The actual operations sum to approximately 25 minutes maximum (20 min AKS creation + ~5 min for other operations). While the 45-minute timeout may be intended for future phases, the comment could be misleading for the current implementation. Consider updating the comment to reflect the current phase or noting that the timeout accounts for future deployment steps.

Suggested change
// Timeout set to 45 minutes to allow for AKS provisioning (~10-15 min) plus deployment.
// Timeout set to 45 minutes to allow for AKS provisioning and verification (currently ~25 min),
// with additional headroom reserved for future phases that will include deployment.

Copilot uses AI. Check for mistakes.
private static readonly TimeSpan s_testTimeout = TimeSpan.FromMinutes(45);

[Fact]
public async Task DeployStarterTemplateToAks()
{
using var cts = new CancellationTokenSource(s_testTimeout);
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
cts.Token, TestContext.Current.CancellationToken);
var cancellationToken = linkedCts.Token;

await DeployStarterTemplateToAksCore(cancellationToken);
}

private async Task DeployStarterTemplateToAksCore(CancellationToken cancellationToken)
{
// Validate prerequisites
var subscriptionId = AzureAuthenticationHelpers.TryGetSubscriptionId();
if (string.IsNullOrEmpty(subscriptionId))
{
Assert.Skip("Azure subscription not configured. Set ASPIRE_DEPLOYMENT_TEST_SUBSCRIPTION.");
}

if (!AzureAuthenticationHelpers.IsAzureAuthAvailable())
{
if (DeploymentE2ETestHelpers.IsRunningInCI)
{
Assert.Fail("Azure authentication not available in CI. Check OIDC configuration.");
}
else
{
Assert.Skip("Azure authentication not available. Run 'az login' to authenticate.");
}
}

var workspace = TemporaryWorkspace.Create(output);
var recordingPath = DeploymentE2ETestHelpers.GetTestResultsRecordingPath(nameof(DeployStarterTemplateToAks));
var startTime = DateTime.UtcNow;

// Generate unique names for Azure resources
var resourceGroupName = DeploymentE2ETestHelpers.GenerateResourceGroupName("aks");
var clusterName = $"aks-{DeploymentE2ETestHelpers.GetRunId()}-{DeploymentE2ETestHelpers.GetRunAttempt()}";
// ACR names must be alphanumeric only, 5-50 chars, globally unique
var acrName = $"acr{DeploymentE2ETestHelpers.GetRunId()}{DeploymentE2ETestHelpers.GetRunAttempt()}".ToLowerInvariant();
// Ensure ACR name is valid (alphanumeric, 5-50 chars)
acrName = new string(acrName.Where(char.IsLetterOrDigit).Take(50).ToArray());
if (acrName.Length < 5)
{
acrName = $"acrtest{Guid.NewGuid():N}"[..24];
}

output.WriteLine($"Test: {nameof(DeployStarterTemplateToAks)}");
output.WriteLine($"Resource Group: {resourceGroupName}");
output.WriteLine($"AKS Cluster: {clusterName}");
output.WriteLine($"ACR Name: {acrName}");
output.WriteLine($"Subscription: {subscriptionId[..8]}...");
output.WriteLine($"Workspace: {workspace.WorkspaceRoot.FullName}");

try
{
var builder = Hex1bTerminal.CreateBuilder()
.WithHeadless()
.WithDimensions(160, 48)
.WithAsciinemaRecording(recordingPath)
.WithPtyProcess("/bin/bash", ["--norc"]);

using var terminal = builder.Build();
var pendingRun = terminal.RunAsync(cancellationToken);

var counter = new SequenceCounter();
var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder();

// Step 1: Prepare environment
output.WriteLine("Step 1: Preparing environment...");
sequenceBuilder.PrepareEnvironment(workspace, counter);

// Step 2: Create resource group
output.WriteLine("Step 2: Creating resource group...");
sequenceBuilder
.Type($"az group create --name {resourceGroupName} --location westus3 --output table")
.Enter()
.WaitForSuccessPrompt(counter, TimeSpan.FromSeconds(60));

// Step 3: Create Azure Container Registry
output.WriteLine("Step 3: Creating Azure Container Registry...");
sequenceBuilder
.Type($"az acr create --resource-group {resourceGroupName} --name {acrName} --sku Basic --output table")
.Enter()
.WaitForSuccessPrompt(counter, TimeSpan.FromMinutes(3));

// Step 4: Create AKS cluster with ACR attached
// Using minimal configuration: 1 node, Standard_B2s (smallest viable)
output.WriteLine("Step 4: Creating AKS cluster (this may take 10-15 minutes)...");
sequenceBuilder
.Type($"az aks create " +
$"--resource-group {resourceGroupName} " +
$"--name {clusterName} " +
$"--node-count 1 " +
$"--node-vm-size Standard_B2s " +
$"--generate-ssh-keys " +
$"--attach-acr {acrName} " +
$"--enable-managed-identity " +
$"--output table")
.Enter()
.WaitForSuccessPrompt(counter, TimeSpan.FromMinutes(20));

// Step 5: Configure kubectl credentials
output.WriteLine("Step 5: Configuring kubectl credentials...");
sequenceBuilder
.Type($"az aks get-credentials --resource-group {resourceGroupName} --name {clusterName} --overwrite-existing")
.Enter()
.WaitForSuccessPrompt(counter, TimeSpan.FromSeconds(30));

// Step 6: Verify kubectl connectivity
output.WriteLine("Step 6: Verifying kubectl connectivity...");
sequenceBuilder
.Type("kubectl get nodes")
.Enter()
.WaitForSuccessPrompt(counter, TimeSpan.FromSeconds(30));

// Step 7: Verify cluster is healthy
output.WriteLine("Step 7: Verifying cluster health...");
sequenceBuilder
.Type("kubectl cluster-info")
.Enter()
.WaitForSuccessPrompt(counter, TimeSpan.FromSeconds(30));

// Step 8: Exit terminal
sequenceBuilder
.Type("exit")
.Enter();

var sequence = sequenceBuilder.Build();
await sequence.ApplyAsync(terminal, cancellationToken);
await pendingRun;

var duration = DateTime.UtcNow - startTime;
output.WriteLine($"AKS cluster creation and verification completed in {duration}");

// Report success
DeploymentReporter.ReportDeploymentSuccess(
nameof(DeployStarterTemplateToAks),
resourceGroupName,
new Dictionary<string, string>
{
["cluster"] = clusterName,
["acr"] = acrName
},
duration);

output.WriteLine("✅ Phase 1 Test passed - AKS cluster created and verified!");
}
catch (Exception ex)
{
var duration = DateTime.UtcNow - startTime;
output.WriteLine($"❌ Test failed after {duration}: {ex.Message}");

DeploymentReporter.ReportDeploymentFailure(
nameof(DeployStarterTemplateToAks),
resourceGroupName,
ex.Message,
ex.StackTrace);

throw;
}
finally
{
// Clean up the resource group we created (includes AKS cluster and ACR)
output.WriteLine($"Triggering cleanup of resource group: {resourceGroupName}");
TriggerCleanupResourceGroup(resourceGroupName, output);
DeploymentReporter.ReportCleanupStatus(resourceGroupName, success: true, "Cleanup triggered (fire-and-forget)");

Copilot AI Feb 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cleanup status is reported as successful (success: true) even though the cleanup process is fire-and-forget and doesn't wait for completion or check if the deletion actually succeeded. This could be misleading in logs and reports. While this pattern is also used in AcaStarterDeploymentTests.cs:281, most other deployment tests (like AzureContainerRegistryDeploymentTests, AzureAppConfigDeploymentTests, etc.) use an async cleanup method that waits for the process and checks the exit code before reporting status. Consider either: (1) waiting for the process to complete and reporting actual success/failure, or (2) using a more accurate message like "Cleanup initiated" and a neutral status indicator.

Copilot uses AI. Check for mistakes.
}
}

/// <summary>
/// Triggers cleanup of a specific resource group.
/// This is fire-and-forget - the hourly cleanup workflow handles any missed resources.
/// </summary>
private static void TriggerCleanupResourceGroup(string resourceGroupName, ITestOutputHelper output)
{
var process = new System.Diagnostics.Process
{
StartInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = "az",
Arguments = $"group delete --name {resourceGroupName} --yes --no-wait",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};

try
{
process.Start();
output.WriteLine($"Cleanup triggered for resource group: {resourceGroupName}");
}
catch (Exception ex)
{
output.WriteLine($"Failed to trigger cleanup: {ex.Message}");
}

Copilot AI Feb 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Process object created in the cleanup method is never disposed, which can lead to resource leaks. The process should be wrapped in a using statement or explicitly disposed after starting. This pattern is correctly implemented in other deployment tests like AzureContainerRegistryDeploymentTests.cs where the process is properly awaited and implicitly disposed through proper async patterns.

Copilot uses AI. Check for mistakes.
}

Copilot AI Feb 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cleanup method pattern is inconsistent with the rest of the codebase. Most other deployment tests (AzureContainerRegistryDeploymentTests, AzureAppConfigDeploymentTests, AzureEventHubsDeploymentTests, etc.) use a private async method named CleanupResourceGroupAsync that awaits WaitForExitAsync() on the process. This test uses a synchronous TriggerCleanupResourceGroup method that doesn't wait for process completion or dispose the process object.

This inconsistency makes the codebase harder to maintain and could lead to resource leaks since the Process object is never disposed. Consider refactoring to match the established pattern in files like AzureContainerRegistryDeploymentTests.cs:204-237.

Copilot uses AI. Check for mistakes.
}
Loading