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);