Skip to content
Open
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
1 change: 0 additions & 1 deletion src/Dockerfile.linux
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ RUN tdnf install -y \
git \
moby-engine \
notation \
oras \
&& tdnf clean all

# install notation trust materials (root CA certs + trust policies)
Expand Down
25 changes: 11 additions & 14 deletions src/ImageBuilder.Tests/AnnotateEolDigestsCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
using Microsoft.DotNet.ImageBuilder.Models.Annotations;
using Microsoft.DotNet.ImageBuilder.Models.Oci;
using Microsoft.DotNet.ImageBuilder.Tests.Helpers;
using Microsoft.Extensions.Logging;
using System.Threading;
using Moq;
using Newtonsoft.Json;
using Xunit.Abstractions;
Expand Down Expand Up @@ -50,11 +50,10 @@ public async Task AnnotateEolDigestsCommand_AnnotationSuccess()
digestAnnotationIsSuccessful: true);
await command.ExecuteAsync();

Manifest manifest;
lifecycleMetadataServiceMock.Verify(
o => o.AnnotateEolDigest("digest1", _globalDate, It.IsAny<ILogger>(), It.IsAny<bool>(), out manifest));
o => o.AnnotateEolDigestAsync("digest1", _globalDate, It.IsAny<CancellationToken>()));
lifecycleMetadataServiceMock.Verify(
o => o.AnnotateEolDigest("digest2", _specificDigestDate, It.IsAny<ILogger>(), It.IsAny<bool>(), out manifest));
o => o.AnnotateEolDigestAsync("digest2", _specificDigestDate, It.IsAny<CancellationToken>()));

string[] expectedAnnotationDigests =
[
Expand Down Expand Up @@ -105,9 +104,8 @@ public async Task AnnotateEolDigestsCommand_CheckAnnotations_AlreadyAnnotated_No
$"(failed: 0, skipped: 2)",
ex.Message);

Manifest manifest;
lifecycleMetadataServiceMock.Verify(
o => o.AnnotateEolDigest(It.IsAny<string>(), It.IsAny<DateOnly>(), It.IsAny<ILogger>(), It.IsAny<bool>(), out manifest),
o => o.AnnotateEolDigestAsync(It.IsAny<string>(), It.IsAny<DateOnly>(), It.IsAny<CancellationToken>()),
Times.Never());
}

Expand All @@ -127,9 +125,8 @@ public async Task AnnotateEolDigestsCommand_CheckAnnotations_AlreadyAnnotated_Ma

await command.ExecuteAsync();

Manifest manifest;
lifecycleMetadataServiceMock.Verify(
o => o.AnnotateEolDigest(It.IsAny<string>(), It.IsAny<DateOnly>(), It.IsAny<ILogger>(), It.IsAny<bool>(), out manifest),
o => o.AnnotateEolDigestAsync(It.IsAny<string>(), It.IsAny<DateOnly>(), It.IsAny<CancellationToken>()),
Times.Never());
}

Expand Down Expand Up @@ -178,17 +175,17 @@ private Mock<ILifecycleMetadataService> CreateLifecycleMetadataServiceMock(bool
};

lifecycleMetadataServiceMock
.Setup(o => o.AnnotateEolDigest(It.Is<string>(digest => digest.Contains("digest1")), It.IsAny<DateOnly>(), It.IsAny<ILogger>(), It.IsAny<bool>(), out digest1Annotation))
.Returns(digestAnnotationIsSuccessful);
.Setup(o => o.AnnotateEolDigestAsync(It.Is<string>(digest => digest.Contains("digest1")), It.IsAny<DateOnly>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(digestAnnotationIsSuccessful ? digest1Annotation : null);

Manifest digest2Annotation = new()
{
Reference = $"{AcrName}/{RepoPrefix}@{AnnotationDigest2}"
};

lifecycleMetadataServiceMock
.Setup(o => o.AnnotateEolDigest(It.Is<string>(digest => digest.Contains("digest2")), It.IsAny<DateOnly>(), It.IsAny<ILogger>(), It.IsAny<bool>(), out digest2Annotation))
.Returns(digestAnnotationIsSuccessful);
.Setup(o => o.AnnotateEolDigestAsync(It.Is<string>(digest => digest.Contains("digest2")), It.IsAny<DateOnly>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(digestAnnotationIsSuccessful ? digest2Annotation : null);

return lifecycleMetadataServiceMock;
}
Expand All @@ -214,8 +211,8 @@ private static void SetupIsDigestAnnotatedForEolMethod(Mock<ILifecycleMetadataSe
}

lifecycleMetadataServiceMock
.Setup(o => o.IsDigestAnnotatedForEol(digest, It.IsAny<ILogger>(), It.IsAny<bool>(), out manifest))
.Returns(digestAlreadyAnnotated);
.Setup(o => o.IsDigestAnnotatedForEolAsync(digest, It.IsAny<CancellationToken>()))
.ReturnsAsync(manifest);
}
}
}
4 changes: 2 additions & 2 deletions src/ImageBuilder.Tests/CleanAcrImagesCommandTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -414,8 +414,8 @@ private static void SetupIsDigestAnnotatedForEolMethod(Mock<ILifecycleMetadataSe
}

lifecycleMetadataServiceMock
.Setup(o => o.IsDigestAnnotatedForEol(reference, It.IsAny<ILogger>(), It.IsAny<bool>(), out manifest))
.Returns(digestAlreadyAnnotated);
.Setup(o => o.IsDigestAnnotatedForEolAsync(reference, It.IsAny<CancellationToken>()))
.ReturnsAsync(manifest);
}
}
}
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.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.ImageBuilder.Commands;
using Microsoft.DotNet.ImageBuilder.Models.Annotations;
Expand Down Expand Up @@ -430,11 +431,11 @@ public async Task GenerateEolAnnotationData_ExcludeDigestsThatAreAlreadyAnnotate
string armDigest = DockerHelper.GetImageName(AcrName, $"{DefaultRepoPrefix}repo1", digest: "platformdigest102-arm64");

// Set the Arm64 digest as already annotated. This should exclude it from the list of digests to annotate.
Manifest lifecycleArtifactManifest;
Manifest lifecycleArtifactManifest = new();
Mock<ILifecycleMetadataService> lifecycleMetadataServiceMock = new();
lifecycleMetadataServiceMock
.Setup(o => o.IsDigestAnnotatedForEol(armDigest, It.IsAny<ILogger>(), It.IsAny<bool>(), out lifecycleArtifactManifest))
.Returns(true);
.Setup(o => o.IsDigestAnnotatedForEolAsync(armDigest, It.IsAny<CancellationToken>()))
.ReturnsAsync(lifecycleArtifactManifest);

IAcrContentClientFactory registryContentClientFactory = CreateAcrContentClientFactory(AcrName,
[
Expand Down Expand Up @@ -1058,16 +1059,18 @@ private static GenerateEolAnnotationDataForPublishCommand InitializeCommand(
private static ILifecycleMetadataService CreateLifecycleMetadataService(Dictionary<string, bool> digestAnnotatedMapping)
{
Mock<ILifecycleMetadataService> lifecycleMetadataServiceMock = new();
Manifest lifecycleArtifactManifest;
lifecycleMetadataServiceMock
.Setup(o => o.IsDigestAnnotatedForEol(It.IsAny<string>(), It.IsAny<ILogger>(), It.IsAny<bool>(), out lifecycleArtifactManifest))
.Returns(false);
.Setup(o => o.IsDigestAnnotatedForEolAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((Manifest)null);

foreach (KeyValuePair<string, bool> digestAnnotated in digestAnnotatedMapping)
{
lifecycleMetadataServiceMock
.Setup(o => o.IsDigestAnnotatedForEol(digestAnnotated.Key, It.IsAny<ILogger>(), It.IsAny<bool>(), out lifecycleArtifactManifest))
.Returns(digestAnnotated.Value);
if (digestAnnotated.Value)
{
lifecycleMetadataServiceMock
.Setup(o => o.IsDigestAnnotatedForEolAsync(digestAnnotated.Key, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Manifest());
}
}

return lifecycleMetadataServiceMock.Object;
Expand Down
17 changes: 11 additions & 6 deletions src/ImageBuilder/Commands/AnnotateEolDigestsCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.ImageBuilder.Models.Annotations;
using Microsoft.DotNet.ImageBuilder.Models.MarBulkDeletion;
Expand Down Expand Up @@ -51,9 +52,10 @@ public override async Task ExecuteAsync()

await _registryCredentialsProvider.ExecuteWithCredentialsAsync(
Options.IsDryRun,
() =>
async () =>
{
Parallel.ForEach(eolAnnotations.EolDigests, digestData => AnnotateDigest(digestData, globalEolDate));
await Parallel.ForEachAsync(eolAnnotations.EolDigests, CancellationToken.None,
async (digestData, ct) => await AnnotateDigestAsync(digestData, globalEolDate, ct));
},
Options.CredentialsOptions,
registryName: Options.AcrName);
Expand Down Expand Up @@ -103,7 +105,7 @@ private void WriteNonEmptySummary(object value, string message)
_logger.LogInformation(string.Empty);
}

private void AnnotateDigest(EolDigestData digestData, DateOnly? globalEolDate)
private async Task AnnotateDigestAsync(EolDigestData digestData, DateOnly? globalEolDate, CancellationToken cancellationToken)
{
if (Options.IsDryRun)
{
Expand All @@ -119,10 +121,12 @@ private void AnnotateDigest(EolDigestData digestData, DateOnly? globalEolDate)
return;
}

if (!_lifecycleMetadataService.IsDigestAnnotatedForEol(digestData.Digest, _logger, Options.IsDryRun, out Manifest? existingAnnotationManifest))
Manifest? existingAnnotationManifest = await _lifecycleMetadataService.IsDigestAnnotatedForEolAsync(digestData.Digest, cancellationToken);
if (existingAnnotationManifest is null)
{
_logger.LogInformation($"Annotating EOL for digest '{digestData.Digest}', date '{eolDate}'");
if (_lifecycleMetadataService.AnnotateEolDigest(digestData.Digest, eolDate.Value, _logger, Options.IsDryRun, out Manifest? createdAnnotationManifest))
Manifest? createdAnnotationManifest = await _lifecycleMetadataService.AnnotateEolDigestAsync(digestData.Digest, eolDate.Value, cancellationToken);
if (createdAnnotationManifest is not null)
{
_createdAnnotationDigests.Add(createdAnnotationManifest.Reference);
}
Expand All @@ -135,7 +139,8 @@ private void AnnotateDigest(EolDigestData digestData, DateOnly? globalEolDate)
}
else
{
if (existingAnnotationManifest.Annotations[LifecycleMetadataService.EndOfLifeAnnotation] == eolDate?.ToString(LifecycleMetadataService.EolDateFormat))
if (existingAnnotationManifest.Annotations.TryGetValue(LifecycleMetadataService.EndOfLifeAnnotation, out string? existingEolValue) &&
existingEolValue == eolDate?.ToString(LifecycleMetadataService.EolDateFormat))
{
_logger.LogInformation($"Skipping digest '{digestData.Digest}' because it is already annotated with a matching EOL date.");
_skippedAnnotationImageDigests.Add(digestData);
Expand Down
16 changes: 11 additions & 5 deletions src/ImageBuilder/Commands/CleanAcrImagesCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Azure.Containers.ContainerRegistry;
using Microsoft.DotNet.ImageBuilder.Configuration;
Expand Down Expand Up @@ -94,7 +95,7 @@ await ProcessManifestsAsync(acrClient, acrContentClient, deletedImages, deletedR
break;
case CleanAcrImagesAction.PruneEol:
await ProcessManifestsAsync(acrClient, acrContentClient, deletedImages, deletedRepos, repository,
async manifest => !(await IsAnnotationManifestAsync(manifest, acrContentClient)) && HasExpiredEol(manifest, Options.Age));
async manifest => !(await IsAnnotationManifestAsync(manifest, acrContentClient)) && await HasExpiredEolAsync(manifest, Options.Age));
break;
case CleanAcrImagesAction.PruneAll:
await ProcessManifestsAsync(acrClient, acrContentClient, deletedImages, deletedRepos, repository,
Expand Down Expand Up @@ -274,12 +275,17 @@ private async Task<bool> IsAnnotationManifestAsync(ArtifactManifestProperties ma
return manifestResult.Manifest["subject"] is not null;
}

private bool HasExpiredEol(ArtifactManifestProperties manifest, int expirationDays)
private async Task<bool> HasExpiredEolAsync(ArtifactManifestProperties manifest, int expirationDays)
{
if(_lifecycleMetadataService.IsDigestAnnotatedForEol(manifest.RegistryLoginServer + "/" + manifest.RepositoryName + "@" + manifest.Digest, _logger, isDryRun: false, out Manifest? lifecycleArtifactManifest) &&
lifecycleArtifactManifest?.Annotations != null)
Manifest? lifecycleArtifactManifest = await _lifecycleMetadataService.IsDigestAnnotatedForEolAsync(
$"{manifest.RegistryLoginServer}/{manifest.RepositoryName}@{manifest.Digest}",
CancellationToken.None);

if (lifecycleArtifactManifest?.Annotations != null &&
lifecycleArtifactManifest.Annotations.TryGetValue(LifecycleMetadataService.EndOfLifeAnnotation, out string? endOfLifeValue) &&
DateTimeOffset.TryParse(endOfLifeValue, out DateTimeOffset endOfLifeDateTime))
{
return IsExpired(DateTimeOffset.Parse(lifecycleArtifactManifest.Annotations[LifecycleMetadataService.EndOfLifeAnnotation]), expirationDays);
return IsExpired(endOfLifeDateTime, expirationDays);
}

return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Azure.Containers.ContainerRegistry;
using Microsoft.DotNet.ImageBuilder.Configuration;
Expand Down Expand Up @@ -46,7 +47,7 @@ public sealed override async Task ExecuteAsync()
IEnumerable<EolDigestData> digestsToAnnotate = [];
await _registryCredentialsProvider.ExecuteWithCredentialsAsync(
Options.IsDryRun,
async () => digestsToAnnotate = GetDigestsWithoutExistingAnnotation(await GetDigestsToAnnotateAsync()),
async () => digestsToAnnotate = await GetDigestsWithoutExistingAnnotationAsync(await GetDigestsToAnnotateAsync()),
Options.CredentialsOptions,
registryName: Options.RegistryOptions.Registry);

Expand Down Expand Up @@ -115,15 +116,19 @@ protected void WriteDigestDataJson(IEnumerable<EolDigestData> digestsToAnnotate)
File.WriteAllText(Options.EolDigestsListPath, annotationsJson);
}

private IEnumerable<EolDigestData> GetDigestsWithoutExistingAnnotation(
private async Task<IEnumerable<EolDigestData>> GetDigestsWithoutExistingAnnotationAsync(
IEnumerable<EolDigestData> unsupportedDigests)
{
// Annotate digests that are not already annotated for EOL
if (Options.IsDryRun)
{
return unsupportedDigests.OrderBy(item => item.Digest).ToList();
}

ConcurrentBag<EolDigestData> digestsToAnnotate = [];
Parallel.ForEach(unsupportedDigests, digest =>
await Parallel.ForEachAsync(unsupportedDigests, CancellationToken.None, async (digest, ct) =>
{
_logger.LogInformation($"Checking digest for existing annotation: {digest.Digest}");
if (!_lifecycleMetadataService.IsDigestAnnotatedForEol(digest.Digest, _logger, Options.IsDryRun, out _))
if (await _lifecycleMetadataService.IsDigestAnnotatedForEolAsync(digest.Digest, ct) is null)
{
digestsToAnnotate.Add(digest);
}
Expand Down
20 changes: 17 additions & 3 deletions src/ImageBuilder/ILifecycleMetadataService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,28 @@
// See the LICENSE file in the project root for more information.

using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.ImageBuilder.Models.Oci;

namespace Microsoft.DotNet.ImageBuilder;

public interface ILifecycleMetadataService
{
bool IsDigestAnnotatedForEol(string digest, ILogger logger, bool isDryRun, [MaybeNullWhen(false)] out Manifest lifecycleArtifactManifest);
/// <summary>
/// Checks whether the given digest has an existing lifecycle (EOL) annotation.
/// </summary>
/// <param name="digest">Fully-qualified digest reference (e.g., "registry.io/repo@sha256:...").</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The lifecycle artifact manifest if annotated, or null if not.</returns>
Task<Manifest?> IsDigestAnnotatedForEolAsync(string digest, CancellationToken cancellationToken = default);

bool AnnotateEolDigest(string digest, DateOnly date, ILogger logger, bool isDryRun, [MaybeNullWhen(false)] out Manifest lifecycleArtifactManifest);
/// <summary>
/// Annotates the given digest with an end-of-life date.
/// </summary>
/// <param name="digest">Fully-qualified digest reference (e.g., "registry.io/repo@sha256:...").</param>
/// <param name="date">The end-of-life date to set.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The created lifecycle artifact manifest, or null on failure.</returns>
Task<Manifest?> AnnotateEolDigestAsync(string digest, DateOnly date, CancellationToken cancellationToken = default);
}
1 change: 0 additions & 1 deletion src/ImageBuilder/ImageBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ public static class ImageBuilder
builder.Services.AddSingleton<INotificationService, NotificationService>();
builder.Services.AddSingleton<Notation.INotationClient, Notation.NotationClient>();
builder.Services.AddSingleton<IOctokitClientFactory, OctokitClientFactory>();
builder.Services.AddSingleton<IOrasClient, OrasClient>();
builder.Services.AddSingleton<Oras.IOrasService, Oras.OrasDotNetService>();
builder.Services.AddSingleton<IProcessService, ProcessService>();
builder.Services.AddSingleton<IRegistryResolver, RegistryResolver>();
Expand Down
Loading
Loading