-
Notifications
You must be signed in to change notification settings - Fork 60
Command for generation of EOL annotation data file #1358
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
mthalman
merged 17 commits into
dotnet:main
from
NikolaMilosavljevic:generate.annotations
Jul 22, 2024
Merged
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
5bdf2ee
Command for generation of EOL annotation data file
NikolaMilosavljevic 8d6b1a9
Update src/Microsoft.DotNet.ImageBuilder/src/DotNetReleasesService.cs
NikolaMilosavljevic 24f2745
Update src/Microsoft.DotNet.ImageBuilder/src/Commands/GenerateEolAnno…
NikolaMilosavljevic 13ce63d
Addressing review comments
NikolaMilosavljevic a2ed75d
Test simplification
mthalman 58254d2
Use different "using" syntax to avoid extra indentation
mthalman e07e9a6
Code review feedback
mthalman 5d54a58
Syntax cleanup
mthalman aa9993a
Code cleanup
mthalman f861a7b
Code refactoring
mthalman db73bc2
Collapse namespace declaration
mthalman 9c891f9
Options refactoring
mthalman dcbd5cf
Azure log updates
mthalman e0d2486
Rewrite logic to compare against current registry state
mthalman bd9f943
Fix failing CleanAcrImagesCommand tests
mthalman 7dbdeb9
Merge branch 'main' into generate.annotations
mthalman 01ec82c
Address review comments
mthalman File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
195 changes: 195 additions & 0 deletions
195
src/Microsoft.DotNet.ImageBuilder/src/Commands/GenerateEolAnnotationDataCommand.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,195 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
| // See the LICENSE file in the project root for more information. | ||
|
|
||
| using System; | ||
| using System.Collections.Concurrent; | ||
| using System.Collections.Generic; | ||
| using System.ComponentModel.Composition; | ||
| using System.IO; | ||
| using System.Linq; | ||
| using System.Threading.Tasks; | ||
| using Azure.Containers.ContainerRegistry; | ||
| using Kusto.Cloud.Platform.Utils; | ||
| using Microsoft.DotNet.ImageBuilder.Models.Annotations; | ||
| using Microsoft.DotNet.ImageBuilder.Models.Image; | ||
| using Newtonsoft.Json; | ||
|
|
||
| #nullable enable | ||
| namespace Microsoft.DotNet.ImageBuilder.Commands; | ||
|
|
||
| [Export(typeof(ICommand))] | ||
| public class GenerateEolAnnotationDataCommand : Command<GenerateEolAnnotationDataOptions, GenerateEolAnnotationDataOptionsBuilder> | ||
| { | ||
| private readonly IDotNetReleasesService _dotNetReleasesService; | ||
| private readonly ILoggerService _loggerService; | ||
| private readonly IContainerRegistryClientFactory _acrClientFactory; | ||
| private readonly IAzureTokenCredentialProvider _tokenCredentialProvider; | ||
| private readonly IOrasService _orasService; | ||
| private readonly DateOnly _eolDate; | ||
|
|
||
| [ImportingConstructor] | ||
| public GenerateEolAnnotationDataCommand( | ||
| IDotNetReleasesService dotNetReleasesService, | ||
| ILoggerService loggerService, | ||
| IContainerRegistryClientFactory acrClientFactory, | ||
| IAzureTokenCredentialProvider tokenCredentialProvider, | ||
| IOrasService orasService) | ||
| { | ||
| _dotNetReleasesService = dotNetReleasesService ?? throw new ArgumentNullException(nameof(dotNetReleasesService)); | ||
| _loggerService = loggerService ?? throw new ArgumentNullException(nameof(loggerService)); | ||
| _acrClientFactory = acrClientFactory ?? throw new ArgumentNullException(nameof(acrClientFactory)); | ||
| _tokenCredentialProvider = tokenCredentialProvider ?? throw new ArgumentNullException(nameof(tokenCredentialProvider)); | ||
| _orasService = orasService ?? throw new ArgumentNullException(nameof(orasService)); | ||
|
|
||
| _eolDate = DateOnly.FromDateTime(DateTime.UtcNow); // default EOL date | ||
| } | ||
|
|
||
| protected override string Description => "Generate EOL annotation data"; | ||
|
|
||
| public override async Task ExecuteAsync() | ||
| { | ||
| List<EolDigestData> digestsToAnnotate = await GetDigestsToAnnotate(); | ||
| WriteDigestDataJson(digestsToAnnotate); | ||
| } | ||
|
|
||
| private void WriteDigestDataJson(List<EolDigestData> digestsToAnnotate) | ||
| { | ||
| EolAnnotationsData eolAnnotations = new(digestsToAnnotate, _eolDate); | ||
|
|
||
| string annotationsJson = JsonConvert.SerializeObject( | ||
| eolAnnotations, Formatting.Indented, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); | ||
| File.WriteAllText(Options.EolDigestsListPath, annotationsJson); | ||
| } | ||
|
|
||
| private async Task<List<EolDigestData>> GetDigestsToAnnotate() | ||
| { | ||
| Dictionary<string, DateOnly> productEolDates = await _dotNetReleasesService.GetProductEolDatesFromReleasesJson(); | ||
| ImageArtifactDetails oldImageArtifactDetails = LoadImageInfoData(Options.OldImageInfoPath); | ||
| ImageArtifactDetails newImageArtifactDetails = LoadImageInfoData(Options.NewImageInfoPath); | ||
|
|
||
| List<EolDigestData> digestDataList = []; | ||
|
|
||
| try | ||
| { | ||
| // Find all the digests that need to be annotated for EOL by querying the registry for all the digests, scoped to those repos associated with | ||
| // the image info. The repo scoping is done because there may be some cases where multiple image info files are used for different repositories. | ||
| // The intent is to annotate all of the digests that do not exist in the image info file. So this scoping ensures we don't annotate digests that | ||
| // are associated with another image info file. However, we also need to account for the deletion of an entire repository. In that case, we want | ||
| // all the digests in that repo to be annotated. But since the repo is deleted, it doesn't show up in the newly generated image info file. So we | ||
| // need the previous version of the image info file to know that the repo had previously existed and so that repo is included in the scope for | ||
| // the query of the digests. | ||
| IEnumerable<string> repoNames = newImageArtifactDetails.Repos.Select(repo => repo.Repo) | ||
| .Union(oldImageArtifactDetails.Repos.Select(repo => repo.Repo)); | ||
| IEnumerable<(string Digest, string? Tag)> registryDigests = await GetAllDigestsFromRegistry(repoNames); | ||
|
|
||
| IEnumerable<string> supportedDigests = GetSupportedDigests(newImageArtifactDetails); | ||
| IEnumerable<EolDigestData> unsupportedDigests = GetUnsupportedDigests(registryDigests, supportedDigests); | ||
|
|
||
| // Annotate digests that are not already annotated for EOL | ||
| ConcurrentBag<EolDigestData> digetsToAnnotate = []; | ||
| Parallel.ForEach(unsupportedDigests, digest => | ||
| { | ||
| if (!_orasService.IsDigestAnnotatedForEol(digest.Digest, _loggerService, Options.IsDryRun)) | ||
| { | ||
| digetsToAnnotate.Add(digest); | ||
| } | ||
| }); | ||
|
|
||
| digestDataList.AddRange(digetsToAnnotate); | ||
|
|
||
| if (Options.AnnotateEolProducts) | ||
| { | ||
| // Annotate images for eol products in new image info | ||
| foreach (ImageData image in newImageArtifactDetails.Repos.SelectMany(repo => repo.Images)) | ||
| { | ||
| digestDataList.AddRange(GetProductEolDigests(image, productEolDates)); | ||
| } | ||
| } | ||
| } | ||
| catch (Exception e) | ||
| { | ||
| _loggerService.WriteError($"Error occurred while generating EOL annotation data: {e}"); | ||
| throw; | ||
| } | ||
|
|
||
| digestDataList = digestDataList.OrderBy(item => item.Digest).ToList(); | ||
|
|
||
| return digestDataList; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Finds all the digests that are in the registry but not in the supported digests list. | ||
| /// </summary> | ||
| private static IEnumerable<EolDigestData> GetUnsupportedDigests(IEnumerable<(string Digest, string? Tag)> registryDigests, IEnumerable<string> supportedDigests) => | ||
| registryDigests | ||
| .Where(registryDigest => !supportedDigests.Contains(registryDigest.Digest)) | ||
| .Select(registryDigest => new EolDigestData(registryDigest.Digest) { Tag = registryDigest.Tag }); | ||
|
|
||
| private static IEnumerable<string> GetSupportedDigests(ImageArtifactDetails newImageArtifactDetails) => | ||
| newImageArtifactDetails.Repos | ||
| .SelectMany(repo => repo.Images) | ||
| .SelectMany(GetImageDigests) | ||
| .Select(digest => digest.Digest); | ||
|
|
||
| private static IEnumerable<(string Digest, string? Tag)> GetImageDigests(ImageData image) | ||
| { | ||
| if (image.Manifest is not null) | ||
| { | ||
| yield return (image.Manifest.Digest, GetLongestTag(image.Manifest.SharedTags)); | ||
| } | ||
|
|
||
| foreach (PlatformData platform in image.Platforms) | ||
| { | ||
| yield return (platform.Digest, GetLongestTag(platform.SimpleTags)); | ||
| } | ||
| } | ||
|
|
||
| private static string? GetLongestTag(IEnumerable<string> tags) => | ||
| tags.OrderByDescending(tag => tag.Length).FirstOrDefault(); | ||
|
|
||
| private async Task<IEnumerable<(string Digest, string? Tag)>> GetAllDigestsFromRegistry(IEnumerable<string> repoNames) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have nothing to say besides I tried to speed up this method and I couldn't :) |
||
| { | ||
| IContainerRegistryClient acrClient = _acrClientFactory.Create(Options.RegistryName, _tokenCredentialProvider.GetCredential()); | ||
| IAsyncEnumerable<string> repositoryNames = acrClient.GetRepositoryNamesAsync(); | ||
|
|
||
| ConcurrentBag<(string Digest, string? Tag)> digests = []; | ||
| await foreach (string repositoryName in repositoryNames.Where(name => repoNames.Contains(name))) | ||
| { | ||
| ContainerRepository repo = acrClient.GetRepository(repositoryName); | ||
| IAsyncEnumerable<ArtifactManifestProperties> manifests = repo.GetAllManifestPropertiesAsync(); | ||
| await foreach (ArtifactManifestProperties manifestProps in manifests) | ||
| { | ||
| string imageName = DockerHelper.GetImageName(Options.RegistryName, repositoryName, digest: manifestProps.Digest); | ||
| digests.Add((imageName, GetLongestTag(manifestProps.Tags))); | ||
| } | ||
| } | ||
|
|
||
| return digests; | ||
| } | ||
|
|
||
| private static IEnumerable<EolDigestData> GetProductEolDigests(ImageData image, Dictionary<string, DateOnly> productEolDates) | ||
| { | ||
| if (image.ProductVersion == null) | ||
| { | ||
| return []; | ||
| } | ||
|
|
||
| string dotnetVersion = Version.Parse(image.ProductVersion).ToString(2); | ||
| if (!productEolDates.TryGetValue(dotnetVersion, out DateOnly date)) | ||
| { | ||
| return []; | ||
| } | ||
|
|
||
| return GetImageDigests(image).Select(val => new EolDigestData(val.Digest) { Tag = val.Tag, EolDate = date }); | ||
| } | ||
|
|
||
| private static ImageArtifactDetails LoadImageInfoData(string imageInfoPath) | ||
| { | ||
| string imageInfoJson = File.ReadAllText(imageInfoPath); | ||
| ImageArtifactDetails? imageArtifactDetails = JsonConvert.DeserializeObject<ImageArtifactDetails>(imageInfoJson); | ||
| return imageArtifactDetails is null | ||
| ? throw new JsonException($"Unable to correctly deserialize path '{imageInfoJson}'.") | ||
| : imageArtifactDetails; | ||
| } | ||
| } | ||
50 changes: 50 additions & 0 deletions
50
src/Microsoft.DotNet.ImageBuilder/src/Commands/GenerateEolAnnotationDataOptions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
| // See the LICENSE file in the project root for more information. | ||
|
|
||
| using System.Collections.Generic; | ||
| using System.CommandLine; | ||
| using System.Linq; | ||
| using static Microsoft.DotNet.ImageBuilder.Commands.CliHelper; | ||
|
|
||
| #nullable enable | ||
| namespace Microsoft.DotNet.ImageBuilder.Commands; | ||
|
|
||
| public class GenerateEolAnnotationDataOptions : Options | ||
| { | ||
| public string EolDigestsListPath { get; set; } = string.Empty; | ||
| public string OldImageInfoPath { get; set; } = string.Empty; | ||
| public string NewImageInfoPath { get; set; } = string.Empty; | ||
| public bool AnnotateEolProducts { get; set; } | ||
| public string RepoPrefix { get; set; } = string.Empty; | ||
| public string RegistryName { get; set; } = string.Empty; | ||
| } | ||
|
|
||
| public class GenerateEolAnnotationDataOptionsBuilder : CliOptionsBuilder | ||
| { | ||
| public override IEnumerable<Option> GetCliOptions() => | ||
| base.GetCliOptions() | ||
| .Concat( | ||
| [ | ||
| CreateOption<bool>("annotate-eol-products", nameof(GenerateEolAnnotationDataOptions.AnnotateEolProducts), | ||
| "Annotate images of EOL products"), | ||
| ] | ||
| ); | ||
|
|
||
| public override IEnumerable<Argument> GetCliArguments() => | ||
| base.GetCliArguments() | ||
| .Concat( | ||
| [ | ||
| new Argument<string>(nameof(GenerateEolAnnotationDataOptions.EolDigestsListPath), | ||
| "EOL annotations digests list output path"), | ||
| new Argument<string>(nameof(GenerateEolAnnotationDataOptions.OldImageInfoPath), | ||
| "Old image-info file"), | ||
| new Argument<string>(nameof(GenerateEolAnnotationDataOptions.NewImageInfoPath), | ||
| "New image-info file"), | ||
| new Argument<string>(nameof(GenerateEolAnnotationDataOptions.RepoPrefix), | ||
| "Prefix to add to the repo names specified in the manifest"), | ||
| new Argument<string>(nameof(GenerateEolAnnotationDataOptions.RegistryName), | ||
| "Name of the registry"), | ||
| ] | ||
| ); | ||
| } |
35 changes: 35 additions & 0 deletions
35
src/Microsoft.DotNet.ImageBuilder/src/DotNetReleasesService.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
| // See the LICENSE file in the project root for more information. | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.ComponentModel.Composition; | ||
| using System.Threading.Tasks; | ||
| using Microsoft.Deployment.DotNet.Releases; | ||
|
|
||
| #nullable enable | ||
| namespace Microsoft.DotNet.ImageBuilder | ||
| { | ||
| [Export(typeof(IDotNetReleasesService))] | ||
| public class DotNetReleasesService : IDotNetReleasesService | ||
| { | ||
| public async Task<Dictionary<string, DateOnly>> GetProductEolDatesFromReleasesJson() | ||
| { | ||
| Dictionary<string, DateOnly> productEolDates = []; | ||
|
|
||
| ProductCollection dotnetProducts = await ProductCollection.GetAsync(); | ||
|
|
||
| foreach (Product product in dotnetProducts) | ||
| { | ||
| if (product.EndOfLifeDate != null && | ||
| product.EndOfLifeDate <= DateTime.Today) | ||
| { | ||
| productEolDates.Add(product.ProductVersion, DateOnly.FromDateTime((DateTime)product.EndOfLifeDate)); | ||
| } | ||
| } | ||
|
|
||
| return productEolDates; | ||
| } | ||
| } | ||
| } |
17 changes: 17 additions & 0 deletions
17
src/Microsoft.DotNet.ImageBuilder/src/IDotNetReleasesService.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
| // See the LICENSE file in the project root for more information. | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Threading.Tasks; | ||
|
|
||
| #nullable enable | ||
| namespace Microsoft.DotNet.ImageBuilder | ||
| { | ||
| public interface IDotNetReleasesService | ||
| { | ||
| Task<Dictionary<string, DateOnly>> GetProductEolDatesFromReleasesJson(); | ||
| } | ||
| } | ||
| #nullable disable |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
10 changes: 10 additions & 0 deletions
10
src/Microsoft.DotNet.ImageBuilder/src/Models/Annotations/AcrEventEntry.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | ||
| // See the LICENSE file in the project root for more information. | ||
|
|
||
| using System; | ||
|
|
||
| #nullable enable | ||
| namespace Microsoft.DotNet.ImageBuilder.Models.Annotations; | ||
|
|
||
| public record AcrEventEntry(DateTime TimeGenerated, string Digest); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry if this has already been discussed. But in what case would we decide to build and ship an image for a product that is already EOL, and mark it as EOL immediately?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For .NET major version EOL. The last patch ships on the EOL date.