tool/TeamCity.Docker/ConfigurationExplorer.cs (173 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 IoC; using Model; /// <summary> /// Locates configuration files for Docker Images. /// </summary> internal class ConfigurationExplorer : IConfigurationExplorer { [NotNull] private readonly ILogger _logger; [NotNull] private readonly IFileSystem _fileSystem; public ConfigurationExplorer( [NotNull] ILogger logger, [NotNull] IFileSystem fileSystem) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); } public Result<IEnumerable<Template>> Explore(string sourcePath, IEnumerable<string> configurationFiles) { if (sourcePath == null) { throw new ArgumentNullException(nameof(sourcePath)); } if (configurationFiles == null) { throw new ArgumentNullException(nameof(configurationFiles)); } var additionalVars = new Dictionary<string, string>(); using (_logger.CreateBlock("Explore")) { foreach (var configurationFile in configurationFiles) { if (!_fileSystem.IsFileExist(configurationFile)) { _logger.Log($"The configuration file \"{configurationFile}\" (\"{Path.GetFullPath(configurationFile)}\") does not exist.", Result.Error); return new Result<IEnumerable<Template>>(Enumerable.Empty<Template>()); } using (_logger.CreateBlock(configurationFile)) { additionalVars = UpdateVariables(additionalVars, GetVariables(configurationFile)); } } _logger.Log($"The configuration path is \"{sourcePath}\" (\"{Path.GetFullPath(sourcePath)}\")"); } return new Result<IEnumerable<Template>>(GetConfigurations(sourcePath, additionalVars)); } /// <summary> /// Generates "variants" - objects based on template Dockerfiles. /// </summary> /// <param name="sourcePath">path to folder with templates</param> /// <param name="additionalVars">Parameters for the substitution within Dockerfile template.</param> /// <returns>Tempalte objects</returns> private IEnumerable<Template> GetConfigurations([NotNull] string sourcePath, [NotNull] IReadOnlyDictionary<string, string> additionalVars) { if (sourcePath == null) { throw new ArgumentNullException(nameof(sourcePath)); } if (additionalVars == null) { throw new ArgumentNullException(nameof(additionalVars)); } var templateCounter = 0; foreach (var dockerfileTemplate in _fileSystem.EnumerateFileSystemEntries(sourcePath, "*.Dockerfile")) { var dockerfileTemplateRelative = Path.GetRelativePath(sourcePath, dockerfileTemplate); using (_logger.CreateBlock($"{++templateCounter:000} {dockerfileTemplateRelative}")) { var dockerfileTemplateDir = Path.GetDirectoryName(dockerfileTemplate) ?? "."; var dockerfileTemplatePath = Path.GetFileName(dockerfileTemplate); // ReSharper disable once IdentifierTypo var dockerignoreTemplatePath = Path.Combine(dockerfileTemplateDir, Path.GetFileNameWithoutExtension(dockerfileTemplatePath) + ".Dockerignore"); var variants = new List<Variant>(); var configCounter = 0; // Get all configuration files for particular OS (e.g. Ubuntu/20.04/..., Ubuntu/18.04/, ... foreach (var configFile in _fileSystem.EnumerateFileSystemEntries(dockerfileTemplateDir, dockerfileTemplatePath + ".config")) { var buildPath = Path.GetDirectoryName(Path.GetRelativePath(sourcePath, configFile)) ?? ""; using (_logger.CreateBlock($"{templateCounter:000}.{++configCounter:000} {Path.GetRelativePath(sourcePath, configFile)}")) { var vars = UpdateVariables(additionalVars, GetVariables(configFile)); variants.Add(new Variant(buildPath, configFile, vars.Select(i => new Variable(i.Key, i.Value)).ToList())); } } var ignore = new List<string>(); if (_fileSystem.IsFileExist(dockerignoreTemplatePath)) { // Add .Dockerignore files ignore.AddRange(_fileSystem.ReadLines(dockerignoreTemplatePath)); } yield return new Template(_fileSystem.ReadLines(dockerfileTemplate).ToImmutableList(), variants.AsReadOnly(), ignore.AsReadOnly()); } } } private IReadOnlyDictionary<string, string> GetVariables([NotNull] string configFile) { if (configFile == null) { throw new ArgumentNullException(nameof(configFile)); } var vars = new Dictionary<string, string>(); foreach (var line in _fileSystem.ReadLines(configFile)) { var text = line.Trim(); if (text.StartsWith('#') || text.Length < 3) { continue; } var eq = text.IndexOf('='); if (eq < 1) { continue; } var key = text.Substring(0, eq); var val = text.Substring(eq + 1); vars[key] = val; _logger.Details($"SET {key}={val}"); } return vars; } private Dictionary<string, string> UpdateVariables([NotNull] IReadOnlyDictionary<string, string> variables, [NotNull] IReadOnlyDictionary<string, string> newVariables) { if (variables == null) { throw new ArgumentNullException(nameof(variables)); } if (newVariables == null) { throw new ArgumentNullException(nameof(newVariables)); } var result = new Dictionary<string, string>(variables); foreach (var (key, value) in newVariables) { if (result.ContainsKey(key)) { _logger.Details($"UPDATE {key}={value}"); result[key] = value; } else { _logger.Details($"SET {key}={value}"); result.Add(key, value); } } var replacements = new Dictionary<string, string>(); var iterations = 0; do { replacements.Clear(); foreach (var (newKey, newValue) in result) { foreach (var (key, value) in result) { var replacement = "${" + newKey + "}"; if (value.Contains(replacement)) { replacements[key] = value.Replace(replacement, newValue); } } } foreach (var (key, value) in replacements) { if (result.ContainsKey(key)) { _logger.Details($"UPDATE {key}={value}"); result[key] = value; } else { _logger.Details($"SET {key}={value}"); result.Add(key, value); } } } while (replacements.Count > 0 && iterations++ < 100); return result; } } }