From 9c533bd0d0428b7d2b02758e31be7cc7b2aa7a25 Mon Sep 17 00:00:00 2001 From: Sasha Kavalchuk Date: Mon, 18 May 2026 12:03:37 +0200 Subject: [PATCH 1/4] Add AppliedResources output variable --- .../FeatureToggles/OctopusFeatureToggle.cs | 2 + .../AppliedResourcesOutputHelperTests.cs | 167 +++++++++ .../Executors/KustomizeExecutorTests.cs | 55 +++ .../Helm/HelmUpgradeExecutorTests.cs | 334 ++++++++++++++++++ .../Executors/AppliedResourcesOutputHelper.cs | 37 ++ .../GatherAndApplyRawYamlExecutor.cs | 2 + .../Commands/Executors/KustomizeExecutor.cs | 6 +- .../Conventions/Helm/HelmUpgradeExecutor.cs | 35 +- .../HelmUpgradeWithKOSConvention.cs | 7 +- 9 files changed, 639 insertions(+), 6 deletions(-) create mode 100644 source/Calamari.Tests/KubernetesFixtures/Commands/Executors/AppliedResourcesOutputHelperTests.cs create mode 100644 source/Calamari.Tests/KubernetesFixtures/Conventions/Helm/HelmUpgradeExecutorTests.cs create mode 100644 source/Calamari/Kubernetes/Commands/Executors/AppliedResourcesOutputHelper.cs diff --git a/source/Calamari.Common/FeatureToggles/OctopusFeatureToggle.cs b/source/Calamari.Common/FeatureToggles/OctopusFeatureToggle.cs index 31f6968dce..8467862ef6 100644 --- a/source/Calamari.Common/FeatureToggles/OctopusFeatureToggle.cs +++ b/source/Calamari.Common/FeatureToggles/OctopusFeatureToggle.cs @@ -9,11 +9,13 @@ public static class KnownSlugs public const string AnsiColorsInTaskLogFeatureToggle = "ansi-colors"; public const string ArgoCDHelmReplacePathFromContainerReferenceFeatureToggle = "argo-cd-helm-replace-path-from-container-reference"; public const string KustomizePatchImageUpdatesFeatureToggle = "kustomize-patch-image-updates"; + public const string ArgoRolloutsSupportFeatureToggle = "argo-rollouts-support"; }; public static readonly OctopusFeatureToggle AnsiColorsInTaskLogFeatureToggle = new(KnownSlugs.AnsiColorsInTaskLogFeatureToggle); public static readonly OctopusFeatureToggle ArgoCDHelmReplacePathFromContainerReferenceFeatureToggle = new(KnownSlugs.ArgoCDHelmReplacePathFromContainerReferenceFeatureToggle); public static readonly OctopusFeatureToggle KustomizePatchImageUpdatesFeatureToggle = new(KnownSlugs.KustomizePatchImageUpdatesFeatureToggle); + public static readonly OctopusFeatureToggle ArgoRolloutsSupportFeatureToggle = new(KnownSlugs.ArgoRolloutsSupportFeatureToggle); public class OctopusFeatureToggle { diff --git a/source/Calamari.Tests/KubernetesFixtures/Commands/Executors/AppliedResourcesOutputHelperTests.cs b/source/Calamari.Tests/KubernetesFixtures/Commands/Executors/AppliedResourcesOutputHelperTests.cs new file mode 100644 index 0000000000..4f77cd06ef --- /dev/null +++ b/source/Calamari.Tests/KubernetesFixtures/Commands/Executors/AppliedResourcesOutputHelperTests.cs @@ -0,0 +1,167 @@ +using System.Collections.Generic; +using System.Linq; +using Calamari.Common.Commands; +using Calamari.Common.FeatureToggles; +using Calamari.Common.Plumbing.Variables; +using Calamari.Kubernetes.Commands.Executors; +using Calamari.Kubernetes.ResourceStatus.Resources; +using Calamari.Testing.Helpers; +using FluentAssertions; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace Calamari.Tests.KubernetesFixtures.Commands.Executors +{ + [TestFixture] + public class AppliedResourcesOutputHelperTests + { + InMemoryLog log; + + [SetUp] + public void SetUp() + { + log = new InMemoryLog(); + } + + [Test] + public void SetsAppliedResourcesOutputVariable_WhenFeatureToggleIsEnabled() + { + // Arrange + var variables = new CalamariVariables + { + [KnownVariables.EnabledFeatureToggles] = OctopusFeatureToggles.KnownSlugs.ArgoRolloutsSupportFeatureToggle + }; + var deployment = new RunningDeployment(variables); + var resources = new[] + { + new ResourceIdentifier(SupportedResourceGroupVersionKinds.DeploymentV1, "my-deployment", "default"), + new ResourceIdentifier(SupportedResourceGroupVersionKinds.ServiceV1, "my-service", "default") + }; + + // Act + AppliedResourcesOutputHelper.SetAppliedResourcesOutputVariable(log, deployment, resources); + + // Assert + var outputVariable = variables.Get("AppliedResources"); + outputVariable.Should().NotBeNullOrEmpty(); + + var deserializedResources = JsonConvert.DeserializeAnonymousType(outputVariable, new[] + { + new { Group = "", Version = "", Kind = "", Name = "", Namespace = "" } + }); + + deserializedResources.Should().HaveCount(2); + deserializedResources[0].Should().BeEquivalentTo(new + { + Group = "apps", + Version = "v1", + Kind = "Deployment", + Name = "my-deployment", + Namespace = "default" + }); + deserializedResources[1].Should().BeEquivalentTo(new + { + Group = "", + Version = "v1", + Kind = "Service", + Name = "my-service", + Namespace = "default" + }); + } + + [Test] + public void DoesNotSetOutputVariable_WhenFeatureToggleIsDisabled() + { + // Arrange + var variables = new CalamariVariables(); + var deployment = new RunningDeployment(variables); + var resources = new[] + { + new ResourceIdentifier(SupportedResourceGroupVersionKinds.DeploymentV1, "my-deployment", "default") + }; + + // Act + AppliedResourcesOutputHelper.SetAppliedResourcesOutputVariable(log, deployment, resources); + + // Assert + var outputVariable = variables.Get("AppliedResources"); + outputVariable.Should().BeNull(); + } + + [Test] + public void HandlesEmptyResourceCollection_WhenFeatureToggleIsEnabled() + { + // Arrange + var variables = new CalamariVariables + { + [KnownVariables.EnabledFeatureToggles] = OctopusFeatureToggles.KnownSlugs.ArgoRolloutsSupportFeatureToggle + }; + var deployment = new RunningDeployment(variables); + var resources = Enumerable.Empty(); + + // Act + AppliedResourcesOutputHelper.SetAppliedResourcesOutputVariable(log, deployment, resources); + + // Assert + var outputVariable = variables.Get("AppliedResources"); + outputVariable.Should().NotBeNullOrEmpty(); + outputVariable.Should().Be("[]"); + } + + [Test] + public void LogsAppliedResourcesJson_WhenFeatureToggleIsEnabled() + { + // Arrange + var variables = new CalamariVariables + { + [KnownVariables.EnabledFeatureToggles] = OctopusFeatureToggles.KnownSlugs.ArgoRolloutsSupportFeatureToggle + }; + var deployment = new RunningDeployment(variables); + var resources = new[] + { + new ResourceIdentifier(SupportedResourceGroupVersionKinds.DeploymentV1, "my-deployment", "default") + }; + + // Act + AppliedResourcesOutputHelper.SetAppliedResourcesOutputVariable(log, deployment, resources); + + // Assert + log.StandardOut.Should().Contain(msg => msg.Contains("Applied resources:")); + } + + [Test] + public void SerializesResourcesWithCorrectJsonFormat() + { + // Arrange + var variables = new CalamariVariables + { + [KnownVariables.EnabledFeatureToggles] = OctopusFeatureToggles.KnownSlugs.ArgoRolloutsSupportFeatureToggle + }; + var deployment = new RunningDeployment(variables); + var resources = new[] + { + new ResourceIdentifier(new ResourceGroupVersionKind("argoproj.io", "v1alpha1", "Rollout"), "my-rollout", "production") + }; + + // Act + AppliedResourcesOutputHelper.SetAppliedResourcesOutputVariable(log, deployment, resources); + + // Assert + var outputVariable = variables.Get("AppliedResources"); + var deserializedResources = JsonConvert.DeserializeAnonymousType(outputVariable, new[] + { + new { Group = "", Version = "", Kind = "", Name = "", Namespace = "" } + }); + + deserializedResources.Should().ContainSingle() + .Which.Should().BeEquivalentTo(new + { + Group = "argoproj.io", + Version = "v1alpha1", + Kind = "Rollout", + Name = "my-rollout", + Namespace = "production" + }); + } + } +} diff --git a/source/Calamari.Tests/KubernetesFixtures/Commands/Executors/KustomizeExecutorTests.cs b/source/Calamari.Tests/KubernetesFixtures/Commands/Executors/KustomizeExecutorTests.cs index 1368fdc8d4..5f15b75c76 100644 --- a/source/Calamari.Tests/KubernetesFixtures/Commands/Executors/KustomizeExecutorTests.cs +++ b/source/Calamari.Tests/KubernetesFixtures/Commands/Executors/KustomizeExecutorTests.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Calamari.Common.Commands; using Calamari.Common.Features.Processes; +using Calamari.Common.FeatureToggles; using Calamari.Common.Plumbing.FileSystem; using Calamari.Common.Plumbing.Variables; using Calamari.Kubernetes; @@ -14,6 +15,7 @@ using Calamari.Testing.Helpers; using Calamari.Tests.Fixtures.Integration.FileSystem; using FluentAssertions; +using Newtonsoft.Json; using NSubstitute; using NSubstitute.ClearExtensions; using NUnit.Framework; @@ -371,6 +373,59 @@ public async Task CommandLineReturnsNonZeroCode_ReturnsFalseToIndicateFailure() AssertNoManifestsReported(); } + [Test] + public async Task SetsAppliedResourcesOutputVariable_WhenFeatureToggleIsEnabled() + { + // Arrange + SetupCommandLineRunnerMock(); + var variables = new CalamariVariables + { + [KnownVariables.OriginalPackageDirectoryPath] = StagingDirectory, + [SpecialVariables.KustomizeOverlayPath] = OverlayPath, + [KnownVariables.EnabledFeatureToggles] = OctopusFeatureToggles.KnownSlugs.ArgoRolloutsSupportFeatureToggle + }; + var runningDeployment = new RunningDeployment(variables); + var executor = CreateExecutor(variables); + + // Act + var result = await executor.Execute(runningDeployment, RecordingCallback); + + // Assert + result.Should().BeTrue(); + var appliedResourcesJson = variables.Get("AppliedResources"); + appliedResourcesJson.Should().NotBeNullOrEmpty(); + + var deserializedResources = JsonConvert.DeserializeAnonymousType(appliedResourcesJson, new[] + { + new { Group = "", Version = "", Kind = "", Name = "", Namespace = "" } + }); + deserializedResources.Should().HaveCount(2); + deserializedResources.Should().Contain(r => r.Kind == "Deployment" && r.Name == "basic-deployment" && r.Namespace == "dev"); + deserializedResources.Should().Contain(r => r.Kind == "Service" && r.Name == "basic-service" && r.Namespace == "dev"); + } + + [Test] + public async Task DoesNotSetAppliedResourcesOutputVariable_WhenFeatureToggleIsDisabled() + { + // Arrange + SetupCommandLineRunnerMock(); + var variables = new CalamariVariables + { + [KnownVariables.OriginalPackageDirectoryPath] = StagingDirectory, + [SpecialVariables.KustomizeOverlayPath] = OverlayPath + }; + var runningDeployment = new RunningDeployment(variables); + var executor = CreateExecutor(variables); + + // Act + var result = await executor.Execute(runningDeployment, RecordingCallback); + + // Assert + result.Should().BeTrue(); + var appliedResourcesJson = variables.Get("AppliedResources"); + appliedResourcesJson.Should().BeNull(); + } + void SetupCommandLineRunnerMock(int kubectlMinor = 28) { const string resourceJson = @"{ diff --git a/source/Calamari.Tests/KubernetesFixtures/Conventions/Helm/HelmUpgradeExecutorTests.cs b/source/Calamari.Tests/KubernetesFixtures/Conventions/Helm/HelmUpgradeExecutorTests.cs new file mode 100644 index 0000000000..168f26324f --- /dev/null +++ b/source/Calamari.Tests/KubernetesFixtures/Conventions/Helm/HelmUpgradeExecutorTests.cs @@ -0,0 +1,334 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using Calamari.Common.Commands; +using Calamari.Common.Features.Packages; +using Calamari.Common.Features.Processes; +using Calamari.Common.FeatureToggles; +using Calamari.Common.Plumbing.Deployment; +using Calamari.Common.Plumbing.FileSystem; +using Calamari.Common.Plumbing.Variables; +using Calamari.Kubernetes; +using Calamari.Kubernetes.Conventions.Helm; +using Calamari.Kubernetes.Helm; +using Calamari.Kubernetes.Integration; +using Calamari.Testing.Helpers; +using Calamari.Tests.Fixtures.Integration.FileSystem; +using Calamari.Tests.KubernetesFixtures.Builders; +using FluentAssertions; +using Newtonsoft.Json; +using NSubstitute; +using NSubstitute.ClearExtensions; +using NUnit.Framework; + +namespace Calamari.Tests.KubernetesFixtures.Conventions.Helm +{ + [TestFixture] + public class HelmUpgradeExecutorTests + { + const string ReleaseName = "my-release"; + const int RevisionNumber = 5; + + readonly ICalamariFileSystem fileSystem = TestCalamariPhysicalFileSystem.GetPhysicalFileSystem(); + ICommandLineRunner commandLineRunner; + + InMemoryLog log; + string tempDirectory; + CancellationTokenSource installCompletedCts; + CancellationTokenSource installErrorCts; + + string ChartDirectory => Path.Combine(tempDirectory, "chart"); + + [SetUp] + public void SetUp() + { + log = new InMemoryLog(); + commandLineRunner = Substitute.For(); + tempDirectory = fileSystem.CreateTemporaryDirectory(); + installCompletedCts = new CancellationTokenSource(); + installErrorCts = new CancellationTokenSource(); + + SetupChartDirectory(); + SetupHelmVersionMock(); + } + + [TearDown] + public void TearDown() + { + fileSystem.DeleteDirectory(tempDirectory, FailureOptions.IgnoreFailure); + commandLineRunner.ClearSubstitute(); + installCompletedCts.Dispose(); + installErrorCts.Dispose(); + } + + [Test] + public void SetsAppliedResourcesOutputVariable_WhenFeatureToggleIsEnabled() + { + // Arrange + var variables = CreateVariables(); + variables.Set(KnownVariables.EnabledFeatureToggles, OctopusFeatureToggles.KnownSlugs.ArgoRolloutsSupportFeatureToggle); + + SetupHelmUpgradeMock(); + SetupHelmGetManifestMock(GetSampleManifest()); + + var deployment = CreateRunningDeployment(variables); + var executor = CreateExecutor(deployment); + + // Act + executor.ExecuteHelmUpgrade(deployment, ReleaseName, RevisionNumber, installCompletedCts, installErrorCts); + + // Assert + var appliedResourcesJson = variables.Get("AppliedResources"); + appliedResourcesJson.Should().NotBeNullOrEmpty(); + + var deserializedResources = JsonConvert.DeserializeAnonymousType(appliedResourcesJson, new[] + { + new { Group = "", Version = "", Kind = "", Name = "", Namespace = "" } + }); + deserializedResources.Should().HaveCount(2); + deserializedResources.Should().Contain(r => r.Kind == "Deployment" && r.Name == "my-app"); + deserializedResources.Should().Contain(r => r.Kind == "Service" && r.Name == "my-app-service"); + } + + [Test] + public void DoesNotSetAppliedResourcesOutputVariable_WhenFeatureToggleIsDisabled() + { + // Arrange + var variables = CreateVariables(); + + SetupHelmUpgradeMock(); + SetupHelmGetManifestMock(GetSampleManifest()); + + var deployment = CreateRunningDeployment(variables); + var executor = CreateExecutor(deployment); + + // Act + executor.ExecuteHelmUpgrade(deployment, ReleaseName, RevisionNumber, installCompletedCts, installErrorCts); + + // Assert + var appliedResourcesJson = variables.Get("AppliedResources"); + appliedResourcesJson.Should().BeNull(); + + commandLineRunner.DidNotReceive().Execute(Arg.Is(i => i.Arguments.Contains("get manifest"))); + } + + [Test] + public void DoesNotCallGetManifest_WhenFeatureToggleIsDisabled() + { + // Arrange + var variables = CreateVariables(); + + SetupHelmUpgradeMock(); + + var deployment = CreateRunningDeployment(variables); + var executor = CreateExecutor(deployment); + + // Act + executor.ExecuteHelmUpgrade(deployment, ReleaseName, RevisionNumber, installCompletedCts, installErrorCts); + + // Assert + commandLineRunner.DidNotReceive().Execute(Arg.Is(i => i.Arguments.Contains("get manifest"))); + } + + [Test] + public void CallsGetManifest_WhenFeatureToggleIsEnabled() + { + // Arrange + var variables = CreateVariables(); + variables.Set(KnownVariables.EnabledFeatureToggles, OctopusFeatureToggles.KnownSlugs.ArgoRolloutsSupportFeatureToggle); + + SetupHelmUpgradeMock(); + SetupHelmGetManifestMock(GetSampleManifest()); + + var deployment = CreateRunningDeployment(variables); + var executor = CreateExecutor(deployment); + + // Act + executor.ExecuteHelmUpgrade(deployment, ReleaseName, RevisionNumber, installCompletedCts, installErrorCts); + + // Assert + commandLineRunner.Received().Execute(Arg.Is(i => + i.Arguments.Contains("get") && + i.Arguments.Contains("manifest") && + i.Arguments.Contains(ReleaseName) && + i.Arguments.Contains($"--revision {RevisionNumber}"))); + } + + [Test] + public void LogsWarningAndContinues_WhenGetManifestFails() + { + // Arrange + var variables = CreateVariables(); + variables.Set(KnownVariables.EnabledFeatureToggles, OctopusFeatureToggles.KnownSlugs.ArgoRolloutsSupportFeatureToggle); + + SetupHelmUpgradeMock(); + SetupHelmGetManifestMockToFail(); + + var deployment = CreateRunningDeployment(variables); + var executor = CreateExecutor(deployment); + + // Act - should not throw + executor.ExecuteHelmUpgrade(deployment, ReleaseName, RevisionNumber, installCompletedCts, installErrorCts); + + // Assert + log.MessagesWarnFormatted.Should().Contain(msg => msg.Contains("Failed to set applied resources output variable")); + installCompletedCts.IsCancellationRequested.Should().BeTrue(); + } + + [Test] + public void SkipsAppliedResourcesVariable_WhenManifestIsEmpty() + { + // Arrange + var variables = CreateVariables(); + variables.Set(KnownVariables.EnabledFeatureToggles, OctopusFeatureToggles.KnownSlugs.ArgoRolloutsSupportFeatureToggle); + + SetupHelmUpgradeMock(); + SetupHelmGetManifestMock(string.Empty); + + var deployment = CreateRunningDeployment(variables); + var executor = CreateExecutor(deployment); + + // Act + executor.ExecuteHelmUpgrade(deployment, ReleaseName, RevisionNumber, installCompletedCts, installErrorCts); + + // Assert + var appliedResourcesJson = variables.Get("AppliedResources"); + appliedResourcesJson.Should().BeNull(); + log.MessagesVerboseFormatted.Should().Contain(msg => msg.Contains("empty, skipping applied resources")); + } + + [Test] + public void CancelsInstallCompletedToken_OnSuccessfulUpgrade() + { + // Arrange + var variables = CreateVariables(); + SetupHelmUpgradeMock(); + + var deployment = CreateRunningDeployment(variables); + var executor = CreateExecutor(deployment); + + // Act + executor.ExecuteHelmUpgrade(deployment, ReleaseName, RevisionNumber, installCompletedCts, installErrorCts); + + // Assert + installCompletedCts.IsCancellationRequested.Should().BeTrue(); + installErrorCts.IsCancellationRequested.Should().BeFalse(); + } + + [Test] + public void ThrowsCommandException_WhenUpgradeFails() + { + // Arrange + var variables = CreateVariables(); + SetupHelmUpgradeMockToFail(); + + var deployment = CreateRunningDeployment(variables); + var executor = CreateExecutor(deployment); + + // Act & Assert + var action = () => executor.ExecuteHelmUpgrade(deployment, ReleaseName, RevisionNumber, installCompletedCts, installErrorCts); + action.Should().Throw().WithMessage("*non-zero exit code*"); + installErrorCts.IsCancellationRequested.Should().BeTrue(); + } + + void SetupChartDirectory() + { + Directory.CreateDirectory(ChartDirectory); + File.WriteAllText(Path.Combine(ChartDirectory, "Chart.yaml"), "apiVersion: v2\nname: test-chart\nversion: 1.0.0"); + } + + void SetupHelmVersionMock() + { + commandLineRunner.Execute(Arg.Is(i => i.Arguments.Contains("version") && i.Arguments.Contains("--client"))) + .Returns(info => + { + var invocation = (CommandLineInvocation)info[0]; + invocation.AdditionalInvocationOutputSink?.WriteInfo("v3.14.0"); + return new CommandResult("helm version", 0); + }); + } + + void SetupHelmUpgradeMock() + { + commandLineRunner.Execute(Arg.Is(i => i.Arguments.Contains("upgrade"))) + .Returns(new CommandResult("helm upgrade", 0)); + } + + void SetupHelmUpgradeMockToFail() + { + commandLineRunner.Execute(Arg.Is(i => i.Arguments.Contains("upgrade"))) + .Returns(new CommandResult("helm upgrade", 1)); + } + + void SetupHelmGetManifestMock(string manifestContent) + { + commandLineRunner.Execute(Arg.Is(i => i.Arguments.Contains("get") && i.Arguments.Contains("manifest"))) + .Returns(info => + { + var invocation = (CommandLineInvocation)info[0]; + if (!string.IsNullOrEmpty(manifestContent)) + { + invocation.AdditionalInvocationOutputSink?.WriteInfo(manifestContent); + } + return new CommandResult("helm get manifest", 0); + }); + } + + void SetupHelmGetManifestMockToFail() + { + commandLineRunner.Execute(Arg.Is(i => i.Arguments.Contains("get") && i.Arguments.Contains("manifest"))) + .Returns(new CommandResult("helm get manifest", 1)); + } + + CalamariVariables CreateVariables() + { + return new CalamariVariables + { + [PackageVariables.Output.InstallationDirectoryPath] = ChartDirectory, + [KnownVariables.OriginalPackageDirectoryPath] = tempDirectory + }; + } + + RunningDeployment CreateRunningDeployment(CalamariVariables variables) + { + return new RunningDeployment(variables, new Dictionary()) + { + CurrentDirectoryProvider = DeploymentWorkingDirectory.StagingDirectory, + StagingDirectory = tempDirectory + }; + } + + HelmUpgradeExecutor CreateExecutor(RunningDeployment deployment) + { + var helmCli = new HelmCli(log, commandLineRunner, deployment, fileSystem); + var templateValueSourcesParser = new HelmTemplateValueSourcesParser(fileSystem, log); + var namespaceResolver = new KubernetesManifestNamespaceResolver( + new ApiResourcesScopeLookupBuilder().WithDefaults().Build(), + log); + + return new HelmUpgradeExecutor(log, fileSystem, templateValueSourcesParser, helmCli, namespaceResolver); + } + + static string GetSampleManifest() + { + return @"--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-app + namespace: default +spec: + replicas: 1 +--- +apiVersion: v1 +kind: Service +metadata: + name: my-app-service + namespace: default +spec: + type: ClusterIP +"; + } + } +} diff --git a/source/Calamari/Kubernetes/Commands/Executors/AppliedResourcesOutputHelper.cs b/source/Calamari/Kubernetes/Commands/Executors/AppliedResourcesOutputHelper.cs new file mode 100644 index 0000000000..00ececebdf --- /dev/null +++ b/source/Calamari/Kubernetes/Commands/Executors/AppliedResourcesOutputHelper.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq; +using Calamari.Common.Commands; +using Calamari.Common.FeatureToggles; +using Calamari.Common.Plumbing.Logging; +using Calamari.Kubernetes.ResourceStatus.Resources; +using Newtonsoft.Json; + +namespace Calamari.Kubernetes.Commands.Executors +{ + public static class AppliedResourcesOutputHelper + { + public static void SetAppliedResourcesOutputVariable( + ILog log, + RunningDeployment deployment, + IEnumerable resources) + { + if (!OctopusFeatureToggles.ArgoRolloutsSupportFeatureToggle.IsEnabled(deployment.Variables)) + { + return; + } + + var resourceList = resources.Select(r => new + { + r.GroupVersionKind.Group, + r.GroupVersionKind.Version, + r.GroupVersionKind.Kind, + r.Name, + r.Namespace + }).ToArray(); + + var json = JsonConvert.SerializeObject(resourceList); + + log.SetOutputVariable("AppliedResources", json, deployment.Variables); + } + } +} diff --git a/source/Calamari/Kubernetes/Commands/Executors/GatherAndApplyRawYamlExecutor.cs b/source/Calamari/Kubernetes/Commands/Executors/GatherAndApplyRawYamlExecutor.cs index 7ab9f2e017..05c6f6354a 100644 --- a/source/Calamari/Kubernetes/Commands/Executors/GatherAndApplyRawYamlExecutor.cs +++ b/source/Calamari/Kubernetes/Commands/Executors/GatherAndApplyRawYamlExecutor.cs @@ -62,6 +62,8 @@ protected override async Task> ApplyAndGetResour resourcesIdentifiers.UnionWith(res); } + AppliedResourcesOutputHelper.SetAppliedResourcesOutputVariable(log, deployment, resourcesIdentifiers); + return resourcesIdentifiers; } diff --git a/source/Calamari/Kubernetes/Commands/Executors/KustomizeExecutor.cs b/source/Calamari/Kubernetes/Commands/Executors/KustomizeExecutor.cs index 31537386f8..07b84cc74d 100644 --- a/source/Calamari/Kubernetes/Commands/Executors/KustomizeExecutor.cs +++ b/source/Calamari/Kubernetes/Commands/Executors/KustomizeExecutor.cs @@ -55,7 +55,11 @@ protected override async Task> ApplyAndGetResour manifestReporter.ReportManifestFileApplied(HydratedManifestFilepath(deployment.CurrentDirectory)); log.Info("Applying kustomization"); - return await ApplyKustomization(deployment, appliedResourcesCallback, overlayPath); + var resourceIdentifiers = await ApplyKustomization(deployment, appliedResourcesCallback, overlayPath); + + AppliedResourcesOutputHelper.SetAppliedResourcesOutputVariable(log, deployment, resourceIdentifiers); + + return resourceIdentifiers; } async Task ApplyKustomization(RunningDeployment deployment, Func appliedResourcesCallback, string overlayPath) diff --git a/source/Calamari/Kubernetes/Conventions/Helm/HelmUpgradeExecutor.cs b/source/Calamari/Kubernetes/Conventions/Helm/HelmUpgradeExecutor.cs index 0538628b2d..feac1f29aa 100644 --- a/source/Calamari/Kubernetes/Conventions/Helm/HelmUpgradeExecutor.cs +++ b/source/Calamari/Kubernetes/Conventions/Helm/HelmUpgradeExecutor.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -9,6 +9,7 @@ using Calamari.Common.Plumbing.FileSystem; using Calamari.Common.Plumbing.Logging; using Calamari.Common.Plumbing.Variables; +using Calamari.Kubernetes.Commands.Executors; using Calamari.Kubernetes.Helm; using Calamari.Kubernetes.Integration; using Calamari.Util; @@ -22,20 +23,24 @@ public class HelmUpgradeExecutor readonly ICalamariFileSystem fileSystem; readonly HelmTemplateValueSourcesParser templateValueSourcesParser; readonly HelmCli helmCli; + readonly IKubernetesManifestNamespaceResolver namespaceResolver; public HelmUpgradeExecutor(ILog log, ICalamariFileSystem fileSystem, HelmTemplateValueSourcesParser templateValueSourcesParser, - HelmCli helmCli) + HelmCli helmCli, + IKubernetesManifestNamespaceResolver namespaceResolver) { this.log = log; this.fileSystem = fileSystem; this.templateValueSourcesParser = templateValueSourcesParser; this.helmCli = helmCli; + this.namespaceResolver = namespaceResolver; } public void ExecuteHelmUpgrade(RunningDeployment deployment, string releaseName, + int newRevisionNumber, CancellationTokenSource installCompletedCts, CancellationTokenSource installErrorCts) { @@ -57,9 +62,35 @@ public void ExecuteHelmUpgrade(RunningDeployment deployment, throw new CommandException("Helm Upgrade returned zero exit code but had error output. Deployment terminated."); } + if (OctopusFeatureToggles.ArgoRolloutsSupportFeatureToggle.IsEnabled(deployment.Variables)) + { + SetAppliedResourcesOutputVariable(deployment, releaseName, newRevisionNumber); + } + installCompletedCts.Cancel(); } + void SetAppliedResourcesOutputVariable(RunningDeployment deployment, string releaseName, int revisionNumber) + { + try + { + var manifest = helmCli.GetManifest(releaseName, revisionNumber); + + if (string.IsNullOrWhiteSpace(manifest)) + { + log.Verbose($"Helm manifest for {releaseName} revision {revisionNumber} is empty, skipping applied resources output variable."); + return; + } + + var resources = ManifestParser.GetResourcesFromManifest(manifest, namespaceResolver, deployment.Variables, log); + AppliedResourcesOutputHelper.SetAppliedResourcesOutputVariable(log, deployment, resources); + } + catch (Exception ex) + { + log.Warn($"Failed to set applied resources output variable: {ex.Message}"); + } + } + List GetUpgradeCommandArgs(RunningDeployment deployment) { var args = new List(); diff --git a/source/Calamari/Kubernetes/Conventions/HelmUpgradeWithKOSConvention.cs b/source/Calamari/Kubernetes/Conventions/HelmUpgradeWithKOSConvention.cs index 1b5ae8b46a..7ff9ee13ad 100644 --- a/source/Calamari/Kubernetes/Conventions/HelmUpgradeWithKOSConvention.cs +++ b/source/Calamari/Kubernetes/Conventions/HelmUpgradeWithKOSConvention.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -69,9 +69,10 @@ public void Install(RunningDeployment deployment) var executor = new HelmUpgradeExecutor(log, fileSystem, valueSourcesParser, - helmCli); + helmCli, + namespaceResolver); - executor.ExecuteHelmUpgrade(deployment, releaseName, helmInstallCompletedCts, helmInstallErrorCts); + executor.ExecuteHelmUpgrade(deployment, releaseName, newRevisionNumber, helmInstallCompletedCts, helmInstallErrorCts); }); var manifestAndStatusCheckTask = Task.Run(async () => From 7caff7d7a1acc145e1eb158c3743918f468eda73 Mon Sep 17 00:00:00 2001 From: Sasha Kavalchuk Date: Mon, 18 May 2026 13:18:21 +0200 Subject: [PATCH 2/4] removed not relevant test --- .../AppliedResourcesOutputHelperTests.cs | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/source/Calamari.Tests/KubernetesFixtures/Commands/Executors/AppliedResourcesOutputHelperTests.cs b/source/Calamari.Tests/KubernetesFixtures/Commands/Executors/AppliedResourcesOutputHelperTests.cs index 4f77cd06ef..5ec984980f 100644 --- a/source/Calamari.Tests/KubernetesFixtures/Commands/Executors/AppliedResourcesOutputHelperTests.cs +++ b/source/Calamari.Tests/KubernetesFixtures/Commands/Executors/AppliedResourcesOutputHelperTests.cs @@ -108,27 +108,6 @@ public void HandlesEmptyResourceCollection_WhenFeatureToggleIsEnabled() outputVariable.Should().Be("[]"); } - [Test] - public void LogsAppliedResourcesJson_WhenFeatureToggleIsEnabled() - { - // Arrange - var variables = new CalamariVariables - { - [KnownVariables.EnabledFeatureToggles] = OctopusFeatureToggles.KnownSlugs.ArgoRolloutsSupportFeatureToggle - }; - var deployment = new RunningDeployment(variables); - var resources = new[] - { - new ResourceIdentifier(SupportedResourceGroupVersionKinds.DeploymentV1, "my-deployment", "default") - }; - - // Act - AppliedResourcesOutputHelper.SetAppliedResourcesOutputVariable(log, deployment, resources); - - // Assert - log.StandardOut.Should().Contain(msg => msg.Contains("Applied resources:")); - } - [Test] public void SerializesResourcesWithCorrectJsonFormat() { From e7864a2873a9db215db65e2432ac8d02ff13298d Mon Sep 17 00:00:00 2001 From: Sasha Kavalchuk Date: Tue, 19 May 2026 12:25:27 +0200 Subject: [PATCH 3/4] cleaned tests --- .../Helm/HelmUpgradeExecutorTests.cs | 37 +------------------ 1 file changed, 2 insertions(+), 35 deletions(-) diff --git a/source/Calamari.Tests/KubernetesFixtures/Conventions/Helm/HelmUpgradeExecutorTests.cs b/source/Calamari.Tests/KubernetesFixtures/Conventions/Helm/HelmUpgradeExecutorTests.cs index 168f26324f..4ad509d904 100644 --- a/source/Calamari.Tests/KubernetesFixtures/Conventions/Helm/HelmUpgradeExecutorTests.cs +++ b/source/Calamari.Tests/KubernetesFixtures/Conventions/Helm/HelmUpgradeExecutorTests.cs @@ -173,7 +173,8 @@ public void LogsWarningAndContinues_WhenGetManifestFails() // Assert log.MessagesWarnFormatted.Should().Contain(msg => msg.Contains("Failed to set applied resources output variable")); - installCompletedCts.IsCancellationRequested.Should().BeTrue(); + var appliedResourcesJson = variables.Get("AppliedResources"); + appliedResourcesJson.Should().BeNull(); } [Test] @@ -198,40 +199,6 @@ public void SkipsAppliedResourcesVariable_WhenManifestIsEmpty() log.MessagesVerboseFormatted.Should().Contain(msg => msg.Contains("empty, skipping applied resources")); } - [Test] - public void CancelsInstallCompletedToken_OnSuccessfulUpgrade() - { - // Arrange - var variables = CreateVariables(); - SetupHelmUpgradeMock(); - - var deployment = CreateRunningDeployment(variables); - var executor = CreateExecutor(deployment); - - // Act - executor.ExecuteHelmUpgrade(deployment, ReleaseName, RevisionNumber, installCompletedCts, installErrorCts); - - // Assert - installCompletedCts.IsCancellationRequested.Should().BeTrue(); - installErrorCts.IsCancellationRequested.Should().BeFalse(); - } - - [Test] - public void ThrowsCommandException_WhenUpgradeFails() - { - // Arrange - var variables = CreateVariables(); - SetupHelmUpgradeMockToFail(); - - var deployment = CreateRunningDeployment(variables); - var executor = CreateExecutor(deployment); - - // Act & Assert - var action = () => executor.ExecuteHelmUpgrade(deployment, ReleaseName, RevisionNumber, installCompletedCts, installErrorCts); - action.Should().Throw().WithMessage("*non-zero exit code*"); - installErrorCts.IsCancellationRequested.Should().BeTrue(); - } - void SetupChartDirectory() { Directory.CreateDirectory(ChartDirectory); From 1dbf87110eabefd209df3d2965b022aaf4b3090e Mon Sep 17 00:00:00 2001 From: Sasha Kavalchuk Date: Tue, 26 May 2026 12:15:04 +0200 Subject: [PATCH 4/4] resolved review comments --- .../AppliedResourcesOutputHelperTests.cs | 9 +-- .../Executors/KustomizeExecutorTests.cs | 2 +- .../Helm/HelmUpgradeExecutorTests.cs | 58 ++++--------------- .../Executors/AppliedResourcesOutputHelper.cs | 6 +- .../Conventions/Helm/HelmUpgradeExecutor.cs | 24 ++++---- .../Calamari/Kubernetes/SpecialVariables.cs | 2 + 6 files changed, 35 insertions(+), 66 deletions(-) diff --git a/source/Calamari.Tests/KubernetesFixtures/Commands/Executors/AppliedResourcesOutputHelperTests.cs b/source/Calamari.Tests/KubernetesFixtures/Commands/Executors/AppliedResourcesOutputHelperTests.cs index 5ec984980f..04f7199bee 100644 --- a/source/Calamari.Tests/KubernetesFixtures/Commands/Executors/AppliedResourcesOutputHelperTests.cs +++ b/source/Calamari.Tests/KubernetesFixtures/Commands/Executors/AppliedResourcesOutputHelperTests.cs @@ -3,6 +3,7 @@ using Calamari.Common.Commands; using Calamari.Common.FeatureToggles; using Calamari.Common.Plumbing.Variables; +using Calamari.Kubernetes; using Calamari.Kubernetes.Commands.Executors; using Calamari.Kubernetes.ResourceStatus.Resources; using Calamari.Testing.Helpers; @@ -42,7 +43,7 @@ public void SetsAppliedResourcesOutputVariable_WhenFeatureToggleIsEnabled() AppliedResourcesOutputHelper.SetAppliedResourcesOutputVariable(log, deployment, resources); // Assert - var outputVariable = variables.Get("AppliedResources"); + var outputVariable = variables.Get(SpecialVariables.AppliedResources); outputVariable.Should().NotBeNullOrEmpty(); var deserializedResources = JsonConvert.DeserializeAnonymousType(outputVariable, new[] @@ -84,7 +85,7 @@ public void DoesNotSetOutputVariable_WhenFeatureToggleIsDisabled() AppliedResourcesOutputHelper.SetAppliedResourcesOutputVariable(log, deployment, resources); // Assert - var outputVariable = variables.Get("AppliedResources"); + var outputVariable = variables.Get(SpecialVariables.AppliedResources); outputVariable.Should().BeNull(); } @@ -103,7 +104,7 @@ public void HandlesEmptyResourceCollection_WhenFeatureToggleIsEnabled() AppliedResourcesOutputHelper.SetAppliedResourcesOutputVariable(log, deployment, resources); // Assert - var outputVariable = variables.Get("AppliedResources"); + var outputVariable = variables.Get(SpecialVariables.AppliedResources); outputVariable.Should().NotBeNullOrEmpty(); outputVariable.Should().Be("[]"); } @@ -126,7 +127,7 @@ public void SerializesResourcesWithCorrectJsonFormat() AppliedResourcesOutputHelper.SetAppliedResourcesOutputVariable(log, deployment, resources); // Assert - var outputVariable = variables.Get("AppliedResources"); + var outputVariable = variables.Get(SpecialVariables.AppliedResources); var deserializedResources = JsonConvert.DeserializeAnonymousType(outputVariable, new[] { new { Group = "", Version = "", Kind = "", Name = "", Namespace = "" } diff --git a/source/Calamari.Tests/KubernetesFixtures/Commands/Executors/KustomizeExecutorTests.cs b/source/Calamari.Tests/KubernetesFixtures/Commands/Executors/KustomizeExecutorTests.cs index 5f15b75c76..9bc913da9b 100644 --- a/source/Calamari.Tests/KubernetesFixtures/Commands/Executors/KustomizeExecutorTests.cs +++ b/source/Calamari.Tests/KubernetesFixtures/Commands/Executors/KustomizeExecutorTests.cs @@ -392,7 +392,7 @@ public async Task SetsAppliedResourcesOutputVariable_WhenFeatureToggleIsEnabled( // Assert result.Should().BeTrue(); - var appliedResourcesJson = variables.Get("AppliedResources"); + var appliedResourcesJson = variables.Get(SpecialVariables.AppliedResources); appliedResourcesJson.Should().NotBeNullOrEmpty(); var deserializedResources = JsonConvert.DeserializeAnonymousType(appliedResourcesJson, new[] diff --git a/source/Calamari.Tests/KubernetesFixtures/Conventions/Helm/HelmUpgradeExecutorTests.cs b/source/Calamari.Tests/KubernetesFixtures/Conventions/Helm/HelmUpgradeExecutorTests.cs index 4ad509d904..1605bf7968 100644 --- a/source/Calamari.Tests/KubernetesFixtures/Conventions/Helm/HelmUpgradeExecutorTests.cs +++ b/source/Calamari.Tests/KubernetesFixtures/Conventions/Helm/HelmUpgradeExecutorTests.cs @@ -79,7 +79,13 @@ public void SetsAppliedResourcesOutputVariable_WhenFeatureToggleIsEnabled() executor.ExecuteHelmUpgrade(deployment, ReleaseName, RevisionNumber, installCompletedCts, installErrorCts); // Assert - var appliedResourcesJson = variables.Get("AppliedResources"); + commandLineRunner.Received().Execute(Arg.Is(i => + i.Arguments.Contains("get") && + i.Arguments.Contains("manifest") && + i.Arguments.Contains(ReleaseName) && + i.Arguments.Contains($"--revision {RevisionNumber}"))); + + var appliedResourcesJson = variables.Get(SpecialVariables.AppliedResources); appliedResourcesJson.Should().NotBeNullOrEmpty(); var deserializedResources = JsonConvert.DeserializeAnonymousType(appliedResourcesJson, new[] @@ -107,54 +113,12 @@ public void DoesNotSetAppliedResourcesOutputVariable_WhenFeatureToggleIsDisabled executor.ExecuteHelmUpgrade(deployment, ReleaseName, RevisionNumber, installCompletedCts, installErrorCts); // Assert - var appliedResourcesJson = variables.Get("AppliedResources"); + var appliedResourcesJson = variables.Get(SpecialVariables.AppliedResources); appliedResourcesJson.Should().BeNull(); commandLineRunner.DidNotReceive().Execute(Arg.Is(i => i.Arguments.Contains("get manifest"))); } - [Test] - public void DoesNotCallGetManifest_WhenFeatureToggleIsDisabled() - { - // Arrange - var variables = CreateVariables(); - - SetupHelmUpgradeMock(); - - var deployment = CreateRunningDeployment(variables); - var executor = CreateExecutor(deployment); - - // Act - executor.ExecuteHelmUpgrade(deployment, ReleaseName, RevisionNumber, installCompletedCts, installErrorCts); - - // Assert - commandLineRunner.DidNotReceive().Execute(Arg.Is(i => i.Arguments.Contains("get manifest"))); - } - - [Test] - public void CallsGetManifest_WhenFeatureToggleIsEnabled() - { - // Arrange - var variables = CreateVariables(); - variables.Set(KnownVariables.EnabledFeatureToggles, OctopusFeatureToggles.KnownSlugs.ArgoRolloutsSupportFeatureToggle); - - SetupHelmUpgradeMock(); - SetupHelmGetManifestMock(GetSampleManifest()); - - var deployment = CreateRunningDeployment(variables); - var executor = CreateExecutor(deployment); - - // Act - executor.ExecuteHelmUpgrade(deployment, ReleaseName, RevisionNumber, installCompletedCts, installErrorCts); - - // Assert - commandLineRunner.Received().Execute(Arg.Is(i => - i.Arguments.Contains("get") && - i.Arguments.Contains("manifest") && - i.Arguments.Contains(ReleaseName) && - i.Arguments.Contains($"--revision {RevisionNumber}"))); - } - [Test] public void LogsWarningAndContinues_WhenGetManifestFails() { @@ -172,8 +136,8 @@ public void LogsWarningAndContinues_WhenGetManifestFails() executor.ExecuteHelmUpgrade(deployment, ReleaseName, RevisionNumber, installCompletedCts, installErrorCts); // Assert - log.MessagesWarnFormatted.Should().Contain(msg => msg.Contains("Failed to set applied resources output variable")); - var appliedResourcesJson = variables.Get("AppliedResources"); + log.MessagesWarnFormatted.Should().Contain(msg => msg.Contains($"Failed to get manifest for {ReleaseName} revision {RevisionNumber}")); + var appliedResourcesJson = variables.Get(SpecialVariables.AppliedResources); appliedResourcesJson.Should().BeNull(); } @@ -194,7 +158,7 @@ public void SkipsAppliedResourcesVariable_WhenManifestIsEmpty() executor.ExecuteHelmUpgrade(deployment, ReleaseName, RevisionNumber, installCompletedCts, installErrorCts); // Assert - var appliedResourcesJson = variables.Get("AppliedResources"); + var appliedResourcesJson = variables.Get(SpecialVariables.AppliedResources); appliedResourcesJson.Should().BeNull(); log.MessagesVerboseFormatted.Should().Contain(msg => msg.Contains("empty, skipping applied resources")); } diff --git a/source/Calamari/Kubernetes/Commands/Executors/AppliedResourcesOutputHelper.cs b/source/Calamari/Kubernetes/Commands/Executors/AppliedResourcesOutputHelper.cs index 00ececebdf..6e5d464d78 100644 --- a/source/Calamari/Kubernetes/Commands/Executors/AppliedResourcesOutputHelper.cs +++ b/source/Calamari/Kubernetes/Commands/Executors/AppliedResourcesOutputHelper.cs @@ -11,8 +11,8 @@ namespace Calamari.Kubernetes.Commands.Executors public static class AppliedResourcesOutputHelper { public static void SetAppliedResourcesOutputVariable( - ILog log, - RunningDeployment deployment, + ILog log, + RunningDeployment deployment, IEnumerable resources) { if (!OctopusFeatureToggles.ArgoRolloutsSupportFeatureToggle.IsEnabled(deployment.Variables)) @@ -31,7 +31,7 @@ public static void SetAppliedResourcesOutputVariable( var json = JsonConvert.SerializeObject(resourceList); - log.SetOutputVariable("AppliedResources", json, deployment.Variables); + log.SetOutputVariable(SpecialVariables.AppliedResources, json, deployment.Variables); } } } diff --git a/source/Calamari/Kubernetes/Conventions/Helm/HelmUpgradeExecutor.cs b/source/Calamari/Kubernetes/Conventions/Helm/HelmUpgradeExecutor.cs index feac1f29aa..0550dc2c3e 100644 --- a/source/Calamari/Kubernetes/Conventions/Helm/HelmUpgradeExecutor.cs +++ b/source/Calamari/Kubernetes/Conventions/Helm/HelmUpgradeExecutor.cs @@ -72,23 +72,25 @@ public void ExecuteHelmUpgrade(RunningDeployment deployment, void SetAppliedResourcesOutputVariable(RunningDeployment deployment, string releaseName, int revisionNumber) { + string manifest = null; try { - var manifest = helmCli.GetManifest(releaseName, revisionNumber); - - if (string.IsNullOrWhiteSpace(manifest)) - { - log.Verbose($"Helm manifest for {releaseName} revision {revisionNumber} is empty, skipping applied resources output variable."); - return; - } - - var resources = ManifestParser.GetResourcesFromManifest(manifest, namespaceResolver, deployment.Variables, log); - AppliedResourcesOutputHelper.SetAppliedResourcesOutputVariable(log, deployment, resources); + manifest = helmCli.GetManifest(releaseName, revisionNumber); } catch (Exception ex) { - log.Warn($"Failed to set applied resources output variable: {ex.Message}"); + log.Warn($"Failed to get manifest for {releaseName} revision {revisionNumber}: {ex.Message}"); + return; + } + + if (string.IsNullOrWhiteSpace(manifest)) + { + log.Verbose($"Helm manifest for {releaseName} revision {revisionNumber} is empty, skipping applied resources output variable."); + return; } + + var resources = ManifestParser.GetResourcesFromManifest(manifest, namespaceResolver, deployment.Variables, log); + AppliedResourcesOutputHelper.SetAppliedResourcesOutputVariable(log, deployment, resources); } List GetUpgradeCommandArgs(RunningDeployment deployment) diff --git a/source/Calamari/Kubernetes/SpecialVariables.cs b/source/Calamari/Kubernetes/SpecialVariables.cs index 9f5b7400a8..86cddcf04b 100644 --- a/source/Calamari/Kubernetes/SpecialVariables.cs +++ b/source/Calamari/Kubernetes/SpecialVariables.cs @@ -42,6 +42,8 @@ public static class SpecialVariables public const string ServerSideApplyEnabled = "Octopus.Action.Kubernetes.ServerSideApply.Enabled"; public const string ServerSideApplyForceConflicts = "Octopus.Action.Kubernetes.ServerSideApply.ForceConflicts"; + public const string AppliedResources = "Octopus.Action.Kubernetes.AppliedResources"; + public static class Helm { public const string ReleaseName = "Octopus.Action.Helm.ReleaseName";