diff --git a/src/Aspire.Hosting/Dcp/DcpExecutor.cs b/src/Aspire.Hosting/Dcp/DcpExecutor.cs index 91985887b46..ecf76cce1d6 100644 --- a/src/Aspire.Hosting/Dcp/DcpExecutor.cs +++ b/src/Aspire.Hosting/Dcp/DcpExecutor.cs @@ -2384,9 +2384,9 @@ await modelResource.ProcessContainerRuntimeArgValues( if (modelResource.TryGetLastAnnotation(out var pathsAnnotation)) { - certificatesDestination ??= pathsAnnotation.CustomCertificatesDestination; - bundlePaths ??= pathsAnnotation.DefaultCertificateBundles; - certificateDirsPaths ??= pathsAnnotation.DefaultCertificateDirectories; + certificatesDestination = pathsAnnotation.CustomCertificatesDestination ?? certificatesDestination; + bundlePaths = pathsAnnotation.DefaultCertificateBundles ?? bundlePaths; + certificateDirsPaths = pathsAnnotation.DefaultCertificateDirectories ?? certificateDirsPaths; } bool failedToApplyConfig = false; @@ -2482,18 +2482,23 @@ await modelResource.ProcessContainerRuntimeArgValues( { // If overriding the default resource CA bundle, then we want to copy our bundle to the well-known locations // used by common Linux distributions to make it easier to ensure applications pick it up. - foreach (var bundlePath in bundlePaths!) + // Group by common directory to avoid creating multiple file system entries for the same root directory. + foreach (var bundlePath in bundlePaths!.Select(bp => + { + var filename = Path.GetFileName(bp); + var dir = bp.Substring(0, bp.Length - filename.Length); + return (dir, filename); + }).GroupBy(parts => parts.dir)) { createFiles.Add(new ContainerCreateFileSystem { - Destination = bundlePath, - Entries = [ + Destination = bundlePath.Key, + Entries = bundlePath.Select(bp => new ContainerFileSystemEntry { - Name = Path.GetFileName(bundlePath), + Name = bp.filename, Contents = caBundleBuilder.ToString(), - }, - ], + }).ToList(), }); } } diff --git a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs index 92a9ff71279..1a606015803 100644 --- a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs +++ b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs @@ -627,16 +627,61 @@ public async Task VerifyContainerCreateFile() [Theory] [RequiresDocker] [RequiresDevCert] - [InlineData(null, null, true)] - [InlineData(null, false, false)] - [InlineData(null, true, true)] - [InlineData(false, null, false)] - [InlineData(false, false, false)] - [InlineData(false, true, true)] - [InlineData(true, null, true)] - [InlineData(true, false, false)] - [InlineData(true, true, true)] - public async Task VerifyContainerIncludesExpectedDevCertificateConfiguration(bool? implicitTrust, bool? explicitTrust, bool expectDevCert) + [InlineData(null, null, true, false, CertificateTrustScope.Append)] + [InlineData(null, false, false, false, CertificateTrustScope.Append)] + [InlineData(null, true, true, false, CertificateTrustScope.Append)] + [InlineData(false, null, false, false, CertificateTrustScope.Append)] + [InlineData(false, false, false, false, CertificateTrustScope.Append)] + [InlineData(false, true, true, false, CertificateTrustScope.Append)] + [InlineData(true, null, true, false, CertificateTrustScope.Append)] + [InlineData(true, false, false, false, CertificateTrustScope.Append)] + [InlineData(true, true, true, false, CertificateTrustScope.Append)] + [InlineData(null, null, true, true, CertificateTrustScope.Append)] + [InlineData(null, false, false, true, CertificateTrustScope.Append)] + [InlineData(null, true, true, true, CertificateTrustScope.Append)] + [InlineData(false, null, false, true, CertificateTrustScope.Append)] + [InlineData(false, false, false, true, CertificateTrustScope.Append)] + [InlineData(false, true, true, true, CertificateTrustScope.Append)] + [InlineData(true, null, true, true, CertificateTrustScope.Append)] + [InlineData(true, false, false, true, CertificateTrustScope.Append)] + [InlineData(true, true, true, true, CertificateTrustScope.Append)] + [InlineData(null, null, true, false, CertificateTrustScope.Override)] + [InlineData(null, false, false, false, CertificateTrustScope.Override)] + [InlineData(null, true, true, false, CertificateTrustScope.Override)] + [InlineData(false, null, false, false, CertificateTrustScope.Override)] + [InlineData(false, false, false, false, CertificateTrustScope.Override)] + [InlineData(false, true, true, false, CertificateTrustScope.Override)] + [InlineData(true, null, true, false, CertificateTrustScope.Override)] + [InlineData(true, false, false, false, CertificateTrustScope.Override)] + [InlineData(true, true, true, false, CertificateTrustScope.Override)] + [InlineData(null, null, true, true, CertificateTrustScope.Override)] + [InlineData(null, false, false, true, CertificateTrustScope.Override)] + [InlineData(null, true, true, true, CertificateTrustScope.Override)] + [InlineData(false, null, false, true, CertificateTrustScope.Override)] + [InlineData(false, false, false, true, CertificateTrustScope.Override)] + [InlineData(false, true, true, true, CertificateTrustScope.Override)] + [InlineData(true, null, true, true, CertificateTrustScope.Override)] + [InlineData(true, false, false, true, CertificateTrustScope.Override)] + [InlineData(true, true, true, true, CertificateTrustScope.Override)] + [InlineData(null, null, false, false, CertificateTrustScope.None)] + [InlineData(null, false, false, false, CertificateTrustScope.None)] + [InlineData(null, true, false, false, CertificateTrustScope.None)] + [InlineData(false, null, false, false, CertificateTrustScope.None)] + [InlineData(false, false, false, false, CertificateTrustScope.None)] + [InlineData(false, true, false, false, CertificateTrustScope.None)] + [InlineData(true, null, false, false, CertificateTrustScope.None)] + [InlineData(true, false, false, false, CertificateTrustScope.None)] + [InlineData(true, true, false, false, CertificateTrustScope.None)] + [InlineData(null, null, false, true, CertificateTrustScope.None)] + [InlineData(null, false, false, true, CertificateTrustScope.None)] + [InlineData(null, true, false, true, CertificateTrustScope.None)] + [InlineData(false, null, false, true, CertificateTrustScope.None)] + [InlineData(false, false, false, true, CertificateTrustScope.None)] + [InlineData(false, true, false, true, CertificateTrustScope.None)] + [InlineData(true, null, false, true, CertificateTrustScope.None)] + [InlineData(true, false, false, true, CertificateTrustScope.None)] + [InlineData(true, true, false, true, CertificateTrustScope.None)] + public async Task VerifyContainerIncludesExpectedDevCertificateConfiguration(bool? implicitTrust, bool? explicitTrust, bool expectDevCert, bool overridePaths, CertificateTrustScope trustScope) { using var testProgram = CreateTestProgram("verify-container-dev-cert", trustDeveloperCertificate: implicitTrust); SetupXUnitLogging(testProgram.AppBuilder.Services); @@ -647,6 +692,27 @@ public async Task VerifyContainerIncludesExpectedDevCertificateConfiguration(boo container.WithDeveloperCertificateTrust(explicitTrust.Value); } + var expectedDestination = "/usr/lib/ssl/aspire"; + var expectedDefaultCertificateDirs = new List(); + var expectedDefaultBundleFiles = new List(); + if (overridePaths) + { + expectedDestination = "/usr/lib/ssl/someotherpath"; + expectedDefaultCertificateDirs.Add("/usr/lib/someothercertpath"); + expectedDefaultCertificateDirs.Add("/usr/share/lib/anotherpath"); + expectedDefaultBundleFiles.Add("/usr/lib/somessl/cert.pem"); + expectedDefaultBundleFiles.Add("/usr/share/certfile.pem"); + + container.WithContainerCertificatePaths(customCertificatesDestination: expectedDestination, defaultCertificateBundlePaths: expectedDefaultBundleFiles, defaultCertificateDirectoryPaths: expectedDefaultCertificateDirs); + } + else + { + expectedDefaultCertificateDirs.AddRange(ContainerCertificatePathsAnnotation.DefaultCertificateDirectoriesPaths); + expectedDefaultBundleFiles.AddRange(ContainerCertificatePathsAnnotation.DefaultCertificateBundlePaths); + } + + container.WithCertificateTrustScope(trustScope); + await using var app = testProgram.Build(); await app.StartAsync().DefaultTimeout(TestConstants.DefaultOrchestratorTestLongTimeout); @@ -665,17 +731,40 @@ public async Task VerifyContainerIncludesExpectedDevCertificateConfiguration(boo if (expectDevCert) { Assert.NotNull(item.Spec.Env); - Assert.Collection(item.Spec.Env.OrderBy(e => e.Name), + if (trustScope == CertificateTrustScope.Append) + { + Assert.DoesNotContain(item.Spec.Env, e => e.Name == "SSL_CERT_FILE"); + } + else if (trustScope == CertificateTrustScope.Override) + { + Assert.Collection(item.Spec.Env.Where(e => e.Name == "SSL_CERT_FILE"), + certFile => + { + Assert.Equal("SSL_CERT_FILE", certFile.Name); + Assert.Equal($"{expectedDestination}/cert.pem", certFile.Value); + }); + } + + Assert.Collection(item.Spec.Env.Where(e => e.Name == "SSL_CERT_DIR"), certDir => { Assert.Equal("SSL_CERT_DIR", certDir.Name); - Assert.StartsWith("/usr/lib/ssl/aspire/certs:", certDir.Value); + Assert.NotNull(certDir.Value); + var certDirPaths = certDir.Value.Split(':'); + Assert.Contains($"{expectedDestination}/certs", certDirPaths); + if (trustScope == CertificateTrustScope.Append) + { + foreach (var expectedPath in expectedDefaultCertificateDirs) + { + Assert.Contains(expectedPath, certDirPaths); + } + } }); + Assert.NotNull(item.Spec.CreateFiles); - Assert.Collection(item.Spec.CreateFiles, + Assert.Collection(item.Spec.CreateFiles.Where(cf => cf.Destination == expectedDestination), createCerts => { - Assert.Equal("/usr/lib/ssl/aspire", createCerts.Destination); Assert.NotNull(createCerts.Entries); Assert.Collection(createCerts.Entries, bundle => @@ -702,6 +791,37 @@ public async Task VerifyContainerIncludesExpectedDevCertificateConfiguration(boo } }); }); + + if (trustScope == CertificateTrustScope.Override) + { + foreach (var bundlePath in expectedDefaultBundleFiles!.Select(bp => + { + var filename = Path.GetFileName(bp); + var dir = bp.Substring(0, bp.Length - filename.Length); + return (dir, filename); + }).GroupBy(parts => parts.dir)) + { + Assert.Collection(item.Spec.CreateFiles.Where(cf => cf.Destination == bundlePath.Key), + createCerts => + { + Assert.NotNull(createCerts.Entries); + Assert.Equal(bundlePath.Count(), createCerts.Entries.Count); + foreach (var expectedFile in bundlePath) + { + Assert.Collection(createCerts.Entries.Where(file => file.Name == expectedFile.filename), + bundle => + { + Assert.Equal(expectedFile.filename, bundle.Name); + Assert.Equal(ContainerFileSystemEntryType.File, bundle.Type); + var certs = new X509Certificate2Collection(); + certs.ImportFromPem(bundle.Contents); + Assert.Equal(dc.Certificates.Count, certs.Count); + Assert.All(certs, (cert) => cert.IsAspNetCoreDevelopmentCertificate()); + }); + } + }); + } + } } else {