diff --git a/.changeset/dry-feet-add.md b/.changeset/dry-feet-add.md new file mode 100644 index 0000000..1910ff4 --- /dev/null +++ b/.changeset/dry-feet-add.md @@ -0,0 +1,5 @@ +--- +"kubernetes-agent": patch +--- + +Correctly set serviceName on NFS StatefulSet diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1b6140d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "yaml.schemas": { + "https://raw.githubusercontent.com/helm-unittest/helm-unittest/v0.5.1/schema/helm-testsuite.json": "file:///**/*_test.yaml" + } +} \ No newline at end of file diff --git a/charts/kubernetes-agent/package.json b/charts/kubernetes-agent/package.json index 0ee73ec..fbf99a9 100644 --- a/charts/kubernetes-agent/package.json +++ b/charts/kubernetes-agent/package.json @@ -5,8 +5,8 @@ "description": "The Octopus Kubernetes Agent", "author": "Octopus Deploy Ptd Ltd", "scripts": { - "test": "cross-env-shell docker run -ti --rm -v $INIT_CWD:/apps helmunittest/helm-unittest:latest .", - "update-test-snapshots": "cross-env-shell docker run -ti --rm -v $INIT_CWD:/apps helmunittest/helm-unittest:latest . -u", + "test": "cross-env-shell docker run -ti --rm -v $INIT_CWD:/apps helmunittest/helm-unittest:3.15.2-0.5.1 .", + "update-test-snapshots": "cross-env-shell docker run -ti --rm -v $INIT_CWD:/apps helmunittest/helm-unittest:3.15.2-0.5.1 . -u", "generate-agent-docs": "docker run --rm --volume \".:/helm-docs\" jnorwood/helm-docs:latest" }, "dependencies": { diff --git a/charts/kubernetes-agent/templates/nfs-statefulset.yaml b/charts/kubernetes-agent/templates/nfs-statefulset.yaml index 8912391..aea723a 100644 --- a/charts/kubernetes-agent/templates/nfs-statefulset.yaml +++ b/charts/kubernetes-agent/templates/nfs-statefulset.yaml @@ -2,14 +2,22 @@ apiVersion: apps/v1 kind: StatefulSet metadata: - name: {{ include "nfs.name" .}} + name: {{ include "nfs.name" . }} namespace: {{ .Release.Namespace | quote }} labels: - app.kubernetes.io/name: {{ include "nfs.name" .}} + app.kubernetes.io/name: {{ include "nfs.name" . }} spec: selector: matchLabels: - app.kubernetes.io/name: {{ include "nfs.name" .}} + app.kubernetes.io/name: {{ include "nfs.name" . }} + {{- $ss := (lookup "apps/v1" "StatefulSet" .Release.Namespace (include "nfs.name" .)) }} + {{- if not $ss }} + {{/* If the stateful set doesn't exist */}} + serviceName: {{ include "nfs.name" . }} + {{- else if (index $ss "spec.serviceName" ) }} + {{/* Or it does exist and it already has a non-empty serviceName */}} + serviceName: {{ include "nfs.name" . }} + {{- end }} template: metadata: {{- with .Values.persistence.nfs.metadata.annotations }} diff --git a/charts/kubernetes-agent/templates/tentacle-deployment.yaml b/charts/kubernetes-agent/templates/tentacle-deployment.yaml index dfc7e39..d70f500 100644 --- a/charts/kubernetes-agent/templates/tentacle-deployment.yaml +++ b/charts/kubernetes-agent/templates/tentacle-deployment.yaml @@ -18,7 +18,7 @@ spec: {{- end }} labels: {{- include "kubernetes-agent.labels" . | nindent 8 }} - {{- with .Values.agent.metadata.labels }} + {{- with .Values.agent.metadata.labels }} {{- toYaml . | nindent 8 }} {{- end }} spec: diff --git a/charts/kubernetes-agent/tests/__snapshot__/nfs-statefulset_test.yaml.snap b/charts/kubernetes-agent/tests/__snapshot__/nfs-statefulset_test.yaml.snap index 6349f10..1b2fff0 100644 --- a/charts/kubernetes-agent/tests/__snapshot__/nfs-statefulset_test.yaml.snap +++ b/charts/kubernetes-agent/tests/__snapshot__/nfs-statefulset_test.yaml.snap @@ -11,6 +11,7 @@ should match snapshot: selector: matchLabels: app.kubernetes.io/name: octopus-agent-nfs + serviceName: octopus-agent-nfs template: metadata: labels: diff --git a/charts/kubernetes-agent/tests/nfs-statefulset_test.yaml b/charts/kubernetes-agent/tests/nfs-statefulset_test.yaml index 0d60e2d..a3507d5 100644 --- a/charts/kubernetes-agent/tests/nfs-statefulset_test.yaml +++ b/charts/kubernetes-agent/tests/nfs-statefulset_test.yaml @@ -1,6 +1,27 @@ suite: "persistence" templates: -- templates/nfs-statefulset.yaml + - templates/nfs-statefulset.yaml +kubernetesProvider: + scheme: + "apps/v1/StatefulSet": + gvr: + group: "apps" + version: "v1" + resource: "statefulsets" + namespaced: true + objects: + - kind: StatefulSet + apiVersion: "apps/v1" + metadata: + name: "empty-serviceName-nfs" + namespace: "NAMESPACE" + - kind: StatefulSet + apiVersion: "apps/v1" + metadata: + name: "populated-serviceName-nfs" + namespace: "NAMESPACE" + spec: + serviceName: "populated-serviceName-nfs" tests: - it: "is not created when storageClassName has a value" set: @@ -21,4 +42,28 @@ tests: asserts: - equal: path: spec.template.spec.volumes[?(@.name == 'octopus-volume')].emptyDir.sizeLimit - value: 100Gi \ No newline at end of file + value: 100Gi + +- it: does not set serviceName if the StatefulSet exists and has an empty serviceName + set: + nameOverride: "empty-serviceName" + asserts: + - equal: + path: metadata.name + value: "empty-serviceName-nfs" + - notExists: + path: spec.serviceName + +- it: sets serviceName if the StatefulSet doesn't exist + asserts: + - equal: + path: spec.serviceName + value: octopus-agent-nfs + +- it: sets serviceName if the StatefulSet exists and is set to same service name + set: + nameOverride: "populated-ServiceName" + asserts: + - equal: + path: spec.serviceName + value: populated-ServiceName-nfs \ No newline at end of file diff --git a/tests/kubernetes-agent/KubernetesAgent.IntegrationTests/HelmChartBuilder.cs b/tests/kubernetes-agent/KubernetesAgent.IntegrationTests/HelmChartBuilder.cs index 0e53dfd..383ac3b 100644 --- a/tests/kubernetes-agent/KubernetesAgent.IntegrationTests/HelmChartBuilder.cs +++ b/tests/kubernetes-agent/KubernetesAgent.IntegrationTests/HelmChartBuilder.cs @@ -32,7 +32,7 @@ static string[] GetHelmChartPackageArguments(string version) ]; } - static DirectoryInfo GetChartsDirectory() + public static DirectoryInfo GetChartsDirectory() { var chartsDirectory = Path.Combine(AppContext.BaseDirectory); var currentDirectory = new DirectoryInfo(chartsDirectory); diff --git a/tests/kubernetes-agent/KubernetesAgent.IntegrationTests/RunsAgentInstall.cs b/tests/kubernetes-agent/KubernetesAgent.IntegrationTests/RunsAgentInstall.cs new file mode 100644 index 0000000..7b69fd2 --- /dev/null +++ b/tests/kubernetes-agent/KubernetesAgent.IntegrationTests/RunsAgentInstall.cs @@ -0,0 +1,73 @@ +using Halibut; +using KubernetesAgent.Integration.Setup; +using KubernetesAgent.Integration.Setup.Common; +using Octopus.Tentacle.Client; +using Octopus.Tentacle.Client.Scripts.Models; +using Octopus.Tentacle.Client.Scripts.Models.Builders; +using Xunit.Abstractions; + +namespace KubernetesAgent.Integration; + +public class HelmInstallTests(ITestOutputHelper output) : IAsyncLifetime +{ + readonly ITestOutputHelper output = output; + ILogger logger = null!; + readonly TemporaryDirectory workingDirectory = new(Directory.CreateTempSubdirectory()); + KubernetesClusterInstaller clusterInstaller = null!; + KubernetesAgentInstaller agentInstaller = null!; + TentacleClient client = null!; + string kindExePath = null!; + string helmExePath = null!; + string kubeCtlPath = null!; + + public async Task InitializeAsync() + { + logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .WriteTo.TestOutput(output) + .CreateLogger(); + + var requiredToolDownloader = new RequiredToolDownloader(workingDirectory, logger); + (kindExePath, helmExePath, kubeCtlPath) = await requiredToolDownloader.DownloadRequiredTools(CancellationToken.None); + clusterInstaller = new KubernetesClusterInstaller(workingDirectory, kindExePath, helmExePath, kubeCtlPath, logger); + await clusterInstaller.Install(); + } + + public async Task DisposeAsync() + { + clusterInstaller.Dispose(); + workingDirectory.Dispose(); + await Task.CompletedTask; + } + + [Fact] + public async Task CanInstallAgentAndRunCommand() + { + var chartDirectory = HelmChartBuilder.GetChartsDirectory(); + + agentInstaller = new KubernetesAgentInstaller(workingDirectory , helmExePath, kubeCtlPath, clusterInstaller.KubeConfigPath, logger, chartDirectory.FullName); + client = await agentInstaller.InstallAgent(); + + var testLogger = new TestLogger(logger); + + var runHelloWorldCommand = new ExecuteKubernetesScriptCommandBuilder(Guid.NewGuid().ToString()) + .WithScriptBody("echo \"hello world\"") + .Build(); + var result = await client.ExecuteScript(runHelloWorldCommand, OnScriptStatusResponseReceived, OnScriptCompleted, testLogger, CancellationToken.None); + if (result.ExitCode != 0) + { + throw new Exception($"Script failed with exit code {result.ExitCode}"); + } + + logger.Information("Script executed successfully"); + return; + + async Task OnScriptCompleted(CancellationToken t) + { + await Task.CompletedTask; + logger.Information("Script completed"); + } + + void OnScriptStatusResponseReceived(ScriptExecutionStatus res) => logger.Information("{Output}", res.ToString()); + } +} \ No newline at end of file diff --git a/tests/kubernetes-agent/KubernetesAgent.IntegrationTests/Setup/Common/FileSystemInfoExtensionMethods.cs b/tests/kubernetes-agent/KubernetesAgent.IntegrationTests/Setup/Common/FileSystemInfoExtensionMethods.cs index 4f52a3a..d7902dd 100644 --- a/tests/kubernetes-agent/KubernetesAgent.IntegrationTests/Setup/Common/FileSystemInfoExtensionMethods.cs +++ b/tests/kubernetes-agent/KubernetesAgent.IntegrationTests/Setup/Common/FileSystemInfoExtensionMethods.cs @@ -1,9 +1,17 @@ +using System.Runtime.InteropServices; + namespace KubernetesAgent.Integration.Setup.Common; public static class FileSystemInfoExtensionMethods { public static void MakeExecutable(this FileSystemInfo fsObject) { + //if we are on windows, we don't need to do this + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return; + } + var result = ProcessRunner.Run("chmod", "-R", "+x", fsObject.FullName); if (result.ExitCode != 0) { diff --git a/tests/kubernetes-agent/KubernetesAgent.IntegrationTests/Setup/KubernetesAgentInstaller.cs b/tests/kubernetes-agent/KubernetesAgent.IntegrationTests/Setup/KubernetesAgentInstaller.cs index c3ad1da..45d79d4 100644 --- a/tests/kubernetes-agent/KubernetesAgent.IntegrationTests/Setup/KubernetesAgentInstaller.cs +++ b/tests/kubernetes-agent/KubernetesAgent.IntegrationTests/Setup/KubernetesAgentInstaller.cs @@ -23,19 +23,21 @@ public class KubernetesAgentInstaller readonly string kubeCtlExePath; readonly TemporaryDirectory temporaryDirectory; readonly ILogger logger; + readonly string? chartDirectoryName; readonly string kubeConfigPath; protected HalibutRuntime ServerHalibutRuntime { get; private set; } = null!; protected TentacleClient TentacleClient { get; private set; } = null!; bool isAgentInstalled; - public KubernetesAgentInstaller(TemporaryDirectory temporaryDirectory, string helmExePath, string kubeCtlExePath, string kubeConfigPath, ILogger logger) + public KubernetesAgentInstaller(TemporaryDirectory temporaryDirectory, string helmExePath, string kubeCtlExePath, string kubeConfigPath, ILogger logger, string? chartDirectoryName = null) { this.temporaryDirectory = temporaryDirectory; this.helmExePath = helmExePath; this.kubeCtlExePath = kubeCtlExePath; this.kubeConfigPath = kubeConfigPath; this.logger = logger; + this.chartDirectoryName = chartDirectoryName; AgentName = Guid.NewGuid().ToString("N"); } @@ -124,7 +126,8 @@ string BuildAgentInstallArguments(string valuesFilePath, bool hasRegistrySecret) NamespaceFlag, KubeConfigFlag, AgentName, - ArtifactoryAgentOciRepository + //Use the local directory if it exists, otherwise pull from artifactory + chartDirectoryName ?? ArtifactoryAgentOciRepository }; return string.Join(" ", args.WhereNotNull()); diff --git a/tests/kubernetes-agent/KubernetesAgent.IntegrationTests/Setup/KubernetesClusterInstaller.cs b/tests/kubernetes-agent/KubernetesAgent.IntegrationTests/Setup/KubernetesClusterInstaller.cs index c982c97..3b716f1 100644 --- a/tests/kubernetes-agent/KubernetesAgent.IntegrationTests/Setup/KubernetesClusterInstaller.cs +++ b/tests/kubernetes-agent/KubernetesAgent.IntegrationTests/Setup/KubernetesClusterInstaller.cs @@ -6,6 +6,7 @@ namespace KubernetesAgent.Integration.Setup; public class KubernetesClusterInstaller : IDisposable { + static int clusterCount = 0; readonly string clusterName; readonly string kubeConfigName; @@ -25,7 +26,9 @@ public KubernetesClusterInstaller(TemporaryDirectory tempDirectory, string kindE this.kubeCtlPath = kubeCtlPath; this.logger = logger; - clusterName = $"helm-octopus-agent-int-{DateTime.Now:yyyyMMddhhmmss}"; + //hack to get a unique time for multiple clusters + var count = Interlocked.Increment(ref clusterCount); + clusterName = $"octo-test-{DateTime.Now.AddSeconds(count):yyyyMMddhhmmss}"; kubeConfigName = $"{clusterName}.config"; } diff --git a/tests/kubernetes-agent/KubernetesAgent.IntegrationTests/Setup/Tooling/HelmDownloader.cs b/tests/kubernetes-agent/KubernetesAgent.IntegrationTests/Setup/Tooling/HelmDownloader.cs index f29f8c8..19b3047 100644 --- a/tests/kubernetes-agent/KubernetesAgent.IntegrationTests/Setup/Tooling/HelmDownloader.cs +++ b/tests/kubernetes-agent/KubernetesAgent.IntegrationTests/Setup/Tooling/HelmDownloader.cs @@ -6,7 +6,7 @@ namespace KubernetesAgent.Integration.Setup.Tooling; public class HelmDownloader : ToolDownloader { - const string LatestVersion = "v3.14.3"; + const string LatestVersion = "v3.15.3"; public HelmDownloader( ILogger logger) : base("helm", logger) {