From 163148ec01330c95f26f2b7ac9fcca938be884b7 Mon Sep 17 00:00:00 2001 From: afscrome Date: Wed, 17 Dec 2025 22:29:26 +0000 Subject: [PATCH] Use the new `WithHttpsCertificateConfiguration` apis for Otel Collector Use the new `WithHttpsCertificateConfiguration` apis in 13.1 rather than the previous custom implementation of exporting the dev cert. --- .../AppHost.cs | 3 +- ...pire.Hosting.OpenTelemetryCollector.csproj | 4 - .../OpenTelemetryCollectorExtensions.cs | 53 +++-- src/Shared/DevCertHostingExtensions.cs | 152 -------------- .../ResourceCreationTests.cs | 198 ------------------ 5 files changed, 27 insertions(+), 383 deletions(-) delete mode 100644 src/Shared/DevCertHostingExtensions.cs diff --git a/examples/opentelemetry-collector/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector.AppHost/AppHost.cs b/examples/opentelemetry-collector/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector.AppHost/AppHost.cs index f640126b5..17a7c83ae 100644 --- a/examples/opentelemetry-collector/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector.AppHost/AppHost.cs +++ b/examples/opentelemetry-collector/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector.AppHost/AppHost.cs @@ -2,8 +2,9 @@ builder.AddProject("api"); -builder.AddOpenTelemetryCollector("opentelemetry-collector") +var collector = builder.AddOpenTelemetryCollector("opentelemetry-collector") .WithAppForwarding() .WithConfig("./config.yaml"); builder.Build().Run(); + diff --git a/src/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector.csproj b/src/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector.csproj index 8ae41a461..31706474d 100644 --- a/src/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector.csproj +++ b/src/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector.csproj @@ -9,8 +9,4 @@ - - - - diff --git a/src/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector/OpenTelemetryCollectorExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector/OpenTelemetryCollectorExtensions.cs index 11344a7c7..cba38a9d8 100644 --- a/src/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector/OpenTelemetryCollectorExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector/OpenTelemetryCollectorExtensions.cs @@ -34,8 +34,6 @@ public static IResourceBuilder AddOpenTelemetryC var settings = new OpenTelemetryCollectorSettings(); configureSettings?.Invoke(settings); - var isHttpsEnabled = !settings.ForceNonSecureReceiver && url.StartsWith("https", StringComparison.OrdinalIgnoreCase); - var resource = new OpenTelemetryCollectorResource(name); var resourceBuilder = builder.AddResource(resource) .WithImage(settings.CollectorImage, settings.CollectorTag) @@ -43,33 +41,13 @@ public static IResourceBuilder AddOpenTelemetryC .WithEnvironment("ASPIRE_API_KEY", builder.Configuration[DashboardOtlpApiKeyVariableName]) .WithIconName("DesktopPulse"); - if (settings.EnableGrpcEndpoint) - resourceBuilder.WithEndpoint(targetPort: 4317, name: OpenTelemetryCollectorResource.GrpcEndpointName, scheme: isHttpsEnabled ? "https" : "http"); - if (settings.EnableHttpEndpoint) - resourceBuilder.WithEndpoint(targetPort: 4318, name: OpenTelemetryCollectorResource.HttpEndpointName, scheme: isHttpsEnabled ? "https" : "http"); - - - if (!settings.ForceNonSecureReceiver && isHttpsEnabled && builder.ExecutionContext.IsRunMode) - { - resourceBuilder.RunWithHttpsDevCertificate(); + var useHttpsForReceivers = !settings.ForceNonSecureReceiver && url.StartsWith("https", StringComparison.OrdinalIgnoreCase); - // Not using `Path.Combine` as we MUST use unix style paths in the container - var certFilePath = $"{DevCertHostingExtensions.DEV_CERT_BIND_MOUNT_DEST_DIR}/{DevCertHostingExtensions.CERT_FILE_NAME}"; - var certKeyPath = $"{DevCertHostingExtensions.DEV_CERT_BIND_MOUNT_DEST_DIR}/{DevCertHostingExtensions.CERT_KEY_FILE_NAME}"; + if (settings.EnableGrpcEndpoint) + ConfigureReceiver(4317, OpenTelemetryCollectorResource.GrpcEndpointName); - if (settings.EnableHttpEndpoint) - { - resourceBuilder.WithArgs( - $@"--config=yaml:receivers::otlp::protocols::http::tls::cert_file: ""{certFilePath}""", - $@"--config=yaml:receivers::otlp::protocols::http::tls::key_file: ""{certKeyPath}"""); - } - if (settings.EnableGrpcEndpoint) - { - resourceBuilder.WithArgs( - $@"--config=yaml:receivers::otlp::protocols::grpc::tls::cert_file: ""{certFilePath}""", - $@"--config=yaml:receivers::otlp::protocols::grpc::tls::key_file: ""{certKeyPath}"""); - } - } + if (settings.EnableHttpEndpoint) + ConfigureReceiver(4318, OpenTelemetryCollectorResource.HttpEndpointName); if (!settings.DisableHealthcheck) { @@ -83,6 +61,26 @@ public static IResourceBuilder AddOpenTelemetryC ); } return resourceBuilder; + + void ConfigureReceiver(int port, string protocol) + { + var scheme = useHttpsForReceivers ? "https" : "http"; + resourceBuilder.WithEndpoint(targetPort: port, name: protocol, scheme: scheme); + + if (!useHttpsForReceivers) + { + return; + } + +#pragma warning disable ASPIRECERTIFICATES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + resourceBuilder.WithHttpsCertificateConfiguration(ctx => + { + ctx.Arguments.Add(ReferenceExpression.Create($@"--config=yaml:receivers::otlp::protocols::{protocol}::tls::cert_file: ""{ctx.CertificatePath}""")); + ctx.Arguments.Add(ReferenceExpression.Create($@"--config=yaml:receivers::otlp::protocols::{protocol}::tls::key_file: ""{ctx.KeyPath}""")); + return Task.CompletedTask; + }); +#pragma warning restore ASPIRECERTIFICATES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + } } /// @@ -123,5 +121,4 @@ public static IResourceBuilder WithConfig(this I return builder.WithBindMount(configPath, $"/config/{configFileInfo.Name}") .WithArgs($"--config=/config/{configFileInfo.Name}"); } - } \ No newline at end of file diff --git a/src/Shared/DevCertHostingExtensions.cs b/src/Shared/DevCertHostingExtensions.cs deleted file mode 100644 index 83ebd0a81..000000000 --- a/src/Shared/DevCertHostingExtensions.cs +++ /dev/null @@ -1,152 +0,0 @@ -using Aspire.Hosting.ApplicationModel; - -namespace Aspire.Hosting; - -/// -/// Extensions for adding Dev Certs to aspire resources. -/// -internal static class DevCertHostingExtensions -{ - /// - /// The destination directory for the certificate files in a container. - /// - public const string DEV_CERT_BIND_MOUNT_DEST_DIR = "/dev-certs"; - - /// - /// The file name of the certificate file. - /// - public const string CERT_FILE_NAME = "dev-cert.pem"; - - /// - /// The file name of the certificate key file. - /// - public const string CERT_KEY_FILE_NAME = "dev-cert.key"; - - /// - /// Injects the ASP.NET Core HTTPS developer certificate into the resource via the specified environment variables when - /// .ApplicationBuilder.ExecutionContext.IsRunMode == true.
- /// If the resource is a , the certificate files will be provided via WithContainerFiles. - ///
- /// - /// This method does not configure an HTTPS endpoint on the resource. - /// Use to configure an HTTPS endpoint. - /// - public static IResourceBuilder RunWithHttpsDevCertificate( - this IResourceBuilder builder, string certFileEnv = "", string certKeyFileEnv = "") - where TResource : IResourceWithEnvironment, IResourceWithWaitSupport - { - if (!builder.ApplicationBuilder.ExecutionContext.IsRunMode) - { - return builder; - } - - if (builder.Resource is not ContainerResource && - (!string.IsNullOrEmpty(certFileEnv) || !string.IsNullOrEmpty(certKeyFileEnv))) - { - throw new InvalidOperationException("RunWithHttpsDevCertificate needs environment variables only for Resources that aren't Containers."); - } - - // Create temp directory for certificate export - var tempDir = Directory.CreateTempSubdirectory("aspire-dev-certs"); - var certExportPath = Path.Combine(tempDir.FullName, "dev-cert.pem"); - var certKeyExportPath = Path.Combine(tempDir.FullName, "dev-cert.key"); - - // Create a unique resource name for the certificate export - var exportResourceName = $"dev-cert-export"; - - // Check if we already have a certificate export resource - var existingResource = builder.ApplicationBuilder.Resources.FirstOrDefault(r => r.Name == exportResourceName); - IResourceBuilder exportExecutable; - - if (existingResource is null) - { - // Create the executable resource to export the certificate - exportExecutable = builder.ApplicationBuilder - .AddExecutable(exportResourceName, "dotnet", tempDir.FullName) - .WithEnvironment("DOTNET_CLI_UI_LANGUAGE", "en") // Ensure consistent output language - .WithIconName("Certificate") - .WithArgs(context => - { - context.Args.Add("dev-certs"); - context.Args.Add("https"); - context.Args.Add("--export-path"); - context.Args.Add(certExportPath); - context.Args.Add("--format"); - context.Args.Add("Pem"); - context.Args.Add("--no-password"); - }); - } - else - { - exportExecutable = builder.ApplicationBuilder.CreateResourceBuilder((ExecutableResource)existingResource); - } - - builder.WaitForCompletion(exportExecutable); - - // Configure the current resource with the certificate paths - if (builder.Resource is ContainerResource containerResource) - { - var certFileDest = $"{DEV_CERT_BIND_MOUNT_DEST_DIR}/{CERT_FILE_NAME}"; - var certKeyFileDest = $"{DEV_CERT_BIND_MOUNT_DEST_DIR}/{CERT_KEY_FILE_NAME}"; - - if (!containerResource.TryGetContainerMounts(out var mounts) && - mounts is not null && - mounts.Any(cm => cm.Target == DEV_CERT_BIND_MOUNT_DEST_DIR)) - { - return builder; - } - - // Use WithContainerFiles to provide the certificate files to the container - builder.ApplicationBuilder.CreateResourceBuilder(containerResource) - .WithContainerFiles(DEV_CERT_BIND_MOUNT_DEST_DIR, (context, cancellationToken) => - { - var files = new List(); - - // Check if certificate files exist before adding them - if (File.Exists(certExportPath)) - { - files.Add(new ContainerFile - { - Name = CERT_FILE_NAME, - SourcePath = certExportPath - }); - } - - if (File.Exists(certKeyExportPath)) - { - files.Add(new ContainerFile - { - Name = CERT_KEY_FILE_NAME, - SourcePath = certKeyExportPath - }); - } - - return Task.FromResult(files.AsEnumerable()); - }); - - if (!string.IsNullOrEmpty(certFileEnv)) - { - builder.WithEnvironment(certFileEnv, certFileDest); - } - if (!string.IsNullOrEmpty(certKeyFileEnv)) - { - builder.WithEnvironment(certKeyFileEnv, certKeyFileDest); - } - } - else - { - // For non-container resources, set the file paths directly - if (!string.IsNullOrEmpty(certFileEnv)) - { - builder.WithEnvironment(certFileEnv, certExportPath); - } - - if (!string.IsNullOrEmpty(certKeyFileEnv)) - { - builder.WithEnvironment(certKeyFileEnv, certKeyExportPath); - } - } - - return builder; - } -} \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector.Tests/ResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector.Tests/ResourceCreationTests.cs index b259b6448..cbdee3777 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector.Tests/ResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector.Tests/ResourceCreationTests.cs @@ -370,75 +370,6 @@ public void DevCertificateLogicIsNotTriggeredWhenForceNonSecureReceiverEnabled() Assert.DoesNotContain(context.Args.Cast(), a => a.Contains("receivers::otlp::protocols::grpc::tls::cert_file")); } - [Fact] - public void RunWithHttpsDevCertificateAddsExecutableResourceInRunMode() - { - var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run); - builder.Configuration["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"] = "https://localhost:18889"; - - builder.AddOpenTelemetryCollector("collector", settings => - { - settings.ForceNonSecureReceiver = false; // Allow HTTPS - }) - .WithAppForwarding(); - - using var app = builder.Build(); - - var appModel = app.Services.GetRequiredService(); - - // Should have created a dev-cert-export executable resource - var devCertExportResource = appModel.Resources.OfType() - .SingleOrDefault(r => r.Name == "dev-cert-export"); - Assert.NotNull(devCertExportResource); - - // Verify it's configured to run dotnet dev-certs - var args = devCertExportResource.Annotations.OfType().ToList(); - Assert.NotEmpty(args); - - var context = new CommandLineArgsCallbackContext([]); - foreach (var arg in args) - { - arg.Callback(context); - } - - Assert.Contains("dev-certs", context.Args.Cast()); - Assert.Contains("https", context.Args.Cast()); - Assert.Contains("--export-path", context.Args.Cast()); - Assert.Contains("--format", context.Args.Cast()); - Assert.Contains("Pem", context.Args.Cast()); - Assert.Contains("--no-password", context.Args.Cast()); - } - - [Fact] - public void RunWithHttpsDevCertificateAddsContainerFilesAndWaitAnnotation() - { - var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run); - builder.Configuration["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"] = "https://localhost:18889"; - - builder.AddOpenTelemetryCollector("collector", settings => - { - settings.ForceNonSecureReceiver = false; // Allow HTTPS - }) - .WithAppForwarding(); - - using var app = builder.Build(); - - var appModel = app.Services.GetRequiredService(); - var collectorResource = appModel.Resources.OfType().SingleOrDefault(); - Assert.NotNull(collectorResource); - - // Should have a WaitAnnotation for the dev-cert-export resource - var waitAnnotations = collectorResource.Annotations.OfType().ToList(); - var devCertWaitAnnotation = waitAnnotations.FirstOrDefault(w => w.Resource.Name == "dev-cert-export"); - Assert.NotNull(devCertWaitAnnotation); - Assert.Equal(WaitType.WaitForCompletion, devCertWaitAnnotation.WaitType); - - // Should have a ContainerFilesAnnotation for the dev certificates - var containerFilesAnnotations = collectorResource.Annotations.OfType().ToList(); - var devCertFilesAnnotation = containerFilesAnnotations.FirstOrDefault(cf => cf.DestinationPath == "/dev-certs"); - Assert.NotNull(devCertFilesAnnotation); - } - [Fact] public void RunWithHttpsDevCertificateNotTriggeredInNonRunMode() { @@ -500,135 +431,6 @@ public void RunWithHttpsDevCertificateNotTriggeredWhenForceNonSecureEnabled() Assert.Null(devCertFilesAnnotation); } - [Fact] - public void DevCertificateResourcesAddedWhenHttpsEnabled() - { - var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run); - builder.Configuration["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"] = "https://localhost:18889"; - - builder.AddOpenTelemetryCollector("collector", settings => - { - settings.ForceNonSecureReceiver = false; // Allow HTTPS - }) - .WithAppForwarding(); - - using var app = builder.Build(); - - var appModel = app.Services.GetRequiredService(); - - // Should have created a dev-cert-export executable resource - var devCertExportResource = appModel.Resources.OfType() - .SingleOrDefault(r => r.Name == "dev-cert-export"); - Assert.NotNull(devCertExportResource); - Assert.Equal("dotnet", devCertExportResource.Command); - - var collectorResource = appModel.Resources.OfType().SingleOrDefault(); - Assert.NotNull(collectorResource); - - // Should have container files annotation for dev certs - var containerFilesAnnotations = collectorResource.Annotations.OfType().ToList(); - var devCertFilesAnnotation = containerFilesAnnotations.FirstOrDefault(cf => cf.DestinationPath == "/dev-certs"); - Assert.NotNull(devCertFilesAnnotation); - - // Should have wait annotation for the dev-cert-export resource - var waitAnnotations = collectorResource.Annotations.OfType().ToList(); - var devCertWaitAnnotation = waitAnnotations.FirstOrDefault(wa => wa.Resource == devCertExportResource); - Assert.NotNull(devCertWaitAnnotation); - } - - [Fact] - public void DevCertificateContainerFilesOnlyAddedForEnabledEndpointsInRunMode() - { - var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run); - builder.Configuration["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"] = "https://localhost:18889"; - - builder.AddOpenTelemetryCollector("collector", settings => - { - settings.EnableGrpcEndpoint = true; - settings.EnableHttpEndpoint = false; // Only enable gRPC - settings.ForceNonSecureReceiver = false; - }) - .WithAppForwarding(); - - using var app = builder.Build(); - - var appModel = app.Services.GetRequiredService(); - var collectorResource = appModel.Resources.OfType().SingleOrDefault(); - Assert.NotNull(collectorResource); - - // Should have container files annotation for dev certs - var containerFilesAnnotations = collectorResource.Annotations.OfType().ToList(); - var devCertFilesAnnotation = containerFilesAnnotations.FirstOrDefault(cf => cf.DestinationPath == "/dev-certs"); - Assert.NotNull(devCertFilesAnnotation); - - // Verify the TLS arguments are only added for enabled endpoints - var args = collectorResource.Annotations.OfType().ToList(); - var context = new CommandLineArgsCallbackContext([]); - foreach (var arg in args) - { - arg.Callback(context); - } - - // Should only contain gRPC TLS args, not HTTP TLS args - Assert.Contains(context.Args.Cast(), a => a.Contains("receivers::otlp::protocols::grpc::tls::cert_file")); - Assert.Contains(context.Args.Cast(), a => a.Contains("receivers::otlp::protocols::grpc::tls::key_file")); - Assert.DoesNotContain(context.Args.Cast(), a => a.Contains("receivers::otlp::protocols::http::tls::cert_file")); - Assert.DoesNotContain(context.Args.Cast(), a => a.Contains("receivers::otlp::protocols::http::tls::key_file")); - } - - [Fact] - public void DevCertificateExecutableResourceHasCorrectConfiguration() - { - var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run); - builder.Configuration["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"] = "https://localhost:18889"; - - builder.AddOpenTelemetryCollector("collector", settings => - { - settings.ForceNonSecureReceiver = false; // Allow HTTPS - }) - .WithAppForwarding(); - - using var app = builder.Build(); - - var appModel = app.Services.GetRequiredService(); - - // Verify the dev-cert-export executable has correct configuration - var devCertExportResource = appModel.Resources.OfType() - .SingleOrDefault(r => r.Name == "dev-cert-export"); - Assert.NotNull(devCertExportResource); - Assert.Equal("dotnet", devCertExportResource.Command); - - // Check the environment variable for consistent language - var envAnnotations = devCertExportResource.Annotations.OfType().ToList(); - Assert.NotEmpty(envAnnotations); - - var envContext = new EnvironmentCallbackContext(new DistributedApplicationExecutionContext(new DistributedApplicationExecutionContextOptions(DistributedApplicationOperation.Run))); - foreach (var env in envAnnotations) - { - env.Callback(envContext); - } - - Assert.Contains("DOTNET_CLI_UI_LANGUAGE", envContext.EnvironmentVariables.Keys); - Assert.Equal("en", envContext.EnvironmentVariables["DOTNET_CLI_UI_LANGUAGE"]); - - // Check the arguments for certificate export - var argsAnnotations = devCertExportResource.Annotations.OfType().ToList(); - Assert.NotEmpty(argsAnnotations); - - var argsContext = new CommandLineArgsCallbackContext([]); - foreach (var arg in argsAnnotations) - { - arg.Callback(argsContext); - } - - Assert.Contains("dev-certs", argsContext.Args); - Assert.Contains("https", argsContext.Args); - Assert.Contains("--export-path", argsContext.Args); - Assert.Contains("--format", argsContext.Args); - Assert.Contains("Pem", argsContext.Args); - Assert.Contains("--no-password", argsContext.Args); - } - [Theory] [InlineData(true)] [InlineData(false)]