Skip to content

Commit

Permalink
Add generateDockerfiles command (#581)
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelSimons authored Jul 14, 2020
1 parent bfc7bb8 commit 68a7510
Show file tree
Hide file tree
Showing 10 changed files with 483 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Cottle;
using Cottle.Exceptions;
using Microsoft.DotNet.ImageBuilder.Models.Manifest;
using Microsoft.DotNet.ImageBuilder.ViewModel;

namespace Microsoft.DotNet.ImageBuilder.Commands
{
[Export(typeof(ICommand))]
public class GenerateDockerfilesCommand : ManifestCommand<GenerateDockerfilesOptions>
{
private readonly DocumentConfiguration _config = new DocumentConfiguration
{
BlockBegin = "{{",
BlockContinue = "^",
BlockEnd = "}}",
Escape = '@',
Trimmer = DocumentConfiguration.TrimNothing
};

private readonly IEnvironmentService _environmentService;

[ImportingConstructor]
public GenerateDockerfilesCommand(IEnvironmentService environmentService) : base()
{
_environmentService = environmentService ?? throw new ArgumentNullException(nameof(environmentService));
}

public override async Task ExecuteAsync()
{
Logger.WriteHeading("GENERATING DOCKERFILES");

List<string> outOfSyncDockerfiles = new List<string>();
List<string> invalidTemplates = new List<string>();

IEnumerable<PlatformInfo> platforms = Manifest.GetFilteredPlatforms()
.Where(platform => platform.DockerfileTemplate != null);
foreach (PlatformInfo platform in platforms)
{
Logger.WriteSubheading($"Generating '{platform.DockerfilePath}' from '{platform.DockerfileTemplate}'");

string template = await File.ReadAllTextAsync(platform.DockerfileTemplate);
if (Options.IsVerbose)
{
Logger.WriteMessage($"Template:{Environment.NewLine}{template}");
}

await GenerateDockerfileAsync(template, platform, outOfSyncDockerfiles, invalidTemplates);
}

if (outOfSyncDockerfiles.Any() || invalidTemplates.Any())
{
if (outOfSyncDockerfiles.Any())
{
string dockerfileList = string.Join(Environment.NewLine, outOfSyncDockerfiles);
Logger.WriteError($"Dockerfiles out of sync with templates:{Environment.NewLine}{dockerfileList}");
}

if (invalidTemplates.Any())
{
string templateList = string.Join(Environment.NewLine, invalidTemplates);
Logger.WriteError($"Invalid Templates:{Environment.NewLine}{templateList}");
}

_environmentService.Exit(1);
}
}

private async Task GenerateDockerfileAsync(string template, PlatformInfo platform, List<string> outOfSyncDockerfiles, List<string> invalidTemplates)
{
try
{
IDocument document = Document.CreateDefault(template, _config).DocumentOrThrow;
string generatedDockerfile = document.Render(Context.CreateBuiltin(GetSymbols(platform)));

string currentDockerfile = File.Exists(platform.DockerfilePath) ?
await File.ReadAllTextAsync(platform.DockerfilePath) : string.Empty;
if (currentDockerfile == generatedDockerfile)
{
Logger.WriteMessage("Dockerfile in sync with template");
}
else if (Options.Validate)
{
Logger.WriteError("Dockerfile out of sync with template");
outOfSyncDockerfiles.Add(platform.DockerfilePath);
}
else
{
if (Options.IsVerbose)
{
Logger.WriteMessage($"Generated Dockerfile:{Environment.NewLine}{generatedDockerfile}");
}

if (!Options.IsDryRun)
{
await File.WriteAllTextAsync(platform.DockerfilePath, generatedDockerfile);
Logger.WriteMessage($"Updated '{platform.DockerfilePath}'");
}
}
}
catch (ParseException e)
{
Logger.WriteError($"Error: {e}{Environment.NewLine}Invalid Syntax:{Environment.NewLine}{template.Substring(e.LocationStart)}");
invalidTemplates.Add(platform.DockerfileTemplate);
}
}

private static string GetOsArchHyphenatedName(PlatformInfo platform)
{
string osName;
if (platform.BaseOsVersion.Contains("nanoserver"))
{
string version = platform.BaseOsVersion.Split('-')[1];
osName = $"NanoServer-{version}";
}
else
{
osName = platform.GetOSDisplayName().Replace(' ', '-');
}

string archName = platform.Model.Architecture != Architecture.AMD64 ? $"-{platform.Model.Architecture.GetDisplayName()}" : string.Empty;

return osName + archName;
}

public IReadOnlyDictionary<Value, Value> GetSymbols(PlatformInfo platform)
{
string versionedArch = platform.Model.Architecture.GetDisplayName(platform.Model.Variant);

return new Dictionary<Value, Value>
{
["ARCH_SHORT"] = platform.Model.Architecture.GetShortName(),
["ARCH_NUPKG"] = platform.Model.Architecture.GetNupkgName(),
["ARCH_VERSIONED"] = versionedArch,
["ARCH_TAG_SUFFIX"] = platform.Model.Architecture != Architecture.AMD64 ? $"-{versionedArch}" : string.Empty,
["OS_VERSION"] = platform.Model.OsVersion,
["OS_VERSION_BASE"] = platform.BaseOsVersion,
["OS_VERSION_NUMBER"] = Regex.Match(platform.Model.OsVersion, @"\d+.\d+").Value,
["OS_ARCH_HYPHENATED"] = GetOsArchHyphenatedName(platform),
["VARIABLES"] = Manifest.Model?.Variables?.ToDictionary(kvp => (Value)kvp.Key, kvp => (Value)kvp.Value)
};
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.CommandLine;

namespace Microsoft.DotNet.ImageBuilder.Commands
{
public class GenerateDockerfilesOptions : ManifestOptions, IFilterableOptions
{
protected override string CommandHelp => "Generates the Dockerfiles from Cottle based templates (http://r3c.github.io/cottle/)";

public ManifestFilterOptions FilterOptions { get; } = new ManifestFilterOptions();

public bool Validate { get; set; }

public GenerateDockerfilesOptions() : base()
{
}

public override void DefineOptions(ArgumentSyntax syntax)
{
base.DefineOptions(syntax);

FilterOptions.DefineOptions(syntax);

bool validate = false;
syntax.DefineOption("validate", ref validate, "Validates the Dockerfiles and templates are in sync");
Validate = validate;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Cottle" Version="2.0.2" />
<PackageReference Include="Microsoft.Azure.Kusto.Ingest" Version="8.0.9" />
<PackageReference Include="Microsoft.Azure.Management.Fluent" Version="1.22.0" />
<PackageReference Include="Microsoft.DotNet.VersionTools" Version="1.0.0-beta.19426.12" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ public class Platform
[JsonProperty(Required = Required.Always)]
public string Dockerfile { get; set; }

[Description(
"Relative path to the template the Dockerfile is generated from."
)]
public string DockerfileTemplate { get; set; }

[Description(
"The generic name of the operating system associated with the image."
)]
Expand Down
38 changes: 38 additions & 0 deletions src/Microsoft.DotNet.ImageBuilder/src/ViewModel/ModelExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,43 @@ public static string GetDisplayName(this Architecture architecture, string varia
return displayName;
}

public static string GetShortName(this Architecture architecture)
{
string shortName;

switch (architecture)
{
case Architecture.AMD64:
shortName = "x64";
break;
default:
shortName = architecture.ToString().ToLowerInvariant();
break;
}

return shortName;
}

public static string GetNupkgName(this Architecture architecture)
{
string nupkgName;

switch (architecture)
{
case Architecture.AMD64:
nupkgName = "x64";
break;
case Architecture.ARM:
nupkgName = "arm32";
break;
default:
nupkgName = architecture.ToString().ToLowerInvariant();
break;
}

return nupkgName;
}

public static string GetDockerName(this Architecture architecture) => architecture.ToString().ToLowerInvariant();

public static string GetDockerName(this OS os) => os.ToString().ToLowerInvariant();
Expand Down Expand Up @@ -109,6 +146,7 @@ private static void ValidateImage(Image image, string manifestDirectory)
private static void ValidatePlatform(Platform platform, string manifestDirectory)
{
ValidateFileReference(platform.ResolveDockerfilePath(manifestDirectory), manifestDirectory);
ValidateFileReference(platform.DockerfileTemplate, manifestDirectory);
}

private static void ValidateUniqueTags(Repo repo)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public class PlatformInfo
public string BuildContextPath { get; private set; }
public string DockerfilePath { get; private set; }
public string DockerfilePathRelativeToManifest { get; private set; }
public string DockerfileTemplate { get; private set; }
public string FinalStageFromImage { get; private set; }
public IEnumerable<string> ExternalFromImages { get; private set; }
public IEnumerable<string> InternalFromImages { get; private set; }
Expand Down Expand Up @@ -57,6 +58,11 @@ public static PlatformInfo Create(Platform model, string fullRepoModelName, stri
platformInfo.BuildContextPath = PathHelper.NormalizePath(Path.GetDirectoryName(dockerfileWithBaseDir));
platformInfo.DockerfilePathRelativeToManifest = PathHelper.TrimPath(baseDirectory, platformInfo.DockerfilePath);

if (model.DockerfileTemplate != null)
{
platformInfo.DockerfileTemplate = Path.Combine(baseDirectory, model.DockerfileTemplate);
}

platformInfo.Tags = model.Tags
.Select(kvp => TagInfo.Create(kvp.Key, kvp.Value, repoName, variableHelper, platformInfo.BuildContextPath))
.ToArray();
Expand Down
Loading

0 comments on commit 68a7510

Please sign in to comment.