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
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Threading;
Expand All @@ -15,18 +16,54 @@ namespace Microsoft.DotNet.ImageBuilder;
internal class AzureTokenCredentialProvider : IAzureTokenCredentialProvider
{
private readonly object _cacheLock = new();
private readonly Dictionary<string, TokenCredential?> _credentialsByScope = [];

public TokenCredential GetCredential(string scope = AzureScopes.DefaultAzureManagementScope) =>
LockHelper.DoubleCheckedLockLookup(
_cacheLock,
_credentialsByScope,
scope,
() =>
private readonly Dictionary<string, TokenCredential?> _credentialsCache = [];
private readonly string _systemAccessToken = Environment.GetEnvironmentVariable("SYSTEM_ACCESSTOKEN") ?? "";

/// <summary>
/// Get a TokenCredential for the specified service connection. Only works in the context of an Azure Pipeline.
/// Ensure that the SYSTEM_ACCESSTOKEN environment variable is set in the pipeline.
/// </summary>
/// <param name="serviceConnection">Details about the Azure DevOps service connection to use.</param>
/// <param name="scope">The scope to request for the token. This might be a URL or a GUID.</param>
/// <returns>A <see cref="TokenCredential"/> that can be used to authenticate to Azure services.</returns>
public TokenCredential GetCredential(
IServiceConnection? serviceConnection,
string scope = AzureScopes.DefaultAzureManagementScope)
{
string cacheKey = $"{serviceConnection?.ServiceConnectionId}:{scope}";

return LockHelper.DoubleCheckedLockLookup(
lockObj: _cacheLock,
dictionary: _credentialsCache,
key: cacheKey,
getValue: () =>
{
AccessToken token = new DefaultAzureCredential().GetToken(new TokenRequestContext([scope]), CancellationToken.None);
return new StaticTokenCredential(token);
TokenCredential? credential = null;

if (serviceConnection is not null)
{
if (string.IsNullOrWhiteSpace(_systemAccessToken))
{
throw new InvalidOperationException(
$"""
Attempted to get Service Connection credential but SYSTEM_ACCESSTOKEN environment variable was not set.
Service connection details: {serviceConnection}
""");
}

credential = new AzurePipelinesCredential(
serviceConnection.TenantId,
serviceConnection.ClientId,
serviceConnection.ServiceConnectionId,
_systemAccessToken);
}

// Fall back to DefaultAzureCredential if no service connection is provided.
credential ??= new DefaultAzureCredential();
var accessToken = credential.GetToken(new TokenRequestContext([scope]), CancellationToken.None);
return new StaticTokenCredential(accessToken);
});
}

private class StaticTokenCredential(AccessToken accessToken) : TokenCredential
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ namespace Microsoft.DotNet.ImageBuilder;
#nullable enable
internal static class AzureTokenCredentialProviderExtensions
{
public static ValueTask<AccessToken> GetTokenAsync(this IAzureTokenCredentialProvider provider, string scope = AzureScopes.DefaultAzureManagementScope) =>
provider.GetCredential(scope).GetTokenAsync(new TokenRequestContext([scope]), CancellationToken.None);
public static ValueTask<AccessToken> GetTokenAsync(
this IAzureTokenCredentialProvider provider,
IServiceConnection serviceConnection,
string scope = AzureScopes.DefaultAzureManagementScope)
{
return provider
.GetCredential(serviceConnection, scope)
.GetTokenAsync(new TokenRequestContext([scope]), CancellationToken.None);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace Microsoft.DotNet.ImageBuilder.Commands
public class AnnotateEolDigestsOptions : Options
{
public RegistryCredentialsOptions CredentialsOptions { get; set; } = new();
public ServiceConnectionOptions? AcrServiceConnection { get; set; }

public string EolDigestsListPath { get; set; } = string.Empty;
public string AcrName { get; set; } = string.Empty;
Expand All @@ -21,11 +22,15 @@ public class AnnotateEolDigestsOptions : Options
public class AnnotateEolDigestsOptionsBuilder : CliOptionsBuilder
{
private readonly RegistryCredentialsOptionsBuilder _registryCredentialsOptionsBuilder = new();
private readonly ServiceConnectionOptionsBuilder _serviceConnectionOptionsBuilder = new();

public override IEnumerable<Option> GetCliOptions() =>
[
..base.GetCliOptions(),
.._registryCredentialsOptionsBuilder.GetCliOptions()
.._registryCredentialsOptionsBuilder.GetCliOptions(),
.._serviceConnectionOptionsBuilder.GetCliOptions(
alias: "acr-service-connection",
propertyName: nameof(AnnotateEolDigestsOptions.AcrServiceConnection))
];

public override IEnumerable<Argument> GetCliArguments() =>
Expand All @@ -43,4 +48,3 @@ public override IEnumerable<Argument> GetCliArguments() =>
];
}
}
#nullable disable
34 changes: 23 additions & 11 deletions src/Microsoft.DotNet.ImageBuilder/src/Commands/BuildCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public class BuildCommand : ManifestCommand<BuildOptions, BuildOptionsBuilder>
private readonly ILoggerService _loggerService;
private readonly IGitService _gitService;
private readonly IProcessService _processService;
private readonly ICopyImageService _copyImageService;
private readonly Lazy<ICopyImageService> _copyImageService;
private readonly Lazy<IManifestService> _manifestService;
private readonly IRegistryCredentialsProvider _registryCredentialsProvider;
private readonly IAzureTokenCredentialProvider _tokenCredentialProvider;
Expand All @@ -46,7 +46,7 @@ public BuildCommand(
ILoggerService loggerService,
IGitService gitService,
IProcessService processService,
ICopyImageService copyImageService,
ICopyImageServiceFactory copyImageServiceFactory,
IManifestServiceFactory manifestServiceFactory,
IRegistryCredentialsProvider registryCredentialsProvider,
IAzureTokenCredentialProvider tokenCredentialProvider,
Expand All @@ -56,19 +56,28 @@ public BuildCommand(
_loggerService = loggerService ?? throw new ArgumentNullException(nameof(loggerService));
_gitService = gitService ?? throw new ArgumentNullException(nameof(gitService));
_processService = processService ?? throw new ArgumentNullException(nameof(processService));
_copyImageService = copyImageService ?? throw new ArgumentNullException(nameof(copyImageService));
_registryCredentialsProvider = registryCredentialsProvider ?? throw new ArgumentNullException(nameof(registryCredentialsProvider));
_tokenCredentialProvider = tokenCredentialProvider ?? throw new ArgumentNullException(nameof(tokenCredentialProvider));
_imageCacheService = imageCacheService ?? throw new ArgumentNullException(nameof(imageCacheService));

// Lazily create the Manifest Service so it can have access to options (not available in this constructor)
// Lazily create services which need access to options
ArgumentNullException.ThrowIfNull(copyImageServiceFactory);
_copyImageService = new Lazy<ICopyImageService>(() =>
copyImageServiceFactory.Create(Options.AcrServiceConnection));
ArgumentNullException.ThrowIfNull(manifestServiceFactory);
_manifestService = new Lazy<IManifestService>(() =>
manifestServiceFactory.Create(ownedAcr: Options.RegistryOverride, Options.CredentialsOptions));
manifestServiceFactory.Create(
ownedAcr: Options.RegistryOverride,
Options.AcrServiceConnection,
Options.CredentialsOptions));
_imageDigestCache = new ImageDigestCache(_manifestService);

_imageNameResolver = new Lazy<ImageNameResolverForBuild>(() =>
new ImageNameResolverForBuild(Options.BaseImageOverrideOptions, Manifest, Options.RepoPrefix, Options.SourceRepoPrefix));
new ImageNameResolverForBuild(
Options.BaseImageOverrideOptions,
Manifest,
Options.RepoPrefix,
Options.SourceRepoPrefix));
}

protected override string Description => "Builds Dockerfiles";
Expand All @@ -86,7 +95,9 @@ public override async Task ExecuteAsync()
// need to query the registry at the end of the command.
if (Options.IsPushEnabled)
{
_tokenCredentialProvider.GetCredential(AzureScopes.ContainerRegistryScope);
_tokenCredentialProvider.GetCredential(
Options.AcrServiceConnection,
AzureScopes.ContainerRegistryScope);
}

await ExecuteWithDockerCredentialsAsync(PullBaseImagesAsync);
Expand All @@ -113,7 +124,8 @@ await _registryCredentialsProvider.ExecuteWithCredentialsAsync(
action: action,
credentialsOptions: Options.CredentialsOptions,
registryName: Manifest.Registry,
ownedAcr: Options.RegistryOverride);
ownedAcr: Options.RegistryOverride,
serviceConnection: Options.AcrServiceConnection);
}

private async Task ExecuteWithDockerCredentialsAsync(Action action)
Expand All @@ -123,7 +135,8 @@ await _registryCredentialsProvider.ExecuteWithCredentialsAsync(
action: action,
credentialsOptions: Options.CredentialsOptions,
registryName: Manifest.Registry,
ownedAcr: Options.RegistryOverride);
ownedAcr: Options.RegistryOverride,
serviceConnection: Options.AcrServiceConnection);
}

private void WriteBuiltImagesToOutputVar()
Expand Down Expand Up @@ -582,7 +595,7 @@ private async Task<string> CopyCachedImage(IEnumerable<TagInfo> allTags, string
.Select(tagInfo => DockerHelper.TrimRegistry(tagInfo.FullyQualifiedName))
.ToArray();
string? srcRegistry = DockerHelper.GetRegistry(sourceDigest);
await _copyImageService.ImportImageAsync(
await _copyImageService.Value.ImportImageAsync(
Options.Subscription,
Options.ResourceGroup,
destTags,
Expand Down Expand Up @@ -744,4 +757,3 @@ private void WriteBuildSummary()
}
}
}
#nullable restore
73 changes: 38 additions & 35 deletions src/Microsoft.DotNet.ImageBuilder/src/Commands/BuildOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class BuildOptions : ManifestOptions, IFilterableOptions
public ManifestFilterOptions FilterOptions { get; set; } = new();
public BaseImageOverrideOptions BaseImageOverrideOptions { get; set; } = new();
public RegistryCredentialsOptions CredentialsOptions { get; set; } = new();
public ServiceConnectionOptions? AcrServiceConnection { get; set; }

public bool IsPushEnabled { get; set; }
public bool IsRetryEnabled { get; set; }
Expand All @@ -36,42 +37,45 @@ public class BuildOptionsBuilder : ManifestOptionsBuilder
private readonly ManifestFilterOptionsBuilder _manifestFilterOptionsBuilder = new();
private readonly BaseImageOverrideOptionsBuilder _baseImageOverrideOptionsBuilder = new();
private readonly RegistryCredentialsOptionsBuilder _registryCredentialsOptionsBuilder = new();
private readonly ServiceConnectionOptionsBuilder _serviceConnectionOptionsBuilder = new();

public override IEnumerable<Option> GetCliOptions() =>
base.GetCliOptions()
.Concat(_manifestFilterOptionsBuilder.GetCliOptions())
.Concat(_baseImageOverrideOptionsBuilder.GetCliOptions())
.Concat(_registryCredentialsOptionsBuilder.GetCliOptions())
.Concat(
new Option[]
{
CreateOption<bool>("push", nameof(BuildOptions.IsPushEnabled),
"Push built images to Docker registry"),
CreateOption<bool>("retry", nameof(BuildOptions.IsRetryEnabled),
"Retry building images upon failure"),
CreateOption<bool>("skip-pulling", nameof(BuildOptions.IsSkipPullingEnabled),
"Skip explicitly pulling the base images of the Dockerfiles"),
CreateOption<string?>("image-info-output-path", nameof(BuildOptions.ImageInfoOutputPath),
"Path to output image info"),
CreateOption<string?>("image-info-source-path", nameof(BuildOptions.ImageInfoSourcePath),
"Path to source image info"),
CreateOption<string?>("source-repo", nameof(BuildOptions.SourceRepoUrl),
"Repo URL of the Dockerfile sources"),
CreateOption<bool>("no-cache", nameof(BuildOptions.NoCache),
"Disables build cache feature"),
CreateOption<string?>("source-repo-prefix", nameof(BuildOptions.SourceRepoPrefix),
"Prefix to add to the external base image names when pulling them"),
CreateDictionaryOption("build-arg", nameof(BuildOptions.BuildArgs),
"Build argument to pass to the Dockerfiles (<name>=<value>)"),
CreateOption<bool>("skip-platform-check", nameof(BuildOptions.SkipPlatformCheck),
"Skips validation that ensures the Dockerfile's base image's platform matches the manifest configuration"),
CreateOption<string>("digests-out-var", nameof(BuildOptions.OutputVariableName),
"Azure DevOps variable name to use for outputting the list of built image digests"),
CreateOption<string>("acr-subscription", nameof(BuildOptions.Subscription),
"Azure subscription to operate on"),
CreateOption<string>("acr-resource-group", nameof(BuildOptions.ResourceGroup),
"Azure resource group to operate on"),
});
[
..base.GetCliOptions(),
.._manifestFilterOptionsBuilder.GetCliOptions(),
.._baseImageOverrideOptionsBuilder.GetCliOptions(),
.._registryCredentialsOptionsBuilder.GetCliOptions(),
.._serviceConnectionOptionsBuilder.GetCliOptions(
alias: "acr-service-connection",
propertyName: nameof(BuildOptions.AcrServiceConnection)),

CreateOption<bool>("push", nameof(BuildOptions.IsPushEnabled),
"Push built images to Docker registry"),
CreateOption<bool>("retry", nameof(BuildOptions.IsRetryEnabled),
"Retry building images upon failure"),
CreateOption<bool>("skip-pulling", nameof(BuildOptions.IsSkipPullingEnabled),
"Skip explicitly pulling the base images of the Dockerfiles"),
CreateOption<string?>("image-info-output-path", nameof(BuildOptions.ImageInfoOutputPath),
"Path to output image info"),
CreateOption<string?>("image-info-source-path", nameof(BuildOptions.ImageInfoSourcePath),
"Path to source image info"),
CreateOption<string?>("source-repo", nameof(BuildOptions.SourceRepoUrl),
"Repo URL of the Dockerfile sources"),
CreateOption<bool>("no-cache", nameof(BuildOptions.NoCache),
"Disables build cache feature"),
CreateOption<string?>("source-repo-prefix", nameof(BuildOptions.SourceRepoPrefix),
"Prefix to add to the external base image names when pulling them"),
CreateDictionaryOption("build-arg", nameof(BuildOptions.BuildArgs),
"Build argument to pass to the Dockerfiles (<name>=<value>)"),
CreateOption<bool>("skip-platform-check", nameof(BuildOptions.SkipPlatformCheck),
"Skips validation that ensures the Dockerfile's base image's platform matches the manifest configuration"),
CreateOption<string>("digests-out-var", nameof(BuildOptions.OutputVariableName),
"Azure DevOps variable name to use for outputting the list of built image digests"),
CreateOption<string>("acr-subscription", nameof(BuildOptions.Subscription),
"Azure subscription to operate on"),
CreateOption<string>("acr-resource-group", nameof(BuildOptions.ResourceGroup),
"Azure resource group to operate on"),
];

public override IEnumerable<Argument> GetCliArguments() =>
base.GetCliArguments()
Expand All @@ -80,4 +84,3 @@ public override IEnumerable<Argument> GetCliArguments() =>
.Concat(_registryCredentialsOptionsBuilder.GetCliArguments());
}
}
#nullable disable
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ public override async Task ExecuteAsync()
_loggerService.WriteHeading("FINDING IMAGES TO CLEAN");

_loggerService.WriteSubheading($"Connecting to ACR '{Options.RegistryName}'");
IContainerRegistryClient acrClient = _acrClientFactory.Create(Options.RegistryName, _tokenCredentialProvider.GetCredential());
IContainerRegistryClient acrClient =
_acrClientFactory.Create(
Options.RegistryName,
_tokenCredentialProvider.GetCredential(Options.AcrServiceConnection));

_loggerService.WriteSubheading($"Querying catalog of ACR '{Options.RegistryName}'");
IAsyncEnumerable<string> repositoryNames = acrClient.GetRepositoryNamesAsync();
Expand All @@ -80,7 +83,11 @@ await _registryCredentialsProvider.ExecuteWithCredentialsAsync(
.Select(repoName => acrClient.GetRepository(repoName))
.Select(repo =>
{
IContainerRegistryContentClient acrContentClient = _acrContentClientFactory.Create(Options.RegistryName, repo.Name, _tokenCredentialProvider.GetCredential());
IContainerRegistryContentClient acrContentClient =
_acrContentClientFactory.Create(
Options.RegistryName,
repo.Name,
Options.AcrServiceConnection);
return ProcessRepoAsync(acrClient, acrContentClient, repo, deletedRepos, deletedImages);
})
.ToArrayAsync();
Expand Down Expand Up @@ -182,7 +189,7 @@ await Parallel.ForEachAsync(allManifests, async (manifest, token) =>
}
});

// If all the images in the repo are expired, delete the whole repo instead of
// If all the images in the repo are expired, delete the whole repo instead of
// deleting each individual image.
if (expiredImages.Count == manifestCount)
{
Expand Down
Loading