Skip to content

Commit df43690

Browse files
Update containerize to prep for shipping (#155)
1 parent 21cc8ec commit df43690

File tree

3 files changed

+170
-74
lines changed

3 files changed

+170
-74
lines changed

Microsoft.NET.Build.Containers/ContainerHelpers.cs

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
namespace Microsoft.NET.Build.Containers;
22

33
using System.Diagnostics.CodeAnalysis;
4+
using System.Text.Json;
45
using System.Text.RegularExpressions;
56

67
public static class ContainerHelpers
@@ -133,12 +134,78 @@ public static bool NormalizeImageName(string containerImageName, [NotNullWhen(fa
133134
}
134135
else
135136
{
136-
if (!Char.IsLetterOrDigit(containerImageName, 0)) {
137+
if (!Char.IsLetterOrDigit(containerImageName, 0))
138+
{
137139
throw new ArgumentException("The first character of the image name must be a lowercase letter or a digit.");
138140
}
139141
var loweredImageName = containerImageName.ToLowerInvariant();
140142
normalizedImageName = imageNameCharacters.Replace(loweredImageName, "-");
141143
return false;
142144
}
143145
}
144-
}
146+
147+
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)
148+
{
149+
Registry baseRegistry = new Registry(new Uri(registryName));
150+
151+
Console.WriteLine($"Reading from {baseRegistry.BaseUri}");
152+
153+
Image img = await baseRegistry.GetImageManifest(baseName, baseTag);
154+
img.WorkingDirectory = workingDir;
155+
156+
JsonSerializerOptions options = new()
157+
{
158+
WriteIndented = true,
159+
};
160+
161+
Console.WriteLine($"Copying from {folder.FullName} to {workingDir}");
162+
Layer l = Layer.FromDirectory(folder.FullName, workingDir);
163+
164+
img.AddLayer(l);
165+
166+
img.SetEntrypoint(entrypoint, entrypointArgs);
167+
168+
var isDockerPush = outputRegistry.StartsWith("docker://");
169+
Registry? outputReg = isDockerPush ? null : new Registry(new Uri(outputRegistry));
170+
171+
foreach (var label in labels)
172+
{
173+
string[] labelPieces = label.Split('=');
174+
175+
// labels are validated by System.Commandline API
176+
img.Label(labelPieces[0], labelPieces[1]);
177+
}
178+
179+
foreach (var tag in imageTags)
180+
{
181+
if (isDockerPush)
182+
{
183+
try
184+
{
185+
LocalDocker.Load(img, imageName, tag, baseName).Wait();
186+
Console.WriteLine("Pushed container '{0}:{1}' to Docker daemon", imageName, tag);
187+
}
188+
catch (AggregateException ex) when (ex.InnerException is DockerLoadException dle)
189+
{
190+
Console.WriteLine(dle);
191+
Environment.ExitCode = -1;
192+
}
193+
}
194+
else
195+
{
196+
try
197+
{
198+
Console.WriteLine($"Trying to push container '{imageName}:{tag}' to registry '{outputRegistry}'");
199+
outputReg?.Push(img, imageName, tag, imageName).Wait();
200+
Console.WriteLine($"Pushed container '{imageName}:{tag}' to registry '{outputRegistry}'");
201+
}
202+
catch (Exception e)
203+
{
204+
Console.WriteLine("Failed to push to output registry: {0}", e);
205+
Environment.ExitCode = -1;
206+
}
207+
}
208+
}
209+
210+
}
211+
}

containerize/Program.cs

Lines changed: 100 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,119 @@
11
using System.CommandLine;
22
using Microsoft.NET.Build.Containers;
33
using System.Text.Json;
4+
using System.CommandLine.Parsing;
45

5-
var fileOption = new Argument<DirectoryInfo>(
6-
name: "folder",
7-
description: "The folder to pack.")
6+
var publishDirectoryArg = new Argument<DirectoryInfo>(
7+
name: "PublishDirectory",
8+
description: "The directory for the build outputs to be published.")
89
.LegalFilePathsOnly().ExistingOnly();
910

10-
Option<string> registryUri = new(
11-
name: "--registry",
12-
description: "Location of the registry to push to.",
13-
getDefaultValue: () => "localhost:5010");
14-
15-
Option<string> baseImageName = new(
16-
name: "--base",
17-
description: "Base image name.",
18-
getDefaultValue: () => "dotnet/runtime");
11+
var baseRegistryOpt = new Option<string>(
12+
name: "--baseregistry",
13+
description: "The registry to use for the base image.")
14+
{
15+
IsRequired = true
16+
};
1917

20-
Option<string> baseImageTag = new(
21-
name: "--baseTag",
22-
description: "Base image tag.",
23-
getDefaultValue: () => $"{System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription[5]}.0");
18+
var baseImageNameOpt = new Option<string>(
19+
name: "--baseimagename",
20+
description: "The base image to pull.")
21+
{
22+
IsRequired = true
23+
};
2424

25-
Option<string[]> entrypoint = new(
26-
name: "--entrypoint",
27-
description: "Entrypoint application command.");
25+
var baseImageTagOpt = new Option<string>(
26+
name: "--baseimagetag",
27+
description: "The base image tag. Ex: 6.0",
28+
getDefaultValue: () => "latest");
2829

29-
Option<string> imageName = new(
30-
name: "--name",
31-
description: "Name of the new image.");
30+
var outputRegistryOpt = new Option<string>(
31+
name: "--outputregistry",
32+
description: "The registry to push to.")
33+
{
34+
IsRequired = true
35+
};
3236

33-
var imageTag = new Option<string>("--tag", description: "Tag of the new image.", getDefaultValue: () => "latest");
37+
var imageNameOpt = new Option<string>(
38+
name: "--imagename",
39+
description: "The name of the output image that will be pushed to the registry.")
40+
{
41+
IsRequired = true
42+
};
3443

35-
var workingDir = new Option<string>("--working-dir", description: "The working directory of the application", getDefaultValue: () => "/app");
44+
var imageTagsOpt = new Option<string[]>(
45+
name: "--imagetags",
46+
description: "The tags to associate with the new image.");
3647

37-
RootCommand rootCommand = new("Containerize an application without Docker."){
38-
fileOption,
39-
registryUri,
40-
baseImageName,
41-
baseImageTag,
42-
entrypoint,
43-
imageName,
44-
imageTag,
45-
workingDir
46-
};
47-
rootCommand.SetHandler(async (folder, containerWorkingDir, uri, baseImageName, baseTag, entrypoint, imageName, imageTag) =>
48-
{
49-
await Containerize(folder, containerWorkingDir, uri, baseImageName, baseTag, entrypoint, imageName, imageTag);
50-
},
51-
fileOption,
52-
workingDir,
53-
registryUri,
54-
baseImageName,
55-
baseImageTag,
56-
entrypoint,
57-
imageName,
58-
imageTag
59-
);
60-
61-
return await rootCommand.InvokeAsync(args);
62-
63-
async Task Containerize(DirectoryInfo folder, string workingDir, string registryName, string baseName, string baseTag, string[] entrypoint, string imageName, string imageTag)
48+
var workingDirectoryOpt = new Option<string>(
49+
name: "--workingdirectory",
50+
description: "The working directory of the container.")
6451
{
65-
Registry registry = new Registry(new Uri($"http://{registryName}"));
52+
IsRequired = true
53+
};
6654

67-
Console.WriteLine($"Reading from {registry.BaseUri}");
55+
var entrypointOpt = new Option<string[]>(
56+
name: "--entrypoint",
57+
description: "The entrypoint application of the container.")
58+
{
59+
IsRequired = true
60+
};
6861

69-
Image x = await registry.GetImageManifest(baseName, baseTag);
70-
x.WorkingDirectory = workingDir;
62+
var entrypointArgsOpt = new Option<string[]>(
63+
name: "--entrypointargs",
64+
description: "Arguments to pass alongside Entrypoint.");
7165

72-
JsonSerializerOptions options = new()
66+
var labelsOpt = new Option<string[]>(
67+
name: "--labels",
68+
description: "Labels that the image configuration will include in metadata.",
69+
parseArgument: result =>
7370
{
74-
WriteIndented = true,
75-
};
76-
77-
Console.WriteLine($"Copying from {folder.FullName} to {workingDir}");
78-
Layer l = Layer.FromDirectory(folder.FullName, workingDir);
79-
80-
x.AddLayer(l);
81-
82-
x.SetEntrypoint(entrypoint);
83-
84-
// File.WriteAllTextAsync("manifest.json", x.manifest.ToJsonString(options));
85-
// File.WriteAllTextAsync("config.json", x.config.ToJsonString(options));
71+
var labels = result.Tokens.Select(x => x.Value).ToArray();
72+
var badLabels = labels.Where((v) => v.Split('=').Length != 2);
73+
74+
// Is there a non-zero number of Labels that didn't split into two elements? If so, assume invalid input and error out
75+
if (badLabels.Count() != 0)
76+
{
77+
result.ErrorMessage = "Incorrectly formatted labels: " + badLabels.Aggregate((x, y) => x = x + ";" + y);
78+
79+
return new string[] { };
80+
}
81+
return labels;
82+
})
83+
{
84+
AllowMultipleArgumentsPerToken = true
85+
};
8686

87-
await LocalDocker.Load(x, imageName, imageTag, baseName);
87+
RootCommand root = new RootCommand("Containerize an application without Docker.")
88+
{
89+
publishDirectoryArg,
90+
baseRegistryOpt,
91+
baseImageNameOpt,
92+
baseImageTagOpt,
93+
outputRegistryOpt,
94+
imageNameOpt,
95+
imageTagsOpt,
96+
workingDirectoryOpt,
97+
entrypointOpt,
98+
entrypointArgsOpt,
99+
labelsOpt
100+
};
88101

89-
Console.WriteLine($"Loaded image into local Docker daemon. Use 'docker run --rm -it --name {imageName} {registryName}/{imageName}:{imageTag}' to run the application.");
90-
}
102+
root.SetHandler(async (context) =>
103+
{
104+
DirectoryInfo _publishDir = context.ParseResult.GetValueForArgument(publishDirectoryArg);
105+
string _baseReg = context.ParseResult.GetValueForOption(baseRegistryOpt) ?? "";
106+
string _baseName = context.ParseResult.GetValueForOption(baseImageNameOpt) ?? "";
107+
string _baseTag = context.ParseResult.GetValueForOption(baseImageTagOpt) ?? "";
108+
string _outputReg = context.ParseResult.GetValueForOption(outputRegistryOpt) ?? "";
109+
string _name = context.ParseResult.GetValueForOption(imageNameOpt) ?? "";
110+
string[] _tags = context.ParseResult.GetValueForOption(imageTagsOpt) ?? Array.Empty<string>();
111+
string _workingDir = context.ParseResult.GetValueForOption(workingDirectoryOpt) ?? "";
112+
string[] _entrypoint = context.ParseResult.GetValueForOption(entrypointOpt) ?? Array.Empty<string>();
113+
string[] _entrypointArgs = context.ParseResult.GetValueForOption(entrypointArgsOpt) ?? Array.Empty<string>();
114+
string[] _labels = context.ParseResult.GetValueForOption(labelsOpt) ?? Array.Empty<string>();
115+
116+
await ContainerHelpers.Containerize(_publishDir, _workingDir, _baseReg, _baseName, _baseTag, _entrypoint, _entrypointArgs, _name, _tags, _outputReg, _labels);
117+
});
118+
119+
return await root.InvokeAsync(args);

containerize/Properties/launchSettings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"profiles": {
33
"containerize": {
44
"commandName": "Project",
5-
"commandLineArgs": "S:\\play\\container-demo\\bin\\Debug\\net6.0\\linux-x64\\ --registry localhost:5010 --base \"dotnet/runtime\" --baseTag \"6.0\" --entrypoint \"/app/container-demo\" --name \"showcase/demo\""
5+
"commandLineArgs": "bin\\Debug\\net7.0\\ --baseregistry https://mcr.microsoft.com --baseimagename dotnet/runtime --outputregistry docker:// --imagename dotnet-test --imagetags 6.0 --workingdirectory app/ --entrypoint dotnet --entrypointargs run --labels foo=bar hello=world"
66
}
77
}
88
}

0 commit comments

Comments
 (0)