diff --git a/Directory.Packages.props b/Directory.Packages.props index 6c54427c..aa725ae3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,6 +8,7 @@ + diff --git a/Microsoft.NET.Build.Containers.UnitTests/ContainerHelpersTests.cs b/Microsoft.NET.Build.Containers.UnitTests/ContainerHelpersTests.cs index eddf49b5..fb2ae211 100644 --- a/Microsoft.NET.Build.Containers.UnitTests/ContainerHelpersTests.cs +++ b/Microsoft.NET.Build.Containers.UnitTests/ContainerHelpersTests.cs @@ -87,8 +87,8 @@ public void CanParsePort(string input, bool shouldParse, int? expectedPortNumber if (shouldParse) { Assert.NotNull(port); - Assert.Equal(port.number, expectedPortNumber); - Assert.Equal(port.type, expectedType); + Assert.Equal(port.Value.Number, expectedPortNumber); + Assert.Equal(port.Value.Type, expectedType); } else { Assert.Null(port); Assert.NotNull(errors); diff --git a/Microsoft.NET.Build.Containers/AmazonECRMessageHandler.cs b/Microsoft.NET.Build.Containers/AmazonECRMessageHandler.cs index f0b5f2a6..dc5b8076 100644 --- a/Microsoft.NET.Build.Containers/AmazonECRMessageHandler.cs +++ b/Microsoft.NET.Build.Containers/AmazonECRMessageHandler.cs @@ -11,7 +11,7 @@ namespace Microsoft.NET.Build.Containers; /// error message. The handler catches the generic error and provides a more informed error /// message to let the user know they need to create the repository. /// -public class AmazonECRMessageHandler : DelegatingHandler +internal sealed class AmazonECRMessageHandler : DelegatingHandler { public AmazonECRMessageHandler(HttpMessageHandler innerHandler) : base(innerHandler) { } diff --git a/Microsoft.NET.Build.Containers/AuthHandshakeMessageHandler.cs b/Microsoft.NET.Build.Containers/AuthHandshakeMessageHandler.cs index 7718606c..c9820fed 100644 --- a/Microsoft.NET.Build.Containers/AuthHandshakeMessageHandler.cs +++ b/Microsoft.NET.Build.Containers/AuthHandshakeMessageHandler.cs @@ -18,7 +18,7 @@ namespace Microsoft.NET.Build.Containers; /// /// A delegating handler that performs the Docker auth handshake as described in their docs if a request isn't authenticated /// -public partial class AuthHandshakeMessageHandler : DelegatingHandler +internal sealed partial class AuthHandshakeMessageHandler : DelegatingHandler { private const int MaxRequestRetries = 5; // Arbitrary but seems to work ok for chunked uploads to ghcr.io diff --git a/Microsoft.NET.Build.Containers/BaseImageNotFoundException.cs b/Microsoft.NET.Build.Containers/BaseImageNotFoundException.cs index eb65add2..6c717b15 100644 --- a/Microsoft.NET.Build.Containers/BaseImageNotFoundException.cs +++ b/Microsoft.NET.Build.Containers/BaseImageNotFoundException.cs @@ -3,8 +3,8 @@ namespace Microsoft.NET.Build.Containers; -public class BaseImageNotFoundException : Exception +public sealed class BaseImageNotFoundException : Exception { - public BaseImageNotFoundException(string specifiedRuntimeIdentifier, string repositoryName, string reference, IEnumerable supportedRuntimeIdentifiers) + internal BaseImageNotFoundException(string specifiedRuntimeIdentifier, string repositoryName, string reference, IEnumerable supportedRuntimeIdentifiers) : base($"The RuntimeIdentifier '{specifiedRuntimeIdentifier}' is not supported by {repositoryName}:{reference}. The supported RuntimeIdentifiers are {String.Join(",", supportedRuntimeIdentifiers)}") {} } diff --git a/Microsoft.NET.Build.Containers/ContainerBuilder.cs b/Microsoft.NET.Build.Containers/ContainerBuilder.cs index f0c0bcd5..a8b21058 100644 --- a/Microsoft.NET.Build.Containers/ContainerBuilder.cs +++ b/Microsoft.NET.Build.Containers/ContainerBuilder.cs @@ -1,22 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.NET.Build.Containers; - -using System; -using System.IO; using System.Text.Json; -using System.Threading.Tasks; + +namespace Microsoft.NET.Build.Containers; public static class ContainerBuilder { - private static LocalDocker GetLocalDaemon(string localDaemonType, Action logger) { - var daemon = localDaemonType switch { - KnownDaemonTypes.Docker => new LocalDocker(logger), - _ => throw new ArgumentException($"Unknown local container daemon type '{localDaemonType}'. Valid local container daemon types are {String.Join(",", KnownDaemonTypes.SupportedLocalDaemonTypes)}", nameof(localDaemonType)) - }; - return daemon; - } public static async Task Containerize(DirectoryInfo folder, string workingDir, string registryName, string baseName, string baseTag, string[] entrypoint, string[] entrypointArgs, string imageName, string[] imageTags, string? outputRegistry, string[] labels, Port[] exposedPorts, string[] envVars, string containerRuntimeIdentifier, string ridGraphPath, string localContainerDaemon) { var isDaemonPull = String.IsNullOrEmpty(registryName); @@ -104,4 +94,14 @@ public static async Task Containerize(DirectoryInfo folder, string workingDir, s } } } + + private static LocalDocker GetLocalDaemon(string localDaemonType, Action logger) + { + var daemon = localDaemonType switch + { + KnownDaemonTypes.Docker => new LocalDocker(logger), + _ => throw new ArgumentException($"Unknown local container daemon type '{localDaemonType}'. Valid local container daemon types are {String.Join(",", KnownDaemonTypes.SupportedLocalDaemonTypes)}", nameof(localDaemonType)) + }; + return daemon; + } } diff --git a/Microsoft.NET.Build.Containers/ContainerHelpers.cs b/Microsoft.NET.Build.Containers/ContainerHelpers.cs index a419df2d..8bcc8f91 100644 --- a/Microsoft.NET.Build.Containers/ContainerHelpers.cs +++ b/Microsoft.NET.Build.Containers/ContainerHelpers.cs @@ -1,31 +1,20 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.NET.Build.Containers; - +#if NETFRAMEWORK using System; +#endif +#if NET using System.Diagnostics.CodeAnalysis; +#endif using System.Text.RegularExpressions; -using static ReferenceParser; - -internal sealed record Label(string name, string value); - -// Explicitly lowercase to ease parsing - the incoming values are -// lowercased by spec -public enum PortType -{ - tcp, - udp -} - -public record Port(int number, PortType type); - +namespace Microsoft.NET.Build.Containers; public static class ContainerHelpers { - public const string HostObjectUser = "SDK_CONTAINER_REGISTRY_UNAME"; - - public const string HostObjectPass = "SDK_CONTAINER_REGISTRY_PWORD"; + internal const string HostObjectUser = "SDK_CONTAINER_REGISTRY_UNAME"; + + internal const string HostObjectPass = "SDK_CONTAINER_REGISTRY_PWORD"; /// /// Matches an environment variable name - must start with a letter or underscore, and can only contain letters, numbers, and underscores. @@ -37,7 +26,7 @@ public static class ContainerHelpers /// that have no registry component. /// See normalize.go. /// - public const string DefaultRegistry = "docker.io"; + internal const string DefaultRegistry = "docker.io"; /// /// Matches if the string is not lowercase or numeric, or ., _, or -. @@ -45,12 +34,101 @@ public static class ContainerHelpers /// Technically the period should be allowed as well, but due to inconsistent support between cloud providers we're removing it. private static Regex imageNameCharacters = new Regex(@"[^a-z0-9_\-/]"); + + /// + /// The enum contains possible error reasons during port parsing using or . + /// + [Flags] + public enum ParsePortError + { + MissingPortNumber, + InvalidPortNumber, + InvalidPortType, + UnknownPortFormat + } + + /// + /// Tries to parse the port from and . + /// + /// The port number to parse. + /// The port type to parse (tcp or udp). + /// Parsed port. + /// The error occurred during parsing. Only returned when method returns . + /// when port was successfully parsed, otherwise. + public static bool TryParsePort(string? portNumber, string? portType, [NotNullWhen(true)] out Port? port, [NotNullWhen(false)] out ParsePortError? error) + { + var portNo = 0; + error = null; + if (String.IsNullOrEmpty(portNumber)) + { + error = ParsePortError.MissingPortNumber; + } + else if (!int.TryParse(portNumber, out portNo)) + { + error = ParsePortError.InvalidPortNumber; + } + + if (!Enum.TryParse(portType, out PortType t)) + { + if (portType is not null) + { + error = (error ?? ParsePortError.InvalidPortType) | ParsePortError.InvalidPortType; + } + else + { + t = PortType.tcp; + } + } + + if (error is null) + { + port = new Port(portNo, t); + return true; + } + else + { + port = null; + return false; + } + + } + + /// + /// Tries to parse the port from . + /// + /// The port number to parse. Expected formats are: port number as int value, or value in format 'port number/port type' where + /// port type can be tcp or udp. If the port type is not present, it is assumed to be tcp. + /// Parsed port. + /// The error occurred during parsing. Only returned when method returns . + /// when port was successfully parsed, otherwise. + public static bool TryParsePort(string input, [NotNullWhen(true)] out Port? port, [NotNullWhen(false)] out ParsePortError? error) + { + var parts = input.Split('/'); + if (parts.Length == 2) + { + string portNumber = parts[0]; + string type = parts[1]; + return TryParsePort(portNumber, type, out port, out error); + } + else if (parts.Length == 1) + { + string portNum = parts[0]; + return TryParsePort(portNum, null, out port, out error); + } + else + { + error = ParsePortError.UnknownPortFormat; + port = null; + return false; + } + } + /// /// Ensures the given registry is valid. /// /// /// - public static bool IsValidRegistry(string registryName) => AnchoredDomainRegexp.IsMatch(registryName); + internal static bool IsValidRegistry(string registryName) => ReferenceParser.AnchoredDomainRegexp.IsMatch(registryName); /// /// Ensures the given image name is valid. @@ -58,9 +136,9 @@ public static class ContainerHelpers /// /// /// - public static bool IsValidImageName(string imageName) + internal static bool IsValidImageName(string imageName) { - return anchoredNameRegexp.IsMatch(imageName); + return ReferenceParser.anchoredNameRegexp.IsMatch(imageName); } /// @@ -69,16 +147,16 @@ public static bool IsValidImageName(string imageName) /// /// /// - public static bool IsValidImageTag(string imageTag) + internal static bool IsValidImageTag(string imageTag) { - return anchoredTagRegexp.IsMatch(imageTag); + return ReferenceParser.anchoredTagRegexp.IsMatch(imageTag); } /// /// Given an already-validated registry domain, this is our hueristic to determine what HTTP protocol should be used to interact with it. /// This is primarily for testing - in the real world almost all usage should be through HTTPS! /// - public static Uri TryExpandRegistryToUri(string alreadyValidatedDomain) + internal static Uri TryExpandRegistryToUri(string alreadyValidatedDomain) { var prefix = alreadyValidatedDomain.StartsWith("localhost", StringComparison.Ordinal) ? "http" : "https"; return new Uri($"{prefix}://{alreadyValidatedDomain}"); @@ -89,7 +167,7 @@ public static Uri TryExpandRegistryToUri(string alreadyValidatedDomain) /// /// /// - public static bool IsValidEnvironmentVariable(string envVar) + internal static bool IsValidEnvironmentVariable(string envVar) { return envVarRegex.IsMatch(envVar); } @@ -109,7 +187,7 @@ public static bool IsValidEnvironmentVariable(string envVar) /// /// /// True if the parse was successful. When false is returned, all out vars are set to empty strings. - public static bool TryParseFullyQualifiedContainerName(string fullyQualifiedContainerName, + internal static bool TryParseFullyQualifiedContainerName(string fullyQualifiedContainerName, [NotNullWhen(true)] out string? containerRegistry, [NotNullWhen(true)] out string? containerName, out string? containerTag, // tag is always optional - we can't guarantee anything here @@ -118,7 +196,7 @@ out string? containerDigest // digest is always optional - we can't guarantee an { /// if we don't have a reference at all, bail out - var referenceMatch = ReferenceRegexp.Match(fullyQualifiedContainerName); + var referenceMatch = ReferenceParser.ReferenceRegexp.Match(fullyQualifiedContainerName); if (referenceMatch is not { Success: true }) { containerRegistry = null; @@ -136,7 +214,7 @@ out string? containerDigest // digest is always optional - we can't guarantee an // this will always be successful if the ReferenceRegexp matched, so it's safe to index into. var namePortion = referenceMatch.Groups[1].Value; // we try to decompose the reference name into registry and image name parts. - var nameMatch = anchoredNameRegexp.Match(namePortion); + var nameMatch = ReferenceParser.anchoredNameRegexp.Match(namePortion); if (nameMatch is { Success: true }) { // the name regex has two groups: @@ -183,7 +261,7 @@ out string? containerDigest // digest is always optional - we can't guarantee an /// /// Checks if a given container image name adheres to the image name spec. If not, and recoverable, then normalizes invalid characters. /// - public static bool NormalizeImageName(string containerImageName, + internal static bool NormalizeImageName(string containerImageName, [NotNullWhen(false)] out string? normalizedImageName) { if (IsValidImageName(containerImageName)) @@ -202,73 +280,4 @@ public static bool NormalizeImageName(string containerImageName, return false; } } - - [Flags] - public enum ParsePortError - { - MissingPortNumber, - InvalidPortNumber, - InvalidPortType, - UnknownPortFormat - } - - public static bool TryParsePort(string? portNumber, string? portType, [NotNullWhen(true)] out Port? port, [NotNullWhen(false)] out ParsePortError? error) - { - var portNo = 0; - error = null; - if (String.IsNullOrEmpty(portNumber)) - { - error = ParsePortError.MissingPortNumber; - } - else if (!int.TryParse(portNumber, out portNo)) - { - error = ParsePortError.InvalidPortNumber; - } - - if (!Enum.TryParse(portType, out PortType t)) - { - if (portType is not null) - { - error = (error ?? ParsePortError.InvalidPortType) | ParsePortError.InvalidPortType; - } - else - { - t = PortType.tcp; - } - } - - if (error is null) - { - port = new Port(portNo, t); - return true; - } - else - { - port = null; - return false; - } - - } - - public static bool TryParsePort(string input, [NotNullWhen(true)] out Port? port, [NotNullWhen(false)] out ParsePortError? error) - { - var parts = input.Split('/'); - if (parts.Length == 2) - { - string portNumber = parts[0]; - string type = parts[1]; - return TryParsePort(portNumber, type, out port, out error); - } - else if (parts.Length == 1) - { - string portNum = parts[0]; - return TryParsePort(portNum, null, out port, out error); - } - else - { - error = ParsePortError.UnknownPortFormat; - port = null; - return false; - } - } } diff --git a/Microsoft.NET.Build.Containers/ContainerHttpException.cs b/Microsoft.NET.Build.Containers/ContainerHttpException.cs index a98d3472..4bcb5a9f 100644 --- a/Microsoft.NET.Build.Containers/ContainerHttpException.cs +++ b/Microsoft.NET.Build.Containers/ContainerHttpException.cs @@ -2,7 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. namespace Microsoft.NET.Build.Containers; -public class ContainerHttpException : Exception + +internal sealed class ContainerHttpException : Exception { private const string ErrorPrefix = "Containerize: error CONTAINER004:"; string? jsonResponse; @@ -13,4 +14,4 @@ public ContainerHttpException(string message, string? targetUri, string? jsonRes jsonResponse = jsonResp; uri = targetUri; } -} \ No newline at end of file +} diff --git a/Microsoft.NET.Build.Containers/ContentStore.cs b/Microsoft.NET.Build.Containers/ContentStore.cs index 74895818..7b8b0c98 100644 --- a/Microsoft.NET.Build.Containers/ContentStore.cs +++ b/Microsoft.NET.Build.Containers/ContentStore.cs @@ -5,7 +5,7 @@ namespace Microsoft.NET.Build.Containers; -public static class ContentStore +internal static class ContentStore { public static string ArtifactRoot { get; set; } = Path.Combine(Path.GetTempPath(), "Containers"); public static string ContentRoot diff --git a/Microsoft.NET.Build.Containers/Credentials/CredentialRetrievalException.cs b/Microsoft.NET.Build.Containers/Credentials/CredentialRetrievalException.cs index a27251a6..eef65050 100644 --- a/Microsoft.NET.Build.Containers/Credentials/CredentialRetrievalException.cs +++ b/Microsoft.NET.Build.Containers/Credentials/CredentialRetrievalException.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; - namespace Microsoft.NET.Build.Containers.Credentials; internal sealed class CredentialRetrievalException : Exception diff --git a/Microsoft.NET.Build.Containers/DockerLoadException.cs b/Microsoft.NET.Build.Containers/DockerLoadException.cs index eba2f1f1..b0269aae 100644 --- a/Microsoft.NET.Build.Containers/DockerLoadException.cs +++ b/Microsoft.NET.Build.Containers/DockerLoadException.cs @@ -1,11 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Runtime.Serialization; namespace Microsoft.NET.Build.Containers; -public class DockerLoadException : Exception +internal sealed class DockerLoadException : Exception { public DockerLoadException() { @@ -18,8 +17,4 @@ public DockerLoadException(string? message) : base(message) public DockerLoadException(string? message, Exception? innerException) : base(message, innerException) { } - - protected DockerLoadException(SerializationInfo info, StreamingContext context) : base(info, context) - { - } } diff --git a/Microsoft.NET.Build.Containers/Globals.cs b/Microsoft.NET.Build.Containers/Globals.cs new file mode 100644 index 00000000..1d566348 --- /dev/null +++ b/Microsoft.NET.Build.Containers/Globals.cs @@ -0,0 +1,7 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.NET.Build.Containers.UnitTests")] +[assembly: InternalsVisibleTo("Microsoft.NET.Build.Containers.IntegrationTests")] diff --git a/Microsoft.NET.Build.Containers/Image.cs b/Microsoft.NET.Build.Containers/Image.cs index 71740fa4..f27350be 100644 --- a/Microsoft.NET.Build.Containers/Image.cs +++ b/Microsoft.NET.Build.Containers/Image.cs @@ -8,7 +8,7 @@ namespace Microsoft.NET.Build.Containers; -public class Image +internal class Image { private readonly HashSet