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
192 changes: 190 additions & 2 deletions src/ImageBuilder.Tests/BuildCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,9 @@ public async Task BuildCommand_Publish()
const string repoName = "runtime";
const string tag = "tag";
const string sharedTag = "shared";
const string syndicatedRepo = "syndicated-runtime";
const string syndicatedTag = "syndicated-tag";
const string syndicatedSharedTag = "syndicated-shared";
const string baseImageRepo = "baserepo";
string baseImageTag = $"{baseImageRepo}:basetag";

Expand All @@ -483,6 +486,7 @@ public async Task BuildCommand_Publish()
copyImageService: copyImageServiceMock.Object,
manifestServiceFactory: CreateManifestServiceFactoryMock().Object,
imageCacheService: new ImageCacheService(Mock.Of<ILogger<ImageCacheService>>(), Mock.Of<IGitService>()));

command.Options.Manifest = Path.Combine(tempFolderContext.Path, "manifest.json");
command.Options.IsPushEnabled = true;

Expand All @@ -492,7 +496,14 @@ public async Task BuildCommand_Publish()
string dockerfileAbsolutePath = PathHelper.NormalizePath(Path.Combine(tempFolderContext.Path, dockerfileRelativePath));
File.WriteAllText(dockerfileAbsolutePath, $"FROM {baseImageTag}");

// Configure both the platform and shared tags to be syndicated to custom names in a
// second repository.
Platform platform = CreatePlatform(dockerfileRelativePath, new string[] { tag }, architecture: Architecture.ARM, variant: "v7");
platform.Tags[tag].Syndication = new TagSyndication
{
Repo = syndicatedRepo,
DestinationTags = [syndicatedTag]
};

Manifest manifest = CreateManifest(
CreateRepo(repoName,
Expand All @@ -503,7 +514,17 @@ public async Task BuildCommand_Publish()
},
new Dictionary<string, Tag>
{
{ sharedTag, new Tag() }
{
sharedTag,
new Tag
{
Syndication = new TagSyndication
{
Repo = syndicatedRepo,
DestinationTags = [syndicatedSharedTag]
}
}
}
}))
);

Expand All @@ -512,6 +533,8 @@ public async Task BuildCommand_Publish()
command.LoadManifest();
await command.ExecuteAsync(testContext.CancellationToken);

// A fresh build must assign every primary and syndicated platform and shared tag to
// the same local image.
dockerServiceMock.Verify(
o => o.BuildImage(
dockerfileAbsolutePath,
Expand All @@ -520,7 +543,9 @@ public async Task BuildCommand_Publish()
new string[]
{
TagInfo.GetFullyQualifiedName(repoName, tag),
TagInfo.GetFullyQualifiedName(repoName, sharedTag)
TagInfo.GetFullyQualifiedName(syndicatedRepo, syndicatedTag),
TagInfo.GetFullyQualifiedName(repoName, sharedTag),
TagInfo.GetFullyQualifiedName(syndicatedRepo, syndicatedSharedTag)
},
It.IsAny<IDictionary<string, string>>(),
It.IsAny<IReadOnlyDictionary<string, string>>(),
Expand All @@ -529,17 +554,180 @@ public async Task BuildCommand_Publish()
It.IsAny<bool>(),
It.IsAny<bool>(), It.IsAny<CancellationToken>()));

// Unlike cache-hit imports, newly built images must explicitly push each alias to
// staging.
dockerServiceMock.Verify(
o => o.PushImage(TagInfo.GetFullyQualifiedName(repoName, tag), It.IsAny<bool>(), It.IsAny<CancellationToken>()));

dockerServiceMock.Verify(
o => o.PushImage(TagInfo.GetFullyQualifiedName(repoName, sharedTag), It.IsAny<bool>(), It.IsAny<CancellationToken>()));

dockerServiceMock.Verify(
o => o.PushImage(TagInfo.GetFullyQualifiedName(syndicatedRepo, syndicatedTag), It.IsAny<bool>(), It.IsAny<CancellationToken>()));

dockerServiceMock.Verify(
o => o.PushImage(TagInfo.GetFullyQualifiedName(syndicatedRepo, syndicatedSharedTag), It.IsAny<bool>(), It.IsAny<CancellationToken>()));

dockerServiceMock.Verify(
o => o.GetImageSize(TagInfo.GetFullyQualifiedName(repoName, tag), false, It.IsAny<CancellationToken>()));

copyImageServiceMock.VerifyNoOtherCalls();
}

/// <summary>
/// Verifies that cached images are imported and tagged with syndicated platform and shared tags.
/// </summary>
[TestMethod]
public async Task BuildCommand_PublishCachedImageWithSyndicatedTags()
{
const string registry = "mcr.microsoft.com";
const string registryOverride = "staging.azurecr.io";
const string repoPrefix = "build/";
const string repoName = "runtime";
const string tag = "tag";
const string sharedTag = "shared";
const string syndicatedRepo = "syndicated-runtime";
const string syndicatedTag = "syndicated-tag";
const string syndicatedSharedTag = "syndicated-shared";
const string digestSha = "sha256:digest";
string sourceDigest = $"{registry}/{repoName}@{digestSha}";
string stagingDigest = $"{registryOverride}/{repoPrefix}{repoName}@{digestSha}";

using TempFolderContext tempFolderContext = TestHelper.UseTempFolder();
Mock<IDockerService> dockerServiceMock = CreateDockerServiceMock();
Mock<ICopyImageService> copyImageServiceMock = new();

// Simulate finding the platform in the image cache so it must be copied into staging
// instead of being rebuilt.
PlatformData cachedPlatform = new()
{
Digest = sourceDigest
};
Mock<IImageCacheService> imageCacheServiceMock = new();
imageCacheServiceMock
.SetupGet(service => service.HasAnyCachedPlatforms)
.Returns(true);

imageCacheServiceMock
.Setup(service => service.CheckForCachedImageAsync(
It.IsAny<ImageData>(),
It.IsAny<PlatformData>(),
It.IsAny<ImageDigestCache>(),
It.IsAny<ImageNameResolver>(),
It.IsAny<string>(),
It.IsAny<bool>(),
It.IsAny<bool>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new ImageCacheResult(ImageCacheState.Cached, true, cachedPlatform));

BuildCommand command = CreateBuildCommand(
dockerService: dockerServiceMock.Object,
copyImageService: copyImageServiceMock.Object,
imageCacheService: imageCacheServiceMock.Object);

command.Options.Manifest = Path.Combine(tempFolderContext.Path, "manifest.json");
command.Options.IsPushEnabled = true;
command.Options.IsSkipPullingEnabled = true;
command.Options.SkipPlatformCheck = true;
command.Options.RegistryOverride = registryOverride;
command.Options.RepoPrefix = repoPrefix;

string dockerfile = DockerfileHelper.CreateDockerfile(
"1.0/runtime/os", tempFolderContext, "scratch");

Platform platform = CreatePlatform(dockerfile, [tag]);
platform.Tags[tag].Syndication = new TagSyndication
{
Repo = syndicatedRepo,
DestinationTags = [syndicatedTag]
};

// Configure both the platform and shared tags to be syndicated to custom names in a
// second repository.
Manifest manifest = CreateManifest(
CreateRepo(
repoName,
CreateImage(
[platform],
new Dictionary<string, Tag>
{
{
sharedTag,
new Tag
{
Syndication = new TagSyndication
{
Repo = syndicatedRepo,
DestinationTags = [syndicatedSharedTag]
}
}
}
})));

manifest.Registry = registry;
File.WriteAllText(command.Options.Manifest, JsonConvert.SerializeObject(manifest));

command.LoadManifest();
await command.ExecuteAsync(testContext.CancellationToken);

string[] expectedTags =
[
$"{repoPrefix}{repoName}:{tag}",
$"{repoPrefix}{syndicatedRepo}:{syndicatedTag}",
$"{repoPrefix}{repoName}:{sharedTag}",
$"{repoPrefix}{syndicatedRepo}:{syndicatedSharedTag}"
];

VerifyImportImage(
copyImageServiceMock,
command,
expectedTags,
DockerHelper.TrimRegistry(sourceDigest),
registryOverride,
registry);

// The imported staging digest must also have every primary and syndicated alias
// available locally for any subsequent Dockerfiles that reference them.
foreach (string expectedTag in expectedTags)
{
dockerServiceMock.Verify(service => service.CreateTag(
stagingDigest,
$"{registryOverride}/{expectedTag}",
false,
It.IsAny<CancellationToken>()));
}

dockerServiceMock.Verify(service => service.PullImage(
stagingDigest,
null,
false,
It.IsAny<CancellationToken>()));

// Importing the cached image publishes its tags to staging, so no image build or
// separate Docker push should occur.
dockerServiceMock.Verify(service => service.BuildImage(
It.IsAny<string>(),
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>(),
It.IsAny<CancellationToken>()),
Times.Never);

dockerServiceMock.Verify(service => service.PushImage(
It.IsAny<string>(),
It.IsAny<bool>(),
It.IsAny<CancellationToken>()),
Times.Never);

copyImageServiceMock.VerifyNoOtherCalls();
}

/// <summary>
/// Verifies that the manifest's platform architecture settings match the architecture of the base image.
/// </summary>
Expand Down
55 changes: 55 additions & 0 deletions src/ImageBuilder.Tests/CopyImageServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,61 @@ await service.ImportImageAsync(
Times.Exactly(3));
}

/// <summary>
/// Referrers are repository-scoped, so each one must be imported into every repository
/// targeted by the main image import.
/// </summary>
[TestMethod]
public async Task ImportImageAsync_CopiesReferrersToEveryDestinationRepository()
{
PublishConfiguration publishConfig = CreateAcrPublishConfig("myacr.azurecr.io");

var mockImporter = new Mock<IAcrImageImporter>();
var mockOras = new Mock<IOrasService>();
mockOras
.Setup(o => o.GetReferrersAsync("myacr.azurecr.io/repo:tag", It.IsAny<CancellationToken>(), It.IsAny<bool>()))
.ReturnsAsync(
[
new ReferrerInfo(
"myacr.azurecr.io/repo@sha256:ref1",
"application/vnd.cncf.notary.signature")
]);

var service = new CopyImageService(
Mock.Of<ILogger<CopyImageService>>(),
mockImporter.Object,
mockOras.Object,
ConfigurationHelper.CreateOptionsMock(publishConfig));

await service.ImportImageAsync(
destTagNames:
[
"primary/repo:tag",
"primary/repo:latest",
"syndicated/repo:tag"
],
destAcrName: "myacr.azurecr.io",
srcTagName: "repo:tag",
srcRegistryName: "myacr.azurecr.io",
sourceCredentials: null,
isDryRun: false,
copyReferrers: true,
cancellationToken: TestContext?.CancellationToken ?? default);

mockImporter.Verify(
x => x.ImportImageAsync(
"myacr.azurecr.io",
It.IsAny<ResourceIdentifier>(),
It.Is<ContainerRegistryImportImageContent>(c =>
c.UntaggedTargetRepositories.Count == 2
&& c.UntaggedTargetRepositories.Contains("primary/repo")
&& c.UntaggedTargetRepositories.Contains("syndicated/repo")
&& c.Source.SourceImage == "repo@sha256:ref1"),
It.IsAny<CancellationToken>()),
Times.Once);

}

/// <summary>
/// When no referrers exist, ImportImageAsync should import only the main image.
/// </summary>
Expand Down
44 changes: 44 additions & 0 deletions src/ImageBuilder.Tests/ImageInfoHelperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.DotNet.ImageBuilder.Commands;
using Microsoft.DotNet.ImageBuilder.Models.Image;
using Microsoft.DotNet.ImageBuilder.Models.Manifest;
using Microsoft.DotNet.ImageBuilder.Tests.Helpers;
Expand All @@ -23,6 +24,49 @@ namespace Microsoft.DotNet.ImageBuilder.Tests
[TestClass]
public class ImageInfoHelperTests
{
[TestMethod]
public void ApplyRegistryOverride_OverridesSyndicatedManifestDigests()
{
ImageArtifactDetails imageArtifactDetails = new()
{
Repos =
{
new RepoData
{
Repo = "runtime",
Images =
{
new ImageData
{
Manifest = new ManifestData
{
Digest = "mcr.microsoft.com/runtime@sha256:primary",
SyndicatedDigests =
{
"mcr.microsoft.com/syndicated/runtime@sha256:syndicated"
}
}
}
}
}
}
};
RegistryOptions registryOverride = new()
{
Registry = "build.azurecr.io",
RepoPrefix = "staging"
};

imageArtifactDetails.ApplyRegistryOverride(registryOverride);

imageArtifactDetails.Repos[0].Images[0].Manifest.Digest
.ShouldBe("build.azurecr.io/staging/runtime@sha256:primary");

imageArtifactDetails.Repos[0].Images[0].Manifest.SyndicatedDigests
.ShouldBe(["build.azurecr.io/staging/syndicated/runtime@sha256:syndicated"]);

}

[TestMethod]
public void LoadFromContent()
{
Expand Down
6 changes: 4 additions & 2 deletions src/ImageBuilder.Tests/Signing/ImageSigningServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ public async Task SignImagesAsync_ResolvesPlatformAndManifestListDigests()
Manifest = new ManifestData
{
Digest = "sha256:manifest123",
SyndicatedDigests = ["sha256:syndicatedManifest123"],
SharedTags = ["8.0", "latest"]
},
Platforms =
Expand All @@ -200,10 +201,11 @@ public async Task SignImagesAsync_ResolvesPlatformAndManifestListDigests()

var results = await service.SignImagesAsync(imageArtifactDetails, signingKeyCode: 100, TestContext?.CancellationToken ?? default);

results.Count.ShouldBe(3);
results.Count.ShouldBe(4);
results.Select(r => r.ImageName).ShouldBe(
["sha256:abc123", "sha256:def456", "sha256:manifest123"],
["sha256:abc123", "sha256:def456", "sha256:manifest123", "sha256:syndicatedManifest123"],
ignoreOrder: true);

}

[TestMethod]
Expand Down
Loading