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
22 changes: 19 additions & 3 deletions src/Cli/dotnet/commands/dotnet-msbuild/MSBuildLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,19 @@ public sealed class MSBuildLogger : INodeLogger
internal const string UseArtifactsOutputTelemetryPropertyKey = "UseArtifactsOutput";
internal const string ArtifactsPathLocationTypeTelemetryPropertyKey = "ArtifactsPathLocationType";

/// <summary>
/// This is defined in <see cref="ComputeDotnetBaseImageAndTag.cs"/>
/// </summary>
internal const string SdkContainerPublishBaseImageInferenceEventName = "sdk/container/inference";
/// <summary>
/// This is defined in <see cref="CreateNewImage.cs"/>
/// </summary>
internal const string SdkContainerPublishSuccessEventName = "sdk/container/publish/success";
/// <summary>
/// This is defined in <see cref="CreateNewImage.cs"/>
/// </summary>
internal const string SdkContainerPublishErrorEventName = "sdk/container/publish/error";

public MSBuildLogger()
{
try
Expand Down Expand Up @@ -130,7 +143,10 @@ internal static void FormatAndSend(ITelemetry telemetry, TelemetryEventArgs args
case PublishPropertiesTelemetryEventName:
case ReadyToRunTelemetryEventName:
case WorkloadPublishPropertiesTelemetryEventName:
TrackEvent(telemetry, args.EventName, args.Properties, Array.Empty<string>(), Array.Empty<string>() );
case SdkContainerPublishBaseImageInferenceEventName:
case SdkContainerPublishSuccessEventName:
case SdkContainerPublishErrorEventName:
TrackEvent(telemetry, args.EventName, args.Properties, Array.Empty<string>(), Array.Empty<string>());
break;
default:
// Ignore unknown events
Expand All @@ -147,7 +163,7 @@ private static void TrackEvent(ITelemetry telemetry, string eventName, IDictiona
{
if (eventProperties.TryGetValue(propertyToBeHashed, out string value))
{
// Lets lazy allocate in case there is tons of telemetry
// Lets lazy allocate in case there is tons of telemetry
properties ??= new Dictionary<string, string>(eventProperties);
properties[propertyToBeHashed] = Sha256Hasher.HashWithNormalizedCasing(value);
}
Expand All @@ -157,7 +173,7 @@ private static void TrackEvent(ITelemetry telemetry, string eventName, IDictiona
{
if (eventProperties.TryGetValue(propertyToBeMeasured, out string value))
{
// Lets lazy allocate in case there is tons of telemetry
// Lets lazy allocate in case there is tons of telemetry
properties ??= new Dictionary<string, string>(eventProperties);
properties.Remove(propertyToBeMeasured);
if (double.TryParse(value, CultureInfo.InvariantCulture, out double realValue))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,11 @@ namespace Microsoft.NET.Build.Containers;
public sealed class BaseImageNotFoundException : Exception
{
internal BaseImageNotFoundException(string specifiedRuntimeIdentifier, string repositoryName, string reference, IEnumerable<string> supportedRuntimeIdentifiers)
: base($"The RuntimeIdentifier '{specifiedRuntimeIdentifier}' is not supported by {repositoryName}:{reference}. The supported RuntimeIdentifiers are {String.Join(",", supportedRuntimeIdentifiers)}") {}
: base($"The RuntimeIdentifier '{specifiedRuntimeIdentifier}' is not supported by {repositoryName}:{reference}. The supported RuntimeIdentifiers are {String.Join(",", supportedRuntimeIdentifiers)}")
{
RequestedRuntimeIdentifier = specifiedRuntimeIdentifier;
AvailableRuntimeIdentifiers = supportedRuntimeIdentifiers;
}
internal string RequestedRuntimeIdentifier { get; }
internal IEnumerable<string> AvailableRuntimeIdentifiers { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
<Compile Include="KnownStrings.cs" />
<Compile Include="DockerLoadException.cs" />
<Compile Include="LocalDaemons/DockerCli.cs" />
<Compile Include="Registry/RegistryConstants.cs" />
<Compile Include="Tasks/ParseContainerProperties.cs" />
<Compile Include="Tasks/CreateNewImage.Interface.cs" />
<Compile Include="Tasks/CreateNewImageToolTask.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ Microsoft.NET.Build.Containers.Port.Port() -> void
Microsoft.NET.Build.Containers.Port.Port(int Number, Microsoft.NET.Build.Containers.PortType Type) -> void
Microsoft.NET.Build.Containers.Port.Type.get -> Microsoft.NET.Build.Containers.PortType
Microsoft.NET.Build.Containers.Port.Type.set -> void
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.ContainerFamily.get -> string?
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.UserBaseImage.get -> string?
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.UserBaseImage.set -> void
~override Microsoft.NET.Build.Containers.Port.ToString() -> string
static Microsoft.NET.Build.Containers.Port.operator !=(Microsoft.NET.Build.Containers.Port left, Microsoft.NET.Build.Containers.Port right) -> bool
static Microsoft.NET.Build.Containers.Port.operator ==(Microsoft.NET.Build.Containers.Port left, Microsoft.NET.Build.Containers.Port right) -> bool
Expand Down Expand Up @@ -126,7 +129,6 @@ Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.UsesInvariantG
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.UsesInvariantGlobalization.set -> void
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.ComputeDotnetBaseImageAndTag() -> void
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.ContainerFamily.get -> string!
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.ContainerFamily.set -> void
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.SdkVersion.get -> string!
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.SdkVersion.set -> void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const Microsoft.NET.Build.Containers.KnownLocalRegistryTypes.Podman = "Podman" -
Microsoft.NET.Build.Containers.BaseImageNotFoundException
Microsoft.NET.Build.Containers.Constants
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.ComputedContainerBaseImage.get -> string?
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.ContainerFamily.get -> string?
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.FrameworkReferences.get -> Microsoft.Build.Framework.ITaskItem![]!
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.FrameworkReferences.set -> void
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.IsAotPublished.get -> bool
Expand All @@ -13,6 +14,8 @@ Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.IsSelfContaine
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.IsSelfContained.set -> void
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.TargetRuntimeIdentifier.get -> string!
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.TargetRuntimeIdentifier.set -> void
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.UserBaseImage.get -> string?
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.UserBaseImage.set -> void
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.UsesInvariantGlobalization.get -> bool
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.UsesInvariantGlobalization.set -> void
static readonly Microsoft.NET.Build.Containers.Constants.Version -> string!
Expand Down Expand Up @@ -125,7 +128,6 @@ Microsoft.NET.Build.Containers.PortType.tcp = 0 -> Microsoft.NET.Build.Container
Microsoft.NET.Build.Containers.PortType.udp = 1 -> Microsoft.NET.Build.Containers.PortType
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.ComputeDotnetBaseImageAndTag() -> void
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.ContainerFamily.get -> string!
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.ContainerFamily.set -> void
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.SdkVersion.get -> string!
Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.SdkVersion.set -> void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public static Uri GetNextLocation(this HttpResponseMessage response)
internal static bool IsAmazonECRRegistry(this Uri uri)
{
// If this the registry is to public ECR the name will contain "public.ecr.aws".
if (uri.Authority.Contains("public.ecr.aws"))
if (uri.Authority.Contains(RegistryConstants.PublicAmazonElasticContainerRegistryDomain))
{
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,12 @@ private static string DeriveRegistryName(Uri baseUri)
/// <summary>
/// Check to see if the registry is GitHub Packages, which always uses ghcr.io.
/// </summary>
public bool IsGithubPackageRegistry => RegistryName.StartsWith("ghcr.io", StringComparison.Ordinal);
public bool IsGithubPackageRegistry => RegistryName.StartsWith(RegistryConstants.GitHubPackageRegistryDomain, StringComparison.Ordinal);

/// <summary>
/// Is this registry the public Microsoft Container Registry.
/// </summary>
public bool IsMcr => RegistryName.Equals(RegistryConstants.MicrosoftContainerRegistryDomain, StringComparison.Ordinal);

/// <summary>
/// Check to see if the registry is Docker Hub, which uses two well-known domains.
Expand All @@ -140,6 +145,8 @@ public bool IsGoogleArtifactRegistry
get => RegistryName.EndsWith("-docker.pkg.dev", StringComparison.Ordinal);
}

public bool IsAzureContainerRegistry => RegistryName.EndsWith(".azurecr.io", StringComparison.OrdinalIgnoreCase);

/// <summary>
/// Pushing to ECR uses a much larger chunk size. To avoid getting too many socket disconnects trying to do too many
/// parallel uploads be more conservative and upload one layer at a time.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.NET.Build.Containers;

internal static class RegistryConstants
{
public static string GitHubPackageRegistryDomain = "ghcr.io";
public static string MicrosoftContainerRegistryDomain = "mcr.microsoft.com";
public static string PublicAmazonElasticContainerRegistryDomain = "public.ecr.aws";
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,12 @@ public sealed class ComputeDotnetBaseImageAndTag : Microsoft.Build.Utilities.Tas
/// If set, this expresses a preference for a variant of the container image that we infer for a project.
/// e.g. 'alpine', or 'noble-chiseled'
/// </summary>
public string ContainerFamily { get; set; }
public string? ContainerFamily { get; set; }

/// <summary>
/// If set, the user has requested a specific base image - in this case we do nothing and echo it out
/// </summary>
public string? UserBaseImage { get; set; }

/// <summary>
/// The final base image computed from the inputs (or explicitly set by the user if IsUsingMicrosoftDefaultImages is true)
Expand All @@ -82,6 +87,8 @@ public sealed class ComputeDotnetBaseImageAndTag : Microsoft.Build.Utilities.Tas
private bool IsMuslRid => TargetRuntimeIdentifier.StartsWith("linux-musl", StringComparison.Ordinal);
private bool IsBundledRuntime => IsSelfContained;

private bool RequiresInference => String.IsNullOrEmpty(UserBaseImage);

// as of March 2024, the -extra images are on stable MCR, but the -aot images are still on nightly. This means AOT, invariant apps need the /nightly/ base.
private bool NeedsNightlyImages => IsAotPublished && UsesInvariantGlobalization;
private bool AllowsExperimentalTagInference => String.IsNullOrEmpty(ContainerFamily);
Expand All @@ -93,16 +100,27 @@ public ComputeDotnetBaseImageAndTag()
ContainerFamily = "";
FrameworkReferences = [];
TargetRuntimeIdentifier = "";
UserBaseImage = "";
}

public override bool Execute()
{
var defaultRegistry = "mcr.microsoft.com";
if (ComputeRepositoryAndTag(out var repository, out var tag))
if (!RequiresInference)
{
ComputedContainerBaseImage = $"{defaultRegistry}/{repository}:{tag}";
ComputedContainerBaseImage = UserBaseImage;
LogNoInferencePerformedTelemetry();
return true;
}
else
{
var defaultRegistry = RegistryConstants.MicrosoftContainerRegistryDomain;
if (ComputeRepositoryAndTag(out var repository, out var tag))
{
ComputedContainerBaseImage = $"{defaultRegistry}/{repository}:{tag}";
LogInferencePerformedTelemetry($"{defaultRegistry}/{repository}", tag!);
}
return !Log.HasLoggedErrors;
}
return !Log.HasLoggedErrors;
}

private string UbuntuCodenameForSDKVersion(SemanticVersion version)
Expand Down Expand Up @@ -152,7 +170,7 @@ private bool ComputeRepositoryAndTag([NotNullWhen(true)] out string? repository,
// in question, and the app is globalized, we can help and add -extra so the app will actually run

if (
(!IsMuslRid && ContainerFamily.EndsWith("-chiseled")) // default for linux RID
(!IsMuslRid && ContainerFamily!.EndsWith("-chiseled")) // default for linux RID
&& !UsesInvariantGlobalization
&& versionAllowsUsingAOTAndExtrasImages
// the extras only became available on the stable tags of the FirstVersionWithNewTaggingScheme
Expand Down Expand Up @@ -278,4 +296,82 @@ private bool ComputeRepositoryAndTag([NotNullWhen(true)] out string? repository,
return null;
};
}

private bool UserImageIsMicrosoftBaseImage => UserBaseImage?.StartsWith("mcr.microsoft.com/dotnet") ?? false;

private void LogNoInferencePerformedTelemetry()
{
// we should only log the base image, tag, containerFamily if we _know_ they are .NET's MCR images
string? userBaseImage = null;
string? userTag = null;
string? containerFamily = null;
if (UserBaseImage is not null && UserImageIsMicrosoftBaseImage)
{
if (ContainerHelpers.TryParseFullyQualifiedContainerName(UserBaseImage, out var containerRegistry, out var containerName, out var containerTag, out var _, out bool isRegistrySpecified))
{
userBaseImage = $"{containerRegistry}/{containerName}";
userTag = containerTag;
containerFamily = ContainerFamily;
}
}
var telemetryData = new InferenceTelemetryData(InferencePerformed: false, TargetFramework: ParseSemVerToMajorMinor(TargetFrameworkVersion), userBaseImage, userTag, containerFamily, GetTelemetryProjectType(), GetTelemetryPublishMode(), UsesInvariantGlobalization, TargetRuntimeIdentifier);
LogTelemetryData(telemetryData);
}

private void LogInferencePerformedTelemetry(string imageName, string tag)
{
// for all inference use cases we will use .NET's images, so we can safely log name, tag, and family
var telemetryData = new InferenceTelemetryData(InferencePerformed: true, TargetFramework: ParseSemVerToMajorMinor(TargetFrameworkVersion), imageName, tag, String.IsNullOrEmpty(ContainerFamily) ? null : ContainerFamily, GetTelemetryProjectType(), GetTelemetryPublishMode(), UsesInvariantGlobalization, TargetRuntimeIdentifier);
LogTelemetryData(telemetryData);
}

private PublishMode GetTelemetryPublishMode() => IsAotPublished ? PublishMode.Aot : IsTrimmed ? PublishMode.Trimmed : IsSelfContained ? PublishMode.SelfContained : PublishMode.FrameworkDependent;
private ProjectType GetTelemetryProjectType() => IsAspNetCoreProject ? ProjectType.AspNetCore : ProjectType.Console;

private string ParseSemVerToMajorMinor(string semver) => SemanticVersion.Parse(semver).ToString("x.y", VersionFormatter.Instance);

private void LogTelemetryData(InferenceTelemetryData telemetryData)
{
var telemetryProperties = new Dictionary<string, string?>
{
{ nameof(telemetryData.InferencePerformed), telemetryData.InferencePerformed.ToString() },
{ nameof(telemetryData.TargetFramework), telemetryData.TargetFramework },
{ nameof(telemetryData.BaseImage), telemetryData.BaseImage },
{ nameof(telemetryData.BaseImageTag), telemetryData.BaseImageTag },
{ nameof(telemetryData.ContainerFamily), telemetryData.ContainerFamily },
{ nameof(telemetryData.ProjectType), telemetryData.ProjectType.ToString() },
{ nameof(telemetryData.PublishMode), telemetryData.PublishMode.ToString() },
{ nameof(telemetryData.IsInvariant), telemetryData.IsInvariant.ToString() },
{ nameof(telemetryData.TargetRuntime), telemetryData.TargetRuntime }
};
Log.LogTelemetry("sdk/container/inference", telemetryProperties);
}


/// <summary>
/// Telemetry data for the inference task.
/// </summary>
/// <param name="InferencePerformed">If the user set an explicit base image or not.</param>
/// <param name="TargetFramework">The TFM the user was targeting</param>
/// <param name="BaseImage">If the user specified a Microsoft image or we inferred one, this will be the name of that image. Otherwise null so we can't leak customer data.</param>
/// <param name="BaseImageTag">If the user specified a Microsoft image or we inferred one, this will be the tag of that image. Otherwise null so we can't leak customer data.</param>
/// <param name="ContainerFamily">If the user specified a ContainerFamily for our images or we inserted one during inference this will be here. Otherwise null so we can't leak customer data.</param>
/// <param name="ProjectType">Classifies the project into categories - currently only the broad categories of web/console are known.</param>
/// <param name="PublishMode">Categorizes the publish mode of the app - FDD, SC, Trimmed, AOT in rough order of complexity/container customization</param>
/// <param name="IsInvariant">We make inference decisions on the invariant-ness of the project, so it's useful to track how often that is used.</param>
/// <param name="TargetRuntime">Different RIDs change the inference calculation, so it's useful to know how different RIDs flow into the results of inference.</param>
private record class InferenceTelemetryData(bool InferencePerformed, string TargetFramework, string? BaseImage, string? BaseImageTag, string? ContainerFamily, ProjectType ProjectType, PublishMode PublishMode, bool IsInvariant, string TargetRuntime);
private enum ProjectType
{
AspNetCore,
Console
}
private enum PublishMode
{
FrameworkDependent,
SelfContained,
Trimmed,
Aot
}

}
Loading