diff --git a/Microsoft.NET.Build.Containers/ContainerHelpers.cs b/Microsoft.NET.Build.Containers/ContainerHelpers.cs
index fc593200..046e02fa 100644
--- a/Microsoft.NET.Build.Containers/ContainerHelpers.cs
+++ b/Microsoft.NET.Build.Containers/ContainerHelpers.cs
@@ -9,6 +9,11 @@ public static class ContainerHelpers
private static Regex imageNameRegex = new Regex("^[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*$");
+ ///
+ /// Matches if the string is not lowercase or numeric, or ., _, or -.
+ ///
+ private static Regex imageNameCharacters = new Regex("[^a-zA-Z0-9._-]");
+
///
/// Given some "fully qualified" image name (e.g. mcr.microsoft.com/dotnet/runtime), return
/// a valid UriBuilder. This means appending 'https' if the URI is not absolute, otherwise UriBuilder will throw.
@@ -39,8 +44,8 @@ public static bool IsValidRegistry(string registryName)
{
// No scheme prefixed onto the registry
if (string.IsNullOrEmpty(registryName) ||
- (!registryName.StartsWith("http://") &&
- !registryName.StartsWith("https://") &&
+ (!registryName.StartsWith("http://") &&
+ !registryName.StartsWith("https://") &&
!registryName.StartsWith("docker://")))
{
return false;
@@ -89,9 +94,9 @@ public static bool IsValidImageTag(string imageTag)
///
///
/// True if the parse was successful. When false is returned, all out vars are set to empty strings.
- public static bool TryParseFullyQualifiedContainerName(string fullyQualifiedContainerName,
- [NotNullWhen(true)] out string? containerRegistry,
- [NotNullWhen(true)] out string? containerName,
+ public static bool TryParseFullyQualifiedContainerName(string fullyQualifiedContainerName,
+ [NotNullWhen(true)] out string? containerRegistry,
+ [NotNullWhen(true)] out string? containerName,
[NotNullWhen(true)] out string? containerTag)
{
Uri? uri = ContainerImageToUri(fullyQualifiedContainerName);
@@ -115,4 +120,27 @@ public static bool TryParseFullyQualifiedContainerName(string fullyQualifiedCont
containerTag = indexOfColon == -1 ? "" : image.Substring(indexOfColon + 1);
return true;
}
+
+ ///
+ /// 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, [NotNullWhen(false)] out string? normalizedImageName)
+ {
+ if (IsValidImageName(containerImageName))
+ {
+ normalizedImageName = null;
+ return true;
+ }
+ else
+ {
+ if (Char.IsUpper(containerImageName, 0))
+ {
+ containerImageName = Char.ToLowerInvariant(containerImageName[0]) + containerImageName[1..];
+ } else if (!Char.IsLetterOrDigit(containerImageName, 0)) {
+ throw new ArgumentException("The first character of the image name must be a lowercase letter or a digit.");
+ }
+ normalizedImageName = imageNameCharacters.Replace(containerImageName, "-");
+ return false;
+ }
+ }
}
diff --git a/Microsoft.NET.Build.Containers/CreateNewImage.cs b/Microsoft.NET.Build.Containers/CreateNewImage.cs
index 42c625bf..718238b8 100644
--- a/Microsoft.NET.Build.Containers/CreateNewImage.cs
+++ b/Microsoft.NET.Build.Containers/CreateNewImage.cs
@@ -109,11 +109,20 @@ public override bool Execute()
Layer newLayer = Layer.FromDirectory(PublishDirectory, WorkingDirectory);
image.AddLayer(newLayer);
+ image.WorkingDirectory = WorkingDirectory;
image.SetEntrypoint(Entrypoint.Select(i => i.ItemSpec).ToArray(), EntrypointArgs.Select(i => i.ItemSpec).ToArray());
if (OutputRegistry.StartsWith("docker://"))
{
- LocalDocker.Load(image, ImageName, ImageTag, BaseImageName).Wait();
+ try
+ {
+ LocalDocker.Load(image, ImageName, ImageTag, BaseImageName).Wait();
+ }
+ catch (AggregateException ex) when (ex.InnerException is DockerLoadException dle)
+ {
+ Log.LogErrorFromException(dle, showStackTrace: false);
+ return !Log.HasLoggedErrors;
+ }
}
else
{
diff --git a/Microsoft.NET.Build.Containers/DockerLoadException.cs b/Microsoft.NET.Build.Containers/DockerLoadException.cs
new file mode 100644
index 00000000..fb08518a
--- /dev/null
+++ b/Microsoft.NET.Build.Containers/DockerLoadException.cs
@@ -0,0 +1,23 @@
+using System.Runtime.Serialization;
+
+namespace Microsoft.NET.Build.Containers
+{
+ public class DockerLoadException : Exception
+ {
+ public DockerLoadException()
+ {
+ }
+
+ public DockerLoadException(string? message) : base(message)
+ {
+ }
+
+ public DockerLoadException(string? message, Exception? innerException) : base(message, innerException)
+ {
+ }
+
+ protected DockerLoadException(SerializationInfo info, StreamingContext context) : base(info, context)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/Microsoft.NET.Build.Containers/Layer.cs b/Microsoft.NET.Build.Containers/Layer.cs
index 62cfe08c..3d35cb68 100644
--- a/Microsoft.NET.Build.Containers/Layer.cs
+++ b/Microsoft.NET.Build.Containers/Layer.cs
@@ -11,20 +11,14 @@ public record struct Layer
public static Layer FromDirectory(string directory, string containerPath)
{
- DirectoryInfo di = new(directory);
-
- IEnumerable<(string path, string containerPath)> fileList =
- di.GetFileSystemInfos()
- .Where(fsi => fsi is FileInfo).Select(
- fsi =>
- {
- string destinationPath =
- Path.Join(containerPath,
- Path.GetRelativePath(directory, fsi.FullName))
- .Replace(Path.DirectorySeparatorChar, '/');
- return (fsi.FullName, destinationPath);
- });
-
+ var fileList =
+ new DirectoryInfo(directory)
+ .EnumerateFiles("*", SearchOption.AllDirectories)
+ .Select(fsi =>
+ {
+ string destinationPath = Path.Join(containerPath, Path.GetRelativePath(directory, fsi.FullName)).Replace(Path.DirectorySeparatorChar, '/');
+ return (fsi.FullName, destinationPath);
+ });
return FromFiles(fileList);
}
diff --git a/Microsoft.NET.Build.Containers/LocalDocker.cs b/Microsoft.NET.Build.Containers/LocalDocker.cs
index a4d0c0fd..d2a4d47b 100644
--- a/Microsoft.NET.Build.Containers/LocalDocker.cs
+++ b/Microsoft.NET.Build.Containers/LocalDocker.cs
@@ -14,6 +14,7 @@ public static async Task Load(Image x, string name, string tag, string baseName)
ProcessStartInfo loadInfo = new("docker", $"load");
loadInfo.RedirectStandardInput = true;
loadInfo.RedirectStandardOutput = true;
+ loadInfo.RedirectStandardError = true;
using Process? loadProcess = Process.Start(loadInfo);
@@ -29,6 +30,11 @@ public static async Task Load(Image x, string name, string tag, string baseName)
loadProcess.StandardInput.Close();
await loadProcess.WaitForExitAsync();
+
+ if (loadProcess.ExitCode != 0)
+ {
+ throw new DockerLoadException($"Failed to load image to local Docker daemon. stdout: {await loadProcess.StandardError.ReadToEndAsync()}");
+ }
}
public static async Task WriteImageToStream(Image x, string name, string tag, Stream imageStream)
diff --git a/Microsoft.NET.Build.Containers/ParseContainerProperties.cs b/Microsoft.NET.Build.Containers/ParseContainerProperties.cs
index 49677333..47487e54 100644
--- a/Microsoft.NET.Build.Containers/ParseContainerProperties.cs
+++ b/Microsoft.NET.Build.Containers/ParseContainerProperties.cs
@@ -62,11 +62,6 @@ public ParseContainerProperties()
public override bool Execute()
{
- if (!ContainerHelpers.IsValidImageName(ContainerImageName))
- {
- Log.LogError($"Invalid {nameof(ContainerImageName)}: {0}", ContainerImageName);
- return !Log.HasLoggedErrors;
- }
if (!string.IsNullOrEmpty(ContainerImageTag) && !ContainerHelpers.IsValidImageTag(ContainerImageTag))
{
@@ -106,11 +101,29 @@ public override bool Execute()
return !Log.HasLoggedErrors;
}
+ try
+ {
+ if (!ContainerHelpers.NormalizeImageName(ContainerImageName, out string? normalizedImageName))
+ {
+ Log.LogWarning(null, "CONTAINER001", null, null, 0, 0, 0, 0, $"{nameof(ContainerImageName)} was not a valid container image name, it was normalized to {normalizedImageName}");
+ NewContainerImageName = normalizedImageName;
+ }
+ else
+ {
+ // name was valid already
+ NewContainerImageName = ContainerImageName;
+ }
+ }
+ catch (ArgumentException)
+ {
+ Log.LogError($"Invalid {nameof(ContainerImageName)}: {{0}}", ContainerImageName);
+ return !Log.HasLoggedErrors;
+ }
+
ParsedContainerRegistry = outputReg;
ParsedContainerImage = outputImage;
ParsedContainerTag = outputTag;
NewContainerRegistry = registryToUse;
- NewContainerImageName = ContainerImageName;
NewContainerTag = ContainerImageTag;
if (BuildEngine != null)
diff --git a/Test.Microsoft.NET.Build.Containers.Filesystem/TargetsTests.cs b/Test.Microsoft.NET.Build.Containers.Filesystem/TargetsTests.cs
index 455688e7..89412aab 100644
--- a/Test.Microsoft.NET.Build.Containers.Filesystem/TargetsTests.cs
+++ b/Test.Microsoft.NET.Build.Containers.Filesystem/TargetsTests.cs
@@ -76,12 +76,26 @@ public void CanSetEntrypointArgsToUseAppHost(bool useAppHost, params string[] en
["UseAppHost"] = useAppHost.ToString()
});
Assert.IsTrue(project.Build("ComputeContainerConfig"));
+ var computedEntrypointArgs = project.GetItems("ContainerEntrypoint").Select(i => i.EvaluatedInclude).ToArray();
+ foreach (var (First, Second) in entrypointArgs.Zip(computedEntrypointArgs))
{
- var computedEntrypointArgs = project.GetItems("ContainerEntrypoint").Select(i => i.EvaluatedInclude).ToArray();
- foreach (var (First, Second) in entrypointArgs.Zip(computedEntrypointArgs))
- {
- Assert.AreEqual(First, Second);
- }
+ Assert.AreEqual(First, Second);
}
}
+
+ [DataRow("WebApplication44", "webApplication44", true)]
+ [DataRow("friendly-suspicious-alligator", "friendly-suspicious-alligator", true)]
+ [DataRow("*friendly-suspicious-alligator", "", false)]
+ [DataRow("web/app2+7", "web-app2-7", true)]
+ [TestMethod]
+ public void CanNormalizeInputContainerNames(string projectName, string expectedContainerImageName, bool shouldPass)
+ {
+ var project = InitProject(new()
+ {
+ ["AssemblyName"] = projectName
+ });
+ var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
+ Assert.AreEqual(shouldPass, instance.Build(new[]{"ComputeContainerConfig"}, null, null, out var outputs), "Build should have succeeded");
+ Assert.AreEqual(expectedContainerImageName, instance.GetPropertyValue("ContainerImageName"));
+ }
}
\ No newline at end of file
diff --git a/version.json b/version.json
index db709d94..6b9c7322 100644
--- a/version.json
+++ b/version.json
@@ -1,6 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
- "version": "0.1-alpha.{height}",
+ "version": "0.2-alpha.{height}",
+ "versionHeightOffset": -1,
"nugetPackageVersion": {
"semVer": 2
},