Skip to content

Commit 68a7510

Browse files
Add generateDockerfiles command (#581)
1 parent bfc7bb8 commit 68a7510

File tree

10 files changed

+483
-6
lines changed

10 files changed

+483
-6
lines changed
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Collections.Generic;
7+
using System.ComponentModel.Composition;
8+
using System.IO;
9+
using System.Linq;
10+
using System.Text.RegularExpressions;
11+
using System.Threading.Tasks;
12+
using Cottle;
13+
using Cottle.Exceptions;
14+
using Microsoft.DotNet.ImageBuilder.Models.Manifest;
15+
using Microsoft.DotNet.ImageBuilder.ViewModel;
16+
17+
namespace Microsoft.DotNet.ImageBuilder.Commands
18+
{
19+
[Export(typeof(ICommand))]
20+
public class GenerateDockerfilesCommand : ManifestCommand<GenerateDockerfilesOptions>
21+
{
22+
private readonly DocumentConfiguration _config = new DocumentConfiguration
23+
{
24+
BlockBegin = "{{",
25+
BlockContinue = "^",
26+
BlockEnd = "}}",
27+
Escape = '@',
28+
Trimmer = DocumentConfiguration.TrimNothing
29+
};
30+
31+
private readonly IEnvironmentService _environmentService;
32+
33+
[ImportingConstructor]
34+
public GenerateDockerfilesCommand(IEnvironmentService environmentService) : base()
35+
{
36+
_environmentService = environmentService ?? throw new ArgumentNullException(nameof(environmentService));
37+
}
38+
39+
public override async Task ExecuteAsync()
40+
{
41+
Logger.WriteHeading("GENERATING DOCKERFILES");
42+
43+
List<string> outOfSyncDockerfiles = new List<string>();
44+
List<string> invalidTemplates = new List<string>();
45+
46+
IEnumerable<PlatformInfo> platforms = Manifest.GetFilteredPlatforms()
47+
.Where(platform => platform.DockerfileTemplate != null);
48+
foreach (PlatformInfo platform in platforms)
49+
{
50+
Logger.WriteSubheading($"Generating '{platform.DockerfilePath}' from '{platform.DockerfileTemplate}'");
51+
52+
string template = await File.ReadAllTextAsync(platform.DockerfileTemplate);
53+
if (Options.IsVerbose)
54+
{
55+
Logger.WriteMessage($"Template:{Environment.NewLine}{template}");
56+
}
57+
58+
await GenerateDockerfileAsync(template, platform, outOfSyncDockerfiles, invalidTemplates);
59+
}
60+
61+
if (outOfSyncDockerfiles.Any() || invalidTemplates.Any())
62+
{
63+
if (outOfSyncDockerfiles.Any())
64+
{
65+
string dockerfileList = string.Join(Environment.NewLine, outOfSyncDockerfiles);
66+
Logger.WriteError($"Dockerfiles out of sync with templates:{Environment.NewLine}{dockerfileList}");
67+
}
68+
69+
if (invalidTemplates.Any())
70+
{
71+
string templateList = string.Join(Environment.NewLine, invalidTemplates);
72+
Logger.WriteError($"Invalid Templates:{Environment.NewLine}{templateList}");
73+
}
74+
75+
_environmentService.Exit(1);
76+
}
77+
}
78+
79+
private async Task GenerateDockerfileAsync(string template, PlatformInfo platform, List<string> outOfSyncDockerfiles, List<string> invalidTemplates)
80+
{
81+
try
82+
{
83+
IDocument document = Document.CreateDefault(template, _config).DocumentOrThrow;
84+
string generatedDockerfile = document.Render(Context.CreateBuiltin(GetSymbols(platform)));
85+
86+
string currentDockerfile = File.Exists(platform.DockerfilePath) ?
87+
await File.ReadAllTextAsync(platform.DockerfilePath) : string.Empty;
88+
if (currentDockerfile == generatedDockerfile)
89+
{
90+
Logger.WriteMessage("Dockerfile in sync with template");
91+
}
92+
else if (Options.Validate)
93+
{
94+
Logger.WriteError("Dockerfile out of sync with template");
95+
outOfSyncDockerfiles.Add(platform.DockerfilePath);
96+
}
97+
else
98+
{
99+
if (Options.IsVerbose)
100+
{
101+
Logger.WriteMessage($"Generated Dockerfile:{Environment.NewLine}{generatedDockerfile}");
102+
}
103+
104+
if (!Options.IsDryRun)
105+
{
106+
await File.WriteAllTextAsync(platform.DockerfilePath, generatedDockerfile);
107+
Logger.WriteMessage($"Updated '{platform.DockerfilePath}'");
108+
}
109+
}
110+
}
111+
catch (ParseException e)
112+
{
113+
Logger.WriteError($"Error: {e}{Environment.NewLine}Invalid Syntax:{Environment.NewLine}{template.Substring(e.LocationStart)}");
114+
invalidTemplates.Add(platform.DockerfileTemplate);
115+
}
116+
}
117+
118+
private static string GetOsArchHyphenatedName(PlatformInfo platform)
119+
{
120+
string osName;
121+
if (platform.BaseOsVersion.Contains("nanoserver"))
122+
{
123+
string version = platform.BaseOsVersion.Split('-')[1];
124+
osName = $"NanoServer-{version}";
125+
}
126+
else
127+
{
128+
osName = platform.GetOSDisplayName().Replace(' ', '-');
129+
}
130+
131+
string archName = platform.Model.Architecture != Architecture.AMD64 ? $"-{platform.Model.Architecture.GetDisplayName()}" : string.Empty;
132+
133+
return osName + archName;
134+
}
135+
136+
public IReadOnlyDictionary<Value, Value> GetSymbols(PlatformInfo platform)
137+
{
138+
string versionedArch = platform.Model.Architecture.GetDisplayName(platform.Model.Variant);
139+
140+
return new Dictionary<Value, Value>
141+
{
142+
["ARCH_SHORT"] = platform.Model.Architecture.GetShortName(),
143+
["ARCH_NUPKG"] = platform.Model.Architecture.GetNupkgName(),
144+
["ARCH_VERSIONED"] = versionedArch,
145+
["ARCH_TAG_SUFFIX"] = platform.Model.Architecture != Architecture.AMD64 ? $"-{versionedArch}" : string.Empty,
146+
["OS_VERSION"] = platform.Model.OsVersion,
147+
["OS_VERSION_BASE"] = platform.BaseOsVersion,
148+
["OS_VERSION_NUMBER"] = Regex.Match(platform.Model.OsVersion, @"\d+.\d+").Value,
149+
["OS_ARCH_HYPHENATED"] = GetOsArchHyphenatedName(platform),
150+
["VARIABLES"] = Manifest.Model?.Variables?.ToDictionary(kvp => (Value)kvp.Key, kvp => (Value)kvp.Value)
151+
};
152+
}
153+
}
154+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.CommandLine;
6+
7+
namespace Microsoft.DotNet.ImageBuilder.Commands
8+
{
9+
public class GenerateDockerfilesOptions : ManifestOptions, IFilterableOptions
10+
{
11+
protected override string CommandHelp => "Generates the Dockerfiles from Cottle based templates (http://r3c.github.io/cottle/)";
12+
13+
public ManifestFilterOptions FilterOptions { get; } = new ManifestFilterOptions();
14+
15+
public bool Validate { get; set; }
16+
17+
public GenerateDockerfilesOptions() : base()
18+
{
19+
}
20+
21+
public override void DefineOptions(ArgumentSyntax syntax)
22+
{
23+
base.DefineOptions(syntax);
24+
25+
FilterOptions.DefineOptions(syntax);
26+
27+
bool validate = false;
28+
syntax.DefineOption("validate", ref validate, "Validates the Dockerfiles and templates are in sync");
29+
Validate = validate;
30+
}
31+
}
32+
}

src/Microsoft.DotNet.ImageBuilder/src/Microsoft.DotNet.ImageBuilder.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
</PropertyGroup>
99

1010
<ItemGroup>
11+
<PackageReference Include="Cottle" Version="2.0.2" />
1112
<PackageReference Include="Microsoft.Azure.Kusto.Ingest" Version="8.0.9" />
1213
<PackageReference Include="Microsoft.Azure.Management.Fluent" Version="1.22.0" />
1314
<PackageReference Include="Microsoft.DotNet.VersionTools" Version="1.0.0-beta.19426.12" />

src/Microsoft.DotNet.ImageBuilder/src/Models/Manifest/Platform.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ public class Platform
3535
[JsonProperty(Required = Required.Always)]
3636
public string Dockerfile { get; set; }
3737

38+
[Description(
39+
"Relative path to the template the Dockerfile is generated from."
40+
)]
41+
public string DockerfileTemplate { get; set; }
42+
3843
[Description(
3944
"The generic name of the operating system associated with the image."
4045
)]

src/Microsoft.DotNet.ImageBuilder/src/ViewModel/ModelExtensions.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,43 @@ public static string GetDisplayName(this Architecture architecture, string varia
3434
return displayName;
3535
}
3636

37+
public static string GetShortName(this Architecture architecture)
38+
{
39+
string shortName;
40+
41+
switch (architecture)
42+
{
43+
case Architecture.AMD64:
44+
shortName = "x64";
45+
break;
46+
default:
47+
shortName = architecture.ToString().ToLowerInvariant();
48+
break;
49+
}
50+
51+
return shortName;
52+
}
53+
54+
public static string GetNupkgName(this Architecture architecture)
55+
{
56+
string nupkgName;
57+
58+
switch (architecture)
59+
{
60+
case Architecture.AMD64:
61+
nupkgName = "x64";
62+
break;
63+
case Architecture.ARM:
64+
nupkgName = "arm32";
65+
break;
66+
default:
67+
nupkgName = architecture.ToString().ToLowerInvariant();
68+
break;
69+
}
70+
71+
return nupkgName;
72+
}
73+
3774
public static string GetDockerName(this Architecture architecture) => architecture.ToString().ToLowerInvariant();
3875

3976
public static string GetDockerName(this OS os) => os.ToString().ToLowerInvariant();
@@ -109,6 +146,7 @@ private static void ValidateImage(Image image, string manifestDirectory)
109146
private static void ValidatePlatform(Platform platform, string manifestDirectory)
110147
{
111148
ValidateFileReference(platform.ResolveDockerfilePath(manifestDirectory), manifestDirectory);
149+
ValidateFileReference(platform.DockerfileTemplate, manifestDirectory);
112150
}
113151

114152
private static void ValidateUniqueTags(Repo repo)

src/Microsoft.DotNet.ImageBuilder/src/ViewModel/PlatformInfo.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public class PlatformInfo
2929
public string BuildContextPath { get; private set; }
3030
public string DockerfilePath { get; private set; }
3131
public string DockerfilePathRelativeToManifest { get; private set; }
32+
public string DockerfileTemplate { get; private set; }
3233
public string FinalStageFromImage { get; private set; }
3334
public IEnumerable<string> ExternalFromImages { get; private set; }
3435
public IEnumerable<string> InternalFromImages { get; private set; }
@@ -57,6 +58,11 @@ public static PlatformInfo Create(Platform model, string fullRepoModelName, stri
5758
platformInfo.BuildContextPath = PathHelper.NormalizePath(Path.GetDirectoryName(dockerfileWithBaseDir));
5859
platformInfo.DockerfilePathRelativeToManifest = PathHelper.TrimPath(baseDirectory, platformInfo.DockerfilePath);
5960

61+
if (model.DockerfileTemplate != null)
62+
{
63+
platformInfo.DockerfileTemplate = Path.Combine(baseDirectory, model.DockerfileTemplate);
64+
}
65+
6066
platformInfo.Tags = model.Tags
6167
.Select(kvp => TagInfo.Create(kvp.Key, kvp.Value, repoName, variableHelper, platformInfo.BuildContextPath))
6268
.ToArray();

0 commit comments

Comments
 (0)