tool/TeamCity.Docker/DockerGraphFactory.cs (40 lines of code) (raw):
// ReSharper disable ClassNeverInstantiated.Global
namespace TeamCity.Docker
{
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Generic;
using IoC;
using Model;
internal class DockerGraphFactory: IFactory<IGraph<IArtifact, Dependency>, IEnumerable<Template>>
{
private static readonly Regex ReferenceRegex = new Regex(@"^\s*(?<reference>.+?)(\s+(?<weight>\d+)|)$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Dependency GenerateDependency = new Dependency(DependencyType.Generate);
private const string CommentPrefix = "##";
private const string IdPrefix = "# Id ";
private const string TagPrefix = "# Tag ";
private const string PlatformPrefix = "# Platform ";
private const string BasedOnPrefix = "# Based on ";
private const string RepoPrefix = "# Repo ";
private const string WeightPrefix = "# Weight ";
private const string RequiresPrefix = "# Requires ";
// Indicates if tool must be reflected within installation, e.g. ...
// ... "@@AddToolToDoc [${dotnetLinuxARM64ComponentName_50}](${dotnetLinuxARM64Component_50})"
private const string ComponentsPrefix = "# @AddToolToDoc ";
private readonly IContentParser _contentParser;
private readonly IPathService _pathService;
public DockerGraphFactory(
[NotNull] IContentParser contentParser,
[NotNull] IPathService pathService)
{
_contentParser = contentParser ?? throw new ArgumentNullException(nameof(contentParser));
_pathService = pathService ?? throw new ArgumentNullException(nameof(pathService));
}
/// <summary>
/// Generates Dockerfile out of given template.
/// </summary>
/// <param name="templates">parsed template files (e.g. from "configs/linux/*.Dockerfile")</param>
public Result<IGraph<IArtifact, Dependency>> Create(IEnumerable<Template> templates)
{
var graph = new Graph<IArtifact, Dependency>();
var nodeDict = new Dictionary<string, INode<IArtifact>>();
foreach (var template in templates)
{
// "variants" are different Dockerfile.config files for the template
foreach (var variant in template.Variants)
{
var lines = _contentParser.Parse(template.Lines, variant.Variables).ToImmutableList();
var imageId = "unknown";
var tags = new List<string>();
var platform = string.Empty;
var references = new List<Reference>();
var components = new List<string>();
var repositories = new List<string>();
var comments = new List<string>();
var dockerfileLines = new List<Line>();
var weight = 0;
var requirements = new List<Requirement>();
foreach (var line in lines)
{
var isMetadata = false;
if (line.Type == LineType.Comment)
{
isMetadata =
TrySetByPrefix(line.Text, CommentPrefix, value => comments.Add(value.Trim())) ||
TrySetByPrefix(line.Text, IdPrefix, value => imageId = value) ||
TrySetByPrefix(line.Text, TagPrefix, value => tags.Add(value)) ||
TrySetByPrefix(line.Text, PlatformPrefix, value => platform = value) ||
TrySetByPrefix(line.Text, BasedOnPrefix, value =>
{
var match = ReferenceRegex.Match(value);
if (match.Success)
{
var weightValue = 0;
if (int.TryParse(match.Groups["weight"].Value, out var refWeightValue))
{
weightValue = refWeightValue;
}
references.Add(new Reference(match.Groups["reference"].Value, new Weight(weightValue)));
}
}) ||
TrySetByPrefix(line.Text, ComponentsPrefix, value => components.Add(value)) ||
TrySetByPrefix(line.Text, RepoPrefix, value => repositories.Add(value)) ||
TrySetByPrefix(line.Text, WeightPrefix, value =>
{
if (int.TryParse(value, out var weightValue))
{
weight = weightValue;
}
}) ||
TrySetByPrefix(line.Text, RequiresPrefix, expression =>
{
var values = expression.Split(" ");
if (values.Length >= 2 && Enum.TryParse<RequirementType>(values[1], true, out var requirementType))
{
// todo: simplify/improve this code, my C#-fu is poor
if (values.Length > 2)
{
if (values.Length > 3)
{
requirements.Add(new Requirement(values[0], requirementType, values[2] + " " + values[3]));
}
else
{
requirements.Add(new Requirement(values[0], requirementType, values[2]));
}
}
else
{
requirements.Add(new Requirement(values[0], requirementType, string.Empty));
}
}
});
}
if (!isMetadata)
{
dockerfileLines.Add(line);
}
}
var dirName = Path.GetDirectoryName(variant.ConfigFile) ?? string.Empty;
var description = Path.GetFileName(dirName);
var dockerfile = new Dockerfile(
_pathService.Normalize(variant.BuildPath),
imageId,
platform,
description,
tags.Where(i => !string.IsNullOrWhiteSpace(i)).ToList(),
components.Where(i => !string.IsNullOrWhiteSpace(i)).ToList(),
repositories.Where(i => !string.IsNullOrWhiteSpace(i)).ToList(),
comments,
references,
new Weight(weight),
dockerfileLines,
template.Ignore,
requirements);
if (graph.TryAddNode(new Image(dockerfile), out var dockerImageNode))
{
foreach (var tag in tags)
{
nodeDict[$"{imageId}:{tag}"] = dockerImageNode;
}
if (graph.TryAddNode(new GeneratedDockerfile(_pathService.Normalize(Path.Combine(dockerfile.Path, "Dockerfile")), dockerfile.Lines), out var dockerfileNode))
{
graph.TryAddLink(dockerImageNode, GenerateDependency, dockerfileNode, out _);
}
}
}
}
var imageNodes =
from node in graph.Nodes
let image = node.Value as Image
where image != null
select new {node, image};
// Add references
foreach (var from in imageNodes.ToList())
{
foreach (var reference in from.image.File.References)
{
if (nodeDict.TryGetValue(reference.RepoTag, out var toNode))
{
graph.TryAddLink(from.node, new Dependency(DependencyType.Build), toNode, out _);
}
else
{
if (graph.TryAddNode(reference, out var referenceNode))
{
nodeDict[reference.RepoTag] = referenceNode;
}
graph.TryAddLink(from.node, new Dependency(DependencyType.Pull), referenceNode, out _);
}
}
}
return new Result<IGraph<IArtifact, Dependency>>(graph);
}
private static bool TrySetByPrefix([NotNull] string text, [NotNull] string prefix, Action<string> setter)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
if (prefix == null)
{
throw new ArgumentNullException(nameof(prefix));
}
if (text.StartsWith(prefix))
{
setter(text.Substring(prefix.Length).Trim());
return true;
}
return false;
}
}
}