Skip to content

Commit 85a3e07

Browse files
Copilotdavidfowl
andcommitted
Refactor ACR authentication to use Azure SDK TokenCredential
- Add LoginToRegistryAsync method to IContainerRuntime interface - Implement LoginToRegistryAsync in ContainerRuntimeBase with stdin support - Add StandardInputContent to ProcessSpec and update ProcessUtil to handle it - Update FakeContainerRuntime for testing - Refactor AzureEnvironmentResourceHelpers to use IContainerRuntime and ITokenCredentialProvider - Use TokenCredential to acquire ACR access token (scope: https://containerregistry.azure.com/.default) - Login with username 00000000-0000-0000-0000-000000000000 and token as password - Add InternalsVisibleTo for Azure projects to access IContainerRuntime - Remove duplicate shared file compile links from Azure projects - Log process output with PipelineStepContext.Logger at debug level - Throw exception on non-zero exit code to fail overall step Co-authored-by: davidfowl <[email protected]>
1 parent 5f6bf0e commit 85a3e07

File tree

10 files changed

+123
-42
lines changed

10 files changed

+123
-42
lines changed

src/Aspire.Hosting.Azure.AppContainers/Aspire.Hosting.Azure.AppContainers.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
</PropertyGroup>
1010

1111
<ItemGroup>
12-
<Compile Include="$(SharedDir)ResourceNameComparer.cs" LinkBase="Shared\ResourceNameComparer.cs" />
1312
<Compile Include="$(SharedDir)BicepFormattingHelpers.cs" LinkBase="Shared\BicepFormattingHelpers.cs" />
1413
</ItemGroup>
1514

src/Aspire.Hosting.Azure.AppService/Aspire.Hosting.Azure.AppService.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
</PropertyGroup>
1111

1212
<ItemGroup>
13-
<Compile Include="$(SharedDir)ResourceNameComparer.cs" LinkBase="Shared\ResourceNameComparer.cs" />
1413
<Compile Include="$(SharedDir)BicepFormattingHelpers.cs" LinkBase="Shared\BicepFormattingHelpers.cs" />
1514
</ItemGroup>
1615

src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,6 @@
1010
</PropertyGroup>
1111

1212
<ItemGroup>
13-
<Compile Include="..\Aspire.Hosting\Dcp\Process\ProcessResult.cs" Link="Provisioning\Utils\ProcessResult.cs" />
14-
<Compile Include="..\Aspire.Hosting\Dcp\Process\ProcessSpec.cs" Link="Provisioning\Utils\ProcessSpec.cs" />
15-
<Compile Include="..\Aspire.Hosting\Dcp\Process\ProcessUtil.cs" Link="Provisioning\Utils\ProcessUtil.cs" />
16-
<Compile Include="$(SharedDir)CustomResourceSnapshotExtensions.cs" Link="Provisioning\Utils\CustomResourceSnapshotExtensions.cs" />
17-
<Compile Include="$(SharedDir)StringComparers.cs" Link="Provisioning\Utils\StringComparers.cs" />
18-
<Compile Include="$(SharedDir)Model\KnownRelationshipTypes.cs" />
1913
</ItemGroup>
2014

2115
<ItemGroup>

src/Aspire.Hosting.Azure/AzureEnvironmentResourceHelpers.cs

Lines changed: 57 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
using Aspire.Hosting.Dcp.Process;
1010
using Aspire.Hosting.Pipelines;
1111
using Aspire.Hosting.Publishing;
12-
using Microsoft.Extensions.Configuration;
12+
using Azure.Core;
1313
using Microsoft.Extensions.DependencyInjection;
14+
using Microsoft.Extensions.Logging;
1415

1516
namespace Aspire.Hosting.Azure;
1617

@@ -19,64 +20,86 @@ namespace Aspire.Hosting.Azure;
1920
/// </summary>
2021
internal static class AzureEnvironmentResourceHelpers
2122
{
23+
private const string AcrUsername = "00000000-0000-0000-0000-000000000000";
24+
private const string AcrScope = "https://containerregistry.azure.com/.default";
25+
2226
public static async Task LoginToRegistryAsync(IContainerRegistry registry, PipelineStepContext context)
2327
{
24-
var processRunner = context.Services.GetRequiredService<IProcessRunner>();
25-
var configuration = context.Services.GetRequiredService<IConfiguration>();
28+
var containerRuntime = context.Services.GetRequiredService<IContainerRuntime>();
29+
var tokenCredentialProvider = context.Services.GetRequiredService<ITokenCredentialProvider>();
2630

2731
var registryName = await registry.Name.GetValueAsync(context.CancellationToken).ConfigureAwait(false) ??
2832
throw new InvalidOperationException("Failed to retrieve container registry information.");
33+
34+
var registryEndpoint = await registry.Endpoint.GetValueAsync(context.CancellationToken).ConfigureAwait(false) ??
35+
throw new InvalidOperationException("Failed to retrieve container registry endpoint.");
2936

3037
var loginTask = await context.ReportingStep.CreateTaskAsync($"Logging in to **{registryName}**", context.CancellationToken).ConfigureAwait(false);
3138
await using (loginTask.ConfigureAwait(false))
3239
{
33-
await AuthenticateToAcrHelper(loginTask, registryName, context.CancellationToken, processRunner, configuration).ConfigureAwait(false);
40+
await AuthenticateToAcrHelper(loginTask, registryEndpoint, containerRuntime, tokenCredentialProvider.TokenCredential, context.Logger, context.CancellationToken).ConfigureAwait(false);
3441
}
3542
}
3643

37-
private static async Task AuthenticateToAcrHelper(IReportingTask loginTask, string registryName, CancellationToken cancellationToken, IProcessRunner processRunner, IConfiguration configuration)
44+
private static async Task AuthenticateToAcrHelper(IReportingTask loginTask, string registryEndpoint, IContainerRuntime containerRuntime, TokenCredential credential, ILogger logger, CancellationToken cancellationToken)
3845
{
39-
var command = BicepCliCompiler.FindFullPathFromPath("az") ?? throw new InvalidOperationException("Failed to find 'az' command");
4046
try
4147
{
42-
var loginSpec = new ProcessSpec(command)
43-
{
44-
Arguments = $"acr login --name {registryName}",
45-
ThrowOnNonZeroReturnCode = false
46-
};
48+
// Acquire access token for Azure Container Registry
49+
var tokenRequestContext = new TokenRequestContext([AcrScope]);
50+
var accessToken = await credential.GetTokenAsync(tokenRequestContext, cancellationToken).ConfigureAwait(false);
4751

48-
// Set DOCKER_COMMAND environment variable if using podman
49-
var containerRuntime = GetContainerRuntime(configuration);
50-
if (string.Equals(containerRuntime, "podman", StringComparison.OrdinalIgnoreCase))
51-
{
52-
loginSpec.EnvironmentVariables["DOCKER_COMMAND"] = "podman";
53-
}
52+
logger.LogDebug("Logging in to registry {RegistryEndpoint} using container runtime {RuntimeName}", registryEndpoint, containerRuntime.Name);
5453

55-
var (pendingResult, processDisposable) = processRunner.Run(loginSpec);
56-
await using (processDisposable.ConfigureAwait(false))
57-
{
58-
var result = await pendingResult.WaitAsync(cancellationToken).ConfigureAwait(false);
54+
// Login to the registry using the container runtime with custom logging
55+
await LoginToRegistryWithLoggingAsync(containerRuntime, registryEndpoint, AcrUsername, accessToken.Token, logger, cancellationToken).ConfigureAwait(false);
5956

60-
if (result.ExitCode != 0)
61-
{
62-
await loginTask.FailAsync($"Login to ACR **{registryName}** failed with exit code {result.ExitCode}", cancellationToken: cancellationToken).ConfigureAwait(false);
63-
}
64-
else
65-
{
66-
await loginTask.CompleteAsync($"Successfully logged in to **{registryName}**", CompletionState.Completed, cancellationToken).ConfigureAwait(false);
67-
}
68-
}
57+
await loginTask.CompleteAsync($"Successfully logged in to **{registryEndpoint}**", CompletionState.Completed, cancellationToken).ConfigureAwait(false);
6958
}
70-
catch (Exception)
59+
catch (Exception ex)
7160
{
61+
await loginTask.FailAsync($"Login to ACR **{registryEndpoint}** failed: {ex.Message}", cancellationToken: cancellationToken).ConfigureAwait(false);
7262
throw;
7363
}
7464
}
7565

76-
private static string? GetContainerRuntime(IConfiguration configuration)
66+
private static async Task LoginToRegistryWithLoggingAsync(IContainerRuntime containerRuntime, string registryServer, string username, string password, ILogger logger, CancellationToken cancellationToken)
7767
{
78-
// Fall back to known config names (primary and legacy)
79-
return configuration["ASPIRE_CONTAINER_RUNTIME"] ?? configuration["DOTNET_ASPIRE_CONTAINER_RUNTIME"];
68+
var arguments = $"login \"{registryServer}\" --username \"{username}\" --password-stdin";
69+
70+
var spec = new ProcessSpec(containerRuntime is DockerContainerRuntime ? "docker" : "podman")
71+
{
72+
Arguments = arguments,
73+
StandardInputContent = password,
74+
OnOutputData = output =>
75+
{
76+
logger.LogDebug("{RuntimeName} login (stdout): {Output}", containerRuntime.Name, output);
77+
},
78+
OnErrorData = error =>
79+
{
80+
logger.LogDebug("{RuntimeName} login (stderr): {Error}", containerRuntime.Name, error);
81+
},
82+
ThrowOnNonZeroReturnCode = false,
83+
InheritEnv = true
84+
};
85+
86+
logger.LogDebug("Running {RuntimeName} login to registry: {RegistryServer}", containerRuntime.Name, registryServer);
87+
var (pendingProcessResult, processDisposable) = ProcessUtil.Run(spec);
88+
89+
await using (processDisposable)
90+
{
91+
var processResult = await pendingProcessResult
92+
.WaitAsync(cancellationToken)
93+
.ConfigureAwait(false);
94+
95+
if (processResult.ExitCode != 0)
96+
{
97+
logger.LogError("{RuntimeName} login to {RegistryServer} failed with exit code {ExitCode}.", containerRuntime.Name, registryServer, processResult.ExitCode);
98+
throw new DistributedApplicationException($"{containerRuntime.Name} login failed with exit code {processResult.ExitCode}.");
99+
}
100+
101+
logger.LogDebug("{RuntimeName} login to {RegistryServer} succeeded.", containerRuntime.Name, registryServer);
102+
}
80103
}
81104

82105
public static async Task PushImageToRegistryAsync(IContainerRegistry registry, IResource resource, PipelineStepContext context, IResourceContainerImageBuilder containerImageBuilder)

src/Aspire.Hosting/Aspire.Hosting.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@
111111
<InternalsVisibleTo Include="Aspire.Hosting.Containers.Tests" />
112112
<InternalsVisibleTo Include="Aspire.TestUtilities" />
113113
<InternalsVisibleTo Include="Aspire.Cli.Tests" />
114+
<InternalsVisibleTo Include="Aspire.Hosting.Azure" />
115+
<InternalsVisibleTo Include="Aspire.Hosting.Azure.AppContainers" />
116+
<InternalsVisibleTo Include="Aspire.Hosting.Azure.AppService" />
114117
</ItemGroup>
115118

116119
</Project>

src/Aspire.Hosting/Dcp/Process/ProcessSpec.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ internal sealed class ProcessSpec
1616
public Action<int>? OnStop { get; init; }
1717
public bool KillEntireProcessTree { get; init; } = true;
1818
public bool ThrowOnNonZeroReturnCode { get; init; } = true;
19+
public string? StandardInputContent { get; init; }
1920

2021
public ProcessSpec(string executablePath)
2122
{

src/Aspire.Hosting/Dcp/Process/ProcessUtil.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public static (Task<ProcessResult>, IAsyncDisposable) Run(ProcessSpec processSpe
2828
Arguments = processSpec.Arguments,
2929
RedirectStandardOutput = true,
3030
RedirectStandardError = true,
31+
RedirectStandardInput = processSpec.StandardInputContent != null,
3132
UseShellExecute = false,
3233
CreateNoWindow = true,
3334
WindowStyle = ProcessWindowStyle.Hidden,
@@ -91,6 +92,14 @@ public static (Task<ProcessResult>, IAsyncDisposable) Run(ProcessSpec processSpe
9192
#endif
9293

9394
process.Start();
95+
96+
// Write standard input if provided
97+
if (processSpec.StandardInputContent != null)
98+
{
99+
process.StandardInput.WriteLine(processSpec.StandardInputContent);
100+
process.StandardInput.Close();
101+
}
102+
94103
process.BeginOutputReadLine();
95104
process.BeginErrorReadLine();
96105
processSpec.OnStart?.Invoke(process.Id);

src/Aspire.Hosting/Publishing/ContainerRuntimeBase.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,45 @@ await ExecuteContainerCommandAsync(
7676
imageName).ConfigureAwait(false);
7777
}
7878

79+
public virtual async Task LoginToRegistryAsync(string registryServer, string username, string password, CancellationToken cancellationToken)
80+
{
81+
var arguments = $"login \"{registryServer}\" --username \"{username}\" --password-stdin";
82+
83+
var spec = new ProcessSpec(RuntimeExecutable)
84+
{
85+
Arguments = arguments,
86+
StandardInputContent = password,
87+
OnOutputData = output =>
88+
{
89+
_logger.LogDebug("{RuntimeName} (stdout): {Output}", RuntimeExecutable, output);
90+
},
91+
OnErrorData = error =>
92+
{
93+
_logger.LogDebug("{RuntimeName} (stderr): {Error}", RuntimeExecutable, error);
94+
},
95+
ThrowOnNonZeroReturnCode = false,
96+
InheritEnv = true
97+
};
98+
99+
_logger.LogDebug("Running {RuntimeName} login to registry: {RegistryServer}", Name, registryServer);
100+
var (pendingProcessResult, processDisposable) = ProcessUtil.Run(spec);
101+
102+
await using (processDisposable)
103+
{
104+
var processResult = await pendingProcessResult
105+
.WaitAsync(cancellationToken)
106+
.ConfigureAwait(false);
107+
108+
if (processResult.ExitCode != 0)
109+
{
110+
_logger.LogError("{RuntimeName} login to {RegistryServer} failed with exit code {ExitCode}.", Name, registryServer, processResult.ExitCode);
111+
throw new DistributedApplicationException($"{Name} login failed with exit code {processResult.ExitCode}.");
112+
}
113+
114+
_logger.LogInformation("{RuntimeName} login to {RegistryServer} succeeded.", Name, registryServer);
115+
}
116+
}
117+
79118
/// <summary>
80119
/// Executes a container runtime command with standard logging and error handling.
81120
/// </summary>

src/Aspire.Hosting/Publishing/IContainerRuntime.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ internal interface IContainerRuntime
1313
Task TagImageAsync(string localImageName, string targetImageName, CancellationToken cancellationToken);
1414
Task RemoveImageAsync(string imageName, CancellationToken cancellationToken);
1515
Task PushImageAsync(string imageName, CancellationToken cancellationToken);
16+
Task LoginToRegistryAsync(string registryServer, string username, string password, CancellationToken cancellationToken);
1617
}

tests/Aspire.Hosting.Tests/Publishing/FakeContainerRuntime.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ internal sealed class FakeContainerRuntime(bool shouldFail = false) : IContainer
1515
public bool WasRemoveImageCalled { get; private set; }
1616
public bool WasPushImageCalled { get; private set; }
1717
public bool WasBuildImageCalled { get; private set; }
18+
public bool WasLoginToRegistryCalled { get; private set; }
1819
public List<(string localImageName, string targetImageName)> TagImageCalls { get; } = [];
1920
public List<string> RemoveImageCalls { get; } = [];
2021
public List<string> PushImageCalls { get; } = [];
2122
public List<(string contextPath, string dockerfilePath, string imageName, ContainerBuildOptions? options)> BuildImageCalls { get; } = [];
23+
public List<(string registryServer, string username, string password)> LoginToRegistryCalls { get; } = [];
2224
public Dictionary<string, string?>? CapturedBuildArguments { get; private set; }
2325
public Dictionary<string, string?>? CapturedBuildSecrets { get; private set; }
2426
public string? CapturedStage { get; private set; }
@@ -79,4 +81,15 @@ public Task BuildImageAsync(string contextPath, string dockerfilePath, string im
7981
// For testing, we don't need to actually build anything
8082
return Task.CompletedTask;
8183
}
84+
85+
public Task LoginToRegistryAsync(string registryServer, string username, string password, CancellationToken cancellationToken)
86+
{
87+
WasLoginToRegistryCalled = true;
88+
LoginToRegistryCalls.Add((registryServer, username, password));
89+
if (shouldFail)
90+
{
91+
throw new InvalidOperationException("Fake container runtime is configured to fail");
92+
}
93+
return Task.CompletedTask;
94+
}
8295
}

0 commit comments

Comments
 (0)