diff --git a/Microsoft.NET.Build.Containers/ContainerHelpers.cs b/Microsoft.NET.Build.Containers/ContainerHelpers.cs index cab9075c..8156bf65 100644 --- a/Microsoft.NET.Build.Containers/ContainerHelpers.cs +++ b/Microsoft.NET.Build.Containers/ContainerHelpers.cs @@ -142,18 +142,63 @@ public static bool NormalizeImageName(string containerImageName, [NotNullWhen(fa } } - public static bool TryParsePort(string input, [NotNullWhen(true)] out Port? port) + [Flags] + public enum ParsePortError + { + MissingPortNumber, + InvalidPortNumber, + InvalidPortType, + UnknownPortFormat + } + + public record ParsePortResult(bool success, Port? port, ParsePortError? parseErrors); + + public static ParsePortResult ParsePort(string? portNumber, string? portType) + { + ParsePortError? errors = null; + int port = 0; + if (String.IsNullOrEmpty(portNumber)) + { + errors = ParsePortError.MissingPortNumber; + } + else if (!int.TryParse(portNumber, out port)) + { + errors = ParsePortError.InvalidPortNumber; + } + + if (!Enum.TryParse(portType, out PortType t)) + { + if (portType is not null) + { + errors = (errors ?? ParsePortError.InvalidPortType) | ParsePortError.InvalidPortType; + } + else + { + t = PortType.tcp; + } + } + + if (errors is null) + { + return new(true, new(port, t), errors); + } + else + { + return new(false, null, errors); + } + + } + + public static ParsePortResult TryParsePort(string input) { var parts = input.Split('/'); - Port? p = + ParsePortResult p = parts switch { - [var portNumber, var type] when int.TryParse(portNumber, out var portInt) - && Enum.TryParse(type, out var portType) => new(portInt, portType), - [var portNumber] when int.TryParse(portNumber, out var portInt) => new(portInt, PortType.tcp), - _ => null + [var portNumber, var type] => ParsePort(portNumber, type), + [var portNumber] => ParsePort(portNumber, null), + _ => new(false, null, ParsePortError.UnknownPortFormat) }; - port = p; - return p != null; + return p; } } diff --git a/Microsoft.NET.Build.Containers/CreateNewImage.cs b/Microsoft.NET.Build.Containers/CreateNewImage.cs index 0750eb57..db5bedd7 100644 --- a/Microsoft.NET.Build.Containers/CreateNewImage.cs +++ b/Microsoft.NET.Build.Containers/CreateNewImage.cs @@ -78,6 +78,8 @@ public class CreateNewImage : Microsoft.Build.Utilities.Task /// public ITaskItem[] Labels { get; set; } + private bool IsDockerPush { get => OutputRegistry == "docker://"; } + public CreateNewImage() { BaseRegistry = ""; @@ -94,6 +96,53 @@ public CreateNewImage() ExposedPorts = Array.Empty(); } + private void SetPorts(Image image, ITaskItem[] exposedPorts) + { + foreach (var port in exposedPorts) + { + var portNo = port.ItemSpec; + var portTy = port.GetMetadata("Type"); + var parsePortResult = ContainerHelpers.ParsePort(portNo, portTy); + if (!parsePortResult.success) + { + ContainerHelpers.ParsePortError errors = (ContainerHelpers.ParsePortError)parsePortResult.parseErrors!; + var portString = portTy == null ? portNo : $"{portNo}/{portTy}"; + if (errors.HasFlag(ContainerHelpers.ParsePortError.MissingPortNumber)) + { + Log.LogError("A ContainerPort item was provided without an Include metadata specifying the port number. Please provide a ContainerPort item with an ItemSpec: "); + } + else + { + var message = "A ContainerPort item was provided with "; + var arguments = new List(2); + if (errors.HasFlag(ContainerHelpers.ParsePortError.InvalidPortNumber) && errors.HasFlag(ContainerHelpers.ParsePortError.InvalidPortNumber)) + { + message += "an invalid port number '{0}' and an invalid port type '{1}'"; + arguments.Add(portNo); + arguments.Add(portTy!); + } + else if (errors.HasFlag(ContainerHelpers.ParsePortError.InvalidPortNumber)) + { + message += "an invalid port number '{0}'"; + arguments.Add(portNo); + } + else if (errors.HasFlag(ContainerHelpers.ParsePortError.InvalidPortNumber)) + { + message += "an invalid port type '{0}'"; + arguments.Add(portTy!); + } + message += ". ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'"; + + Log.LogError(message, arguments); + } + } + else + { + image.ExposePort(parsePortResult.port!.number, parsePortResult.port.type); + } + } + + } public override bool Execute() { @@ -131,17 +180,15 @@ public override bool Execute() image.Label(label.ItemSpec, label.GetMetadata("Value")); } - foreach (var port in ExposedPorts) + SetPorts(image, ExposedPorts); + + // at the end of this step, if any failed then bail out. + if (Log.HasLoggedErrors) { - if (int.TryParse(port.ItemSpec, out int portNumber) - && port.GetMetadata("Type") is { } portType - && Enum.TryParse(portType, out var parsedPortType)) - { - image.ExposePort(portNumber, parsedPortType); - } + return false; } - if (OutputRegistry.StartsWith("docker://")) + if (IsDockerPush) { try { @@ -172,7 +219,14 @@ public override bool Execute() if (BuildEngine != null) { - Log.LogMessage(MessageImportance.High, "Pushed container '{0}:{1}' to registry '{2}'", ImageName, ImageTag, OutputRegistry); + if (IsDockerPush) + { + Log.LogMessage(MessageImportance.High, "Pushed container '{0}:{1}' to local Docker daemon", ImageName, ImageTag); + } + else + { + Log.LogMessage(MessageImportance.High, "Pushed container '{0}:{1}' to registry '{2}'", ImageName, ImageTag, OutputRegistry); + } } return !Log.HasLoggedErrors; diff --git a/Microsoft.NET.Build.Containers/Image.cs b/Microsoft.NET.Build.Containers/Image.cs index 07777713..4bff6208 100644 --- a/Microsoft.NET.Build.Containers/Image.cs +++ b/Microsoft.NET.Build.Containers/Image.cs @@ -125,9 +125,9 @@ private static HashSet ReadPortsFromConfig(JsonNode inputConfig) { if (property.Key is { } propertyName && property.Value is JsonObject propertyValue - && ContainerHelpers.TryParsePort(propertyName, out var port)) + && ContainerHelpers.TryParsePort(propertyName) is { success: true } result) { - ports.Add(port); + ports.Add(result.port!); } } return ports; diff --git a/Test.Microsoft.NET.Build.Containers/ContainerHelpersTests.cs b/Test.Microsoft.NET.Build.Containers/ContainerHelpersTests.cs index 6e7ab430..21a99c8f 100644 --- a/Test.Microsoft.NET.Build.Containers/ContainerHelpersTests.cs +++ b/Test.Microsoft.NET.Build.Containers/ContainerHelpersTests.cs @@ -77,21 +77,25 @@ public void IsValidImageTag_InvalidLength() } [TestMethod] - [DataRow("80/tcp", true, 80, PortType.tcp)] - [DataRow("80", true, 80, PortType.tcp)] - [DataRow("125/dup", false, 125, PortType.tcp)] - [DataRow("invalidNumber", false, null, null)] - [DataRow("80/unknowntype", false, null, null)] - public void CanParsePort(string input, bool shouldParse, int? expectedPortNumber, PortType? expectedType) { - var parseSuccess = ContainerHelpers.TryParsePort(input, out var parsedPort); - Assert.AreEqual(shouldParse, parseSuccess, $"Should have parsed {input} into a port"); + [DataRow("80/tcp", true, 80, PortType.tcp, null)] + [DataRow("80", true, 80, PortType.tcp, null)] + [DataRow("125/dup", false, 125, PortType.tcp, ContainerHelpers.ParsePortError.InvalidPortType)] + [DataRow("invalidNumber", false, null, null, ContainerHelpers.ParsePortError.InvalidPortNumber)] + [DataRow("welp/unknowntype", false, null, null, (ContainerHelpers.ParsePortError)3)] + [DataRow("a/b/c", false, null, null, ContainerHelpers.ParsePortError.UnknownPortFormat)] + [DataRow("/tcp", false, null, null, ContainerHelpers.ParsePortError.MissingPortNumber)] + public void CanParsePort(string input, bool shouldParse, int? expectedPortNumber, PortType? expectedType, ContainerHelpers.ParsePortError? expectedError) { + var parseSuccess = ContainerHelpers.TryParsePort(input); + Assert.AreEqual(shouldParse, parseSuccess.success, $"{(shouldParse ? "Should" : "Shouldn't")} have parsed {input} into a port"); if (!shouldParse) { - Assert.IsNull(parsedPort); + Assert.IsNull(parseSuccess.port); + Assert.IsNotNull(parseSuccess.parseErrors); + Assert.AreEqual(expectedError, parseSuccess.parseErrors); } if (shouldParse) { - Assert.IsNotNull(parsedPort); - Assert.AreEqual(parsedPort.number, expectedPortNumber); - Assert.AreEqual(parsedPort.type, expectedType); + Assert.IsNotNull(parseSuccess.port); + Assert.AreEqual(parseSuccess.port.number, expectedPortNumber); + Assert.AreEqual(parseSuccess.port.type, expectedType); } } }