Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add upgrade test for helm chart #151

Merged
merged 16 commits into from
May 13, 2024
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ octopus*.tgz
index.yaml
node_modules

.DS_STORE

*.orig
7 changes: 7 additions & 0 deletions Global.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"sdk": {
"version": "8.0.101",
"rollForward": "latestFeature",
"allowPrerelease": false
}
}
1 change: 0 additions & 1 deletion tests/kubernetes-agent/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,6 @@ ClientBin/
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Global using directives

global using Serilog;
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using KubernetesAgent.Integration.Setup.Common;

namespace KubernetesAgent.Integration
{
public static class HelmChartBuilder
{
public static string BuildHelmChart(string helmExecutable, TemporaryDirectory directory)
{
var version = GetChartVersion();
version = $"{version}-{DateTime.Now:yyyymdHHmmss}";

var packager = ProcessRunner.Run(helmExecutable, directory, GetHelmChartPackageArguments(version));
if (packager.ExitCode != 0)
{
throw new Exception($"Failed to package Helm chart. Exit code: {packager.ExitCode}");
}

var output = packager.StandardOutput.ReadToEnd();
return output.Split(":")[^1].ToString().Trim();
}

static string[] GetHelmChartPackageArguments(string version)
{
var chartsDirectory = GetChartsDirectory().FullName;
return
[
"package",
chartsDirectory,
"--version",
version
];
}

static DirectoryInfo GetChartsDirectory()
{
var chartsDirectory = Path.Combine(AppContext.BaseDirectory);
var currentDirectory = new DirectoryInfo(chartsDirectory);
while (currentDirectory != null && currentDirectory.EnumerateDirectories().All(d => d.Name != "charts"))
{
currentDirectory = currentDirectory.Parent;
}
if (currentDirectory == null)
{
throw new DirectoryNotFoundException("Could not find the charts directory");
}
return new DirectoryInfo(Path.Combine(currentDirectory.FullName, "charts", "kubernetes-agent"));
}

static string GetChartVersion()
{
var chartDirectory = GetChartsDirectory();
var chartYaml = Path.Combine(chartDirectory.FullName, "Chart.yaml");
var chart = File.ReadAllText(chartYaml);
return chart.Split("\n").First(l => l.StartsWith("version:")).Split(":")[1].Trim('"').Trim();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
<PackageReference Include="Octopus.Client" Version="14.3.1381" />
<PackageReference Include="Octopus.Tentacle.Client" Version="8.1.1594" />
<PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.XUnit" Version="3.0.5" />
<PackageReference Include="xunit" Version="2.5.3"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3"/>
</ItemGroup>
Expand All @@ -21,4 +25,31 @@
<Using Include="Xunit"/>
</ItemGroup>

<ItemGroup>
<None Remove="Setup\linux-network-routing.yaml" />
<EmbeddedResource Include="Setup\linux-network-routing.yaml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<None Remove="Setup\docker-desktop-network-routing.yaml" />
<EmbeddedResource Include="Setup\docker-desktop-network-routing.yaml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<None Remove="Setup\kind-config.yaml" />
<EmbeddedResource Include="Setup\kind-config.yaml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<None Remove="Setup\agent-values.yaml" />
<EmbeddedResource Include="Setup\agent-values.yaml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<None Remove="Setup\Common\Certificates\Server.pfx" />
<EmbeddedResource Include="Setup\Common\Certificates\Server.pfx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<None Remove="Setup\Common\Certificates\Tentacle.pfx" />
<EmbeddedResource Include="Setup\Common\Certificates\Tentacle.pfx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
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 Octopus.Tentacle.Contracts;
using Xunit.Abstractions;

namespace KubernetesAgent.Integration;

public class HelmUpgradeTests(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();
agentInstaller = new KubernetesAgentInstaller(workingDirectory , helmExePath, kubeCtlPath, clusterInstaller.KubeConfigPath, logger);
client = await agentInstaller.InstallAgent();
}

public async Task DisposeAsync()
{
clusterInstaller.Dispose();
workingDirectory.Dispose();
await Task.CompletedTask;
}

[Fact]
public async Task CanUpgradeAgentAndRunCommand()
{
var helmPackage = HelmChartBuilder.BuildHelmChart(helmExePath, workingDirectory);
var helmPackageFile = new FileInfo(helmPackage);
var packageName = helmPackageFile.Name;
var packageBytes = await File.ReadAllBytesAsync(helmPackage);
var helmUpgradeScript =
$"""
helm upgrade \
--atomic \
--namespace {agentInstaller.Namespace} \
{agentInstaller.AgentName} \
/tmp/{packageName}
""";
var upgradeHelmChartCommand = new ExecuteKubernetesScriptCommandBuilder(Guid.NewGuid().ToString())
.WithScriptBody($"cp ./{packageName} /tmp/{packageName} && {helmUpgradeScript}")
liam-mackie marked this conversation as resolved.
Show resolved Hide resolved
.WithScriptFile(new ScriptFile(packageName, DataStream.FromBytes(packageBytes)))
.Build();
void onScriptStatusResponseReceived(ScriptExecutionStatus res) => logger.Information("{Output}", res.ToString());
async Task onScriptCompleted(CancellationToken t)
{
await Task.CompletedTask;
logger.Information("Script completed");
}
var testLogger = new TestLogger(logger);
var result = await client.ExecuteScript(upgradeHelmChartCommand, onScriptStatusResponseReceived, onScriptCompleted, testLogger, CancellationToken.None);
if (result.ExitCode != 0)
{
throw new Exception($"Script failed with exit code {result.ExitCode}");
}
logger.Information("Upgrade executed successfully");

var runHelloWorldCommand = new ExecuteKubernetesScriptCommandBuilder(Guid.NewGuid().ToString())
.WithScriptBody("echo \"hello world\"")
.Build();
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");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;
using System.Reflection;

namespace KubernetesAgent.Integration.Setup.Common;

public static class AssemblyExtensions
{
public static Stream GetManifestResourceStreamFromPartialName(this Assembly assembly, string filename)
{
var valuesFileName = assembly.GetManifestResourceNames().Single(n => n.Contains(filename, StringComparison.OrdinalIgnoreCase));
return assembly.GetManifestResourceStream(valuesFileName)!;
}
}
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace KubernetesAgent.Integration.Setup.Common;

public static class EnumerableExtensions
{
public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> items)
where T : class
=> items.Where(i => i != null)!;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Formats.Tar;
using System.IO.Compression;

namespace KubernetesAgent.Integration.Setup.Common.Extraction;

public static class TarFileExtractor
{
public static void Extract(FileSystemInfo tarFile, FileSystemInfo targetDirectory)
{
if (!targetDirectory.Exists)
{
Directory.CreateDirectory(targetDirectory.FullName);
}
using var compressedFileStream = File.Open(tarFile.FullName, FileMode.Open);
using var tarStream = new GZipStream(compressedFileStream, CompressionMode.Decompress);
TarFile.ExtractToDirectory(tarStream, targetDirectory.FullName, true);
}

public static void ExtractAndMakeExecutable(FileSystemInfo tarFile, FileSystemInfo targetDirectory)
{
Extract(tarFile, targetDirectory);
targetDirectory.MakeExecutable();
}
public static void ExtractAndMakeExecutable(string tarFilePath, string targetDirectoryPath)
{
var tarFile = new FileInfo(tarFilePath);
var targetDirectory = new FileInfo(targetDirectoryPath);
ExtractAndMakeExecutable(tarFile, targetDirectory);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace KubernetesAgent.Integration.Setup.Common;

public static class FileSystemInfoExtensionMethods
{
public static void MakeExecutable(this FileSystemInfo fsObject)
{
var result = ProcessRunner.Run("chmod", "-R", "+x", fsObject.FullName);
if (result.ExitCode != 0)
{
throw new Exception($"Failed to make {fsObject.FullName} executable. Exit code: {result.ExitCode}. stdout: {result.StandardOutput.ReadToEnd()}, stderr: {result.StandardError.ReadToEnd()}.");
}
result.Close();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace KubernetesAgent.Integration.Setup.Common;

public class PollingSubscriptionIdGenerator
{
private static Random random = new Random();
public static Uri Generate()
{
return new Uri("poll://" + RandomString(20).ToLowerInvariant() + "/");
}

static string RandomString(int length)
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
return new string(Enumerable.Repeat(chars, length)
.Select(s => s[random.Next(s.Length)]).ToArray());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System.Diagnostics;

namespace KubernetesAgent.Integration.Setup.Common;

public static class ProcessRunner
{
public static Process Run(string command, TemporaryDirectory workingDirectory, params string[] arguments)
{
var process = new Process();
process.StartInfo.FileName = command;
process.StartInfo.Arguments = arguments.Length > 0 ? string.Join(" ", arguments) : string.Empty;
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.WorkingDirectory = workingDirectory.Directory.FullName;
process.Start();
process.WaitForExit();
return process;
}
public static Process Run(string command, params string[] arguments)
{
var process = new Process();
process.StartInfo.FileName = command;
process.StartInfo.Arguments = arguments.Length > 0 ? string.Join(" ", arguments) : string.Empty;
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.Start();
process.WaitForExit();
return process;
}


public static Process RunWithLogger(string command, TemporaryDirectory directory, ILogger logger, params string[] arguments)
{

var process = new Process();
process.StartInfo.FileName = command;
process.StartInfo.Arguments = arguments.Length > 0 ? string.Join(" ", arguments) : string.Empty;
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.WorkingDirectory = directory.Directory.FullName;

process.OutputDataReceived += (sender, e) =>
{
if (e.Data != null)
{
logger.Information("{Data}",e.Data);
}
};

process.ErrorDataReceived += (sender, e) =>
{
if (e.Data != null)
{
logger.Error("{Data}",e.Data);
}
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
process.WaitForExit();
return process;
}
}