Skip to content

Commit

Permalink
Report manifests applied via service messages (#1330)
Browse files Browse the repository at this point in the history
* Report manifests applied via service messages

* typo

* Send manifests as JSON

---------

Co-authored-by: Eddy Moulton <[email protected]>
  • Loading branch information
zentron and eddymoulton authored Sep 1, 2024
1 parent b3c520b commit 082dcf6
Show file tree
Hide file tree
Showing 9 changed files with 274 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using Calamari.Common.Commands;
using Calamari.Common.Features.Processes;
using Calamari.Common.Plumbing.FileSystem;
using Calamari.Common.Plumbing.ServiceMessages;
using Calamari.Common.Plumbing.Variables;
using Calamari.Kubernetes;
using Calamari.Kubernetes.Commands;
Expand All @@ -27,6 +26,7 @@ public class GatherAndApplyRawYamlExecutorTests
{
readonly ICalamariFileSystem fileSystem = TestCalamariPhysicalFileSystem.GetPhysicalFileSystem();
readonly ICommandLineRunner commandLineRunner = Substitute.For<ICommandLineRunner>();
readonly IManifestReporter manifestReporter = Substitute.For<IManifestReporter>();

InMemoryLog log;
List<ResourceIdentifier> receivedCallbacks;
Expand Down Expand Up @@ -337,7 +337,8 @@ void SetupCommandLineRunnerMocks()
IRawYamlKubernetesApplyExecutor CreateExecutor(IVariables variables, ICalamariFileSystem fs)
{
var kubectl = new Kubectl(variables, log, commandLineRunner);
return new GatherAndApplyRawYamlExecutor(log, fs, kubectl);

return new GatherAndApplyRawYamlExecutor(log, fs, manifestReporter, kubectl);
}

Task RecordingCallback(ResourceIdentifier[] identifiers)
Expand Down
116 changes: 116 additions & 0 deletions source/Calamari.Tests/KubernetesFixtures/ManifestReporterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Calamari.Common.Plumbing.FileSystem;
using Calamari.Common.Plumbing.ServiceMessages;
using Calamari.Common.Plumbing.Variables;
using Calamari.Kubernetes;
using Calamari.Testing.Helpers;
using FluentAssertions;
using NUnit.Framework;

namespace Calamari.Tests.KubernetesFixtures
{
[TestFixture]
public class ManifestReporterTests
{
[Test]
public void GivenValidYaml_ShouldPostSingleServiceMessage()
{
var memoryLog = new InMemoryLog();
var variables = new CalamariVariables();

var yaml = @"foo: bar";
var expectedJson = "{\"foo\": \"bar\"}";
using (CreateFile(yaml, out var filePath))
{
var mr = new ManifestReporter(variables, CalamariPhysicalFileSystem.GetPhysicalFileSystem(), memoryLog);

mr.ReportManifestApplied(filePath);

var expected = ServiceMessage.Create(SpecialVariables.ServiceMessageNames.ManifestApplied.Name, ("ns", "default"), ("manifest", expectedJson));
memoryLog.ServiceMessages.Should().BeEquivalentTo(new List<ServiceMessage> { expected });
}
}

[Test]
public void GivenInValidManifest_ShouldNotPostServiceMessage()
{
var memoryLog = new InMemoryLog();
var variables = new CalamariVariables();

var yaml = @"text - Bar";
using (CreateFile(yaml, out var filePath))
{
var mr = new ManifestReporter(variables, CalamariPhysicalFileSystem.GetPhysicalFileSystem(), memoryLog);

mr.ReportManifestApplied(filePath);

memoryLog.ServiceMessages.Should().BeEmpty();
}
}

[Test]
public void GivenNamespaceInManifest_ShouldReportManifestNamespace()
{
var memoryLog = new InMemoryLog();
var variables = new CalamariVariables();
var yaml = @"metadata:
name: game-demo
namespace: XXX";
using (CreateFile(yaml, out var filePath))
{
var variableNs = Some.String();
variables.Set(SpecialVariables.Namespace, variableNs);
var mr = new ManifestReporter(variables, CalamariPhysicalFileSystem.GetPhysicalFileSystem(), memoryLog);

mr.ReportManifestApplied(filePath);

memoryLog.ServiceMessages.First().Properties.Should().Contain(new KeyValuePair<string, string>("ns", "XXX"));
}
}

[Test]
public void GivenNamespaceNotInManifest_ShouldReportVariableNamespace()
{
var memoryLog = new InMemoryLog();
var variables = new CalamariVariables();
var yaml = @"foo: bar";
using (CreateFile(yaml, out var filePath))
{
var variableNs = Some.String();
variables.Set(SpecialVariables.Namespace, variableNs);
var mr = new ManifestReporter(variables, CalamariPhysicalFileSystem.GetPhysicalFileSystem(), memoryLog);

mr.ReportManifestApplied(filePath);

memoryLog.ServiceMessages.First().Properties.Should().Contain(new KeyValuePair<string, string>("ns", variableNs));
}
}

[Test]
public void GiveNoNamespaces_ShouldDefaultNamespace()
{
var memoryLog = new InMemoryLog();
var variables = new CalamariVariables();
var yaml = @"foo: bar";
using (CreateFile(yaml, out var filePath))
{
var mr = new ManifestReporter(variables, CalamariPhysicalFileSystem.GetPhysicalFileSystem(), memoryLog);

mr.ReportManifestApplied(filePath);

memoryLog.ServiceMessages.First().Properties.Should().Contain(new KeyValuePair<string, string>("ns", "default"));
}
}

IDisposable CreateFile(string yaml, out string filePath)
{
var tempDir = TemporaryDirectory.Create();
filePath = Path.Combine(tempDir.DirectoryPath, $"{Guid.NewGuid():d}.tmp");
File.WriteAllText(filePath, yaml);
return tempDir;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public void ReportsCreatedResourcesCorrectly()
reporter.ReportUpdatedResources(originalStatuses, newStatuses, 1);

var serviceMessages = log.ServiceMessages
.Where(message => message.Name == SpecialVariables.KubernetesResourceStatusServiceMessageName)
.Where(message => message.Name == SpecialVariables.ServiceMessageNames.ResourceStatus.Name)
.ToList();

serviceMessages.Select(message => message.Properties["name"])
Expand Down Expand Up @@ -72,7 +72,7 @@ public void ReportsUpdatedResourcesCorrectly()
reporter.ReportUpdatedResources(originalStatuses, newStatuses, 1);

var serviceMessages = log.ServiceMessages
.Where(message => message.Name == SpecialVariables.KubernetesResourceStatusServiceMessageName)
.Where(message => message.Name == SpecialVariables.ServiceMessageNames.ResourceStatus.Name)
.ToList();

serviceMessages.Should().ContainSingle().Which.Properties
Expand Down Expand Up @@ -102,7 +102,7 @@ public void ReportsRemovedResourcesCorrectly()
reporter.ReportUpdatedResources(originalStatuses, newStatuses, 1);

var serviceMessages = log.ServiceMessages
.Where(message => message.Name == SpecialVariables.KubernetesResourceStatusServiceMessageName)
.Where(message => message.Name == SpecialVariables.ServiceMessageNames.ResourceStatus.Name)
.ToList();

serviceMessages.Should().ContainSingle().Which.Properties
Expand All @@ -129,7 +129,7 @@ public void ClusterScopedResourcesAreIgnored()
reporter.ReportUpdatedResources(new Dictionary<string, Resource>(), newStatuses, 1);

var serviceMessages = log.ServiceMessages
.Where(message => message.Name == SpecialVariables.KubernetesResourceStatusServiceMessageName)
.Where(message => message.Name == SpecialVariables.ServiceMessageNames.ResourceStatus.Name)
.ToList();

serviceMessages.Should().ContainSingle().Which.Properties
Expand Down
14 changes: 14 additions & 0 deletions source/Calamari.Tests/Some.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;
using System.Threading;

namespace Calamari.Tests
{
public static class Some
{
static int next;

public static string String() => "S__" + Int();

public static int Int() => Interlocked.Increment(ref next);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using Calamari.Common.Commands;
using Calamari.Common.Plumbing.FileSystem;
using Calamari.Common.Plumbing.Logging;
using Calamari.Common.Plumbing.Variables;
using Calamari.Kubernetes.Integration;
using Calamari.Kubernetes.ResourceStatus.Resources;
using Octopus.CoreUtilities.Extensions;
Expand All @@ -21,23 +20,26 @@ class GatherAndApplyRawYamlExecutor : BaseKubernetesApplyExecutor, IRawYamlKuber
{
readonly ILog log;
readonly ICalamariFileSystem fileSystem;
readonly IManifestReporter manifestReporter;
readonly Kubectl kubectl;

public GatherAndApplyRawYamlExecutor(
ILog log,
ICalamariFileSystem fileSystem,
IManifestReporter manifestReporter,
Kubectl kubectl) : base(log)
{
this.log = log;
this.fileSystem = fileSystem;
this.manifestReporter = manifestReporter;
this.kubectl = kubectl;
}

protected override async Task<IEnumerable<ResourceIdentifier>> ApplyAndGetResourceIdentifiers(RunningDeployment deployment, Func<ResourceIdentifier[], Task> appliedResourcesCallback = null)
{
var variables = deployment.Variables;
var globs = variables.GetPaths(SpecialVariables.CustomResourceYamlFileName);

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

Expand Down Expand Up @@ -65,38 +67,31 @@ protected override async Task<IEnumerable<ResourceIdentifier>> ApplyAndGetResour

IEnumerable<ResourceIdentifier> ApplyBatchAndReturnResourceIdentifiers(RunningDeployment deployment, GlobDirectory globDirectory)
{
if (!LogFoundFiles(globDirectory))
var files = fileSystem.EnumerateFilesRecursively(globDirectory.Directory).ToArray();
if (!files.Any())
{
log.Warn($"No files found matching '{globDirectory.Glob}'");
return Array.Empty<ResourceIdentifier>();
}

ReportEachManifestBeingApplied(globDirectory, files);

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);
}

/// <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)
void ReportEachManifestBeingApplied(GlobDirectory globDirectory, string[] files)
{
var directoryWithTrailingSlash = globDirectory.Directory + Path.DirectorySeparatorChar;
var files = fileSystem.EnumerateFilesRecursively(globDirectory.Directory).ToArray();
if (!files.Any())
{
log.Warn($"No files found matching '{globDirectory.Glob}'");
return false;
}

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

return true;
}
}
}
104 changes: 104 additions & 0 deletions source/Calamari/Kubernetes/ManifestReporter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.IO;
using Calamari.Common.Plumbing.FileSystem;
using Calamari.Common.Plumbing.Logging;
using Calamari.Common.Plumbing.ServiceMessages;
using Calamari.Common.Plumbing.Variables;
using YamlDotNet.Core;
using YamlDotNet.RepresentationModel;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;

namespace Calamari.Kubernetes
{
public interface IManifestReporter
{
void ReportManifestApplied(string filePath);
}

public class ManifestReporter : IManifestReporter
{
readonly IVariables variables;
readonly ICalamariFileSystem fileSystem;
readonly ILog log;

static readonly IDeserializer YamlDeserializer = new Deserializer();

static readonly ISerializer YamlSerializer = new SerializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.JsonCompatible()
.Build();

public ManifestReporter(IVariables variables, ICalamariFileSystem fileSystem, ILog log)
{
this.variables = variables;
this.fileSystem = fileSystem;
this.log = log;
}

string GetNamespace(YamlMappingNode yamlRoot)
{
var implicitNamespace = variables.Get(SpecialVariables.Namespace) ?? "default";

if (yamlRoot.Children.TryGetValue("metadata", out var metadataNode) && metadataNode is YamlMappingNode metadataMappingNode &&
metadataMappingNode.Children.TryGetValue("namespace", out var namespaceNode) && namespaceNode is YamlScalarNode namespaceScalarNode &&
!string.IsNullOrWhiteSpace(namespaceScalarNode.Value))
{
implicitNamespace = namespaceScalarNode.Value;
}

return implicitNamespace;
}

public void ReportManifestApplied(string filePath)
{
using (var yamlFile = fileSystem.OpenFile(filePath, FileAccess.ReadWrite))
{
try
{
var yamlStream = new YamlStream();
yamlStream.Load(new StreamReader(yamlFile));

foreach (var document in yamlStream.Documents)
{
if (!(document.RootNode is YamlMappingNode rootNode))
{
log.Warn("Could not parse manifest, resources will not be added to live object status");
continue;
}

var updatedDocument = YamlNodeToJson(rootNode);

var ns = GetNamespace(rootNode);
log.WriteServiceMessage(new ServiceMessage(SpecialVariables.ServiceMessageNames.ManifestApplied.Name,
new Dictionary<string, string>
{
{ SpecialVariables.ServiceMessageNames.ManifestApplied.ManifestAttribute, updatedDocument },
{ SpecialVariables.ServiceMessageNames.ManifestApplied.NamespaceAttribute, ns }
}));
}
}
catch (SemanticErrorException)
{
log.Warn("Invalid YAML syntax found, resources will not be added to live object status");
}
}
}

static string YamlNodeToJson(YamlNode node)
{
var stream = new YamlStream { new YamlDocument(node) };
using (var writer = new StringWriter())
{
stream.Save(writer);

using (var reader = new StringReader(writer.ToString()))
{
var yamlObject = YamlDeserializer.Deserialize(reader);
return yamlObject is null ? string.Empty : YamlSerializer.Serialize(yamlObject).Trim();
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ private void SendServiceMessage(Resource resource, bool removed, int checkCount)
{"checkCount", checkCount.ToString()}
};

var message = new ServiceMessage(SpecialVariables.KubernetesResourceStatusServiceMessageName, parameters);
var message = new ServiceMessage(SpecialVariables.ServiceMessageNames.ResourceStatus.Name, parameters);
log.WriteServiceMessage(message);
}
}
Expand Down
Loading

0 comments on commit 082dcf6

Please sign in to comment.