diff --git a/docker/kubernetes-agent-tentacle/bootstrapRunner/bootstrapRunner.go b/docker/kubernetes-agent-tentacle/bootstrapRunner/bootstrapRunner.go
index c05c690b8..e9586122a 100644
--- a/docker/kubernetes-agent-tentacle/bootstrapRunner/bootstrapRunner.go
+++ b/docker/kubernetes-agent-tentacle/bootstrapRunner/bootstrapRunner.go
@@ -2,10 +2,16 @@ package main
import (
"bufio"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/rand"
+ "encoding/base64"
"errors"
"fmt"
+ "io"
"os"
"os/exec"
+ "path"
"sync"
)
@@ -30,6 +36,12 @@ func main() {
args := os.Args[2:]
cmd := exec.Command("bash", args[0:]...)
cmd.Dir = workspacePath
+
+ gcm, err := CreateCipher(workspacePath)
+ if err != nil {
+ panic(err)
+ }
+
stdOutCmdReader, _ := cmd.StdoutPipe()
stdErrCmdReader, _ := cmd.StderrPipe()
@@ -39,14 +51,14 @@ func main() {
doneStd := make(chan bool)
doneErr := make(chan bool)
- go reader(stdOutScanner, "stdout", &doneStd, &lineCounter)
- go reader(stdErrScanner, "stderr", &doneErr, &lineCounter)
+ go reader(stdOutScanner, "stdout", &doneStd, &lineCounter, gcm)
+ go reader(stdErrScanner, "stderr", &doneErr, &lineCounter, gcm)
- Write("stdout", "##octopus[stdout-verbose]", &lineCounter)
- Write("stdout", "Kubernetes Script Pod started", &lineCounter)
- Write("stdout", "##octopus[stdout-default]", &lineCounter)
+ Write("stdout", "##octopus[stdout-verbose]", &lineCounter, gcm)
+ Write("stdout", "Kubernetes Script Pod started", &lineCounter, gcm)
+ Write("stdout", "##octopus[stdout-default]", &lineCounter, gcm)
- err := cmd.Start()
+ err = cmd.Start()
// Wait for output buffering first
<-doneStd
@@ -66,29 +78,73 @@ func main() {
exitCode := cmd.ProcessState.ExitCode()
- Write("stdout", "##octopus[stdout-verbose]", &lineCounter)
- Write("stdout", "Kubernetes Script Pod completed", &lineCounter)
- Write("stdout", "##octopus[stdout-default]", &lineCounter)
+ Write("stdout", "##octopus[stdout-verbose]", &lineCounter, gcm)
+ Write("stdout", "Kubernetes Script Pod completed", &lineCounter, gcm)
+ Write("stdout", "##octopus[stdout-default]", &lineCounter, gcm)
- Write("debug", fmt.Sprintf("EOS-075CD4F0-8C76-491D-BA76-0879D35E9CFE<<>>%d", exitCode), &lineCounter)
+ Write("debug", fmt.Sprintf("EOS-075CD4F0-8C76-491D-BA76-0879D35E9CFE<<>>%d", exitCode), &lineCounter, gcm)
os.Exit(exitCode)
}
-func reader(scanner *bufio.Scanner, stream string, done *chan bool, counter *SafeCounter) {
+func reader(scanner *bufio.Scanner, stream string, done *chan bool, counter *SafeCounter, gcm cipher.AEAD) {
for scanner.Scan() {
- Write(stream, scanner.Text(), counter)
+ Write(stream, scanner.Text(), counter, gcm)
}
*done <- true
}
-func Write(stream string, text string, counter *SafeCounter) {
+func Write(stream string, text string, counter *SafeCounter, gcm cipher.AEAD) {
//Use a mutex to prevent race conditions updating the line number
//https://go.dev/tour/concurrency/9
counter.Mutex.Lock()
- fmt.Printf("|%d|%s|%s\n", counter.Value, stream, text)
+ nonce := make([]byte, gcm.NonceSize())
+ if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
+ panic(err)
+ }
+
+ ciphertext := gcm.Seal(nonce, nonce, []byte(text), nil)
+
+ fmt.Printf("|%d|%s|%x\n", counter.Value, stream, ciphertext)
counter.Value++
counter.Mutex.Unlock()
}
+
+func CreateCipher(workspaceDir string) (cipher.AEAD, error) {
+ // Read the key from the file
+ fileBytes, err := os.ReadFile(path.Join(workspaceDir, "keyfile"))
+ if err != nil {
+ return nil, err
+ }
+
+ //the key is encoded in the file in Base64
+ key := make([]byte, base64.StdEncoding.DecodedLen(len(fileBytes)))
+ length, err := base64.StdEncoding.Decode(key, fileBytes)
+ if err != nil {
+ return nil, err
+ }
+
+ // use the decoded length to slice the array to the correct length (removes padding bytes)
+ key = key[:length]
+
+ // Ensure the key length is valid for AES (16, 24, or 32 bytes for AES-128, AES-192, or AES-256)
+ keyLength := len(key)
+ if keyLength != 16 && keyLength != 24 && keyLength != 32 {
+ return nil, fmt.Errorf("invalid key size: %d bytes. Key must be 16, 24, or 32 bytes", keyLength)
+ }
+ // Create the AES cipher
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return nil, err
+ }
+
+ //we specify a known 12 byte nonce size so we can easily retrieve it in Tentacle
+ gcm, err := cipher.NewGCMWithNonceSize(block, 12)
+ if err != nil {
+ return nil, err
+ }
+
+ return gcm, nil
+}
diff --git a/source/Octopus.Manager.Tentacle/Octopus.Manager.Tentacle.csproj b/source/Octopus.Manager.Tentacle/Octopus.Manager.Tentacle.csproj
index 1319c5b67..d8bd4002b 100644
--- a/source/Octopus.Manager.Tentacle/Octopus.Manager.Tentacle.csproj
+++ b/source/Octopus.Manager.Tentacle/Octopus.Manager.Tentacle.csproj
@@ -132,7 +132,7 @@
-
+
diff --git a/source/Octopus.Tentacle.Kubernetes.Tests.Integration/KubernetesClusterOneTimeSetUp.cs b/source/Octopus.Tentacle.Kubernetes.Tests.Integration/KubernetesClusterOneTimeSetUp.cs
index 28e49e21b..cf6fe017a 100644
--- a/source/Octopus.Tentacle.Kubernetes.Tests.Integration/KubernetesClusterOneTimeSetUp.cs
+++ b/source/Octopus.Tentacle.Kubernetes.Tests.Integration/KubernetesClusterOneTimeSetUp.cs
@@ -36,6 +36,10 @@ public async Task OneTimeSetUp()
var tag = Environment.GetEnvironmentVariable("KubernetesAgentTests_ImageTag");
imageAndTag = $"docker.packages.octopushq.com/octopusdeploy/kubernetes-agent-tentacle:{tag}";
}
+ else if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("KubernetesAgentTests_ImageAndTag")))
+ {
+ imageAndTag = Environment.GetEnvironmentVariable("KubernetesAgentTests_ImageAndTag");
+ }
else if(bool.TryParse(Environment.GetEnvironmentVariable("KubernetesAgentTests_UseLatestLocalImage"), out var useLocal) && useLocal)
{
//if we should use the latest locally build image, load the tag from docker and load it into kind
diff --git a/source/Octopus.Tentacle.Kubernetes.Tests.Integration/Setup/KubernetesAgentInstaller.cs b/source/Octopus.Tentacle.Kubernetes.Tests.Integration/Setup/KubernetesAgentInstaller.cs
index 172d1e40f..f28a0c687 100644
--- a/source/Octopus.Tentacle.Kubernetes.Tests.Integration/Setup/KubernetesAgentInstaller.cs
+++ b/source/Octopus.Tentacle.Kubernetes.Tests.Integration/Setup/KubernetesAgentInstaller.cs
@@ -146,7 +146,7 @@ static string GetChartVersion()
if (tentacleImageAndTag is null)
return null;
- var parts = tentacleImageAndTag.Split(":");
+ var parts = tentacleImageAndTag.Split(':',2,StringSplitOptions.TrimEntries);
var repo = parts[0];
var tag = parts[1];
diff --git a/source/Octopus.Tentacle.Kubernetes.Tests.Integration/Setup/Tooling/HelmDownloader.cs b/source/Octopus.Tentacle.Kubernetes.Tests.Integration/Setup/Tooling/HelmDownloader.cs
index cf7f9d694..a0b121c7c 100644
--- a/source/Octopus.Tentacle.Kubernetes.Tests.Integration/Setup/Tooling/HelmDownloader.cs
+++ b/source/Octopus.Tentacle.Kubernetes.Tests.Integration/Setup/Tooling/HelmDownloader.cs
@@ -9,7 +9,7 @@ namespace Octopus.Tentacle.Kubernetes.Tests.Integration.Setup.Tooling;
public class HelmDownloader : ToolDownloader
{
- const string LatestVersion = "v3.14.3";
+ const string LatestVersion = "v3.16.3";
public HelmDownloader( ILogger logger)
: base("helm", logger)
{
diff --git a/source/Octopus.Tentacle.Kubernetes.Tests.Integration/Setup/Tooling/KindDownloader.cs b/source/Octopus.Tentacle.Kubernetes.Tests.Integration/Setup/Tooling/KindDownloader.cs
index 78322c189..df7658f66 100644
--- a/source/Octopus.Tentacle.Kubernetes.Tests.Integration/Setup/Tooling/KindDownloader.cs
+++ b/source/Octopus.Tentacle.Kubernetes.Tests.Integration/Setup/Tooling/KindDownloader.cs
@@ -6,7 +6,7 @@ namespace Octopus.Tentacle.Kubernetes.Tests.Integration.Setup.Tooling
{
public class KindDownloader : ToolDownloader
{
- const string LatestKindVersion = "v0.22.0";
+ const string LatestKindVersion = "v0.25.0";
public KindDownloader(ILogger logger)
: base("kind", logger)
diff --git a/source/Octopus.Tentacle.Kubernetes.Tests.Integration/Setup/Tooling/KubeCtlDownloader.cs b/source/Octopus.Tentacle.Kubernetes.Tests.Integration/Setup/Tooling/KubeCtlDownloader.cs
index 53575ab50..c5106babe 100644
--- a/source/Octopus.Tentacle.Kubernetes.Tests.Integration/Setup/Tooling/KubeCtlDownloader.cs
+++ b/source/Octopus.Tentacle.Kubernetes.Tests.Integration/Setup/Tooling/KubeCtlDownloader.cs
@@ -4,7 +4,7 @@ namespace Octopus.Tentacle.Kubernetes.Tests.Integration.Setup.Tooling;
public class KubeCtlDownloader : ToolDownloader
{
- public const string LatestKubeCtlVersion = "v1.29.3";
+ public const string LatestKubeCtlVersion = "v1.30.6";
public KubeCtlDownloader(ILogger logger)
: base("kubectl", logger)
diff --git a/source/Octopus.Tentacle.Kubernetes.Tests.Integration/Setup/kind-config.yaml b/source/Octopus.Tentacle.Kubernetes.Tests.Integration/Setup/kind-config.yaml
index 37eb950b6..bc028e15e 100644
--- a/source/Octopus.Tentacle.Kubernetes.Tests.Integration/Setup/kind-config.yaml
+++ b/source/Octopus.Tentacle.Kubernetes.Tests.Integration/Setup/kind-config.yaml
@@ -3,6 +3,6 @@ apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- image: kindest/node:v1.29.2@sha256:51a1434a5397193442f0be2a297b488b6c919ce8a3931be0ce822606ea5ca245
+ image: kindest/node:v1.30.6@sha256:b6d08db72079ba5ae1f4a88a09025c0a904af3b52387643c285442afb05ab994
- role: worker
- image: kindest/node:v1.29.2@sha256:51a1434a5397193442f0be2a297b488b6c919ce8a3931be0ce822606ea5ca245
+ image: kindest/node:v1.30.6@sha256:b6d08db72079ba5ae1f4a88a09025c0a904af3b52387643c285442afb05ab994
diff --git a/source/Octopus.Tentacle.Tests/Configuration/ApplicationInstanceSelectorFixture.cs b/source/Octopus.Tentacle.Tests/Configuration/ApplicationInstanceSelectorFixture.cs
index e473ef60f..e34be43b1 100644
--- a/source/Octopus.Tentacle.Tests/Configuration/ApplicationInstanceSelectorFixture.cs
+++ b/source/Octopus.Tentacle.Tests/Configuration/ApplicationInstanceSelectorFixture.cs
@@ -8,6 +8,8 @@
using Octopus.Tentacle.Configuration.Crypto;
using Octopus.Tentacle.Configuration.Instances;
using Octopus.Tentacle.Kubernetes;
+using Octopus.Tentacle.Kubernetes.Configuration;
+using Octopus.Tentacle.Kubernetes.Crypto;
using Octopus.Tentacle.Util;
namespace Octopus.Tentacle.Tests.Configuration
diff --git a/source/Octopus.Tentacle.Tests/Kubernetes/KubernetesOrphanedPodCleanerTests.cs b/source/Octopus.Tentacle.Tests/Kubernetes/KubernetesOrphanedPodCleanerTests.cs
index 994b394d9..bc026e045 100644
--- a/source/Octopus.Tentacle.Tests/Kubernetes/KubernetesOrphanedPodCleanerTests.cs
+++ b/source/Octopus.Tentacle.Tests/Kubernetes/KubernetesOrphanedPodCleanerTests.cs
@@ -7,6 +7,7 @@
using NUnit.Framework;
using Octopus.Tentacle.Contracts;
using Octopus.Tentacle.Kubernetes;
+using Octopus.Tentacle.Kubernetes.Crypto;
using Octopus.Tentacle.Tests.Support;
using Octopus.Time;
@@ -26,6 +27,7 @@ public class KubernetesOrphanedPodCleanerTests
DateTimeOffset startTime;
ITentacleScriptLogProvider scriptLogProvider;
IScriptPodSinceTimeStore scriptPodSinceTimeStore;
+ IScriptPodLogEncryptionKeyProvider scriptPodLogEncryptionKeyProvider;
[SetUp]
public void Setup()
@@ -37,10 +39,11 @@ public void Setup()
clock = new FixedClock(startTime);
scriptLogProvider = Substitute.For();
scriptPodSinceTimeStore = Substitute.For();
+ scriptPodLogEncryptionKeyProvider = Substitute.For();
monitor = Substitute.For();
scriptTicket = new ScriptTicket(Guid.NewGuid().ToString());
- cleaner = new KubernetesOrphanedPodCleaner(monitor, podService, log, clock, scriptLogProvider, scriptPodSinceTimeStore);
+ cleaner = new KubernetesOrphanedPodCleaner(monitor, podService, log, clock, scriptLogProvider, scriptPodSinceTimeStore, scriptPodLogEncryptionKeyProvider);
overCutoff = cleaner.CompletedPodConsideredOrphanedAfterTimeSpan + 1.Minutes();
underCutoff = cleaner.CompletedPodConsideredOrphanedAfterTimeSpan - 1.Minutes();
@@ -71,6 +74,7 @@ public async Task OrphanedPodCleanedUpIfOver10MinutesHavePassed()
await podService.Received().DeleteIfExists(scriptTicket, Arg.Any());
scriptLogProvider.Received().Delete(scriptTicket);
scriptPodSinceTimeStore.Received().Delete(scriptTicket);
+ scriptPodLogEncryptionKeyProvider.Received().Delete(scriptTicket);
}
[TestCase(TrackedScriptPodPhase.Succeeded, true)]
@@ -95,12 +99,14 @@ public async Task OrphanedPodOnlyCleanedUpWhenNotRunning(TrackedScriptPodPhase p
await podService.Received().DeleteIfExists(scriptTicket, Arg.Any());
scriptLogProvider.Received().Delete(scriptTicket);
scriptPodSinceTimeStore.Received().Delete(scriptTicket);
+ scriptPodLogEncryptionKeyProvider.Received().Delete(scriptTicket);
}
else
{
await podService.DidNotReceiveWithAnyArgs().DeleteIfExists(scriptTicket, Arg.Any());
scriptLogProvider.DidNotReceiveWithAnyArgs().Delete(scriptTicket);
scriptPodSinceTimeStore.DidNotReceiveWithAnyArgs().Delete(scriptTicket);
+ scriptPodLogEncryptionKeyProvider.DidNotReceiveWithAnyArgs().Delete(scriptTicket);
}
TrackedScriptPodState CreateState(TrackedScriptPodPhase phase)
@@ -136,6 +142,7 @@ public async Task OrphanedPodNotCleanedUpIfOnly9MinutesHavePassed()
//Assert
await podService.DidNotReceiveWithAnyArgs().DeleteIfExists(scriptTicket, Arg.Any());
scriptLogProvider.DidNotReceiveWithAnyArgs().Delete(scriptTicket);
+ scriptPodLogEncryptionKeyProvider.DidNotReceiveWithAnyArgs().Delete(scriptTicket);
}
[Test]
@@ -157,6 +164,7 @@ public async Task OrphanedPodNotCleanedUpIfPodCleanupIsDisabled()
await podService.DidNotReceive().DeleteIfExists(scriptTicket, Arg.Any());
scriptLogProvider.Received().Delete(scriptTicket);
scriptPodSinceTimeStore.Received().Delete(scriptTicket);
+ scriptPodLogEncryptionKeyProvider.Received().Delete(scriptTicket);
}
[TestCase(1, false)]
@@ -167,7 +175,7 @@ public async Task EnvironmentVariableDictatesWhenPodsAreConsideredOrphaned(int c
Environment.SetEnvironmentVariable("OCTOPUS__K8STENTACLE__PODSCONSIDEREDORPHANEDAFTERMINUTES", "2");
// We need to reinitialise the sut after changing the env var value
- cleaner = new KubernetesOrphanedPodCleaner(monitor, podService, log, clock, scriptLogProvider, scriptPodSinceTimeStore);
+ cleaner = new KubernetesOrphanedPodCleaner(monitor, podService, log, clock, scriptLogProvider, scriptPodSinceTimeStore, scriptPodLogEncryptionKeyProvider);
var pods = new List
{
CreatePod(TrackedScriptPodState.Succeeded(0, startTime))
@@ -184,12 +192,14 @@ public async Task EnvironmentVariableDictatesWhenPodsAreConsideredOrphaned(int c
await podService.Received().DeleteIfExists(scriptTicket, Arg.Any());
scriptLogProvider.Received().Delete(scriptTicket);
scriptPodSinceTimeStore.Received().Delete(scriptTicket);
+ scriptPodLogEncryptionKeyProvider.Received().Delete(scriptTicket);
}
else
{
await podService.DidNotReceiveWithAnyArgs().DeleteIfExists(scriptTicket, Arg.Any());
scriptLogProvider.DidNotReceiveWithAnyArgs().Delete(scriptTicket);
scriptPodSinceTimeStore.DidNotReceiveWithAnyArgs().Delete(scriptTicket);
+ scriptPodLogEncryptionKeyProvider.DidNotReceiveWithAnyArgs().Delete(scriptTicket);
}
}
diff --git a/source/Octopus.Tentacle.Tests/Kubernetes/PodLogEncryptionProviderTests.cs b/source/Octopus.Tentacle.Tests/Kubernetes/PodLogEncryptionProviderTests.cs
new file mode 100644
index 000000000..5739d9f5e
--- /dev/null
+++ b/source/Octopus.Tentacle.Tests/Kubernetes/PodLogEncryptionProviderTests.cs
@@ -0,0 +1,45 @@
+using System.Text;
+using FluentAssertions;
+using NUnit.Framework;
+using Octopus.Tentacle.Kubernetes;
+using Octopus.Tentacle.Kubernetes.Crypto;
+
+namespace Octopus.Tentacle.Tests.Kubernetes
+{
+ [TestFixture]
+ public class PodLogEncryptionProviderTests
+ {
+ IPodLogEncryptionProvider sut;
+
+ public static readonly byte[] Nonce =
+ {
+ 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1
+ };
+
+ [SetUp]
+ public void SetUp()
+ {
+ var key = Encoding.UTF8.GetBytes("qwertyuioplkjhgfdsazxcvbnmqwertd");
+ sut = PodLogEncryptionProvider.Create(key);
+ }
+
+ [TestCase("531f69408fcc09129a42b46b93c7c14fe7c36fec74ac77929b4e1f29b6b0c1e7cfe78055ceee24fbca5f9097501b5cb548f78928b5", "##octopus[stdout-verbose]")]
+ [TestCase("4463ba9b0672e65fdabcd99681ee20e5f003a59ce2c1b1751303e6399d5515ae4a715ed47ce5b7c5727ca66a127e485cd22de2d1dad76d8f704922dae0036d99c4a7e151498043b9d8", "EOS-075CD4F0-8C76-491D-BA76-0879D35E9CFE<<>>0")]
+ public void Decrypt_ShouldProduceCorrectOutput(string encryptedMessage, string expectedDecryptedMessage)
+ {
+ var result = sut.Decrypt(encryptedMessage);
+
+ result.Should().Be(expectedDecryptedMessage);
+ }
+
+ [TestCase("a cool message", "0c0b0a090807060504030201416ce819a896623cee784f92807857b9be36bd6075461789031a3d29aef1")]
+ [TestCase("##octopus[stdout-verbose]", "0c0b0a090807060504030201036fe415b3953224f8504f8783723ca65dca38cd3eab365c4d10d4efad161112e96cd449c6a52348ef")]
+ [TestCase("EOS-075CD4F0-8C76-491D-BA76-0879D35E9CFE<<>>0", "0c0b0a0908070605040302016503d85bf7cd7712cf3f7ac3ca250ae5469169866d80687b51b3cede4af296732090b9a9577055be3ced266ceb9eecd094a74f8df65c95ab33d9f5170c")]
+ public void Encrypt_ShouldProduceCorrectOutput(string plaintextMessage, string expectedEncryptedMessage)
+ {
+ var result = sut.Encrypt(plaintextMessage, Nonce);
+
+ result.Should().Be(expectedEncryptedMessage);
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/Octopus.Tentacle.Tests/Kubernetes/PodLogLineParserFixture.cs b/source/Octopus.Tentacle.Tests/Kubernetes/PodLogLineParserFixture.cs
index 5fcea5b69..237e9d784 100644
--- a/source/Octopus.Tentacle.Tests/Kubernetes/PodLogLineParserFixture.cs
+++ b/source/Octopus.Tentacle.Tests/Kubernetes/PodLogLineParserFixture.cs
@@ -1,47 +1,61 @@
using System;
using System.Linq;
using FluentAssertions;
+using NSubstitute;
using NUnit.Framework;
using Octopus.Tentacle.Contracts;
using Octopus.Tentacle.Kubernetes;
+using Octopus.Tentacle.Kubernetes.Crypto;
namespace Octopus.Tentacle.Tests.Kubernetes
{
[TestFixture]
public class PodLogLineParserFixture
{
+ IPodLogEncryptionProvider encryptionProvider;
+
+ [SetUp]
+ public void SetUp()
+ {
+ encryptionProvider = Substitute.For();
+
+ //for the purpose of this, don't do any testing of the encryption
+ encryptionProvider.Decrypt(Arg.Any())
+ .Returns(ci => ci.ArgAt(0));
+ }
+
[TestCase("a|b|c", Reason = "Doesn't have 4 parts")]
public void NotCorrectlyPipeDelimited(string line)
{
- var result = PodLogLineParser.ParseLine(line).Should().BeOfType().Subject;
+ var result = PodLogLineParser.ParseLine(line, encryptionProvider).Should().BeOfType().Subject;
result.Error.Should().Contain("delimited").And.Contain(line);
}
[TestCase("1 |b|c|d", Reason = "Not a date")]
public void FirstPartIsNotALineDate(string line)
{
- var result = PodLogLineParser.ParseLine(line).Should().BeOfType().Subject;
+ var result = PodLogLineParser.ParseLine(line, encryptionProvider).Should().BeOfType().Subject;
result.Error.Should().Contain("log timestamp").And.Contain(line);
}
[TestCase("2024-04-03T06:03:10.501025551Z |b|c|d", Reason = "Not a line number")]
public void SecondPartIsNotALineNumber(string line)
{
- var result = PodLogLineParser.ParseLine(line).Should().BeOfType().Subject;
+ var result = PodLogLineParser.ParseLine(line, encryptionProvider).Should().BeOfType().Subject;
result.Error.Should().Contain("line number").And.Contain(line);
}
[TestCase("2024-04-03T06:03:10.501025551Z |1|c|d", Reason = "Not a valid source")]
public void ThirdPartIsNotAValidSource(string line)
{
- var result = PodLogLineParser.ParseLine(line).Should().BeOfType().Subject;
+ var result = PodLogLineParser.ParseLine(line, encryptionProvider).Should().BeOfType().Subject;
result.Error.Should().Contain("log level").And.Contain(line);
}
-
+
[Test]
public void SimpleMessage()
{
- var logLine = PodLogLineParser.ParseLine($"2024-04-03T06:03:10.501025551Z |123|stdout|This is the message")
+ var logLine = PodLogLineParser.ParseLine($"2024-04-03T06:03:10.501025551Z |123|stdout|This is the message", encryptionProvider)
.Should().BeOfType().Subject.LogLine;
logLine.LineNumber.Should().Be(123);
@@ -49,11 +63,11 @@ public void SimpleMessage()
logLine.Message.Should().Be("This is the message");
logLine.Occurred.Should().BeCloseTo(new DateTimeOffset(2024, 4, 3, 6, 3, 10, 501, TimeSpan.Zero), TimeSpan.FromMilliseconds(1));
}
-
+
[Test]
public void ServiceMessage()
{
- var logLine = PodLogLineParser.ParseLine("2024-04-03T06:03:10.501025551Z |123|stdout|##octopus[stdout-verbose]")
+ var logLine = PodLogLineParser.ParseLine("2024-04-03T06:03:10.501025551Z |123|stdout|##octopus[stdout-verbose]", encryptionProvider)
.Should().BeOfType().Subject.LogLine;
logLine.LineNumber.Should().Be(123);
@@ -61,11 +75,11 @@ public void ServiceMessage()
logLine.Message.Should().Be("##octopus[stdout-verbose]");
logLine.Occurred.Should().BeCloseTo(new DateTimeOffset(2024, 4, 3, 6, 3, 10, 501, TimeSpan.Zero), TimeSpan.FromMilliseconds(1));
}
-
+
[Test]
public void ErrorMessage()
{
- var logLine = PodLogLineParser.ParseLine("2024-04-03T06:03:10.501025551Z |123|stderr|Error!")
+ var logLine = PodLogLineParser.ParseLine("2024-04-03T06:03:10.501025551Z |123|stderr|Error!", encryptionProvider)
.Should().BeOfType().Subject.LogLine;
logLine.LineNumber.Should().Be(123);
@@ -73,11 +87,11 @@ public void ErrorMessage()
logLine.Message.Should().Be("Error!");
logLine.Occurred.Should().BeCloseTo(new DateTimeOffset(2024, 4, 3, 6, 3, 10, 501, TimeSpan.Zero), TimeSpan.FromMilliseconds(1));
}
-
+
[Test]
public void MessageHasPipeInIt()
{
- var logLine = PodLogLineParser.ParseLine("2024-04-03T06:03:10.501025551Z |123|stdout|This is the me|ss|age")
+ var logLine = PodLogLineParser.ParseLine("2024-04-03T06:03:10.501025551Z |123|stdout|This is the me|ss|age", encryptionProvider)
.Should().BeOfType().Subject.LogLine;
logLine.LineNumber.Should().Be(123);
@@ -85,40 +99,40 @@ public void MessageHasPipeInIt()
logLine.Message.Should().Be("This is the me|ss|age");
logLine.Occurred.Should().BeCloseTo(new DateTimeOffset(2024, 4, 3, 6, 3, 10, 501, TimeSpan.Zero), TimeSpan.FromMilliseconds(1));
}
-
+
[Test]
public void ValidEndOfStreamWithPositiveExitCode()
{
- var result = PodLogLineParser.ParseLine("2024-04-03T06:03:10.501025551Z |123|debug|EOS-075CD4F0-8C76-491D-BA76-0879D35E9CFE<<>>4")
+ var result = PodLogLineParser.ParseLine("2024-04-03T06:03:10.501025551Z |123|debug|EOS-075CD4F0-8C76-491D-BA76-0879D35E9CFE<<>>4", encryptionProvider)
.Should().BeOfType().Subject;
result.ExitCode.Should().Be(4);
-
+
var logLine = result.LogLine;
logLine.LineNumber.Should().Be(123);
logLine.Source.Should().Be(ProcessOutputSource.Debug);
logLine.Message.Should().Be("EOS-075CD4F0-8C76-491D-BA76-0879D35E9CFE<<>>4");
}
-
+
[Test]
public void ValidEndOfStreamWithNegativeExitCode()
{
- var result = PodLogLineParser.ParseLine("2024-04-03T06:03:10.501025551Z |123|debug|EOS-075CD4F0-8C76-491D-BA76-0879D35E9CFE<<>>-64")
+ var result = PodLogLineParser.ParseLine("2024-04-03T06:03:10.501025551Z |123|debug|EOS-075CD4F0-8C76-491D-BA76-0879D35E9CFE<<>>-64", encryptionProvider)
.Should().BeOfType().Subject;
result.ExitCode.Should().Be(-64);
-
+
var logLine = result.LogLine;
logLine.LineNumber.Should().Be(123);
logLine.Source.Should().Be(ProcessOutputSource.Debug);
logLine.Message.Should().Be("EOS-075CD4F0-8C76-491D-BA76-0879D35E9CFE<<>>-64");
}
-
+
[Test]
public void InvalidEndOfStream()
{
var line = "2024-04-03T06:03:10.501025551Z |123|stdout|EOS-075CD4F0-8C76-491D-BA76-0879D35E9CFE<<>>";
- var result = PodLogLineParser.ParseLine(line)
+ var result = PodLogLineParser.ParseLine(line, encryptionProvider)
.Should().BeOfType().Subject;
result.Error.Should().Contain("end of stream").And.Contain(line);
diff --git a/source/Octopus.Tentacle.Tests/Kubernetes/PodLogReaderFixture.cs b/source/Octopus.Tentacle.Tests/Kubernetes/PodLogReaderFixture.cs
index b43bc45a0..d965b5fbe 100644
--- a/source/Octopus.Tentacle.Tests/Kubernetes/PodLogReaderFixture.cs
+++ b/source/Octopus.Tentacle.Tests/Kubernetes/PodLogReaderFixture.cs
@@ -5,15 +5,29 @@
using System.Text;
using System.Threading.Tasks;
using FluentAssertions;
+using NSubstitute;
using NUnit.Framework;
using Octopus.Tentacle.Contracts;
using Octopus.Tentacle.Kubernetes;
+using Octopus.Tentacle.Kubernetes.Crypto;
namespace Octopus.Tentacle.Tests.Kubernetes
{
[TestFixture]
public class PodLogReaderFixture
{
+ IPodLogEncryptionProvider encryptionProvider;
+
+ [SetUp]
+ public void SetUp()
+ {
+ encryptionProvider = Substitute.For();
+
+ //for the purpose of this, don't do any testing of the encryption
+ encryptionProvider.Decrypt(Arg.Any())
+ .Returns(ci => ci.ArgAt(0));
+ }
+
[TestCase(0, Reason = "Initial position")]
[TestCase(4, Reason = "Subsequent position")]
[TestCase(12387126, Reason = "Large position")]
@@ -22,7 +36,7 @@ public async Task NoLines_SameSequenceNumber(long lastLogSequence)
string[] podLines = Array.Empty();
var reader = SetupReader(podLines);
- var result = await PodLogReader.ReadPodLogs(lastLogSequence, reader);
+ var result = await PodLogReader.ReadPodLogs(lastLogSequence, reader, encryptionProvider);
result.NextSequenceNumber.Should().Be(lastLogSequence);
result.Lines.Should().BeEmpty();
}
@@ -36,7 +50,7 @@ public async Task FirstLine_SequenceNumberIncreasesByOne()
};
var reader = SetupReader(podLines);
- var result = await PodLogReader.ReadPodLogs(0, reader);
+ var result = await PodLogReader.ReadPodLogs(0, reader, encryptionProvider);
result.NextSequenceNumber.Should().Be(1);
result.Lines.Should().BeEquivalentTo(new[]
{
@@ -55,7 +69,7 @@ public async Task ThreeSubsequentLines_SequenceNumberIncreasesByThree()
};
var reader = SetupReader(podLines);
- var result = await PodLogReader.ReadPodLogs(4, reader);
+ var result = await PodLogReader.ReadPodLogs(4, reader, encryptionProvider);
result.NextSequenceNumber.Should().Be(7);
result.Lines.Should().BeEquivalentTo(new[]
{
@@ -77,12 +91,12 @@ public async Task StreamContainsPreviousLines_Deduplicates()
var allTaskLogs = new List();
var reader = SetupReader(podLines.Take(1).ToArray());
- var result = await PodLogReader.ReadPodLogs(4, reader);
+ var result = await PodLogReader.ReadPodLogs(4, reader, encryptionProvider);
result.NextSequenceNumber.Should().Be(5);
allTaskLogs.AddRange(result.Lines);
reader = SetupReader(podLines.ToArray());
- result = await PodLogReader.ReadPodLogs(5, reader);
+ result = await PodLogReader.ReadPodLogs(5, reader,encryptionProvider);
result.NextSequenceNumber.Should().Be(7);
allTaskLogs.AddRange(result.Lines);
@@ -100,7 +114,7 @@ public async Task ParseError_AppearsAsError()
var line = "abcdefg";
var reader = SetupReader(new[] { line });
- var result = await PodLogReader.ReadPodLogs(0, reader);
+ var result = await PodLogReader.ReadPodLogs(0, reader, encryptionProvider);
result.NextSequenceNumber.Should().Be(0, "The sequence number doesn't move on parse errors");
var outputLine = result.Lines.Should().ContainSingle().Subject;
@@ -118,7 +132,7 @@ public async Task MissingLine_Throws()
};
var reader = SetupReader(podLines);
- Func action = async () => await PodLogReader.ReadPodLogs(50, reader);
+ Func action = async () => await PodLogReader.ReadPodLogs(50, reader, encryptionProvider);
await action.Should().ThrowAsync();
}
@@ -132,7 +146,7 @@ public async Task LineOutOfOrderAtStart_Throws()
};
var reader = SetupReader(podLines);
- Func action = async () => await PodLogReader.ReadPodLogs(4, reader);
+ Func action = async () => await PodLogReader.ReadPodLogs(4, reader, encryptionProvider);
await action.Should().ThrowAsync();
}
@@ -147,7 +161,7 @@ public async Task LineOutOfOrderMidway_Throws()
};
var reader = SetupReader(podLines);
- Func action = async () => await PodLogReader.ReadPodLogs(2, reader);
+ Func action = async () => await PodLogReader.ReadPodLogs(2, reader, encryptionProvider);
await action.Should().ThrowAsync();
}
diff --git a/source/Octopus.Tentacle/Configuration/ConfigurationModule.cs b/source/Octopus.Tentacle/Configuration/ConfigurationModule.cs
index f508c2434..0949edb90 100644
--- a/source/Octopus.Tentacle/Configuration/ConfigurationModule.cs
+++ b/source/Octopus.Tentacle/Configuration/ConfigurationModule.cs
@@ -3,6 +3,8 @@
using Octopus.Tentacle.Configuration.Crypto;
using Octopus.Tentacle.Configuration.EnvironmentVariableMappings;
using Octopus.Tentacle.Configuration.Instances;
+using Octopus.Tentacle.Kubernetes.Configuration;
+using Octopus.Tentacle.Kubernetes.Crypto;
using Octopus.Tentacle.Startup;
using Octopus.Tentacle.Util;
using Octopus.Tentacle.Watchdog;
@@ -56,9 +58,6 @@ protected override void Load(ContainerBuilder builder)
.As()
.SingleInstance();
- builder.RegisterType().As().SingleInstance();
- builder.RegisterType().SingleInstance();
-
builder.RegisterType()
.As()
.SingleInstance();
@@ -96,6 +95,12 @@ protected override void Load(ContainerBuilder builder)
builder.RegisterType().As();
builder.RegisterType().As();
builder.RegisterType().As().SingleInstance();
+
+ //Even though these are Kubernetes types, we need to include them in this module as they are used lazily in the base types
+ builder.RegisterType().SingleInstance();
+ builder.RegisterType().As().SingleInstance();
+ builder.RegisterType().As().SingleInstance();
+
RegisterWatchdog(builder);
}
diff --git a/source/Octopus.Tentacle/Configuration/Crypto/KubernetesMachineKeyEncryptor.cs b/source/Octopus.Tentacle/Configuration/Crypto/KubernetesMachineKeyEncryptor.cs
deleted file mode 100644
index f7d8f889e..000000000
--- a/source/Octopus.Tentacle/Configuration/Crypto/KubernetesMachineKeyEncryptor.cs
+++ /dev/null
@@ -1,88 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Security.Cryptography;
-using System.Text;
-using System.Threading;
-using k8s.Models;
-using Octopus.Diagnostics;
-using Octopus.Tentacle.Kubernetes;
-
-namespace Octopus.Tentacle.Configuration.Crypto
-{
- public interface IKubernetesMachineKeyEncryptor : IMachineKeyEncryptor
- {
- }
-
- public class KubernetesMachineKeyEncryptor : IKubernetesMachineKeyEncryptor
- {
- const string SecretName = "tentacle-secret";
- const string MachineKeyName = "machine-key";
- const string MachineIvName = "machine-iv";
-
- readonly IKubernetesSecretService kubernetesSecretService;
- readonly ISystemLog log;
-
- readonly Lazy<(byte[] Key, byte[] Iv)> machineKey;
- readonly Lazy secret;
-
- public KubernetesMachineKeyEncryptor(IKubernetesSecretService kubernetesSecretService, ISystemLog log)
- {
- this.kubernetesSecretService = kubernetesSecretService;
- this.log = log;
- secret = new Lazy(GetSecret);
- machineKey = new Lazy<(byte[] Key, byte[] Iv)>(GetMachineKey);
- }
-
- public string Encrypt(string raw)
- {
- using var aes = Aes.Create();
- using var enc = aes.CreateEncryptor(machineKey.Value.Key, machineKey.Value.Iv);
- var inBlock = Encoding.UTF8.GetBytes(raw);
- var trans = enc.TransformFinalBlock(inBlock, 0, inBlock.Length);
- return Convert.ToBase64String(trans);
- }
-
- public string Decrypt(string encrypted)
- {
- using var aes = Aes.Create();
- using var dec = aes.CreateDecryptor(machineKey.Value.Key, machineKey.Value.Iv);
- var fromBase = Convert.FromBase64String(encrypted);
- var asd = dec.TransformFinalBlock(fromBase, 0, fromBase.Length);
- return Encoding.UTF8.GetString(asd);
- }
- V1Secret GetSecret()
- {
- return kubernetesSecretService.TryGetSecretAsync(SecretName, CancellationToken.None).GetAwaiter().GetResult() ?? throw new InvalidOperationException($"Unable to retrieve MachineKey from secret for namespace {KubernetesConfig.Namespace}");
- }
-
- (byte[] key, byte[] iv) GetMachineKey()
- {
- var data = secret.Value.Data;
- if (data is null ||
- !data.TryGetValue(MachineKeyName, out var key) ||
- !data.TryGetValue(MachineIvName, out var iv))
- {
- (key, iv) = GenerateMachineKey(log);
- data = new Dictionary { { MachineKeyName, key }, { MachineIvName, iv } };
-
- kubernetesSecretService.UpdateSecretDataAsync(SecretName, data, CancellationToken.None).GetAwaiter().GetResult();
- }
-
- if (key == null || iv == null)
- {
- throw new InvalidOperationException("Unable to retrieve or create a machine key for encryption.");
- }
-
- return (key, iv);
- }
-
- static (byte[] key, byte[] iv) GenerateMachineKey(ILog log)
- {
- log.Info("Machine key file does not yet exist. Generating key that will be used to encrypt data for this tentacle.");
- var aes = Aes.Create();
- aes.GenerateIV();
- aes.GenerateKey();
- return (aes.Key, aes.IV);
- }
- }
-}
\ No newline at end of file
diff --git a/source/Octopus.Tentacle/Configuration/Instances/ApplicationInstanceSelector.cs b/source/Octopus.Tentacle/Configuration/Instances/ApplicationInstanceSelector.cs
index 1f12ac1c4..5a3765ae7 100644
--- a/source/Octopus.Tentacle/Configuration/Instances/ApplicationInstanceSelector.cs
+++ b/source/Octopus.Tentacle/Configuration/Instances/ApplicationInstanceSelector.cs
@@ -3,6 +3,7 @@
using System.Linq;
using Octopus.Diagnostics;
using Octopus.Tentacle.Kubernetes;
+using Octopus.Tentacle.Kubernetes.Configuration;
using Octopus.Tentacle.Util;
namespace Octopus.Tentacle.Configuration.Instances
diff --git a/source/Octopus.Tentacle/Configuration/Instances/ConfigMapKeyValueStore.cs b/source/Octopus.Tentacle/Kubernetes/Configuration/ConfigMapKeyValueStore.cs
similarity index 95%
rename from source/Octopus.Tentacle/Configuration/Instances/ConfigMapKeyValueStore.cs
rename to source/Octopus.Tentacle/Kubernetes/Configuration/ConfigMapKeyValueStore.cs
index 39660de48..ab5c4a172 100644
--- a/source/Octopus.Tentacle/Configuration/Instances/ConfigMapKeyValueStore.cs
+++ b/source/Octopus.Tentacle/Kubernetes/Configuration/ConfigMapKeyValueStore.cs
@@ -3,10 +3,11 @@
using System.Threading;
using k8s.Models;
using Newtonsoft.Json;
-using Octopus.Tentacle.Kubernetes;
-using Octopus.Tentacle.Configuration.Crypto;
+using Octopus.Tentacle.Configuration;
+using Octopus.Tentacle.Configuration.Instances;
+using Octopus.Tentacle.Kubernetes.Crypto;
-namespace Octopus.Tentacle.Configuration.Instances
+namespace Octopus.Tentacle.Kubernetes.Configuration
{
class ConfigMapKeyValueStore : IWritableKeyValueStore, IAggregatableKeyValueStore
{
diff --git a/source/Octopus.Tentacle/Kubernetes/Crypto/KubernetesMachineEncryptionKeyProvider.cs b/source/Octopus.Tentacle/Kubernetes/Crypto/KubernetesMachineEncryptionKeyProvider.cs
new file mode 100644
index 000000000..09d8c1a7f
--- /dev/null
+++ b/source/Octopus.Tentacle/Kubernetes/Crypto/KubernetesMachineEncryptionKeyProvider.cs
@@ -0,0 +1,81 @@
+using System;
+using System.Collections.Generic;
+using System.Security.Cryptography;
+using System.Threading;
+using System.Threading.Tasks;
+using k8s.Models;
+using Nito.AsyncEx;
+using Octopus.Diagnostics;
+
+namespace Octopus.Tentacle.Kubernetes.Crypto
+{
+ public interface IKubernetesMachineEncryptionKeyProvider
+ {
+ Task<(byte[] key, byte[] iv)> GetMachineKey(CancellationToken cancellationToken);
+ }
+
+ public class KubernetesMachineEncryptionKeyProvider : IKubernetesMachineEncryptionKeyProvider
+ {
+ const string SecretName = "tentacle-secret";
+ const string MachineKeyName = "machine-key";
+ const string MachineIvName = "machine-iv";
+
+ readonly IKubernetesSecretService secretService;
+ readonly ISystemLog log;
+ V1Secret? secret;
+ readonly SemaphoreSlim accessSemaphore;
+
+ public KubernetesMachineEncryptionKeyProvider(IKubernetesSecretService secretService, ISystemLog log)
+ {
+ this.secretService = secretService;
+ this.log = log;
+ accessSemaphore = new SemaphoreSlim(1, 1);
+ }
+
+ public async Task<(byte[] key, byte[] iv)> GetMachineKey(CancellationToken cancellationToken)
+ {
+ //We lock access to avoid 2 threads creating the machine key twice
+ await accessSemaphore.WaitAsync(cancellationToken);
+
+ try
+ {
+ secret ??= await secretService.TryGetSecretAsync(SecretName, CancellationToken.None);
+
+ if (secret is null)
+ throw new InvalidOperationException($"Unable to retrieve MachineKey from secret for namespace {KubernetesConfig.Namespace}");
+
+ var data = secret.Data;
+ if (data is null ||
+ !data.TryGetValue(MachineKeyName, out var key) ||
+ !data.TryGetValue(MachineIvName, out var iv))
+ {
+ (key, iv) = GenerateMachineKey(log);
+ data = new Dictionary { { MachineKeyName, key }, { MachineIvName, iv } };
+
+ //make sure we update the local secret with the updated data
+ secret = await secretService.UpdateSecretDataAsync(SecretName, data, CancellationToken.None);
+ }
+
+ if (key == null || iv == null)
+ {
+ throw new InvalidOperationException("Unable to retrieve or create a machine key for encryption.");
+ }
+
+ return (key, iv);
+ }
+ finally
+ {
+ accessSemaphore.Release();
+ }
+ }
+
+ static (byte[] key, byte[] iv) GenerateMachineKey(ILog log)
+ {
+ log.Info("Machine key file does not yet exist. Generating key that will be used to encrypt data for this tentacle.");
+ var aes = Aes.Create();
+ aes.GenerateIV();
+ aes.GenerateKey();
+ return (aes.Key, aes.IV);
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/Octopus.Tentacle/Kubernetes/Crypto/KubernetesMachineKeyEncryptor.cs b/source/Octopus.Tentacle/Kubernetes/Crypto/KubernetesMachineKeyEncryptor.cs
new file mode 100644
index 000000000..dbd165f47
--- /dev/null
+++ b/source/Octopus.Tentacle/Kubernetes/Crypto/KubernetesMachineKeyEncryptor.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading;
+using Octopus.Tentacle.Configuration.Crypto;
+
+namespace Octopus.Tentacle.Kubernetes.Crypto
+{
+ public interface IKubernetesMachineKeyEncryptor : IMachineKeyEncryptor
+ {
+ }
+
+ public class KubernetesMachineKeyEncryptor : IKubernetesMachineKeyEncryptor
+ {
+ readonly IKubernetesMachineEncryptionKeyProvider encryptionKeyProvider;
+ byte[]? key;
+ byte[]? iv;
+
+ public KubernetesMachineKeyEncryptor(IKubernetesMachineEncryptionKeyProvider encryptionKeyProvider)
+ {
+ this.encryptionKeyProvider = encryptionKeyProvider;
+ }
+
+ public string Encrypt(string raw)
+ {
+ EnsureMachineKeyAndIvLoaded();
+
+ using var aes = Aes.Create();
+ using var enc = aes.CreateEncryptor(key, iv);
+ var inBlock = Encoding.UTF8.GetBytes(raw);
+ var trans = enc.TransformFinalBlock(inBlock, 0, inBlock.Length);
+ return Convert.ToBase64String(trans);
+ }
+
+ public string Decrypt(string encrypted)
+ {
+ EnsureMachineKeyAndIvLoaded();
+
+ using var aes = Aes.Create();
+ using var dec = aes.CreateDecryptor(key, iv);
+ var fromBase = Convert.FromBase64String(encrypted);
+ var asd = dec.TransformFinalBlock(fromBase, 0, fromBase.Length);
+ return Encoding.UTF8.GetString(asd);
+ }
+
+ [MemberNotNull(nameof(key), nameof(iv))]
+ void EnsureMachineKeyAndIvLoaded()
+ {
+ //if either is null, load it again
+ if (key is null || iv is null)
+ {
+ (key, iv) = encryptionKeyProvider.GetMachineKey(CancellationToken.None).GetAwaiter().GetResult();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/Octopus.Tentacle/Kubernetes/Crypto/PodLogEncryptionProvider.cs b/source/Octopus.Tentacle/Kubernetes/Crypto/PodLogEncryptionProvider.cs
new file mode 100644
index 000000000..d19ffb049
--- /dev/null
+++ b/source/Octopus.Tentacle/Kubernetes/Crypto/PodLogEncryptionProvider.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Security.Cryptography;
+using System.Text;
+using Org.BouncyCastle.Crypto.Engines;
+using Org.BouncyCastle.Crypto.Modes;
+using Org.BouncyCastle.Crypto.Parameters;
+using Org.BouncyCastle.Utilities.Encoders;
+
+namespace Octopus.Tentacle.Kubernetes.Crypto
+{
+ public interface IPodLogEncryptionProvider
+ {
+ string Decrypt(string encryptedLogMessage);
+ string Encrypt(string plainText, byte[]? nonce = null);
+ }
+
+ public class PodLogEncryptionProvider : IPodLogEncryptionProvider
+ {
+ readonly byte[] keyBytes;
+ const int NonceLength = 12;
+
+ private PodLogEncryptionProvider(byte[] keyBytes)
+ {
+ this.keyBytes = keyBytes;
+ }
+
+ public static IPodLogEncryptionProvider Create(byte[] keyBytes) => new PodLogEncryptionProvider(keyBytes);
+
+ public string Decrypt(string encryptedLogMessage)
+ {
+ var allEncryptedBytes = Hex.Decode(encryptedLogMessage).AsSpan();
+
+ var nonceSpan = allEncryptedBytes.Slice(0, NonceLength);
+ var logMessageBytes = allEncryptedBytes.Slice(NonceLength);
+
+ var cipher = new GcmBlockCipher(new AesEngine());
+ var macSize = 8 * cipher.GetBlockSize();
+ cipher.Init(false, new AeadParameters(new KeyParameter(keyBytes), macSize, nonceSpan.ToArray()));
+
+ var outputSize = cipher.GetOutputSize(logMessageBytes.Length);
+ var plainTextData = new byte[outputSize];
+
+ var result = cipher.ProcessBytes(logMessageBytes.ToArray(), 0, logMessageBytes.Length, plainTextData, 0);
+ cipher.DoFinal(plainTextData, result);
+
+ return Encoding.UTF8.GetString(plainTextData);
+ }
+
+ public string Encrypt(string plainText, byte[]? nonce = null)
+ {
+ var plainTextBytes = Encoding.UTF8.GetBytes(plainText);
+
+ //if no nonce is provided, generate one
+ nonce ??= GenerateNonce();
+
+ var cipher = new GcmBlockCipher(new AesEngine());
+ var macSize = 8 * cipher.GetBlockSize();
+ var parameters = new AeadParameters(new KeyParameter(keyBytes), macSize, nonce, null);
+ cipher.Init(true, parameters);
+
+ var cipherText = new byte[cipher.GetOutputSize(plainTextBytes.Length)];
+ var len = cipher.ProcessBytes(plainTextBytes, 0, plainTextBytes.Length, cipherText, 0);
+ cipher.DoFinal(cipherText, len);
+
+ var allBytes = new byte[nonce.Length + cipherText.Length];
+ Array.Copy(nonce,0,allBytes, 0, nonce.Length);
+ Array.Copy(cipherText, 0, allBytes, nonce.Length, cipherText.Length);
+
+ return Hex.ToHexString(allBytes);
+ }
+
+#if NETFRAMEWORK
+ static readonly RNGCryptoServiceProvider RandomCryptoServiceProvider = new RNGCryptoServiceProvider();
+#endif
+ static byte[] GenerateNonce()
+ {
+#if NETFRAMEWORK
+ var buffer = new byte[NonceLength];
+ RandomCryptoServiceProvider.GetBytes(buffer);
+ return buffer;
+#else
+ return RandomNumberGenerator.GetBytes(NonceLength);
+#endif
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/Octopus.Tentacle/Kubernetes/Crypto/ScriptPodLogEncryptionKeyGenerator.cs b/source/Octopus.Tentacle/Kubernetes/Crypto/ScriptPodLogEncryptionKeyGenerator.cs
new file mode 100644
index 000000000..c448c75c7
--- /dev/null
+++ b/source/Octopus.Tentacle/Kubernetes/Crypto/ScriptPodLogEncryptionKeyGenerator.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Octopus.Tentacle.Contracts;
+using Org.BouncyCastle.Crypto;
+using Org.BouncyCastle.Crypto.Digests;
+using Org.BouncyCastle.Crypto.Generators;
+using Org.BouncyCastle.Crypto.Parameters;
+
+namespace Octopus.Tentacle.Kubernetes.Crypto
+{
+ public interface IScriptPodLogEncryptionKeyGenerator
+ {
+ Task GenerateKey(ScriptTicket scriptTicket, int keySizeInBytes, CancellationToken cancellationToken);
+ }
+
+ public class ScriptPodLogEncryptionKeyGenerator : IScriptPodLogEncryptionKeyGenerator
+ {
+ readonly IKubernetesMachineEncryptionKeyProvider machineEncryptionKeyProvider;
+
+ public ScriptPodLogEncryptionKeyGenerator(IKubernetesMachineEncryptionKeyProvider machineEncryptionKeyProvider)
+ {
+ this.machineEncryptionKeyProvider = machineEncryptionKeyProvider;
+ }
+
+ public async Task GenerateKey(ScriptTicket scriptTicket, int keySizeInBytes, CancellationToken cancellationToken)
+ {
+ var (machineEncryptionKey, _) = await machineEncryptionKeyProvider.GetMachineKey(cancellationToken);
+
+ var pdb = new Pkcs5S2ParametersGenerator(new Sha256Digest());
+
+ //we use the machine encryption key as the password and the script ticket is the salt
+ pdb.Init(PbeParametersGenerator.Pkcs5PasswordToBytes(Encoding.ASCII.GetChars(machineEncryptionKey)), Encoding.UTF8.GetBytes(scriptTicket.TaskId), 1000);
+ var key = (KeyParameter)pdb.GenerateDerivedMacParameters(keySizeInBytes * 8);
+
+ return key.GetKey();
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/Octopus.Tentacle/Kubernetes/Crypto/ScriptPodLogEncryptionKeyProvider.cs b/source/Octopus.Tentacle/Kubernetes/Crypto/ScriptPodLogEncryptionKeyProvider.cs
new file mode 100644
index 000000000..9bdab8c03
--- /dev/null
+++ b/source/Octopus.Tentacle/Kubernetes/Crypto/ScriptPodLogEncryptionKeyProvider.cs
@@ -0,0 +1,113 @@
+using System;
+using System.Collections.Concurrent;
+using System.Threading;
+using System.Threading.Tasks;
+using Octopus.Diagnostics;
+using Octopus.Tentacle.Contracts;
+using Octopus.Tentacle.Scripts;
+
+namespace Octopus.Tentacle.Kubernetes.Crypto
+{
+ public interface IScriptPodLogEncryptionKeyProvider
+ {
+ Task WriteEncryptionKeyfileToWorkspace(ScriptTicket scriptTicket, CancellationToken cancellationToken);
+ Task GetEncryptionKey(ScriptTicket scriptTicket, CancellationToken cancellationToken);
+ void Delete(ScriptTicket scriptTicket);
+ }
+
+ public class ScriptPodLogEncryptionKeyProvider : IScriptPodLogEncryptionKeyProvider
+ {
+ const int KeyLengthInBytes = 32;
+ const string Filename = "keyfile";
+ readonly IScriptWorkspaceFactory scriptWorkspaceFactory;
+ readonly IScriptPodLogEncryptionKeyGenerator encryptionKeyGenerator;
+ readonly ISystemLog log;
+
+ readonly ConcurrentDictionary encryptionKeyCache = new();
+
+ public ScriptPodLogEncryptionKeyProvider(IScriptWorkspaceFactory scriptWorkspaceFactory, IScriptPodLogEncryptionKeyGenerator encryptionKeyGenerator, ISystemLog log)
+ {
+ this.scriptWorkspaceFactory = scriptWorkspaceFactory;
+ this.encryptionKeyGenerator = encryptionKeyGenerator;
+ this.log = log;
+ }
+
+ public async Task WriteEncryptionKeyfileToWorkspace(ScriptTicket scriptTicket, CancellationToken cancellationToken)
+ {
+ if (encryptionKeyCache.ContainsKey(scriptTicket))
+ {
+ throw new PodLogEncryptionKeyException($"An encryption key already exists for script {scriptTicket.TaskId}");
+ }
+
+ var workspace = scriptWorkspaceFactory.GetWorkspace(scriptTicket);
+ await GenerateAndWriteEncryptionKeyfileToWorkspace(scriptTicket, workspace, cancellationToken);
+ }
+
+ async Task GenerateAndWriteEncryptionKeyfileToWorkspace(ScriptTicket scriptTicket, IScriptWorkspace workspace,CancellationToken cancellationToken)
+ {
+ log.Verbose($"Generating log encryption key for script pod {scriptTicket.TaskId}");
+ var encryptionKeyBytes = await encryptionKeyGenerator.GenerateKey(scriptTicket, KeyLengthInBytes, cancellationToken);
+ if (!encryptionKeyCache.TryAdd(scriptTicket, encryptionKeyBytes))
+ {
+ throw new PodLogEncryptionKeyException($"Failed to store encryption key in memory cache for script {scriptTicket.TaskId}");
+ }
+
+ try
+ {
+ var fileContents = Convert.ToBase64String(encryptionKeyBytes);
+ workspace.WriteFile(Filename, fileContents);
+ return encryptionKeyBytes;
+ }
+ catch (Exception e)
+ {
+ throw new PodLogEncryptionKeyException($"Failed to write encryption key to workspace for script {scriptTicket.TaskId}", e);
+ }
+ }
+
+ public async Task GetEncryptionKey(ScriptTicket scriptTicket, CancellationToken cancellationToken)
+ {
+ if (encryptionKeyCache.TryGetValue(scriptTicket, out var keyBytes))
+ {
+ return keyBytes;
+ }
+
+ var workspace = scriptWorkspaceFactory.GetWorkspace(scriptTicket);
+ var fileContents = workspace.TryReadFile(Filename);
+ //If we can't load the encryption key from the filesystem
+ if (fileContents == null)
+ {
+ //regenerate the encryption key, write to the filesystem and return the key
+ return await GenerateAndWriteEncryptionKeyfileToWorkspace(scriptTicket, workspace, cancellationToken);
+ }
+
+ if (string.IsNullOrWhiteSpace(fileContents))
+ {
+ throw new PodLogEncryptionKeyException($"Encryption key loaded from workspace for script {scriptTicket.TaskId} is empty or whitespace");
+ }
+
+ var encryptionKeyBytes = Convert.FromBase64String(fileContents);
+ if (!encryptionKeyCache.TryAdd(scriptTicket, encryptionKeyBytes))
+ {
+ throw new PodLogEncryptionKeyException($"Failed to store encryption key in memory cache for script {scriptTicket.TaskId}");
+ }
+
+ return encryptionKeyBytes;
+ }
+
+ public void Delete(ScriptTicket scriptTicket)
+ {
+ encryptionKeyCache.TryRemove(scriptTicket, out _);
+ }
+ }
+
+ public class PodLogEncryptionKeyException : Exception
+ {
+ public PodLogEncryptionKeyException(string message) : base(message)
+ {
+ }
+
+ public PodLogEncryptionKeyException(string message, Exception innerException) : base(message, innerException)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/Octopus.Tentacle/Kubernetes/IKubernetesPodLogService.cs b/source/Octopus.Tentacle/Kubernetes/IKubernetesPodLogService.cs
index eb10414f4..f2ef7fc93 100644
--- a/source/Octopus.Tentacle/Kubernetes/IKubernetesPodLogService.cs
+++ b/source/Octopus.Tentacle/Kubernetes/IKubernetesPodLogService.cs
@@ -9,6 +9,7 @@
using k8s.Models;
using Octopus.Diagnostics;
using Octopus.Tentacle.Contracts;
+using Octopus.Tentacle.Kubernetes.Crypto;
namespace Octopus.Tentacle.Kubernetes
{
@@ -22,6 +23,7 @@ class KubernetesPodLogService : KubernetesService, IKubernetesPodLogService
readonly IKubernetesPodMonitor podMonitor;
readonly ITentacleScriptLogProvider scriptLogProvider;
readonly IScriptPodSinceTimeStore scriptPodSinceTimeStore;
+ readonly IScriptPodLogEncryptionKeyProvider scriptPodLogEncryptionKeyProvider;
readonly IKubernetesEventService eventService;
public KubernetesPodLogService(
@@ -29,6 +31,7 @@ public KubernetesPodLogService(
IKubernetesPodMonitor podMonitor,
ITentacleScriptLogProvider scriptLogProvider,
IScriptPodSinceTimeStore scriptPodSinceTimeStore,
+ IScriptPodLogEncryptionKeyProvider scriptPodLogEncryptionKeyProvider,
IKubernetesEventService eventService,
ISystemLog log)
: base(configProvider, log)
@@ -36,6 +39,7 @@ public KubernetesPodLogService(
this.podMonitor = podMonitor;
this.scriptLogProvider = scriptLogProvider;
this.scriptPodSinceTimeStore = scriptPodSinceTimeStore;
+ this.scriptPodLogEncryptionKeyProvider = scriptPodLogEncryptionKeyProvider;
this.eventService = eventService;
}
@@ -83,16 +87,28 @@ public KubernetesPodLogService(
var sinceTime = scriptPodSinceTimeStore.GetPodLogsSinceTime(scriptTicket);
try
{
- return await GetPodLogsWithSinceTime(sinceTime);
+ try
+ {
+ return await GetPodLogsWithSinceTime(sinceTime);
+ }
+ catch (UnexpectedPodLogLineNumberException ex)
+ {
+ var message = $"Unexpected Pod log line numbers found with sinceTime='{sinceTime}', loading all logs";
+ tentacleScriptLog.Verbose(message);
+ Log.Warn(ex, message);
+
+ //If we somehow come across weird/missing line numbers, try load the whole Pod logs to see if that helps
+ return await GetPodLogsWithSinceTime(null);
+ }
}
- catch (UnexpectedPodLogLineNumberException ex)
+ catch (PodLogEncryptionKeyException ex)
{
- var message = $"Unexpected Pod log line numbers found with sinceTime='{sinceTime}', loading all logs";
+ //if we can't read the pod log encryption key for a while
+ var message = $"Failed to read pod log encryption key. No new pod logs will be read.";
tentacleScriptLog.Verbose(message);
Log.Warn(ex, message);
- //If we somehow come across weird/missing line numbers, try load the whole Pod logs to see if that helps
- return await GetPodLogsWithSinceTime(null);
+ return (new List(), lastLogSequence, null);
}
}
@@ -105,7 +121,9 @@ public KubernetesPodLogService(
async Task<(IReadOnlyCollection Outputs, long NextSequenceNumber, int? ExitCode)> ReadPodLogsFromStream(Stream stream)
{
using var reader = new StreamReader(stream);
- return await PodLogReader.ReadPodLogs(lastLogSequence, reader);
+ var encryptionKey = await scriptPodLogEncryptionKeyProvider.GetEncryptionKey(scriptTicket, CancellationToken.None);
+ var encryptionProvider = PodLogEncryptionProvider.Create(encryptionKey);
+ return await PodLogReader.ReadPodLogs(lastLogSequence, reader, encryptionProvider);
}
}
@@ -116,8 +134,7 @@ async Task> GetPodEvents(ScriptTicket scriptTicket, s
{
return Array.Empty();
}
-
-
+
var sinceTime = scriptPodSinceTimeStore.GetPodEventsSinceTime(scriptTicket);
var allEvents = await eventService.FetchAllEventsAsync(KubernetesConfig.Namespace, podName, cancellationToken);
@@ -215,6 +232,5 @@ public static class EventExtensions
public static bool IsPullingReason(this Corev1Event @event) => @event.Reason.Equals("Pulling", StringComparison.OrdinalIgnoreCase);
public static bool IsPulledReason(this Corev1Event @event) => @event.Reason.Equals("Pulled", StringComparison.OrdinalIgnoreCase);
public static bool IsWarning(this Corev1Event @event) => @event.Type.Equals("Warning", StringComparison.OrdinalIgnoreCase);
-
}
}
\ No newline at end of file
diff --git a/source/Octopus.Tentacle/Kubernetes/KubernetesModule.cs b/source/Octopus.Tentacle/Kubernetes/KubernetesModule.cs
index 619bfba9d..da64cba9d 100644
--- a/source/Octopus.Tentacle/Kubernetes/KubernetesModule.cs
+++ b/source/Octopus.Tentacle/Kubernetes/KubernetesModule.cs
@@ -3,6 +3,8 @@
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Octopus.Tentacle.Background;
+using Octopus.Tentacle.Kubernetes.Configuration;
+using Octopus.Tentacle.Kubernetes.Crypto;
using Octopus.Tentacle.Kubernetes.Diagnostics;
using Octopus.Tentacle.Kubernetes.Synchronisation;
using Octopus.Tentacle.Kubernetes.Synchronisation.Internal;
@@ -31,6 +33,9 @@ protected override void Load(ContainerBuilder builder)
builder.RegisterType().As().SingleInstance();
builder.RegisterType().As().SingleInstance();
builder.RegisterType().As().SingleInstance();
+
+ builder.RegisterType().As().SingleInstance();
+ builder.RegisterType().As().SingleInstance();
builder.RegisterType().As().As().SingleInstance();
builder.RegisterType().As().As().SingleInstance();
diff --git a/source/Octopus.Tentacle/Kubernetes/KubernetesOrphanedPodCleaner.cs b/source/Octopus.Tentacle/Kubernetes/KubernetesOrphanedPodCleaner.cs
index e67285db8..738c9558a 100644
--- a/source/Octopus.Tentacle/Kubernetes/KubernetesOrphanedPodCleaner.cs
+++ b/source/Octopus.Tentacle/Kubernetes/KubernetesOrphanedPodCleaner.cs
@@ -3,6 +3,7 @@
using System.Threading;
using System.Threading.Tasks;
using Octopus.Diagnostics;
+using Octopus.Tentacle.Kubernetes.Crypto;
using Octopus.Tentacle.Time;
using Octopus.Time;
using Polly;
@@ -22,12 +23,19 @@ public class KubernetesOrphanedPodCleaner : IKubernetesOrphanedPodCleaner
readonly IClock clock;
readonly ITentacleScriptLogProvider scriptLogProvider;
readonly IScriptPodSinceTimeStore scriptPodSinceTimeStore;
-
+ readonly IScriptPodLogEncryptionKeyProvider scriptPodLogEncryptionKeyProvider;
+
readonly TimeSpan initialDelay = TimeSpan.FromMinutes(1);
internal readonly TimeSpan CompletedPodConsideredOrphanedAfterTimeSpan = KubernetesConfig.PodsConsideredOrphanedAfterTimeSpan;
- public KubernetesOrphanedPodCleaner(IKubernetesPodStatusProvider podStatusProvider, IKubernetesPodService podService, ISystemLog log, IClock clock,
- ITentacleScriptLogProvider scriptLogProvider, IScriptPodSinceTimeStore scriptPodSinceTimeStore)
+ public KubernetesOrphanedPodCleaner(
+ IKubernetesPodStatusProvider podStatusProvider,
+ IKubernetesPodService podService,
+ ISystemLog log,
+ IClock clock,
+ ITentacleScriptLogProvider scriptLogProvider,
+ IScriptPodSinceTimeStore scriptPodSinceTimeStore,
+ IScriptPodLogEncryptionKeyProvider scriptPodLogEncryptionKeyProvider)
{
this.podStatusProvider = podStatusProvider;
this.podService = podService;
@@ -35,6 +43,7 @@ public KubernetesOrphanedPodCleaner(IKubernetesPodStatusProvider podStatusProvid
this.clock = clock;
this.scriptLogProvider = scriptLogProvider;
this.scriptPodSinceTimeStore = scriptPodSinceTimeStore;
+ this.scriptPodLogEncryptionKeyProvider = scriptPodLogEncryptionKeyProvider;
}
async Task IKubernetesOrphanedPodCleaner.StartAsync(CancellationToken cancellationToken)
@@ -93,6 +102,7 @@ state.FinishedAt is not null &&
{
scriptLogProvider.Delete(pod.ScriptTicket);
scriptPodSinceTimeStore.Delete(pod.ScriptTicket);
+ scriptPodLogEncryptionKeyProvider.Delete(pod.ScriptTicket);
if (KubernetesConfig.DisableAutomaticPodCleanup)
{
diff --git a/source/Octopus.Tentacle/Kubernetes/KubernetesRawScriptPodCreator.cs b/source/Octopus.Tentacle/Kubernetes/KubernetesRawScriptPodCreator.cs
index 1b63a9b01..2ed5ea06e 100644
--- a/source/Octopus.Tentacle/Kubernetes/KubernetesRawScriptPodCreator.cs
+++ b/source/Octopus.Tentacle/Kubernetes/KubernetesRawScriptPodCreator.cs
@@ -7,6 +7,7 @@
using Octopus.Tentacle.Configuration;
using Octopus.Tentacle.Configuration.Instances;
using Octopus.Tentacle.Contracts.KubernetesScriptServiceV1;
+using Octopus.Tentacle.Kubernetes.Crypto;
using Octopus.Tentacle.Util;
namespace Octopus.Tentacle.Kubernetes
@@ -28,8 +29,9 @@ public KubernetesRawScriptPodCreator(
ISystemLog log,
ITentacleScriptLogProvider scriptLogProvider,
IHomeConfiguration homeConfiguration,
- KubernetesPhysicalFileSystem kubernetesPhysicalFileSystem)
- : base(podService, podMonitor, secretService, containerResolver, appInstanceSelector, log, scriptLogProvider, homeConfiguration, kubernetesPhysicalFileSystem)
+ KubernetesPhysicalFileSystem kubernetesPhysicalFileSystem,
+ IScriptPodLogEncryptionKeyProvider scriptPodLogEncryptionKeyProvider)
+ : base(podService, podMonitor, secretService, containerResolver, appInstanceSelector, log, scriptLogProvider, homeConfiguration, kubernetesPhysicalFileSystem, scriptPodLogEncryptionKeyProvider)
{
this.containerResolver = containerResolver;
}
@@ -94,4 +96,4 @@ string GetInitExecutionScript(string nfsVolumeDirectory, string homeDir, string
";
}
}
-}
+}
\ No newline at end of file
diff --git a/source/Octopus.Tentacle/Kubernetes/KubernetesScriptPodCreator.cs b/source/Octopus.Tentacle/Kubernetes/KubernetesScriptPodCreator.cs
index dd29fbe00..2c7021a0b 100644
--- a/source/Octopus.Tentacle/Kubernetes/KubernetesScriptPodCreator.cs
+++ b/source/Octopus.Tentacle/Kubernetes/KubernetesScriptPodCreator.cs
@@ -14,6 +14,7 @@
using Octopus.Tentacle.Configuration;
using Octopus.Tentacle.Configuration.Instances;
using Octopus.Tentacle.Contracts.KubernetesScriptServiceV1;
+using Octopus.Tentacle.Kubernetes.Crypto;
using Octopus.Tentacle.Scripts;
using Octopus.Tentacle.Util;
using Octopus.Tentacle.Variables;
@@ -36,6 +37,7 @@ public class KubernetesScriptPodCreator : IKubernetesScriptPodCreator
readonly ITentacleScriptLogProvider scriptLogProvider;
readonly IHomeConfiguration homeConfiguration;
readonly KubernetesPhysicalFileSystem kubernetesPhysicalFileSystem;
+ readonly IScriptPodLogEncryptionKeyProvider scriptPodLogEncryptionKeyProvider;
public KubernetesScriptPodCreator(
IKubernetesPodService podService,
@@ -46,7 +48,8 @@ public KubernetesScriptPodCreator(
ISystemLog log,
ITentacleScriptLogProvider scriptLogProvider,
IHomeConfiguration homeConfiguration,
- KubernetesPhysicalFileSystem kubernetesPhysicalFileSystem)
+ KubernetesPhysicalFileSystem kubernetesPhysicalFileSystem,
+ IScriptPodLogEncryptionKeyProvider scriptPodLogEncryptionKeyProvider)
{
this.podService = podService;
this.podMonitor = podMonitor;
@@ -57,6 +60,7 @@ public KubernetesScriptPodCreator(
this.scriptLogProvider = scriptLogProvider;
this.homeConfiguration = homeConfiguration;
this.kubernetesPhysicalFileSystem = kubernetesPhysicalFileSystem;
+ this.scriptPodLogEncryptionKeyProvider = scriptPodLogEncryptionKeyProvider;
}
public async Task CreatePod(StartKubernetesScriptCommandV1 command, IScriptWorkspace workspace, CancellationToken cancellationToken)
@@ -74,6 +78,9 @@ public async Task CreatePod(StartKubernetesScriptCommandV1 command, IScriptWorks
cancellationToken,
log))
{
+ //Write the log encryption key here
+ await scriptPodLogEncryptionKeyProvider.WriteEncryptionKeyfileToWorkspace(command.ScriptTicket, cancellationToken);
+
//Possibly create the image pull secret name
var imagePullSecretName = await CreateImagePullSecret(command, cancellationToken);
diff --git a/source/Octopus.Tentacle/Kubernetes/KubernetesSecretService.cs b/source/Octopus.Tentacle/Kubernetes/KubernetesSecretService.cs
index c94c84f13..866c77f40 100644
--- a/source/Octopus.Tentacle/Kubernetes/KubernetesSecretService.cs
+++ b/source/Octopus.Tentacle/Kubernetes/KubernetesSecretService.cs
@@ -14,7 +14,7 @@ public interface IKubernetesSecretService
{
Task TryGetSecretAsync(string name, CancellationToken cancellationToken);
Task CreateSecretAsync(V1Secret secret, CancellationToken cancellationToken);
- Task UpdateSecretDataAsync(string secretName, IDictionary secretData, CancellationToken cancellationToken);
+ Task UpdateSecretDataAsync(string secretName, IDictionary secretData, CancellationToken cancellationToken);
}
public class KubernetesSecretService : KubernetesService, IKubernetesSecretService
@@ -48,7 +48,7 @@ public async Task CreateSecretAsync(V1Secret secret, CancellationToken cancellat
await Client.CreateNamespacedSecretAsync(secret, KubernetesConfig.Namespace, cancellationToken: cancellationToken);
}
- public async Task UpdateSecretDataAsync(string secretName, IDictionary secretData, CancellationToken cancellationToken)
+ public async Task UpdateSecretDataAsync(string secretName, IDictionary secretData, CancellationToken cancellationToken)
{
var patchSecret = new V1Secret
{
@@ -57,7 +57,7 @@ public async Task UpdateSecretDataAsync(string secretName, IDictionary
+ return await RetryPolicy.ExecuteAsync(async () =>
await Client.PatchNamespacedSecretAsync(new V1Patch(patchYaml, V1Patch.PatchType.MergePatch), secretName, KubernetesConfig.Namespace, cancellationToken: cancellationToken));
}
}
diff --git a/source/Octopus.Tentacle/Kubernetes/PodLogLineParser.cs b/source/Octopus.Tentacle/Kubernetes/PodLogLineParser.cs
index d5120a52f..75b02a68b 100644
--- a/source/Octopus.Tentacle/Kubernetes/PodLogLineParser.cs
+++ b/source/Octopus.Tentacle/Kubernetes/PodLogLineParser.cs
@@ -1,7 +1,6 @@
using System;
using Octopus.Tentacle.Contracts;
-using Octopus.Tentacle.Startup;
-using YamlDotNet.Core.Tokens;
+using Octopus.Tentacle.Kubernetes.Crypto;
namespace Octopus.Tentacle.Kubernetes
{
@@ -61,7 +60,7 @@ static class PodLogLineParser
const string EndOfStreamMarkerPrefix = "EOS-075CD4F0-8C76-491D-BA76-0879D35E9CFE";
const string EndOfStreamMarkerExitCodeDelimiter = "<<>>";
- public static PodLogLineParseResult ParseLine(string line)
+ public static PodLogLineParseResult ParseLine(string line, IPodLogEncryptionProvider encryptionProvider)
{
var logParts = line.Split(new[] { '|' }, 4);
if (logParts.Length != 4)
@@ -72,7 +71,7 @@ public static PodLogLineParseResult ParseLine(string line)
var datePart = logParts[0];
var lineNumberPart = logParts[1];
var outputSourcePart = logParts[2];
- var messagePart = logParts[3];
+ var encryptedMessagePart = logParts[3];
if (!DateTimeOffset.TryParse(datePart, out var occurred))
{
@@ -88,13 +87,15 @@ public static PodLogLineParseResult ParseLine(string line)
{
return new InvalidPodLogLineParseResult($"Pod log level '{outputSourcePart}' is invalid: '{line}'");
}
-
- if (messagePart.StartsWith(EndOfStreamMarkerPrefix))
+
+ //the log messages are being returned from the pods encrypted, decrypt them here
+ var decryptedMessagePath = encryptionProvider.Decrypt(encryptedMessagePart);
+ if (decryptedMessagePath.StartsWith(EndOfStreamMarkerPrefix))
{
try
{
- var exitCode = int.Parse(messagePart.Split(new[] { EndOfStreamMarkerExitCodeDelimiter }, StringSplitOptions.None)[1]);
- return new EndOfStreamPodLogLineParseResult(new PodLogLine(lineNumber, source, messagePart, occurred), exitCode);
+ var exitCode = int.Parse(decryptedMessagePath.Split(new[] { EndOfStreamMarkerExitCodeDelimiter }, StringSplitOptions.None)[1]);
+ return new EndOfStreamPodLogLineParseResult(new PodLogLine(lineNumber, source, decryptedMessagePath, occurred), exitCode);
}
catch (Exception)
{
@@ -102,7 +103,7 @@ public static PodLogLineParseResult ParseLine(string line)
}
}
- return new ValidPodLogLineParseResult(new PodLogLine(lineNumber, source, messagePart, occurred));
+ return new ValidPodLogLineParseResult(new PodLogLine(lineNumber, source, decryptedMessagePath, occurred));
}
}
}
\ No newline at end of file
diff --git a/source/Octopus.Tentacle/Kubernetes/PodLogReader.cs b/source/Octopus.Tentacle/Kubernetes/PodLogReader.cs
index 792224995..6f95e087e 100644
--- a/source/Octopus.Tentacle/Kubernetes/PodLogReader.cs
+++ b/source/Octopus.Tentacle/Kubernetes/PodLogReader.cs
@@ -3,21 +3,22 @@
using System.IO;
using System.Threading.Tasks;
using Octopus.Tentacle.Contracts;
+using Octopus.Tentacle.Kubernetes.Crypto;
using Octopus.Tentacle.Util;
namespace Octopus.Tentacle.Kubernetes
{
static class PodLogReader
{
- public static async Task<(IReadOnlyCollection Lines, long NextSequenceNumber, int? exitCode)> ReadPodLogs(long lastLogSequence, StreamReader reader)
+ public static async Task<(IReadOnlyCollection Lines, long NextSequenceNumber, int? exitCode)> ReadPodLogs(long lastLogSequence, StreamReader reader, IPodLogEncryptionProvider encryptionProvider)
{
int? exitCode = null;
var results = new List();
var nextSequenceNumber = lastLogSequence;
- long expectedLineNumber = lastLogSequence+1;
-
- bool haveReadPastPreviousBatchOfRows = false;
-
+ var expectedLineNumber = lastLogSequence + 1;
+
+ var haveReadPastPreviousBatchOfRows = false;
+
while (true)
{
var line = await reader.ReadLineAsync();
@@ -28,7 +29,7 @@ static class PodLogReader
return (results, nextSequenceNumber, exitCode);
}
- var parseResult = PodLogLineParser.ParseLine(line!);
+ var parseResult = PodLogLineParser.ParseLine(line!, encryptionProvider);
switch (parseResult)
{
@@ -50,7 +51,7 @@ static class PodLogReader
throw new UnexpectedPodLogLineNumberException(expectedLineNumber, podLogLine.LineNumber);
expectedLineNumber++;
-
+
if (validParseResult is EndOfStreamPodLogLineParseResult endOfStreamParseResult)
exitCode = endOfStreamParseResult.ExitCode;
@@ -74,7 +75,7 @@ static class PodLogReader
class UnexpectedPodLogLineNumberException : Exception
{
public UnexpectedPodLogLineNumberException(long expectedLineNumber, long actualLineNumber)
- : base($"Unexpected Script Pod log line number, expected: {expectedLineNumber}, actual: {actualLineNumber}")
+ : base($"Unexpected Script Pod log line number, expected: {expectedLineNumber}, actual: {actualLineNumber}")
{
}
}
diff --git a/source/Octopus.Tentacle/NullableReferenceTypeAttributes.cs b/source/Octopus.Tentacle/NullableReferenceTypeAttributes.cs
index 25ed995d8..004ec67b0 100644
--- a/source/Octopus.Tentacle/NullableReferenceTypeAttributes.cs
+++ b/source/Octopus.Tentacle/NullableReferenceTypeAttributes.cs
@@ -44,5 +44,24 @@ public NotNullWhenAttribute(bool returnValue)
/// Gets the return value condition.
public bool ReturnValue { get; }
}
+
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
+ public sealed class MemberNotNullAttribute : Attribute
+ {
+ /// Initializes the attribute with a field or property member.
+ ///
+ /// The field or property member that is promised to be not-null.
+ ///
+ public MemberNotNullAttribute(string member) => Members = new[] { member };
+
+ /// Initializes the attribute with the list of field and property members.
+ ///
+ /// The list of field and property members that are promised to be not-null.
+ ///
+ public MemberNotNullAttribute(params string[] members) => Members = members;
+
+ /// Gets field or property member names.
+ public string[] Members { get; }
+ }
}
#endif
\ No newline at end of file
diff --git a/source/Octopus.Tentacle/Octopus.Tentacle.csproj b/source/Octopus.Tentacle/Octopus.Tentacle.csproj
index 308291fc5..fb648dd8b 100644
--- a/source/Octopus.Tentacle/Octopus.Tentacle.csproj
+++ b/source/Octopus.Tentacle/Octopus.Tentacle.csproj
@@ -52,6 +52,7 @@
+
@@ -65,7 +66,6 @@
-
diff --git a/source/Octopus.Tentacle/Services/Scripts/Kubernetes/KubernetesScriptServiceV1.cs b/source/Octopus.Tentacle/Services/Scripts/Kubernetes/KubernetesScriptServiceV1.cs
index bf7d119b9..e6ed60700 100644
--- a/source/Octopus.Tentacle/Services/Scripts/Kubernetes/KubernetesScriptServiceV1.cs
+++ b/source/Octopus.Tentacle/Services/Scripts/Kubernetes/KubernetesScriptServiceV1.cs
@@ -6,6 +6,7 @@
using Octopus.Tentacle.Contracts;
using Octopus.Tentacle.Contracts.KubernetesScriptServiceV1;
using Octopus.Tentacle.Kubernetes;
+using Octopus.Tentacle.Kubernetes.Crypto;
using Octopus.Tentacle.Kubernetes.Synchronisation;
using Octopus.Tentacle.Maintenance;
using Octopus.Tentacle.Scripts;
@@ -23,6 +24,7 @@ public class KubernetesScriptServiceV1 : IAsyncKubernetesScriptServiceV1, IRunni
readonly IKubernetesPodLogService podLogService;
readonly ITentacleScriptLogProvider scriptLogProvider;
readonly IScriptPodSinceTimeStore scriptPodSinceTimeStore;
+ readonly IScriptPodLogEncryptionKeyProvider scriptPodLogEncryptionKeyProvider;
readonly IKeyedSemaphore keyedSemaphore;
public KubernetesScriptServiceV1(
@@ -34,6 +36,7 @@ public KubernetesScriptServiceV1(
IKubernetesPodLogService podLogService,
ITentacleScriptLogProvider scriptLogProvider,
IScriptPodSinceTimeStore scriptPodSinceTimeStore,
+ IScriptPodLogEncryptionKeyProvider scriptPodLogEncryptionKeyProvider,
IKeyedSemaphore keyedSemaphore)
{
this.podService = podService;
@@ -44,6 +47,7 @@ public KubernetesScriptServiceV1(
this.podLogService = podLogService;
this.scriptLogProvider = scriptLogProvider;
this.scriptPodSinceTimeStore = scriptPodSinceTimeStore;
+ this.scriptPodLogEncryptionKeyProvider = scriptPodLogEncryptionKeyProvider;
this.keyedSemaphore = keyedSemaphore;
}
@@ -123,6 +127,7 @@ public async Task CompleteScriptAsync(CompleteKubernetesScriptCommandV1 command,
scriptLogProvider.Delete(command.ScriptTicket);
scriptPodSinceTimeStore.Delete(command.ScriptTicket);
+ scriptPodLogEncryptionKeyProvider.Delete(command.ScriptTicket);
if (!KubernetesConfig.DisableAutomaticPodCleanup)
await podService.DeleteIfExists(command.ScriptTicket, cancellationToken);