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
68 changes: 63 additions & 5 deletions src/ImageBuilder.Tests/BuildCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Azure.Core;
using Azure.ResourceManager.ContainerRegistry.Models;
using FluentAssertions;
using Microsoft.DotNet.ImageBuilder.Commands;
Expand Down Expand Up @@ -521,6 +523,8 @@ public async Task BuildCommand_Publish()
TagInfo.GetFullyQualifiedName(repoName, sharedTag)
},
It.IsAny<IDictionary<string, string>>(),
It.IsAny<IReadOnlyDictionary<string, string>>(),
It.IsAny<BuildSecretMode>(),
It.IsAny<IEnumerable<string>>(),
It.IsAny<bool>(),
It.IsAny<bool>()));
Expand Down Expand Up @@ -637,31 +641,51 @@ public async Task BuildCommand_ArmVariantCompatibility(string manifestVariant, s
}

/// <summary>
/// Verifies that manifest-defined and globally-defined build args can be used.
/// Verifies build argument precedence and platform-specific handling of internal build secrets.
/// </summary>
[TestMethod]
public async Task BuildCommand_BuildArgs()
[DataRow(OS.Linux, false, BuildSecretMode.SecretMounts)]
[DataRow(OS.Linux, true, BuildSecretMode.SecretMounts)]
[DataRow(OS.Windows, false, BuildSecretMode.BuildArgs)]
[DataRow(OS.Windows, true, BuildSecretMode.BuildArgs)]
public async Task BuildCommand_BuildArgs(OS os, bool isInternal, BuildSecretMode expectedSecretMode)
{
const string repoName = "runtime";
const string tag = "tag";
const string baseImageRepo = "baserepo";
string baseImageTag = $"{baseImageRepo}:basetag";
const string accessToken = "test-storage-token";
const string storageScope = "https://storage.azure.com/.default";

using TempFolderContext tempFolderContext = TestHelper.UseTempFolder();
Mock<IDockerService> dockerServiceMock = CreateDockerServiceMock();
Mock<TokenCredential> credentialMock = new();
credentialMock
.Setup(credential => credential.GetToken(
It.Is<TokenRequestContext>(context => context.Scopes.SequenceEqual(new[] { storageScope })),
It.IsAny<CancellationToken>()))
.Returns(new AccessToken(accessToken, DateTimeOffset.UtcNow.AddHours(1)));
Mock<IAzureTokenCredentialProvider> credentialProviderMock = new();
credentialProviderMock
.Setup(provider => provider.GetCredential(It.IsAny<IServiceConnection>()))
.Returns(credentialMock.Object);

BuildCommand command = CreateBuildCommand(
dockerService: dockerServiceMock.Object,
copyImageService: Mock.Of<ICopyImageService>(),
manifestServiceFactory: CreateManifestServiceFactoryMock().Object,
azureTokenCredentialProvider: credentialProviderMock.Object,
imageCacheService: new ImageCacheService(Mock.Of<ILogger<ImageCacheService>>(), Mock.Of<IGitService>()));
command.Options.Manifest = Path.Combine(tempFolderContext.Path, "manifest.json");
command.Options.Internal = isInternal;
command.Options.BuildArgs.Add("arg1", "val1");
command.Options.BuildArgs.Add("arg2", "val2a");

Platform platform = CreatePlatform(
DockerfileHelper.CreateDockerfile("1.0/runtime/os", tempFolderContext, baseImageTag),
new string[] { tag });
new string[] { tag },
os: os,
osVersion: os == OS.Windows ? "nanoserver-ltsc2022" : "noble");
platform.BuildArgs.Add("arg2", "val2b");
platform.BuildArgs.Add("arg3", "val3");

Expand All @@ -687,11 +711,23 @@ public async Task BuildCommand_BuildArgs()
It.IsAny<List<string>>(),
It.Is<Dictionary<string, string>>(
args => args.Count == 3 && args["arg1"] == "val1" && args["arg2"] == "val2b" && args["arg3"] == "val3"),
It.Is<IReadOnlyDictionary<string, string>>(secrets => isInternal
? secrets.Count == 1 && secrets["ACCESSTOKEN"] == accessToken
: secrets.Count == 0),
expectedSecretMode,
It.IsAny<IEnumerable<string>>(),
It.IsAny<bool>(),
It.IsAny<bool>()));
dockerServiceMock.Verify(
o => o.GetImageSize(It.IsAny<string>(), false));
credentialProviderMock.Verify(
provider => provider.GetCredential(command.Options.StorageServiceConnection),
isInternal ? Times.Once() : Times.Never());
credentialMock.Verify(
credential => credential.GetToken(
It.Is<TokenRequestContext>(context => context.Scopes.SequenceEqual(new[] { storageScope })),
It.IsAny<CancellationToken>()),
isInternal ? Times.Once() : Times.Never());
}

/// <summary>
Expand Down Expand Up @@ -741,6 +777,8 @@ public async Task BuildCommand_DockerBuildOptions()
It.IsAny<string>(),
It.IsAny<List<string>>(),
It.IsAny<IDictionary<string, string>>(),
It.IsAny<IReadOnlyDictionary<string, string>>(),
It.IsAny<BuildSecretMode>(),
It.Is<IEnumerable<string>>(args => args.SequenceEqual(command.Options.DockerBuildOptions)),
It.IsAny<bool>(),
It.IsAny<bool>()));
Expand Down Expand Up @@ -805,6 +843,8 @@ public async Task BuildCommand_NoBaseImage_Build()
TagInfo.GetFullyQualifiedName(repoName, sharedTag)
},
It.IsAny<IDictionary<string, string>>(),
It.IsAny<IReadOnlyDictionary<string, string>>(),
It.IsAny<BuildSecretMode>(),
It.IsAny<IEnumerable<string>>(),
It.IsAny<bool>(),
It.IsAny<bool>()));
Expand Down Expand Up @@ -998,6 +1038,7 @@ public async Task BuildCommand_NoBaseImage_Cached()
o.BuildImage(
PathHelper.NormalizePath(Path.Combine(tempFolderContext.Path, runtimeDepsLinuxDockerfileRelativePath)),
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IEnumerable<string>>(), It.IsAny<IDictionary<string, string>>(),
It.IsAny<IReadOnlyDictionary<string, string>>(), It.IsAny<BuildSecretMode>(),
It.IsAny<IEnumerable<string>>(), It.IsAny<bool>(), It.IsAny<bool>()),
Times.Never);
dockerServiceMock.Verify(
Expand Down Expand Up @@ -1726,7 +1767,8 @@ public async Task BuildCommand_Caching_SharedDockerfile_MissingSourceImageInfoEn
dockerServiceMock.Verify(o =>
o.BuildImage(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IEnumerable<string>>(),
It.IsAny<IDictionary<string, string>>(), It.IsAny<IEnumerable<string>>(), It.IsAny<bool>(), It.IsAny<bool>()),
It.IsAny<IDictionary<string, string>>(), It.IsAny<IReadOnlyDictionary<string, string>>(), It.IsAny<BuildSecretMode>(),
It.IsAny<IEnumerable<string>>(), It.IsAny<bool>(), It.IsAny<bool>()),
Times.Never);

dockerServiceMock.VerifyNoOtherCalls();
Expand Down Expand Up @@ -2034,6 +2076,8 @@ public async Task BuildCommand_Caching_SharedDockerfile_MissingSourceImageInfoEn
It.IsAny<string>(),
It.IsAny<IEnumerable<string>>(),
It.IsAny<IDictionary<string, string>>(),
It.IsAny<IReadOnlyDictionary<string, string>>(),
It.IsAny<BuildSecretMode>(),
It.IsAny<IEnumerable<string>>(),
It.IsAny<bool>(),
It.IsAny<bool>()));
Expand Down Expand Up @@ -2241,6 +2285,8 @@ public async Task BuildCommand_Caching_SharedDockerfile_NoExistingImageInfoEntri
It.IsAny<string>(),
new string[] { expectedTag },
It.IsAny<IDictionary<string, string>>(),
It.IsAny<IReadOnlyDictionary<string, string>>(),
It.IsAny<BuildSecretMode>(),
It.IsAny<IEnumerable<string>>(),
It.IsAny<bool>(),
It.IsAny<bool>()),
Expand Down Expand Up @@ -2485,6 +2531,8 @@ public async Task BuildCommand_SharedDockerfile()
It.IsAny<string>(),
new string[] { expectedTag },
It.IsAny<IDictionary<string, string>>(),
It.IsAny<IReadOnlyDictionary<string, string>>(),
It.IsAny<BuildSecretMode>(),
It.IsAny<IEnumerable<string>>(),
It.IsAny<bool>(),
It.IsAny<bool>()),
Expand Down Expand Up @@ -2703,6 +2751,7 @@ public async Task BuildCommand_Caching_TagUpdate()
o.BuildImage(
PathHelper.NormalizePath(Path.Combine(tempFolderContext.Path, runtimeDepsLinuxDockerfileRelativePath)),
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IEnumerable<string>>(), It.IsAny<IDictionary<string, string>>(),
It.IsAny<IReadOnlyDictionary<string, string>>(), It.IsAny<BuildSecretMode>(),
It.IsAny<IEnumerable<string>>(), It.IsAny<bool>(), It.IsAny<bool>()),
Times.Never);
dockerServiceMock.Verify(
Expand Down Expand Up @@ -2962,7 +3011,8 @@ public async Task BuildCommand_Caching_SharedDockerfile_TagUpdate()
dockerServiceMock.Verify(o =>
o.BuildImage(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IEnumerable<string>>(),
It.IsAny<IDictionary<string, string>>(), It.IsAny<IEnumerable<string>>(), It.IsAny<bool>(), It.IsAny<bool>()),
It.IsAny<IDictionary<string, string>>(), It.IsAny<IReadOnlyDictionary<string, string>>(), It.IsAny<BuildSecretMode>(),
It.IsAny<IEnumerable<string>>(), It.IsAny<bool>(), It.IsAny<bool>()),
Times.Never);
dockerServiceMock.Verify(o => o.GetCreatedDate(It.IsAny<string>(), false));

Expand Down Expand Up @@ -3322,6 +3372,8 @@ public async Task BuildCommand_MirroredImages(bool hasCachedImage, string srcBas
It.IsAny<string>(),
It.IsAny<IEnumerable<string>>(),
It.IsAny<IDictionary<string, string>>(),
It.IsAny<IReadOnlyDictionary<string, string>>(),
It.IsAny<BuildSecretMode>(),
It.IsAny<IEnumerable<string>>(),
It.IsAny<bool>(),
It.IsAny<bool>()));
Expand Down Expand Up @@ -3468,6 +3520,8 @@ public async Task BuildCommand_MirroredImages_External(string baseImageRegistry,
It.IsAny<string>(),
It.IsAny<IEnumerable<string>>(),
It.IsAny<IDictionary<string, string>>(),
It.IsAny<IReadOnlyDictionary<string, string>>(),
It.IsAny<BuildSecretMode>(),
It.IsAny<IEnumerable<string>>(),
It.IsAny<bool>(),
It.IsAny<bool>()));
Expand Down Expand Up @@ -3610,6 +3664,8 @@ public async Task BuildCommand_MirroredImages_BaseImageTagOverride()
It.IsAny<string>(),
It.IsAny<IEnumerable<string>>(),
It.IsAny<IDictionary<string, string>>(),
It.IsAny<IReadOnlyDictionary<string, string>>(),
It.IsAny<BuildSecretMode>(),
It.IsAny<IEnumerable<string>>(),
It.IsAny<bool>(),
It.IsAny<bool>()));
Expand Down Expand Up @@ -3674,6 +3730,8 @@ private static Mock<IDockerService> CreateDockerServiceMock(string buildOutput =
It.IsAny<string>(),
It.IsAny<IEnumerable<string>>(),
It.IsAny<IDictionary<string, string>>(),
It.IsAny<IReadOnlyDictionary<string, string>>(),
It.IsAny<BuildSecretMode>(),
It.IsAny<IEnumerable<string>>(),
It.IsAny<bool>(),
It.IsAny<bool>()))
Expand Down
10 changes: 10 additions & 0 deletions src/ImageBuilder/BuildSecretMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.DotNet.ImageBuilder;

public enum BuildSecretMode
{
SecretMounts,
BuildArgs,
}
30 changes: 14 additions & 16 deletions src/ImageBuilder/Commands/BuildCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public class BuildCommand : ManifestCommand<BuildOptions>
private readonly List<TagInfo> _processedTags = new List<TagInfo>();
private readonly HashSet<PlatformData> _builtPlatforms = new();
private readonly Lazy<ImageNameResolverForBuild> _imageNameResolver;
private readonly Lazy<string?> _storageAccountToken;
private readonly Lazy<string> _storageAccountToken;

/// <summary>
/// Maps a source digest from the image info file to the corresponding digest in the copied location for image caching.
Expand Down Expand Up @@ -74,13 +74,8 @@ public BuildCommand(
Options.RepoPrefix,
Options.SourceRepoPrefix));

_storageAccountToken = new Lazy<string?>(() =>
_storageAccountToken = new Lazy<string>(() =>
{
if (!Options.Internal)
{
return null;
}

var tokenObject = _tokenCredentialProvider.GetToken(
Options.StorageServiceConnection,
AzureScopes.StorageAccount);
Expand Down Expand Up @@ -487,12 +482,18 @@ private void BuildImage(PlatformInfo platform, IEnumerable<string> allTags)

try
{
BuildSecretMode buildSecretMode = platform.IsWindows
? BuildSecretMode.BuildArgs
: BuildSecretMode.SecretMounts;

string? buildOutput = _dockerService.BuildImage(
dockerfilePath,
platform.BuildContextPath,
platform.PlatformLabel,
allTags,
GetBuildArgs(platform),
GetBuildSecrets(),
buildSecretMode,
GetDockerBuildOptions(),
Options.IsRetryEnabled,
Options.IsDryRun);
Expand Down Expand Up @@ -523,10 +524,7 @@ private void BuildImage(PlatformInfo platform, IEnumerable<string> allTags)
}

/// <summary>
/// Gets all the necessary Docker build-args for the specified platform. When building internal images, this
/// also includes the access token for the storage account service connection's access token. This refers to
/// arguments passed via the <c>--build-arg</c> option to the <c>docker build</c> command, not the arguments
/// passed directly to <c>docker build</c>.
/// Gets all the necessary Docker build arguments for the specified platform.
/// </summary>
/// <remarks>
/// Platform build args (from the manifest) take precedence over any build args specified via the command line.
Expand All @@ -537,11 +535,6 @@ private void BuildImage(PlatformInfo platform, IEnumerable<string> allTags)
{
Dictionary<string, string?> buildArgs = [];

if (Options.Internal)
{
buildArgs["ACCESSTOKEN"] = _storageAccountToken.Value;
}

foreach (var kvp in Options.BuildArgs)
{
buildArgs[kvp.Key] = kvp.Value;
Expand All @@ -555,6 +548,11 @@ private void BuildImage(PlatformInfo platform, IEnumerable<string> allTags)
return buildArgs;
}

private IReadOnlyDictionary<string, string> GetBuildSecrets() =>
Options.Internal
? new Dictionary<string, string> { ["ACCESSTOKEN"] = _storageAccountToken.Value }
: new Dictionary<string, string>();

private IEnumerable<string> GetDockerBuildOptions() =>
Options.DockerBuildOptions.Where(option => !string.IsNullOrWhiteSpace(option));

Expand Down
Loading