Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System;
using Calamari.Common.Commands;
using Calamari.Common.Features.Processes;
using Calamari.Common.Plumbing.Variables;
using Calamari.Kubernetes;
using Calamari.Kubernetes.Commands;
using Calamari.Kubernetes.Integration;
using Calamari.Kubernetes.ResourceStatus;
using Calamari.Testing.Helpers;
using Calamari.Tests.Fixtures.Integration.FileSystem;
using FluentAssertions;
using NSubstitute;
using NUnit.Framework;

namespace Calamari.Tests.KubernetesFixtures.Commands
{
[TestFixture]
public class KubernetesVerifyResourcesCommandFixture
{
[Test]
public void WhenNoAppliedResourcesVariableIsSet_ShouldThrowCommandException()
{
var variables = new CalamariVariables();
var statusReporter = Substitute.For<IResourceStatusReportExecutor>();
var command = CreateCommand(variables, statusReporter, new InMemoryLog());

Action execute = () => command.Execute(new string[] { });

execute.Should()
.Throw<CommandException>()
.WithMessage("The applied resources variable was not found. This variable is required to verify the deployed resources.");
statusReporter.ReceivedCalls().Should().BeEmpty();
}

[Test]
public void WhenAppliedResourcesIsAnEmptyList_ShouldDoNothingAndSucceed()
{
var variables = new CalamariVariables
{
[SpecialVariables.AppliedResources] = "[]"
};
var statusReporter = Substitute.For<IResourceStatusReportExecutor>();
var log = new InMemoryLog();
var command = CreateCommand(variables, statusReporter, log);

var result = command.Execute(new string[] { });

result.Should().Be(0);
statusReporter.ReceivedCalls().Should().BeEmpty();
log.MessagesInfoFormatted.Should().Contain("Applied resources list is empty; nothing to verify.");
}

[Test]
public void WhenAppliedResourcesIsNotValidJson_ShouldThrowCommandException()
{
var variables = new CalamariVariables
{
[SpecialVariables.AppliedResources] = "this is not json"
};
var statusReporter = Substitute.For<IResourceStatusReportExecutor>();
var command = CreateCommand(variables, statusReporter, new InMemoryLog());

Action execute = () => command.Execute(new string[] { });

execute.Should()
.Throw<CommandException>()
.WithMessage("Could not parse applied resources output variable:*");
statusReporter.ReceivedCalls().Should().BeEmpty();
}

static KubernetesVerifyResourcesCommand CreateCommand(
IVariables variables,
IResourceStatusReportExecutor statusReporter,
InMemoryLog log)
{
var fileSystem = new TestCalamariPhysicalFileSystem();
var commandLineRunner = Substitute.For<ICommandLineRunner>();
var kubectl = new Kubectl(variables, log, commandLineRunner);

return new KubernetesVerifyResourcesCommand(
log,
variables,
fileSystem,
commandLineRunner,
kubectl,
statusReporter);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Calamari.Common.Features.Packages;
using Calamari.Common.Features.StructuredVariables;
using Calamari.Common.Features.Substitutions;
using Calamari.Common.FeatureToggles;
using Calamari.Common.Plumbing.Deployment.Journal;
using Calamari.Common.Plumbing.FileSystem;
using Calamari.Common.Plumbing.Logging;
Expand Down Expand Up @@ -44,14 +45,17 @@ public KubernetesApplyRawYamlCommand(

protected override async Task<bool> ExecuteCommand(RunningDeployment runningDeployment)
{
if (!variables.GetFlag(SpecialVariables.ResourceStatusCheck))
//When ArgoRollouts support is enabled, status checking is performed by a separate verification action
var argoRolloutsEnabled = OctopusFeatureToggles.ArgoRolloutsSupportFeatureToggle.IsEnabled(variables);

if (argoRolloutsEnabled || !variables.GetFlag(SpecialVariables.ResourceStatusCheck))
{
return await kubernetesApplyExecutor.Execute(runningDeployment);
}

var timeoutSeconds = variables.GetInt32(SpecialVariables.Timeout) ?? 0;
var waitForJobs = variables.GetFlag(SpecialVariables.WaitForJobs);

var statusCheck = statusReporter.Start(timeoutSeconds, waitForJobs);

return await kubernetesApplyExecutor.Execute(runningDeployment, (newResources) => statusCheck.AddResources(newResources)) &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Calamari.Common.Features.Packages;
using Calamari.Common.Features.StructuredVariables;
using Calamari.Common.Features.Substitutions;
using Calamari.Common.FeatureToggles;
using Calamari.Common.Plumbing.Deployment.Journal;
using Calamari.Common.Plumbing.FileSystem;
using Calamari.Common.Plumbing.Logging;
Expand Down Expand Up @@ -52,7 +53,10 @@ public KubernetesKustomizeCommand(

protected override async Task<bool> ExecuteCommand(RunningDeployment runningDeployment)
{
if (!variables.GetFlag(SpecialVariables.ResourceStatusCheck))
//When ArgoRollouts support is enabled, status checking is performed by a separate verification action
var argoRolloutsEnabled = OctopusFeatureToggles.ArgoRolloutsSupportFeatureToggle.IsEnabled(variables);

if (argoRolloutsEnabled || !variables.GetFlag(SpecialVariables.ResourceStatusCheck))
{
return await kubernetesApplyExecutor.Execute(runningDeployment);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Calamari.Aws.Integration;
using Calamari.Commands.Support;
using Calamari.Common.Commands;
using Calamari.Common.Features.Processes;
using Calamari.Common.Plumbing.FileSystem;
using Calamari.Common.Plumbing.Logging;
using Calamari.Common.Plumbing.Variables;
using Calamari.Deployment;
using Calamari.Deployment.Conventions;
using Calamari.Kubernetes.Conventions;
using Calamari.Kubernetes.Integration;
using Calamari.Kubernetes.ResourceStatus;
using Calamari.Kubernetes.ResourceStatus.Resources;
using Newtonsoft.Json;

namespace Calamari.Kubernetes.Commands
{
[Command(Name, Description = "Verifies that resources applied by a Kubernetes deploy step have reached their desired state")]
public class KubernetesVerifyResourcesCommand : Command
{
public const string Name = "kubernetes-verify-resources";

readonly ILog log;
readonly IVariables variables;
readonly ICalamariFileSystem fileSystem;
readonly ICommandLineRunner commandLineRunner;
readonly Kubectl kubectl;
readonly IResourceStatusReportExecutor statusReporter;

public KubernetesVerifyResourcesCommand(
ILog log,
IVariables variables,
ICalamariFileSystem fileSystem,
ICommandLineRunner commandLineRunner,
Kubectl kubectl,
IResourceStatusReportExecutor statusReporter)
{
this.log = log;
this.variables = variables;
this.fileSystem = fileSystem;
this.commandLineRunner = commandLineRunner;
this.kubectl = kubectl;
this.statusReporter = statusReporter;
}

public override int Execute(string[] commandLineArguments)
{
Options.Parse(commandLineArguments);

var json = variables.Get(SpecialVariables.AppliedResources);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the variable doesn't exist we should fail, I think. Otherwise people might think verification is working correctly when there is actually some issue with it.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the variable exists and it's an empty array [], then the info log is good.

if (string.IsNullOrWhiteSpace(json))
{
throw new CommandException($"The applied resources variable was not found. This variable is required to verify the deployed resources.");
}

List<ResourceIdentifier> resources;
try
{
resources = DeserializeResources(json);
}
catch (JsonException ex)
{
throw new CommandException($"Could not parse applied resources output variable: {ex.Message}");
}

if (resources.Count == 0)
{
log.Info("Applied resources list is empty; nothing to verify.");
return 0;
}

WarnForUnverifiableResources(resources);

AuthenticateKubectl();

var timeoutSeconds = variables.GetInt32(SpecialVariables.Timeout) ?? 0;
var waitForJobs = variables.GetFlag(SpecialVariables.WaitForJobs);

var statusCheck = statusReporter.Start(timeoutSeconds, waitForJobs, resources);
var success = statusCheck.WaitForCompletionOrTimeout(CancellationToken.None).GetAwaiter().GetResult();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sucks that Command doesn't handle async natively. We can probably refactor this in Calamari now that we aren't support .net framework. Maybe I'll look at it for sharpening.


if (!success)
{
throw new CommandException("Resource verification failed. Check verbose logs for more details.");
}

return 0;
}

static List<ResourceIdentifier> DeserializeResources(string json)
{
var raw = JsonConvert.DeserializeObject<List<AppliedResourceDto>>(json) ?? new List<AppliedResourceDto>();
return raw
.Select(r => new ResourceIdentifier(
new ResourceGroupVersionKind(r.Group ?? string.Empty, r.Version, r.Kind),
r.Name,
r.Namespace))
.ToList();
}

void WarnForUnverifiableResources(IEnumerable<ResourceIdentifier> resources)
{
foreach (var resource in resources)
{
var gvk = resource.GroupVersionKind;
if (ResourceFactory.IsVerifiable(gvk))
continue;

var name = string.IsNullOrEmpty(resource.Namespace) ? resource.Name : $"{resource.Namespace}/{resource.Name}";
log.Warn($"Unable to fully verify resource '{gvk}' '{name}'. Calamari does not know the readiness criteria for this resource type; only its existence will be confirmed.");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe one for Idan, but do we want to indicate that using the Agent with kubernetes monitor may unlock verification for their type. Possibly a link to a public doc which will indicate which types are supported for verification? It should be pretty expansive once https://linear.app/octopus/issue/SIE-143/add-argo-rollout-support-to-kubernetes-monitor is completed.

}
}

void AuthenticateKubectl()
{
var deployment = new RunningDeployment(variables);
kubectl.SetWorkingDirectory(deployment.CurrentDirectory);
kubectl.SetEnvironmentVariables(deployment.EnvironmentVariables);

var conventions = new List<IConvention>();
if (variables.Get(Deployment.SpecialVariables.Account.AccountType) == "AmazonWebServicesAccount")
{
conventions.Add(new AwsAuthConvention(log, variables));
}
conventions.Add(new KubernetesAuthContextConvention(log, commandLineRunner, kubectl, fileSystem));

new ConventionProcessor(deployment, conventions, log).RunConventions();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there no shared code we can leverage for auth stuff?

}

class AppliedResourceDto
{
public string Group { get; set; }
public string Version { get; set; }
public string Kind { get; set; }
public string Name { get; set; }
public string Namespace { get; set; }
}
}
}
23 changes: 16 additions & 7 deletions source/Calamari/Kubernetes/Conventions/Helm/HelmUpgradeExecutor.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand All @@ -24,18 +24,21 @@ public class HelmUpgradeExecutor
readonly HelmTemplateValueSourcesParser templateValueSourcesParser;
readonly HelmCli helmCli;
readonly IKubernetesManifestNamespaceResolver namespaceResolver;
readonly IManifestReporter manifestReporter;

public HelmUpgradeExecutor(ILog log,
ICalamariFileSystem fileSystem,
HelmTemplateValueSourcesParser templateValueSourcesParser,
HelmCli helmCli,
IKubernetesManifestNamespaceResolver namespaceResolver)
IKubernetesManifestNamespaceResolver namespaceResolver,
IManifestReporter manifestReporter = null)
{
this.log = log;
this.fileSystem = fileSystem;
this.templateValueSourcesParser = templateValueSourcesParser;
this.helmCli = helmCli;
this.namespaceResolver = namespaceResolver;
this.manifestReporter = manifestReporter;
}

public void ExecuteHelmUpgrade(RunningDeployment deployment,
Expand Down Expand Up @@ -64,15 +67,15 @@ public void ExecuteHelmUpgrade(RunningDeployment deployment,

if (OctopusFeatureToggles.ArgoRolloutsSupportFeatureToggle.IsEnabled(deployment.Variables))
{
SetAppliedResourcesOutputVariable(deployment, releaseName, newRevisionNumber);
ReportManifestAndSetAppliedResources(deployment, releaseName, newRevisionNumber);
}

installCompletedCts.Cancel();
}

void SetAppliedResourcesOutputVariable(RunningDeployment deployment, string releaseName, int revisionNumber)
void ReportManifestAndSetAppliedResources(RunningDeployment deployment, string releaseName, int revisionNumber)
{
string manifest = null;
string manifest;
try
{
manifest = helmCli.GetManifest(releaseName, revisionNumber);
Expand All @@ -89,6 +92,10 @@ void SetAppliedResourcesOutputVariable(RunningDeployment deployment, string rele
return;
}

//Manifest reporting normally happens inside HelmManifestAndStatusReporter, which is
//skipped on this path; emit it inline so the UI still gets the applied manifest.
manifestReporter?.ReportManifestApplied(manifest);

var resources = ManifestParser.GetResourcesFromManifest(manifest, namespaceResolver, deployment.Variables, log);
AppliedResourcesOutputHelper.SetAppliedResourcesOutputVariable(log, deployment, resources);
}
Expand All @@ -104,8 +111,10 @@ List<string> GetUpgradeCommandArgs(RunningDeployment deployment)
SetValuesParameters(deployment, args);
var hasAdditionalArgs = SetAdditionalArguments(deployment, args);

//Adjust args based on KOS
if (deployment.Variables.GetFlag(SpecialVariables.ResourceStatusCheck))
//Adjust args based on KOS. When ArgoRollouts support is enabled, status checking moves
//to a separate verification action, so we don't force --wait on the deploy step.
if (deployment.Variables.GetFlag(SpecialVariables.ResourceStatusCheck)
&& !OctopusFeatureToggles.ArgoRolloutsSupportFeatureToggle.IsEnabled(deployment.Variables))
{
AddKOSArgs(deployment.Variables, hasAdditionalArgs, args);
}
Expand Down
Loading