// ReSharper disable ClassNeverInstantiated.Global
namespace TeamCity.Docker
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Generic;
using IoC;
using Model;
using Constants;
///
/// DEPRECATION NOTICE: The use of Kotlin DSL generator had been removed from TeamCity Delivery Pipeline.
///
internal class TeamCityKotlinSettingsGenerator : IGenerator
{
private const string MinDockerVersion = "18.05.0";
private const string BuildNumberParam = "dockerImage.teamcity.buildNumber";
private readonly string _buildNumberPattern = $"buildNumberPattern=\"%{BuildNumberParam}%-%build.counter%\"";
private const string RemoveManifestsScript = "\"\"if exist \"%%USERPROFILE%%\\.docker\\manifests\\\" rmdir \"%%USERPROFILE%%\\.docker\\manifests\\\" /s /q\"\"";
// Apart from other conditions, the optional flag for ARM-based Docker Image allows to enable them for investigation / development purposes.
private const bool IsArmBasedImageBuildEnabled = false;
[NotNull] private readonly string BuildRepositoryName = "%docker.buildRepository%";
[NotNull] private readonly string BuildImagePostfix = "%docker.buildImagePostfix%";
[NotNull] private readonly string DeployRepositoryName = "%docker.deployRepository%";
[NotNull] private readonly IGenerateOptions _options;
[NotNull] private readonly IFactory>, IGraph> _buildGraphsFactory;
[NotNull] private readonly IPathService _pathService;
[NotNull] private readonly IBuildPathProvider _buildPathProvider;
[NotNull] private readonly IFactory>> _nodesDescriptionsFactory;
public TeamCityKotlinSettingsGenerator(
[NotNull] IGenerateOptions options,
[NotNull] IFactory>, IGraph> buildGraphsFactory,
[NotNull] IPathService pathService,
[NotNull] IBuildPathProvider buildPathProvider,
[NotNull] IFactory>> nodesDescriptionFactory)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_buildGraphsFactory = buildGraphsFactory ?? throw new ArgumentNullException(nameof(buildGraphsFactory));
_pathService = pathService ?? throw new ArgumentNullException(nameof(pathService));
_buildPathProvider = buildPathProvider ?? throw new ArgumentNullException(nameof(buildPathProvider));
_nodesDescriptionsFactory = nodesDescriptionFactory ?? throw new ArgumentNullException(nameof(nodesDescriptionFactory));
}
///
/// Generates the following TeamCity Build Configurations (KotlinDSL) responsible for:
/// - 1. Build and push to local registry. Name pattern: "PushLocal*.kts".
/// - 2. Publishing docker manifests into registry. Name pattern: "PublishHub*.kts".
/// - 3. Pushing into Dockerhub. Name pattern: "PushHub*.kts"
///
/// graph - RDF graph containing description of target TeamCity build chain.
///
public void Generate(IGraph graph)
{
if (graph == null) throw new ArgumentNullException(nameof(graph));
if (string.IsNullOrWhiteSpace(_options.TeamCityDslPath))
{
return;
}
if (string.IsNullOrWhiteSpace(_options.TeamCityBuildConfigurationId))
{
return;
}
var buildGraphResult = _buildGraphsFactory.Create(graph);
if (buildGraphResult.State == Result.Error)
{
return;
}
var names = new Dictionary();
var buildGraphs = (
from buildGraph in buildGraphResult.Value
let weight = buildGraph.Nodes.Select(i => i.Value.Weight.Value).Sum()
let nodesDescription = _nodesDescriptionsFactory.Create(buildGraph.Nodes)
orderby nodesDescription.State != Result.Error ? nodesDescription.Value.Name : string.Empty
select new { graph = buildGraph, weight })
.ToList();
var localBuildTypes = new List();
var hubBuildTypes = new List();
var allImages = buildGraphs
.SelectMany(i => i.graph.Nodes.Select(j => j.Value).OfType())
.Where(i => i.File.Repositories.Any())
// ARM-based Docker Images Are not currently supported by TeamCity.
.Where(i => IsArmBasedImageBuildEnabled || i.File.Tags.First().IndexOf("arm", StringComparison.OrdinalIgnoreCase) < 0)
.ToList();
// Build and push on local registry
var buildAndPushLocalBuildTypes = new List();
foreach (var buildGraph in buildGraphs)
{
var path = _buildPathProvider.GetPath(buildGraph.graph).ToList();
var description = _nodesDescriptionsFactory.Create(path);
var name = description.State != Result.Error
? description.Value.Name
: string.Join("", path.Select(i => i.Value).OfType().Select(i => i.File.Platform).Distinct().OrderBy(i => i));
if (names.TryGetValue(name, out var counter))
{
name = $"{name} {++counter}";
}
else
{
names[name] = 1;
}
var buildTypeId = $"push_local_{NormalizeName(name)}";
var onPause = !path.Select(i => i.Value).OfType().Any(i => i.File.Repositories.Any());
localBuildTypes.Add(buildTypeId);
if (!onPause)
{
buildAndPushLocalBuildTypes.Add(buildTypeId);
}
graph.TryAddNode(AddFile(buildTypeId, GenerateBuildAndPushType(buildTypeId, name, path, buildGraph.weight, onPause)), out _);
}
// Publish on staging registry
var localPublishGroups =
from image in allImages
from tag in image.File.Tags.Skip(1)
group image by tag;
var publishLocalId = "publish_local";
localBuildTypes.Add(publishLocalId);
graph.TryAddNode(AddFile(publishLocalId, CreateManifestBuildConfiguration(publishLocalId, BuildRepositoryName, "Publish", localPublishGroups.ToList(), BuildImagePostfix, true, buildAndPushLocalBuildTypes.ToArray())), out _);
// Push on docker hub
var pushOnHubBuildTypes = new List();
var platforms = allImages.Select(i => i.File.Platform).Distinct();
foreach (var platform in platforms)
{
var buildTypeId = $"push_hub_{NormalizeName(platform)}";
pushOnHubBuildTypes.Add(buildTypeId);
graph.TryAddNode(AddFile(buildTypeId, CreatePushBuildConfiguration(buildTypeId, platform, allImages, publishLocalId)), out _);
}
hubBuildTypes.AddRange(pushOnHubBuildTypes);
// Publish on docker hub
var publishOnHubBuildTypes = new List();
var publishOnHubGroups =
from grp in
from image in allImages
from tag in image.File.Tags.Skip(1)
group image by tag
group grp by grp.Key.ToLowerInvariant() == "latest" ? "latest" : "version";
foreach (var group in publishOnHubGroups)
{
string publishToDockerhubBuildId = $"publish_hub_{NormalizeName(group.Key)}";
publishOnHubBuildTypes.Add(publishToDockerhubBuildId);
graph.TryAddNode(AddFile(publishToDockerhubBuildId,CreateManifestBuildConfiguration(publishToDockerhubBuildId,
DeployRepositoryName, $"Publish as {group.Key}",
group.ToList(), string.Empty, false,
pushOnHubBuildTypes.ToArray())), out _);
}
hubBuildTypes.AddRange(publishOnHubBuildTypes);
// Validation of Docker Image Size
const string validationBuildTypeId = "image_validation";
graph.TryAddNode(AddFile(validationBuildTypeId, CreateImageValidationConfig(validationBuildTypeId, allImages, BuildImagePostfix)), out _);
localBuildTypes.Add(validationBuildTypeId);
// Local project
// ReSharper disable once UseObjectOrCollectionInitializer
var lines = new List();
lines.Add("object LocalProject : Project({");
lines.Add("\t name = \"Staging registry\"");
foreach (var buildType in localBuildTypes.Distinct())
{
lines.Add($"\t buildType({NormalizeFileName(buildType)}.{buildType})");
}
lines.Add("})");
graph.TryAddNode(AddFile("LocalProject", lines), out _);
lines.Clear();
// Hub project
lines.Add("object HubProject : Project({");
lines.Add("\t name = \"Docker hub\"");
foreach (var buildType in hubBuildTypes.Distinct())
{
lines.Add($"\t buildType({NormalizeFileName(buildType)}.{buildType})");
}
lines.Add("})");
graph.TryAddNode(AddFile("HubProject", lines), out _);
lines.Clear();
}
///
/// Adds imports on top of the Build Configuration (Kotlin DSL) file.
///
/// target KotlinDSL file
/// import lines to be added. Please, check method's body to see pre-defined importing packages.
///
private FileArtifact AddFile(string fileName, IEnumerable lines)
{
var curLines = new List
{
"// NOTE: THIS IS AN AUTO-GENERATED FILE. IT HAD BEEN CREATED USING TEAMCITY.DOCKER PROJECT. ...",
"// ... IF NEEDED, PLEASE, EDIT DSL GENERATOR RATHER THAN THE FILES DIRECTLY. ... ",
"// ... FOR MORE DETAILS, PLEASE, REFER TO DOCUMENTATION WITHIN THE REPOSITORY.",
"package generated",
string.Empty,
"import jetbrains.buildServer.configs.kotlin.v2019_2.*",
"import jetbrains.buildServer.configs.kotlin.v2019_2.ui.*",
"import jetbrains.buildServer.configs.kotlin.v2019_2.vcs.GitVcsRoot",
// ReSharper disable once StringLiteralTypo
"import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.swabra",
"import common.TeamCityDockerImagesRepo.TeamCityDockerImagesRepo",
// -- build features
"import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.dockerSupport",
"import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.freeDiskSpace",
// -- Failure Conditions
"import jetbrains.buildServer.configs.kotlin.v2019_2.failureConditions.BuildFailureOnText",
"import jetbrains.buildServer.configs.kotlin.v2019_2.failureConditions.failOnText",
"import jetbrains.buildServer.configs.kotlin.v2019_2.failureConditions.BuildFailureOnMetric",
"import jetbrains.buildServer.configs.kotlin.v2019_2.failureConditions.failOnMetricChange",
// -- Build Steps
"import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.kotlinFile",
"import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.gradle",
"import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.script",
"import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.dockerCommand",
// -- All Triggers
"import jetbrains.buildServer.configs.kotlin.v2019_2.Trigger",
"import jetbrains.buildServer.configs.kotlin.v2019_2.triggers.VcsTrigger",
"import jetbrains.buildServer.configs.kotlin.v2019_2.triggers.finishBuildTrigger",
"import jetbrains.buildServer.configs.kotlin.v2019_2.triggers.vcs",
string.Empty
};
// ReSharper disable once StringLiteralTypo
curLines.AddRange(lines);
return new FileArtifact(_pathService.Normalize(Path.Combine(_options.TeamCityDslPath, NormalizeFileName(fileName) + ".kts")), curLines);
}
private string NormalizeFileName(string fileName) => new string(FixFileName(fileName).ToArray());
private IEnumerable FixFileName(IEnumerable chars)
{
var first = true;
foreach (var c in chars)
{
if (c == '_')
{
first = true;
continue;
}
if (first)
{
yield return char.ToUpper(c);
}
else
{
yield return c;
}
first = false;
}
}
///
/// Creates KotlinDSL's TeamCity build configuration for the creation and uploading of TeamCity's Docker images.
///
/// ID of build configuration within TeamCity
/// target platform for the images (e.g. specific distributive of Linux, Windows)
/// list of Docker images
/// types of TeamCity builds (e.g. publish_local - the naming is up to user)
///
private IEnumerable CreatePushBuildConfiguration(string buildTypeId, string platform, IEnumerable allImages, params string[] buildBuildTypes)
{
var images = allImages.Where(i => i.File.Platform == platform).ToList();
yield return $"object {buildTypeId}: BuildType(" + "{";
yield return $"\t name = \"Push {platform}\"";
yield return $"\t{_buildNumberPattern}";
yield return "\t steps {";
foreach (var image in images)
{
// docker pull
var tag = image.File.Tags.First();
var repo = $"{image.File.ImageId}{BuildImagePostfix}:{tag}";
var repoTag = $"{BuildRepositoryName}{repo}";
foreach (var pullCommand in CreatePullCommand(repoTag, repo))
{
yield return $"\t\t{pullCommand}";
}
var newRepo = $"{DeployRepositoryName}{image.File.ImageId}";
var newRepoTag = $"{newRepo}:{image.File.Tags.First()}";
foreach (var tagCommand in CreateTagCommand(repoTag, newRepoTag, repo))
{
yield return $"\t\t{tagCommand}";
}
foreach (var pushCommand in CreatePushCommand($"{newRepo}", repo, tag))
{
yield return $"\t\t{pushCommand}";
}
}
yield return "\t }";
yield return "\t features {";
var weight = images.Sum(i => i.File.Weight.Value);
if (weight > 0)
{
foreach (var feature in CreateFreeDiskSpaceFeature(weight))
{
yield return $"\t\t {feature}";
}
}
foreach (var feature in CreateDockerFeature())
{
yield return $"\t\t {feature}";
}
// ReSharper disable once StringLiteralTypo
foreach (var feature in CreateSwabraFeature())
{
yield return $"\t\t {feature}";
}
yield return "\t }";
foreach (var param in CreateSpaceParams(weight))
{
yield return $"\t{param}";
}
var requirements = images.SelectMany(i => i.File.Requirements).Distinct().ToList();
foreach (var lines in CreateDockerRequirements(requirements, platform))
{
yield return $"\t {lines}";
}
foreach (var dependencies in CreateSnapshotDependencies(buildBuildTypes, null))
{
yield return $"\t{dependencies}";
}
yield return "})";
yield return string.Empty;
}
///
/// Generates Kotlin DSL file with build configuration for post-push Docker image check.
/// A post-push validation build had been done for the purpose of lower cost for failure within build chain.
///
/// Identifier the the creating build configuration.
/// Images that will be checked in context of build configuration.
/// (might be empty) postfix to image repository name, e.g.: "-staging"
private IEnumerable CreateImageValidationConfig(string buildTypeId, IEnumerable allImages, string imagePostfix) {
yield return $"object {buildTypeId}: BuildType(" + "{";
yield return "\t name = \"Validation of Size Regression - Staging Docker Images (Windows / Linux)\"";
yield return $"\t {_buildNumberPattern}";
// VCS Root Is needed in order to launch automation framework
yield return String.Join('\n',
"\t vcs {",
"\t\t root(TeamCityDockerImagesRepo)",
"\t }"
);
// Trigger is needed to execute the image once they've been published into Dockerhub
yield return String.Join('\n',
"\n\t triggers {",
"\t\t // Execute the build once the images are available within %deployRepository%",
"\t\t finishBuildTrigger {",
"\t\t\t buildType = \"${PublishHubVersion.publish_hub_version.id}\"",
"\t\t\t // if filter won't be specified, only branch would be included",
"\t\t\t branchFilter = \"\"\"",
"\t\t\t\t +:development/*",
"\t\t\t\t +:release/*",
"\t\t\t \"\"\".trimIndent()",
"\t\t }",
"\t }"
);
// Parameters are needed in order to prevent unnecessarry dependency from an inherited parameter
yield return String.Join('\n',
"\n\t params {",
"\t\t // -- inherited parameter, removed in debug purposes",
"\t\t param(\"dockerImage.teamcity.buildNumber\", \"-\")",
"\t }"
);
// -- Declare a set of images that we'd need to iterate over
// -- containing elements for Kotlin hashmap declaration: ...
// -- ... -
List validationHashmapEntries = new List();
foreach (var image in allImages) {
var newRepo = $"{DeployRepositoryName}{image.File.ImageId}{imagePostfix}";
var newRepoTag = $"{newRepo}:{image.File.Tags.First()}";
// Add as as Kotlin DSL list element
// -- hashmap: ,
validationHashmapEntries.Add($"\"{image.File.ImageId}-{image.File.Tags.First()}\" to \"{newRepoTag}\"");
}
// -- Create declaration of HashMap within DSL which will be used for generation of step "{ ... }" blocks
yield return String.Join('\n',
"\n\t val targetImages: HashMap = hashMapOf(",
// concatenate previously created hashmap entries for the declaration within DSL
String.Join(", \n\t\t", validationHashmapEntries),
"\t )"
);
// Generate steps in order to validate the images within the list above
yield return String.Join('\n',
"\n\t steps {",
"\t\t targetImages.forEach { (imageVerificationStepId, imageDomainName) ->",
"\t\t // Generate validation for each image fully-qualified domain name (FQDN)",
"\t\t gradle {",
"\t\t\t name = \"Image Verification - $imageVerificationStepId\"",
// "%docker.buildRepository.login% %docker.stagingRepository.token%" are defined within TeamCity server
"\t\t\t tasks = \"clean build run --args=\\\"validate $imageDomainName %docker.stagingRepository.login% %docker.stagingRepository.token%\\\"\"",
"\t\t\t workingDir = \"tool/automation/framework\"",
"\t\t\t buildFile = \"build.gradle\"",
"\t\t\t jdkHome = \"%env.JDK_11_x64%\"",
"\t\t\t executionMode = BuildStep.ExecutionMode.ALWAYS",
"\t\t\t }",
"\t\t }",
"\t }"
);
// Generate failure conditions
yield return String.Join('\n',
"\n\t failureConditions {",
"\t\t // Failed in case the validation via framework didn't succeed",
"\t\t failOnText {",
"\t\t\t conditionType = BuildFailureOnText.ConditionType.CONTAINS",
"\t\t\t pattern = \"DockerImageValidationException\"",
"\t\t\t failureMessage = \"Docker Image validation have failed\"",
"\t\t\t // allows the steps to continue running even in case of one problem",
"\t\t\t reportOnlyFirstMatch = false",
"\t\t }",
"\t }"
);
// Generate requirements
yield return String.Join('\n',
"\n\t requirements {",
"\t\t exists(\"env.JDK_11\")",
"\t\t // Images are validated mostly via DockerHub REST API. In case ...",
"\t\t // ... Docker agent will be used, platform-compatibility must be addressed, ...",
"\t\t // ... especially in case of Windows images.",
"\t\t contains(\"teamcity.agent.jvm.os.name\", \"Linux\")",
"\t }"
);
// Generate build features
yield return String.Join('\n',
"\n\t features {",
"\t\t dockerSupport {",
"\t\t\t cleanupPushedImages = true",
"\t\t\t loginToRegistry = on {",
"\t\t\t dockerRegistryId = \"PROJECT_EXT_774,PROJECT_EXT_315\"",
"\t\t\t }",
"\t\t }",
"\t }"
);
// Generation of build configuration is deprecated, thus no dependencies are set.
yield return String.Join('\n',
"\n\t dependencies {",
"\t}"
);
yield return "})";
yield return string.Empty;
}
///
/// Generates TeamCity build configuration (Kotlin DSL) for publishing of Docker image manifests.
///
/// creating build ID
/// target repository for image publishing
/// build name
/// list of Docker images
/// postfix that should be appended to the tags of all images
/// indicates if the build is being created for staging purposes
/// dependencies of the build (other TeamCity build configuration, if any)
private IEnumerable CreateManifestBuildConfiguration(string buildTypeId, string repositoryName, string name, IReadOnlyCollection> images, string imagePostfix, bool? onStaging, params string[] dependencies)
{
yield return $"object {buildTypeId}: BuildType(" + "{";
yield return $"\t name = \"{name}\"";
yield return $"\t{_buildNumberPattern}";
yield return "\t enablePersonalBuilds = false";
yield return "\t type = BuildTypeSettings.Type.DEPLOYMENT";
yield return "\t maxRunningBuilds = 1";
yield return "\t steps {";
foreach (var line in AddScript("remove manifests", RemoveManifestsScript))
{
yield return $"\t\t {line}";
}
foreach (var group in images.OrderBy(i => i.Key))
{
var groupedByImageId = group.GroupBy(i => i.File.ImageId);
foreach (var groupByImageId in groupedByImageId)
{
foreach (var line in CreateManifestCommands(repositoryName, group.Key, groupByImageId.Key, imagePostfix, groupByImageId))
{
yield return $"\t{line}";
}
}
}
yield return "\t }";
foreach (var line in CreateSnapshotDependencies(dependencies, onStaging))
{
yield return $"\t{line}";
}
var requirements = images.SelectMany(i => i).SelectMany(i => i.File.Requirements).Distinct().ToList();
foreach (var lines in CreateDockerRequirements(requirements, "windows", MinDockerVersion))
{
yield return $"\t{lines}";
}
yield return "\t features {";
foreach (var line in CreateDockerFeature())
{
yield return $"\t\t {line}";
}
yield return "\t}";
yield return "})";
yield return string.Empty;
}
private static IEnumerable CreateDockerRequirements(IReadOnlyCollection requirements, string platform = "", string minDockerVersion = "")
{
yield return "requirements {";
if (!string.IsNullOrWhiteSpace(minDockerVersion))
{
yield return $"\t noLessThanVer(\"docker.version\", \"{minDockerVersion}\")";
}
if (!string.IsNullOrWhiteSpace(platform))
{
yield return $"\t contains(\"docker.server.osType\", \"{platform}\")";
}
if (!IsArmBasedImageBuildEnabled) {
yield return "\t// In order to correctly build AMD-based images, we wouldn't want it to be scheduled on ARM-based agent";
yield return $"\tdoesNotContain(\"teamcity.agent.name\", \"arm\")";
}
foreach (var requirement in requirements)
{
if (string.IsNullOrWhiteSpace(requirement.Value))
{
yield return $"\t {requirement.Type.ToString().ToLowerInvariant()}(\"{requirement.Property}\")";
}
else
{
yield return $"\t {requirement.Type.ToString().ToLowerInvariant()}(\"{requirement.Property}\", \"{requirement.Value}\")";
}
}
yield return "}";
}
private IEnumerable CreateManifestCommands(string repositoryName, string tag, string imageId, string imagePostfix, IEnumerable images)
{
if (string.IsNullOrWhiteSpace(tag) || !char.IsLetterOrDigit(tag[0]))
{
yield break;
}
var repo = $"{repositoryName}{imageId}{imagePostfix}";
var manifestName = $"{repo}:{tag}";
var createArgs = new List
{
"create",
manifestName
};
foreach (var image in images)
{
createArgs.Add($"{repo}:{image.File.Tags.First()}");
}
foreach (var line in CreateDockerCommand($"manifest create {imageId}:{tag}", "manifest", createArgs))
{
yield return line;
}
var pushArgs = new List
{
"push",
manifestName
};
foreach (var line in CreateDockerCommand($"manifest push {imageId}:{tag}", "manifest", pushArgs))
{
yield return line;
}
var inspectArgs = new List
{
"inspect",
manifestName,
"--verbose"
};
foreach (var line in CreateDockerCommand($"manifest inspect {imageId}:{tag}", "manifest", inspectArgs))
{
yield return line;
}
}
private IEnumerable GenerateBuildAndPushType(
string buildTypeId,
string name,
IReadOnlyCollection> path,
int weight,
bool onPause)
{
var images = path.Select(i => i.Value).OfType().Where(i => onPause || i.File.Repositories.Any()).ToList();
var references = path.Select(i => i.Value).OfType().ToList();
var groups =
from image in images
group image by image.File.ImageId
into groupsByImageId
from groupByImageId in
from image in groupsByImageId
group image by image.File
group groupByImageId by groupsByImageId.Key;
var description = new StringBuilder();
foreach (var grp in groups)
{
if (description.Length > 0)
{
description.Append(" ");
}
description.Append(grp.Key);
foreach (var dockerfile in grp)
{
var tags = string.Join(",", dockerfile.Key.Tags);
if (!string.IsNullOrWhiteSpace(tags))
{
description.Append(':');
description.Append(tags);
}
}
}
var pauseStr = onPause ? "ON PAUSE " : "";
yield return $"object {buildTypeId} : BuildType({{";
yield return $"\t name = \"{pauseStr}Build and push {name}\"";
yield return $"\t {_buildNumberPattern}";
yield return $"\t description = \"{description}\"";
if (!onPause)
{
yield return "\t vcs {";
yield return "\t\t root(TeamCityDockerImagesRepo)";
yield return "\t }";
yield return "\n \t steps {";
// "docker pull" command types @ steps { ... } block
foreach (var command in references.SelectMany(refer => CreatePullCommand(refer.RepoTag, refer.RepoTag)))
{
yield return $"\t\t{command}";
}
// "docker build" command types @ steps { ... } block
foreach (var command in images.SelectMany(image => CreatePrepareContextCommand(image).Concat(CreateBuildCommand(image))))
{
yield return $"\t\t{command}";
}
// "docker image tag" command types @ steps { ... } block
foreach (var image in images)
{
if (image.File.Tags.Any())
{
var tag = image.File.Tags.First();
// "tag" command
foreach (var tagCommand in CreateTagCommand($"{image.File.ImageId}:{tag}", $"{BuildRepositoryName}{image.File.ImageId}{BuildImagePostfix}:{tag}", $"{image.File.ImageId}:{tag}"))
{
yield return $"\t\t{tagCommand}";
}
}
}
// "docker push" command types @ steps { ... } block
foreach (var image in images)
{
var tag = image.File.Tags.First();
foreach (var pushCommand in CreatePushCommand($"{BuildRepositoryName}{image.File.ImageId}{BuildImagePostfix}", $"{image.File.ImageId}:{tag}", tag))
{
yield return $"\t\t{pushCommand}";
}
}
// end of steps {...} block
yield return "\t}";
yield return "\tfeatures {";
if (weight > 0)
{
foreach (var feature in CreateFreeDiskSpaceFeature(weight))
{
yield return $"\t\t{feature}";
}
}
foreach (var feature in CreateDockerFeature())
{
yield return $"\t\t{feature}";
}
// ReSharper disable once StringLiteralTypo
foreach (var feature in CreateSwabraFeature())
{
yield return $"\t\t{feature}";
}
// end of features { ... } block
yield return "\t}";
foreach (var dependencies in CreateArtifactsDependencies())
{
yield return $"\t{dependencies}";
}
foreach (var param in CreateSpaceParams(weight))
{
yield return $"\t{param}";
}
var requirements = images.SelectMany(i => i.File.Requirements).Distinct().ToList();
foreach (var lines in CreateDockerRequirements(requirements))
{
yield return $"\t{lines}";
}
}
yield return "})";
yield return string.Empty;
}
private static IEnumerable CreateSpaceParams(int weight)
{
if (weight > 0)
{
yield return "params {";
yield return $"\t param(\"system.teamcity.agent.ensure.free.space\", \"{weight}gb\")";
yield return "}";
}
}
private IEnumerable CreateSnapshotDependencies(IEnumerable dependencies, bool? onStaging)
{
yield return "\t dependencies {";
if (onStaging != null)
{
yield return $"\t\t snapshot(AbsoluteId(\"{_options.TeamCityBuildConfigurationId}\"))" + " {\n";
if (onStaging == true)
{
yield return "\t\t\t onDependencyFailure = FailureAction.FAIL_TO_START \n \t\t\t reuseBuilds = ReuseBuilds.ANY \n \t\t\t synchronizeRevisions = false \n \t\t }";
}
else
{
yield return "\t\t\t reuseBuilds = ReuseBuilds.ANY \n \t\t\t onDependencyFailure = FailureAction.IGNORE \n\t }";
}
}
foreach (var buildTypeId in dependencies.OrderBy(i => i))
{
yield return $"\t\t snapshot({NormalizeFileName(buildTypeId)}.{buildTypeId})" + " {\n";
yield return "\t\t\t onDependencyFailure = FailureAction.FAIL_TO_START \n \t\t }";
}
yield return "\t }";
}
///
/// Generates dependencies {...} block of Kotlin DSL pipeline. Within this scope, ...
/// ... it includes dependencies from builds responsible for the creation of Docker images.
///
private IEnumerable CreateArtifactsDependencies()
{
if(string.IsNullOrWhiteSpace(_options.TeamCityBuildConfigurationId))
{
yield break;
}
yield return "dependencies {";
yield return $"\t dependency(AbsoluteId(\"{_options.TeamCityBuildConfigurationId}\")) {{";
yield return "\t\t snapshot {";
yield return "\t\t\t onDependencyFailure = FailureAction.IGNORE";
yield return "\t\t\t reuseBuilds = ReuseBuilds.ANY";
yield return "\t\t }";
yield return "\t\t artifacts {";
yield return $"\t\t\t artifactRules = \"TeamCity.zip!/**=>{_pathService.Normalize(_options.ContextPath)}/TeamCity\"";
yield return "\t\t }";
yield return "\t }";
yield return "}";
}
///
/// Creates dependencies {...} block for build configuration responsible for post-push ...
/// ... validation of Docker images.
/// IDs of dependant build configuration
/// none
private IEnumerable CreateDockerImageValidationSnapDependencies(string[] dependencyIds) {
if (dependencyIds == null || dependencyIds.Length == 0) {
// dependency IDs must be specified, otherwise the block wouldn't be useful
yield break;
}
yield return "dependencies {";
foreach (string dependantBuildId in dependencyIds)
{
yield return $"\t dependency(AbsoluteId(\"{dependantBuildId}\")) {{";
// -- build problem is reported, but not termeinated, as some of the dependencies might successfully ...
// -- ... create images.
yield return "\t\t snapshot { onDependencyFailure = FailureAction.ADD_PROBLEM }";
// dependency {...}
yield return "\t }";
}
// dependencies {...}
yield return "}";
}
///
/// Creates failure conditions that terminated the build if an error message with given pattern had occurred.
///
/// Error pattern.
/// Indicates if the steps must continue execution even if after failure.
///
private IEnumerable CreateFailureConditionRegExpPattern(string pattern, bool reportOnlyFirstMatch = false) {
if (pattern == null) {
yield break;
}
yield return "failureConditions {";
yield return "\t failOnText {";
yield return $"\t\t conditionType = {TeamCityConstants.Conditions.REGEXP}";
yield return $"\t\t pattern = \"{pattern}\"";
yield return "\t\t // allows the steps to continue running even in case of one problem";
yield return $"\t\t reportOnlyFirstMatch = {reportOnlyFirstMatch.ToString().ToLower()}";
// end of "failOnText{...}
yield return "\t }";
// end of failureConditions {...}
yield return "}";
}
// ReSharper disable once IdentifierTypo
private static IEnumerable CreateSwabraFeature()
{
// ReSharper disable once StringLiteralTypo
yield return "swabra {";
yield return "\t forceCleanCheckout = true";
yield return "}";
}
private IEnumerable CreateDockerFeature()
{
if (string.IsNullOrWhiteSpace(_options.TeamCityDockerRegistryId))
{
yield break;
}
yield return "dockerSupport {";
yield return "\t cleanupPushedImages = true";
yield return "\t loginToRegistry = on {";
yield return $"\t\t dockerRegistryId = \"{_options.TeamCityDockerRegistryId}\"";
yield return "\t }";
yield return "}";
}
private static IEnumerable CreateFreeDiskSpaceFeature(int weight)
{
yield return "freeDiskSpace {";
yield return $"\t requiredSpace = \"{weight}gb\"";
yield return "\t failBuild = true";
yield return "}";
}
///
/// Constructs Kotlin DSL's dockerCommand {...} for image push.
///
/// Docker image ID
/// step name
/// target Docker image tags
private IEnumerable CreatePushCommand(string imageId, string name, params string[] tags)
{
yield return "dockerCommand {";
yield return $"\t {GetDockerStepStatusDsl(name)}";
yield return $"\t name = \"push {name}\"";
yield return "\t commandType = push {";
yield return "\t\t namesAndTags = \"\"\"";
foreach (var tag in tags)
{
yield return $"{imageId}:{tag}";
}
yield return "\"\"\".trimIndent()";
yield return "\t\t removeImageAfterPush = false";
yield return "\t }";
yield return "}";
yield return string.Empty;
}
///
/// Constructs Kotlin DSL's dockerCommand {...} for image re-tag.
///
/// original Docker image tag
/// target Docker image tag
/// step name
private IEnumerable CreateTagCommand(string repoTag, string newRepoTag, string name)
{
yield return "dockerCommand {";
yield return $"\t{GetDockerStepStatusDsl(repoTag)}";
yield return $"\t name = \"tag {name}\"";
yield return "\t commandType = other {";
yield return "\t\t subCommand = \"tag\"";
yield return $"\t\t commandArgs = \"{repoTag} {newRepoTag}\"";
yield return "\t}";
yield return "}";
yield return string.Empty;
}
///
/// Constructs Kotlin DSL's step for preparation to dockerCommand {...}, such as ...
/// ... the creation of .dockerignore, append of the entries into it.
///
/// info about Docker image
private IEnumerable CreatePrepareContextCommand(Image image)
{
var tag = image.File.Tags.First();
yield return "script {";
yield return $"\t{GetDockerStepStatusDsl(tag)}";
yield return $"\t name = \"context {image.File.ImageId}:{tag}\"";
yield return "\t scriptContent = \"\"\"";
// ReSharper disable once IdentifierTypo
// ReSharper disable once StringLiteralTypo
var dockerignore = Path.Combine(_options.ContextPath, ".dockerignore").Replace("\\", "/");
yield return $"echo 2> {dockerignore}";
foreach (var ignore in image.File.Ignores)
{
yield return $"echo {ignore} >> {dockerignore}";
}
yield return "\"\"\".trimIndent()";
yield return "}";
yield return string.Empty;
}
///
/// Constructs Kotlin DSL's dockerCommand {...} for image build.
///
/// info about Docker image
private IEnumerable CreateBuildCommand(Image image)
{
var tag = image.File.Tags.First();
yield return "dockerCommand {";
yield return $"{GetDockerStepStatusDsl(tag)}";
yield return $"\t name = \"build {image.File.ImageId}:{tag}\"";
yield return "\t commandType = build {";
yield return "\t\t source = file {";
yield return $"\t\t\t path = \"\"\"{_pathService.Normalize(Path.Combine(_options.TargetPath, image.File.Path, "Dockerfile"))}\"\"\"";
yield return "\t\t }";
yield return $"\t contextDir = \"{_pathService.Normalize(_options.ContextPath)}\"";
yield return "\t commandArgs = \"--no-cache\"";
yield return "\t namesAndTags = \"\"\"";
yield return $"{image.File.ImageId}:{tag}";
yield return "\"\"\".trimIndent()";
yield return "}";
yield return $"param(\"dockerImage.platform\", \"{image.File.Platform}\")";
yield return "}";
yield return string.Empty;
}
///
/// Constructs Kotlin DSL's dockerCommand {...} for Docker Image pull.
///
/// image's registry
/// image's tag
private static IEnumerable CreatePullCommand(string repoTag, string name)
{
yield return "dockerCommand {";
yield return $"\t {GetDockerStepStatusDsl(repoTag)}";
yield return $"\t name = \"pull {name}\"";
yield return "\t commandType = other {";
yield return $"\t\t subCommand = \"pull\"";
yield return $"\t\t commandArgs = \"{repoTag}\"";
yield return "\t }";
yield return "}";
yield return string.Empty;
}
private IEnumerable CreateDockerCommand(string name, string command, IEnumerable args)
{
yield return "dockerCommand {";
yield return $"\t name = \"{name}\"";
yield return "\t commandType = other {";
yield return $"\t\t subCommand = \"{command}\"";
yield return $"\t\t commandArgs = \"{string.Join(" ", args)}\"";
yield return "\t }";
yield return "}";
}
private static string NormalizeName(string name) =>
name
.Replace(' ', '_')
.Replace('-', '_')
.Replace("%", "")
.Replace(".", "_");
private static IEnumerable AddScript(string name, string script)
{
yield return "script {";
yield return $"\t name = \"{name}\"";
yield return $"\t scriptContent = \"{script}\"";
yield return "}";
}
///
/// Constructs Kotlin DSL instructions responsible for the determination of status for ...
/// ... dockerCommand { ... } build configuration step.
///
/// Docker Image ID
private static string GetDockerStepStatusDsl(string imageId) {
// -- ARM images are currently not supported
bool isArmImage = imageId.IndexOf("arm", StringComparison.OrdinalIgnoreCase) >= 0;
Console.WriteLine(imageId);
return (!isArmImage || IsArmBasedImageBuildEnabled)
? string.Empty
: "// ARM-based images are currently not supported by TeamCity \n"
+ "\t\t\tenabled = false";
}
}
}