Skip to content

Commit

Permalink
BaseKubernetesApplyExecutor - Composition over inheritance (#1317)
Browse files Browse the repository at this point in the history
Tidy up some Kube execution
  • Loading branch information
zentron authored Aug 19, 2024
1 parent 5f0892d commit 8242c8a
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 101 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Calamari.Common.Plumbing.FileSystem;
using Calamari.Kubernetes.Commands;
using Calamari.Kubernetes.Commands.Executors;
using Calamari.Tests.Fixtures.Integration.FileSystem;
using FluentAssertions;
using NUnit.Framework;

namespace Calamari.Tests.KubernetesFixtures.Commands.Executors
{
[TestFixture]
public class GlobberGroupingTests
{
readonly ICalamariFileSystem fileSystem = TestCalamariPhysicalFileSystem.GetPhysicalFileSystem();
string StagingDirectory => Path.Combine(tempDirectory, "staging");
string tempDirectory;
string PackageDirectory => Path.Combine(tempDirectory, "staging", KubernetesDeploymentCommandBase.PackageDirectoryName);

[Test]
public void FilesInDirectoryWithGlob_StageAllFilesInDirectory()
{
var dirName = "dirA";
var dirA = Path.Combine(PackageDirectory, dirName);
CreateTemporaryTestFile(dirA);
CreateTemporaryTestFile(dirA);

var globber = new GlobberGrouping(fileSystem);

var globDirectories = globber.Group(StagingDirectory, new List<string>(){ "dirA/*"});
globDirectories.Count().Should().Be(1);
fileSystem.EnumerateFiles(globDirectories[0].Directory, globDirectories[0].Glob).Count().Should().Be(2);
}

[Test]
public void GlobsAreSortedInOrderOfPattern()
{
CreateTemporaryTestFile( Path.Combine(PackageDirectory, "dirB"));
CreateTemporaryTestFile( Path.Combine(PackageDirectory, "dirA"));
CreateTemporaryTestFile( Path.Combine(PackageDirectory, "dirC"));

var globber = new GlobberGrouping(fileSystem);
var globDirectories = globber.Group(StagingDirectory, new List<string>(){"dirC/*", "dirB/*", "dirA/*"});

globDirectories.Select(d => d.Glob).Should().BeEquivalentTo(new[] { "dirC/*", "dirB/*", "dirA/*" });
}

[Test]
public void EmptyGlobPatternReturnsNoResults()
{
CreateTemporaryTestFile( Path.Combine(PackageDirectory, "dirB"));

var globber = new GlobberGrouping(fileSystem);
var globDirectories = globber.Group(StagingDirectory, new List<string>());

globDirectories.Should().BeEmpty();
}

void CreateTemporaryTestFile(string directory)
{
if (!Directory.Exists(directory))
Directory.CreateDirectory(directory);
var path = Path.Combine(directory, Guid.NewGuid() + ".tmp");
using (fileSystem.OpenFile(path, FileMode.OpenOrCreate, FileAccess.Read))
{
}
}


[SetUp]
public void Init()
{
tempDirectory = fileSystem.CreateTemporaryDirectory();
}

[TearDown]
public void Cleanup()
{
fileSystem.DeleteDirectory(tempDirectory, FailureOptions.IgnoreFailure);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#if !NET40
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Calamari.Common.Commands;
Expand Down Expand Up @@ -29,10 +28,6 @@ public async Task<bool> Execute(RunningDeployment deployment, Func<ResourceIdent
await ApplyAndGetResourceIdentifiers(deployment, appliedResourcesCallback);
return true;
}
catch (KubernetesApplyException)
{
return false;
}
catch (Exception e)
{
log.Error($"Deployment Failed due to exception: {e.Message}");
Expand All @@ -41,38 +36,15 @@ public async Task<bool> Execute(RunningDeployment deployment, Func<ResourceIdent
}

protected abstract Task<IEnumerable<ResourceIdentifier>> ApplyAndGetResourceIdentifiers(RunningDeployment deployment, Func<ResourceIdentifier[], Task> appliedResourcesCallback = null);

protected void CheckResultForErrors(CommandResultWithOutput commandResult, string directory)
{
var directoryWithTrailingSlash = directory + Path.DirectorySeparatorChar;

foreach (var message in commandResult.Output.Messages)
{
switch (message.Level)
{
case Level.Info:
//No need to log as it's the output json from above.
break;
case Level.Error:
//Files in the error are shown with the full path in their batch directory,
//so we'll remove that for the user.
log.Error(message.Text.Replace($"{directoryWithTrailingSlash}", ""));
break;
default:
throw new ArgumentOutOfRangeException();
}
}

protected IEnumerable<ResourceIdentifier> ProcessKubectlCommandOutput(RunningDeployment deployment, CommandResultWithOutput commandResult, string directory)
{
commandResult.LogErrorsWithSanitizedDirectory(log, directory);
if (commandResult.Result.ExitCode != 0)
{
throw new KubernetesApplyException();
throw new KubectlException("Command Failed");
}
}

protected IEnumerable<ResourceIdentifier> ProcessKubectlCommandOutput(RunningDeployment deployment, CommandResultWithOutput commandResult, string directory)
{
CheckResultForErrors(commandResult, directory);

// If it did not error, the output should be valid json.
var outputJson = commandResult.Output.InfoLogs.Join(Environment.NewLine);
try
Expand All @@ -99,8 +71,7 @@ protected IEnumerable<ResourceIdentifier> ProcessKubectlCommandOutput(RunningDep
catch
{
LogParsingError(outputJson, deployment.Variables.GetFlag(SpecialVariables.PrintVerboseKubectlOutputOnError));

throw new KubernetesApplyException();
throw new KubectlException("Log Parsing Error");
}
}

Expand All @@ -121,10 +92,6 @@ void LogParsingError(string outputJson, bool logKubectlOutputOnError)
log.Error("See https://github.com/kubernetes/kubernetes/issues/58834 for more details.");
log.Error("Custom resources will not be saved as output variables.");
}

protected class KubernetesApplyException : Exception
{
}

class ResourceMetadata
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#if !NET40
using System;
using System.IO;
using Calamari.Common.Plumbing.Logging;
using Calamari.Kubernetes.Integration;

namespace Calamari.Kubernetes.Commands.Executors
{
public static class CommandResultWithOutputExtensionMethods
{
public static void LogErrorsWithSanitizedDirectory(this CommandResultWithOutput commandResult, ILog log, string directory)
{
var directoryWithTrailingSlash = directory + Path.DirectorySeparatorChar;

foreach (var message in commandResult.Output.Messages)
{
switch (message.Level)
{
case Level.Info:
//No need to log as it's the output json from above.
break;
case Level.Error:
//Files in the error are shown with the full path in their batch directory,
//so we'll remove that for the user.
log.Error(message.Text.Replace($"{directoryWithTrailingSlash}", ""));
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}

}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,9 @@ namespace Calamari.Kubernetes.Commands.Executors
public interface IRawYamlKubernetesApplyExecutor : IKubernetesApplyExecutor
{
}

class GatherAndApplyRawYamlExecutor : BaseKubernetesApplyExecutor, IRawYamlKubernetesApplyExecutor
{
const string GroupedDirectoryName = "grouped";

readonly ILog log;
readonly ICalamariFileSystem fileSystem;
readonly Kubectl kubectl;
Expand All @@ -43,12 +41,16 @@ protected override async Task<IEnumerable<ResourceIdentifier>> ApplyAndGetResour

if (globs.IsNullOrEmpty())
return Enumerable.Empty<ResourceIdentifier>();

var directories = GroupFilesIntoDirectories(deployment, globs, variables);


var globberGrouping = new GlobberGrouping(fileSystem);
var globDirectories = globberGrouping.Group(deployment.CurrentDirectory, globs);
variables.Set(SpecialVariables.GroupedYamlDirectories, string.Join(";", globDirectories.Select(d => d.Directory)));

var resourcesIdentifiers = new HashSet<ResourceIdentifier>();
foreach (var directory in directories)
for (int i = 0; i < globDirectories.Count(); i++)
{
var directory = globDirectories[i];
log.Info($"Applying Batch #{i+1} for YAML matching '{directory.Glob}'");
var res = ApplyBatchAndReturnResourceIdentifiers(deployment, directory).ToArray();

if (appliedResourcesCallback != null)
Expand All @@ -62,80 +64,40 @@ protected override async Task<IEnumerable<ResourceIdentifier>> ApplyAndGetResour
return resourcesIdentifiers;
}

IEnumerable<GlobDirectory> GroupFilesIntoDirectories(RunningDeployment deployment, List<string> globs, IVariables variables)
IEnumerable<ResourceIdentifier> ApplyBatchAndReturnResourceIdentifiers(RunningDeployment deployment, GlobDirectory globDirectory)
{
var stagingDirectory = deployment.CurrentDirectory;
var packageDirectory =
Path.Combine(stagingDirectory, KubernetesDeploymentCommandBase.PackageDirectoryName) +
Path.DirectorySeparatorChar;

var directories = new List<GlobDirectory>();
for (var i = 1; i <= globs.Count; i ++)
{
var glob = globs[i-1];
var directoryPath = Path.Combine(stagingDirectory, GroupedDirectoryName, i.ToString());
var directory = new GlobDirectory(i, glob, directoryPath);
fileSystem.CreateDirectory(directoryPath);
if (!LogFoundFiles(globDirectory))
return Array.Empty<ResourceIdentifier>();

var results = fileSystem.EnumerateFilesWithGlob(packageDirectory, glob);
foreach (var file in results)
{
var relativeFilePath = fileSystem.GetRelativePath(packageDirectory, file);
var targetPath = Path.Combine(directoryPath, relativeFilePath);
var targetDirectory = Path.GetDirectoryName(targetPath);
if (targetDirectory != null)
{
fileSystem.CreateDirectory(targetDirectory);
}
fileSystem.CopyFile(file, targetPath);
}
string[] executeArgs = {"apply", "-f", $@"""{globDirectory.Directory}""", "--recursive", "-o", "json"};
executeArgs = executeArgs.AddOptionsForServerSideApply(deployment.Variables, log);

directories.Add(directory);
}
var result = kubectl.ExecuteCommandAndReturnOutput(executeArgs);

variables.Set(SpecialVariables.GroupedYamlDirectories,
string.Join(";", directories.Select(d => d.Directory)));
return directories;
return ProcessKubectlCommandOutput(deployment, result, globDirectory.Directory);
}

IEnumerable<ResourceIdentifier> ApplyBatchAndReturnResourceIdentifiers(RunningDeployment deployment, GlobDirectory globDirectory)
/// <summary>
/// Logs files that are found at the relevant glob locations.
/// </summary>
/// <param name="globDirectory"></param>
/// <returns>True if files are found, False if no files exist at this location</returns>
bool LogFoundFiles(GlobDirectory globDirectory)
{
var index = globDirectory.Index;
var directoryWithTrailingSlash = globDirectory.Directory + Path.DirectorySeparatorChar;
log.Info($"Applying Batch #{index} for YAML matching '{globDirectory.Glob}'");

var files = fileSystem.EnumerateFilesRecursively(globDirectory.Directory).ToArray();
if (!files.Any())
{
log.Warn($"No files found matching '{globDirectory.Glob}'");
return Enumerable.Empty<ResourceIdentifier>();
return false;
}

foreach (var file in files)
{
log.Verbose($"Matched file: {fileSystem.GetRelativePath(directoryWithTrailingSlash, file)}");
}

string[] executeArgs = {"apply", "-f", $@"""{globDirectory.Directory}""", "--recursive", "-o", "json"};
executeArgs = executeArgs.AddOptionsForServerSideApply(deployment.Variables, log);

var result = kubectl.ExecuteCommandAndReturnOutput(executeArgs);

return ProcessKubectlCommandOutput(deployment, result, globDirectory.Directory);
}

private class GlobDirectory
{
public GlobDirectory(int index, string glob, string directory)
{
Index = index;
Glob = glob;
Directory = directory;
}

public int Index { get; }
public string Glob { get; }
public string Directory { get; }
return true;
}
}
}
Expand Down
Loading

0 comments on commit 8242c8a

Please sign in to comment.