Skip to content

Add image/layer size to image-info and ingest in Kusto #1604

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
merged 10 commits into from
Apr 24, 2025
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,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// 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.

Expand Down Expand Up @@ -381,7 +381,7 @@ private void CopyPlatformDataFromCachedPlatform(PlatformData dstPlatform, Platfo
// When a cache hit occurs for a Dockerfile, we want to transfer some of the metadata about the previously
// published image so we don't need to recalculate it again.
dstPlatform.BaseImageDigest = srcPlatform.BaseImageDigest;
dstPlatform.Layers = new List<string>(srcPlatform.Layers);
dstPlatform.Layers = new List<Layer>(srcPlatform.Layers);
}

private RepoData CreateRepoData(RepoInfo repoInfo) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// 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.

Expand Down Expand Up @@ -99,9 +99,8 @@ public override async Task ExecuteAsync()

for (int i = 0; i < platform.Layers.Count; i++)
{
// TODO: Track layer size (currently set to 0) https://github.com/dotnet/docker-tools/issues/745
layerInfo.AppendLine(FormatLayerCsv(
platform.Layers[i], 0, platform.Layers.Count - i, sha, platform, image, repo.Repo, timestamp));
platform.Layers[i].Digest, platform.Layers[i].Size, platform.Layers.Count - i, sha, platform, image, repo.Repo, timestamp));
}
}
}
Expand All @@ -117,7 +116,7 @@ private static string FormatImageCsv(string imageId, PlatformData platform, Imag

private static string FormatLayerCsv(
string layerDigest,
int size,
long size,
int ordinal,
string imageDigest,
PlatformData platform,
Expand Down
7 changes: 5 additions & 2 deletions src/Microsoft.DotNet.ImageBuilder/src/IManifestService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Linq;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Microsoft.DotNet.ImageBuilder.Models.Image;

#nullable enable
namespace Microsoft.DotNet.ImageBuilder;
Expand All @@ -15,7 +16,7 @@ public interface IManifestService
{
Task<ManifestQueryResult> GetManifestAsync(string image, bool isDryRun);

public async Task<IEnumerable<string>> GetImageLayersAsync(string tag, bool isDryRun)
public async Task<IEnumerable<Layer>> GetImageLayersAsync(string tag, bool isDryRun)
{
if (isDryRun)
{
Expand All @@ -32,7 +33,9 @@ public async Task<IEnumerable<string>> GetImageLayersAsync(string tag, bool isDr
}

return ((JsonArray)manifestResult.Manifest["layers"]!)
.Select(layer => (layer!["digest"] ?? throw new InvalidOperationException("Expected digest property")).ToString())
.Select(layer => new Layer(
Digest: (layer!["digest"] ?? throw new InvalidOperationException("Expected digest property")).ToString(),
Size: (layer!["size"] ?? throw new InvalidOperationException("Expected size property")).GetValue<long>()))
.Reverse();
}

Expand Down
16 changes: 8 additions & 8 deletions src/Microsoft.DotNet.ImageBuilder/src/ImageInfoHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ public static ImageArtifactDetails DeserializeImageArtifactDetails(string path)
public static ImageArtifactDetails LoadFromContent(string imageInfoContent, ManifestInfo manifest,
bool skipManifestValidation = false, bool useFilteredManifest = false)
{
ImageArtifactDetails imageArtifactDetails = JsonConvert.DeserializeObject<ImageArtifactDetails>(imageInfoContent);
ImageArtifactDetails imageArtifactDetails = ImageArtifactDetails.FromJson(imageInfoContent);

foreach (RepoData repoData in imageArtifactDetails.Repos)
{
Expand Down Expand Up @@ -160,7 +160,7 @@ public static ImageArtifactDetails LoadFromContent(string imageInfoContent, Mani
{
imageData.ManifestImage = manifestImage;
}

platformData.PlatformInfo = matchingManifestPlatform;
platformData.ImageInfo = manifestImage;
break;
Expand Down Expand Up @@ -327,17 +327,17 @@ private static void MergeData(object srcObj, object targetObj, ImageInfoMergeOpt

ReplaceValue(property, srcObj, targetObj);
}
else if (srcObj is PlatformData && property.Name == nameof(PlatformData.Layers))
{
// Layers are always unique and should never get merged.
// Layers are already sorted according to their position in the image. They should not be sorted alphabetically.
ReplaceValue(property, srcObj, targetObj, skipListSorting: true);
}
else
{
MergeStringLists(property, srcObj, targetObj);
}
}
else if (typeof(IList<Layer>).IsAssignableFrom(property.PropertyType))
{
// Layers are always unique and should never get merged.
// Layers are already sorted according to their position in the image. They should not be sorted alphabetically.
ReplaceValue(property, srcObj, targetObj, skipListSorting: true);
}
else if (typeof(IList<ImageData>).IsAssignableFrom(property.PropertyType))
{
MergeLists<ImageData>(property, srcObj, targetObj, options);
Expand Down
14 changes: 7 additions & 7 deletions src/Microsoft.DotNet.ImageBuilder/src/JsonHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ namespace Microsoft.DotNet.ImageBuilder
{
public static class JsonHelper
{
public static string SerializeObject(object value)
public static JsonSerializerSettings JsonSerializerSettings => new()
{
JsonSerializerSettings settings = new()
{
ContractResolver = new CustomContractResolver(),
Formatting = Formatting.Indented
};
ContractResolver = new CustomContractResolver(),
Formatting = Formatting.Indented
};

return JsonConvert.SerializeObject(value, settings);
public static string SerializeObject(object value)
{
return JsonConvert.SerializeObject(value, JsonSerializerSettings);
}

private class CustomContractResolver : DefaultContractResolver
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,93 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable enable

using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Microsoft.DotNet.ImageBuilder.Models.Image;

namespace Microsoft.DotNet.ImageBuilder.Models.Image
public class ImageArtifactDetails
{
public class ImageArtifactDetails
private static readonly JsonSerializerSettings s_jsonSettings = new()
{
Converters =
[
new SchemaVersion2LayerConverter()
]
};

public string SchemaVersion => "2.0";

public List<RepoData> Repos { get; set; } = [];

public static ImageArtifactDetails FromJson(string json)
{
ImageArtifactDetails imageArtifactDetails =
JsonConvert.DeserializeObject<ImageArtifactDetails>(json, s_jsonSettings)
?? throw new SerializationException(
$"""
Failed to deserialize {nameof(ImageArtifactDetails)} from content:
{json}
""");

return imageArtifactDetails;
}

private class SchemaVersion2LayerConverter : JsonConverter
{
public string SchemaVersion
private static readonly JsonSerializer s_jsonSerializer =
JsonSerializer.Create(JsonHelper.JsonSerializerSettings);

// We do not want to handle writing at all. We only want to convert
// the old Layer format to the new format, and all writing should be
// done using the new format (via the default conversion settings).
public override bool CanWrite => false;

public override bool CanConvert(Type objectType)
{
get { return "1.0"; }
set { }
return objectType == typeof(Layer);
}

public List<RepoData> Repos { get; set; } = new List<RepoData>();
public override object? ReadJson(
JsonReader reader,
Type objectType,
object? existingValue,
JsonSerializer serializer)
{
JToken token = JToken.Load(reader);
return token.Type switch
{
// If the token is an object, proceed as normal.
// We can't use the JsonSerializer passed into the method since
// it contains this converter. Doing so would cause a stack
// overflow since this method would be called again recursively.
JTokenType.Object => token.ToObject<Layer>(s_jsonSerializer),

// If we encounter a string, we want to convert it to the Layer
// object defined in schema version 2. Assume a size of 0. The
// next time an image is built, the size will be updated.
JTokenType.String =>
new Layer(
Digest: token.Value<string>()
?? throw new JsonSerializationException(
$"Unable to serialize digest from '{token}'"),
Size: 0),

// Handle null and other token types
JTokenType.Null => null,
_ => throw new JsonSerializationException(
$"Unexpected token type: {token.Type} when parsing Layer.")
};
}

public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public PlatformData(ImageInfo imageInfo, PlatformInfo platformInfo)
[JsonProperty(Required = Required.Always)]
public string CommitUrl { get; set; } = string.Empty;

public List<string> Layers { get; set; } = new();
public List<Layer> Layers { get; set; } = new();

/// <summary>
/// Gets or sets whether the image or its associated tag names have changed since it was last published.
Expand Down Expand Up @@ -78,7 +78,7 @@ public int CompareTo([AllowNull] PlatformData other)
return 1;
}


if (HasDifferentTagState(other))
{
return 1;
Expand Down Expand Up @@ -134,5 +134,7 @@ private bool IsNullOrEmpty<T>(List<T>? list) =>
return new Version(fullVersion).ToString(2);
}
}

public record Layer(string Digest, long Size);
}
#nullable disable
27 changes: 20 additions & 7 deletions src/Microsoft.DotNet.ImageBuilder/tests/BuildCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,29 @@ public async Task BuildCommand_ImageInfoOutput_Basic()
const string runtimeDepsRepo = "runtime-deps";
const string runtimeRepo = "runtime";
const string aspnetRepo = "aspnet";

string runtimeDepsDigest = $"{runtimeDepsRepo}@sha256:c74364a9f125ca612f9a67e4a0551937b7a37c82fabb46172c4867b73edd638c";
string runtimeDigest = $"{runtimeRepo}@sha256:adc914a9f125ca612f9a67e4a0551937b7a37c82fabb46172c4867b73ed99227";
string aspnetDigest = $"{aspnetRepo}@sha256:781914a9f125ca612f9a67e4a0551937b7a37c82fabb46172c4867b73ed0045a";
IEnumerable<string> runtimeDepsLayers = new[] {
"sha256:777b2c648970480f50f5b4d0af8f9a8ea798eea43dbcf40ce4a8c7118736bdcf",
"sha256:b9dfc8eed8d66f1eae8ffe46be9a26fe047a7f6554e9dbc2df9da211e59b4786" };
IEnumerable<string> runtimeLayers =
runtimeDepsLayers.Concat(new[] { "sha256:466982335a8bacfe63b8f75a2e8c6484dfa7f7e92197550643b3c1457fa445b4" });
IEnumerable<string> aspnetLayers =
runtimeLayers.Concat(new[] { "sha256:d305fbfc4bd0d9f38662e979dced9831e3b5e4d85442397a8ec0a0e7bcf5458b" });

IEnumerable<Layer> runtimeDepsLayers =
[
new Layer("sha256:777b2c648970480f50f5b4d0af8f9a8ea798eea43dbcf40ce4a8c7118736bdcf", 0),
new Layer("sha256:b9dfc8eed8d66f1eae8ffe46be9a26fe047a7f6554e9dbc2df9da211e59b4786", 0)
];

IEnumerable<Layer> runtimeLayers =
[
..runtimeDepsLayers,
new Layer("sha256:466982335a8bacfe63b8f75a2e8c6484dfa7f7e92197550643b3c1457fa445b4", 0),
];

IEnumerable<Layer> aspnetLayers =
[
..runtimeLayers,
new Layer("sha256:d305fbfc4bd0d9f38662e979dced9831e3b5e4d85442397a8ec0a0e7bcf5458b", 0)
];

const string tag = "tag";
const string baseImageRepo = "baserepo";
string baseImageTag = $"{baseImageRepo}:basetag";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ public static PlatformData CreatePlatform(
List<string> simpleTags = null,
string baseImageDigest = null,
DateTime? created = null,
List<string> layers = null,
List<Layer> layers = null,
bool isUnchanged = false,
string commitUrl = "")
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using Microsoft.DotNet.ImageBuilder.Models.Image;
using Microsoft.DotNet.ImageBuilder.Tests.Helpers;
using Moq;

Expand All @@ -13,7 +14,7 @@ internal static class ManifestServiceHelper
{
public record ImageDigestResults(string Image, string Digest, int OnCallCount = 1);

public record ImageLayersResults(string Image, IEnumerable<string> Layers);
public record ImageLayersResults(string Image, IEnumerable<Layer> Layers);

public static Mock<IManifestServiceFactory> CreateManifestServiceFactoryMock(
IEnumerable<ImageDigestResults>? localImageDigestResults = null,
Expand Down Expand Up @@ -67,7 +68,7 @@ public static Mock<IManifestService> CreateManifestServiceMock(
.ReturnsAsync(callIndex => callIndex >= onCallIndex ? digest : throw new Exception());
}

foreach ((string image, IEnumerable<string> layers) in imageLayersResults)
foreach ((string image, IEnumerable<Layer> layers) in imageLayersResults)
{
manifestServiceMock
.Setup(o => o.GetImageLayersAsync(image, false))
Expand Down
Loading
Loading