Skip to content

Commit f2a478e

Browse files
authored
Merge pull request #138 from dotnet/multiple-tags
Support building multiple tags at once
2 parents c0ea823 + 3a1f601 commit f2a478e

File tree

6 files changed

+141
-55
lines changed

6 files changed

+141
-55
lines changed

Microsoft.NET.Build.Containers/CreateNewImage.cs

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public class CreateNewImage : Microsoft.Build.Utilities.Task
4040
/// <summary>
4141
/// The tag to associate with the new image.
4242
/// </summary>
43-
public string ImageTag { get; set; }
43+
public string[] ImageTags { get; set; }
4444

4545
/// <summary>
4646
/// The directory for the build outputs to be published.
@@ -78,7 +78,7 @@ public CreateNewImage()
7878
BaseImageTag = "";
7979
OutputRegistry = "";
8080
ImageName = "";
81-
ImageTag = "";
81+
ImageTags = Array.Empty<string>();
8282
PublishDirectory = "";
8383
WorkingDirectory = "";
8484
Entrypoint = Array.Empty<ITaskItem>();
@@ -112,7 +112,7 @@ public override bool Execute()
112112
{
113113
Log.LogMessage($"Loading from directory: {PublishDirectory}");
114114
}
115-
115+
116116
Layer newLayer = Layer.FromDirectory(PublishDirectory, WorkingDirectory);
117117
image.AddLayer(newLayer);
118118
image.WorkingDirectory = WorkingDirectory;
@@ -122,39 +122,45 @@ public override bool Execute()
122122
{
123123
image.Label(label.ItemSpec, label.GetMetadata("Value"));
124124
}
125-
126-
if (OutputRegistry.StartsWith("docker://"))
127-
{
128-
try
129-
{
130-
LocalDocker.Load(image, ImageName, ImageTag, BaseImageName).Wait();
131-
}
132-
catch (AggregateException ex) when (ex.InnerException is DockerLoadException dle)
133-
{
134-
Log.LogErrorFromException(dle, showStackTrace: false);
135-
return !Log.HasLoggedErrors;
136-
}
137-
}
138-
else
125+
126+
var isDockerPush = OutputRegistry.StartsWith("docker://");
127+
Registry? outputReg = isDockerPush ? null : new Registry(new Uri(OutputRegistry));
128+
foreach (var tag in ImageTags)
139129
{
140-
Registry outputReg = new Registry(new Uri(OutputRegistry));
141-
try
130+
if (isDockerPush)
142131
{
143-
outputReg.Push(image, ImageName, ImageTag, BaseImageName).Wait();
132+
try
133+
{
134+
LocalDocker.Load(image, ImageName, tag, BaseImageName).Wait();
135+
if (BuildEngine != null)
136+
{
137+
Log.LogMessage(MessageImportance.High, "Pushed container '{0}:{1}' to Docker daemon", ImageName, tag);
138+
}
139+
}
140+
catch (AggregateException ex) when (ex.InnerException is DockerLoadException dle)
141+
{
142+
Log.LogErrorFromException(dle, showStackTrace: false);
143+
}
144144
}
145-
catch (Exception e)
145+
else
146146
{
147-
if (BuildEngine != null)
147+
try
148+
{
149+
outputReg?.Push(image, ImageName, tag, BaseImageName).Wait();
150+
if (BuildEngine != null)
151+
{
152+
Log.LogMessage(MessageImportance.High, "Pushed container '{0}:{1}' to registry '{2}'", ImageName, tag, OutputRegistry);
153+
}
154+
}
155+
catch (Exception e)
148156
{
149-
Log.LogError("Failed to push to the output registry: {0}", e);
157+
if (BuildEngine != null)
158+
{
159+
Log.LogError("Failed to push to the output registry: {0}", e);
160+
}
150161
}
151-
return !Log.HasLoggedErrors;
152162
}
153-
}
154163

155-
if (BuildEngine != null)
156-
{
157-
Log.LogMessage(MessageImportance.High, "Pushed container '{0}:{1}' to registry '{2}'", ImageName, ImageTag, OutputRegistry);
158164
}
159165

160166
return !Log.HasLoggedErrors;

Microsoft.NET.Build.Containers/ParseContainerProperties.cs

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Diagnostics.CodeAnalysis;
12
using Microsoft.Build.Framework;
23

34
namespace Microsoft.NET.Build.Containers.Tasks;
@@ -25,8 +26,11 @@ public class ParseContainerProperties : Microsoft.Build.Utilities.Task
2526
/// <summary>
2627
/// The tag for the container to be created.
2728
/// </summary>
28-
[Required]
2929
public string ContainerImageTag { get; set; }
30+
/// <summary>
31+
/// The tags for the container to be created.
32+
/// </summary>
33+
public string[] ContainerImageTags { get; set; }
3034

3135
[Output]
3236
public string ParsedContainerRegistry { get; private set; }
@@ -44,31 +48,83 @@ public class ParseContainerProperties : Microsoft.Build.Utilities.Task
4448
public string NewContainerImageName { get; private set; }
4549

4650
[Output]
47-
public string NewContainerTag { get; private set; }
51+
public string[] NewContainerTags { get; private set; }
4852

4953
public ParseContainerProperties()
5054
{
5155
FullyQualifiedBaseImageName = "";
5256
ContainerRegistry = "";
5357
ContainerImageName = "";
5458
ContainerImageTag = "";
59+
ContainerImageTags = Array.Empty<string>();
5560
ParsedContainerRegistry = "";
5661
ParsedContainerImage = "";
5762
ParsedContainerTag = "";
5863
NewContainerRegistry = "";
5964
NewContainerImageName = "";
60-
NewContainerTag = "";
65+
NewContainerTags = Array.Empty<string>();
6166
}
6267

63-
public override bool Execute()
68+
private static bool TryValidateTags(string[] inputTags, out string[] validTags, out string[] invalidTags)
6469
{
70+
var v = new List<string>();
71+
var i = new List<string>();
72+
foreach (var tag in inputTags)
73+
{
74+
if (ContainerHelpers.IsValidImageTag(tag))
75+
{
76+
v.Add(tag);
77+
}
78+
else
79+
{
80+
i.Add(tag);
81+
}
82+
}
83+
validTags = v.ToArray();
84+
invalidTags = i.ToArray();
85+
return invalidTags.Count() == 0;
86+
}
6587

66-
if (!string.IsNullOrEmpty(ContainerImageTag) && !ContainerHelpers.IsValidImageTag(ContainerImageTag))
88+
public override bool Execute()
89+
{
90+
string[] validTags;
91+
if (!String.IsNullOrEmpty(ContainerImageTag) && ContainerImageTags.Length >= 1)
6792
{
68-
Log.LogError($"Invalid {nameof(ContainerImageTag)}: {0}", ContainerImageTag);
93+
Log.LogError(null, "CONTAINER005", "Container.AmbiguousTags", null, 0, 0, 0, 0, $"Both {nameof(ContainerImageTag)} and {nameof(ContainerImageTags)} were provided, but only one or the other is allowed.");
6994
return !Log.HasLoggedErrors;
7095
}
7196

97+
if (!String.IsNullOrEmpty(ContainerImageTag))
98+
{
99+
if (ContainerHelpers.IsValidImageTag(ContainerImageTag))
100+
{
101+
validTags = new[] { ContainerImageTag };
102+
}
103+
else
104+
{
105+
validTags = Array.Empty<string>();
106+
Log.LogError(null, "CONTAINER003", "Container.InvalidTag", null, 0, 0, 0, 0, $"Invalid {nameof(ContainerImageTag)} provided: {{0}}. Image tags must be alphanumeric, underscore, hyphen, or period.", ContainerImageTag);
107+
}
108+
}
109+
else if (ContainerImageTags.Length != 0 && !TryValidateTags(ContainerImageTags, out var valids, out var invalids))
110+
{
111+
validTags = valids;
112+
if (invalids.Any())
113+
{
114+
(string message, string args) = invalids switch
115+
{
116+
{ Length: 1 } => ($"Invalid {nameof(ContainerImageTags)} provided: {{0}}. {nameof(ContainerImageTags)} must be a semicolon-delimited list of valid image tags. Image tags must be alphanumeric, underscore, hyphen, or period.", invalids[0]),
117+
_ => ($"Invalid {nameof(ContainerImageTags)} provided: {{0}}. {nameof(ContainerImageTags)} must be a semicolon-delimited list of valid image tags. Image tags must be alphanumeric, underscore, hyphen, or period.", String.Join(", ", invalids))
118+
};
119+
Log.LogError(null, "CONTAINER003", "Container.InvalidTag", null, 0, 0, 0, 0, message, args);
120+
return !Log.HasLoggedErrors;
121+
}
122+
}
123+
else
124+
{
125+
validTags = Array.Empty<string>();
126+
}
127+
72128
string registryToUse = string.Empty;
73129

74130
if (!ContainerRegistry.StartsWith("http://") &&
@@ -105,7 +161,7 @@ public override bool Execute()
105161
{
106162
if (!ContainerHelpers.NormalizeImageName(ContainerImageName, out string? normalizedImageName))
107163
{
108-
Log.LogWarning(null, "CONTAINER001", null, null, 0, 0, 0, 0, $"{nameof(ContainerImageName)} was not a valid container image name, it was normalized to {normalizedImageName}");
164+
Log.LogWarning(null, "CONTAINER001", "Container.InvalidImageName", null, 0, 0, 0, 0, $"{nameof(ContainerImageName)} was not a valid container image name, it was normalized to {normalizedImageName}");
109165
NewContainerImageName = normalizedImageName;
110166
}
111167
else
@@ -124,7 +180,7 @@ public override bool Execute()
124180
ParsedContainerImage = outputImage;
125181
ParsedContainerTag = outputTag;
126182
NewContainerRegistry = registryToUse;
127-
NewContainerTag = ContainerImageTag;
183+
NewContainerTags = validTags;
128184

129185
if (BuildEngine != null)
130186
{
@@ -133,7 +189,7 @@ public override bool Execute()
133189
Log.LogMessage(MessageImportance.Low, "Image: {0}", ParsedContainerImage);
134190
Log.LogMessage(MessageImportance.Low, "Tag: {0}", ParsedContainerTag);
135191
Log.LogMessage(MessageImportance.Low, "Image Name: {0}", NewContainerImageName);
136-
Log.LogMessage(MessageImportance.Low, "Image Tag: {0}", NewContainerTag);
192+
Log.LogMessage(MessageImportance.Low, "Image Tags: {0}", String.Join(", ", NewContainerTags));
137193
}
138194

139195
return !Log.HasLoggedErrors;

Microsoft.NET.Build.Containers/build/Microsoft.NET.Build.Containers.targets

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
<ContainerRegistry Condition="'$(ContainerRegistry)' == ''">docker://</ContainerRegistry>
2222
<!-- Note: spaces will be replaced with '-' in ContainerImageName and ContainerImageTag -->
2323
<ContainerImageName Condition="'$(ContainerImageName)' == ''">$(AssemblyName)</ContainerImageName>
24-
<ContainerImageTag Condition="'$(ContainerImageTag)' == ''">$(Version)</ContainerImageTag>
24+
<!-- Only default a tag name if no tag names at all are provided -->
25+
<ContainerImageTag Condition="'$(ContainerImageTag)' == '' and '$(ContainerImageTags)' == ''">$(Version)</ContainerImageTag>
2526
<ContainerWorkingDirectory Condition="'$(ContainerWorkingDirectory)' == ''">/app</ContainerWorkingDirectory>
2627
<!-- Could be semicolon-delimited -->
2728
</PropertyGroup>
@@ -48,14 +49,15 @@
4849
<ParseContainerProperties FullyQualifiedBaseImageName="$(ContainerBaseImage)"
4950
ContainerRegistry="$(ContainerRegistry)"
5051
ContainerImageName="$(ContainerImageName)"
51-
ContainerImageTag="$(ContainerImageTag)">
52+
ContainerImageTag="$(ContainerImageTag)"
53+
ContainerImageTags="$(ContainerImageTags)">
5254

5355
<Output TaskParameter="ParsedContainerRegistry" PropertyName="ContainerBaseRegistry" />
5456
<Output TaskParameter="ParsedContainerImage" PropertyName="ContainerBaseName" />
5557
<Output TaskParameter="ParsedContainerTag" PropertyName="ContainerBaseTag" />
5658
<Output TaskParameter="NewContainerRegistry" PropertyName="ContainerRegistry" />
5759
<Output TaskParameter="NewContainerImageName" PropertyName="ContainerImageName" />
58-
<Output TaskParameter="NewContainerTag" PropertyName="ContainerImageTag" />
60+
<Output TaskParameter="NewContainerTags" ItemName="ContainerImageTags" />
5961
</ParseContainerProperties>
6062
</Target>
6163

@@ -71,7 +73,7 @@
7173
BaseImageTag="$(ContainerBaseTag)"
7274
OutputRegistry="$(ContainerRegistry)"
7375
ImageName="$(ContainerImageName)"
74-
ImageTag="$(ContainerImageTag)"
76+
ImageTags="@(ContainerImageTags)"
7577
PublishDirectory="$(PublishDir)"
7678
WorkingDirectory="$(ContainerWorkingDirectory)"
7779
Entrypoint="@(ContainerEntrypoint)"

Test.Microsoft.NET.Build.Containers.Filesystem/CreateNewImageTests.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,15 +93,15 @@ public void ParseContainerProperties_EndToEnd()
9393
pcp.FullyQualifiedBaseImageName = "https://mcr.microsoft.com/dotnet/runtime:6.0";
9494
pcp.ContainerRegistry = "http://localhost:5010";
9595
pcp.ContainerImageName = "dotnet/testimage";
96-
pcp.ContainerImageTag = "5.0";
96+
pcp.ContainerImageTags = new [] {"5.0", "latest"};
9797

9898
Assert.IsTrue(pcp.Execute());
9999
Assert.AreEqual("https://mcr.microsoft.com", pcp.ParsedContainerRegistry);
100100
Assert.AreEqual("dotnet/runtime", pcp.ParsedContainerImage);
101101
Assert.AreEqual("6.0", pcp.ParsedContainerTag);
102102

103103
Assert.AreEqual("dotnet/testimage", pcp.NewContainerImageName);
104-
Assert.AreEqual("5.0", pcp.NewContainerTag);
104+
new []{ "5.0", "latest"}.SequenceEqual(pcp.NewContainerTags);
105105

106106
CreateNewImage cni = new CreateNewImage();
107107
cni.BaseRegistry = pcp.ParsedContainerRegistry;
@@ -112,6 +112,7 @@ public void ParseContainerProperties_EndToEnd()
112112
cni.PublishDirectory = Path.Combine(newProjectDir.FullName, "bin", "release", "net7.0");
113113
cni.WorkingDirectory = "app/";
114114
cni.Entrypoint = new TaskItem[] { new("ParseContainerProperties_EndToEnd") };
115+
cni.ImageTags = pcp.NewContainerTags;
115116

116117
Assert.IsTrue(cni.Execute());
117118
newProjectDir.Delete(true);

0 commit comments

Comments
 (0)