From 93364363e352299e4ece1ac307f5772ee5eea97f Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Tue, 29 Oct 2024 17:22:49 +1100 Subject: [PATCH] Enable devcontainers in repo. (#6491) Enable the use of DevContainers in the dotnet/aspire repository. This change includes the .devcontainer file optimized for use with our repo including some initial port forwards for demonstration purposes. From where we can enable pre-builds so we can more easily start using Codespaces to help trash out issues as are improving support for some of the various remote dev scenarios. --- .devcontainer/devcontainer.json | 94 +++++++++++++++++++ playground/Redis/Redis.AppHost/Program.cs | 4 +- .../waitfor/WaitForSandbox.AppHost/Program.cs | 5 +- .../PostgresBuilderExtensions.cs | 11 +++ .../Codespaces/CodespacesOptions.cs | 66 +++++++++++++ .../Codespaces/CodespacesUrlRewriter.cs | 21 +---- .../DistributedApplicationBuilder.cs | 1 + .../Codespaces/CodespacesUrlRewriterTests.cs | 4 + 8 files changed, 186 insertions(+), 20 deletions(-) create mode 100644 .devcontainer/devcontainer.json create mode 100644 src/Aspire.Hosting/Codespaces/CodespacesOptions.cs diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..f4d1001aba --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,94 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet +{ + "name": "C# (.NET)", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/dotnet:1-9.0", + "features": { + "ghcr.io/devcontainers/features/azure-cli:1": {}, + "ghcr.io/azure/azure-dev/azd:0": {}, + "ghcr.io/devcontainers/features/docker-in-docker": {}, + "ghcr.io/devcontainers/features/dotnet": { + "additionalVersions": [ + "8.0.403" + ] + } + }, + + "hostRequirements": { + "cpus": 8, + "memory": "32gb", + "storage": "64gb" + }, + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [ + 15887, + 5180, + 7024, + 15551, + 33803, + 5350, + 41567, + 15306 + ], + "portsAttributes": { + "5180": { + "label": "WaitFor Playground: ApiService", + "protocol": "http" + }, + "5350": { + "label": "Redis Playground: Api Service" + }, + "7024": { + "label": "WaitFor Playground: Frontend", + "protocol": "https" + }, + "15306": { + "label": "Redis Playground: App Host" + }, + "15551": { + "label": "WaitFor Playground: PGAdmin", + "protocol": "http" + }, + "15887": { + "label": "WaitFor Playground: AppHost", + "protocol": "https" + }, + "33803": { + "label": "Redis Playground: Redis Commander" + }, + "41567": { + "label": "Redis Playground: Redis Insight" + } + }, + "otherPortsAttributes": { + "onAutoForward": "ignore" + }, + + // Use 'postCreateCommand' to run commands after the container is created. + "customizations": { + "vscode": { + "extensions": [ + "ms-dotnettools.csdevkit", + "ms-azuretools.vscode-bicep", + "ms-azuretools.azure-dev" + ], + "settings": { + "remote.autoForwardPorts": false, + "dotnet.defaultSolution": "Aspire.sln" + } + } + }, + "onCreateCommand": "dotnet restore", + "postStartCommand": "dotnet dev-certs https --trust" + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/playground/Redis/Redis.AppHost/Program.cs b/playground/Redis/Redis.AppHost/Program.cs index b2cbe95d6a..93669bc6be 100644 --- a/playground/Redis/Redis.AppHost/Program.cs +++ b/playground/Redis/Redis.AppHost/Program.cs @@ -2,8 +2,8 @@ var redis = builder.AddRedis("redis") .WithDataVolume() - .WithRedisCommander() - .WithRedisInsight(); + .WithRedisCommander(c => c.WithHostPort(33803)) + .WithRedisInsight(c => c.WithHostPort(41567)); var garnet = builder.AddGarnet("garnet") .WithDataVolume(); diff --git a/playground/waitfor/WaitForSandbox.AppHost/Program.cs b/playground/waitfor/WaitForSandbox.AppHost/Program.cs index 6ee1f5ac79..e7ad817dd6 100644 --- a/playground/waitfor/WaitForSandbox.AppHost/Program.cs +++ b/playground/waitfor/WaitForSandbox.AppHost/Program.cs @@ -7,7 +7,10 @@ .WithPasswordAuthentication() .RunAsContainer(c => { - c.WithPgAdmin(); + c.WithPgAdmin(c => + { + c.WithHostPort(15551); + }); }) .AddDatabase("db"); diff --git a/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs b/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs index 7ba2558646..e588c37c51 100644 --- a/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs +++ b/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs @@ -6,6 +6,7 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Postgres; using Aspire.Hosting.Utils; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace Aspire.Hosting; @@ -333,6 +334,16 @@ private static void SetPgAdminEnvironmentVariables(EnvironmentCallbackContext co // You need to define the PGADMIN_DEFAULT_EMAIL and PGADMIN_DEFAULT_PASSWORD or PGADMIN_DEFAULT_PASSWORD_FILE environment variables. context.EnvironmentVariables.Add("PGADMIN_DEFAULT_EMAIL", "admin@domain.com"); context.EnvironmentVariables.Add("PGADMIN_DEFAULT_PASSWORD", "admin"); + + // When running in the context of Codespaces we need to set some additional environment + // varialbes so that PGAdmin will trust the forwarded headers that Codespaces port + // forwarding will send. + var config = context.ExecutionContext.ServiceProvider.GetRequiredService(); + if (context.ExecutionContext.IsRunMode && config.GetValue("CODESPACES", false)) + { + context.EnvironmentVariables["PGADMIN_CONFIG_PROXY_X_HOST_COUNT"] = "1"; + context.EnvironmentVariables["PGADMIN_CONFIG_PROXY_X_PREFIX_COUNT"] = "1"; + } } /// diff --git a/src/Aspire.Hosting/Codespaces/CodespacesOptions.cs b/src/Aspire.Hosting/Codespaces/CodespacesOptions.cs new file mode 100644 index 0000000000..8e54dbf695 --- /dev/null +++ b/src/Aspire.Hosting/Codespaces/CodespacesOptions.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; + +namespace Aspire.Hosting.Codespaces; + +/// +/// GitHub Codespaces configuration values. +/// +internal class CodespacesOptions +{ + /// + /// When set to true, the app host is running in a GitHub Codespace. + /// + /// + /// Maps to the CODESPACE environment variable. + /// + public bool IsCodespace { get; set; } + + /// + /// When set it is the domain suffix used when port forwarding services hosted on the Codespace. + /// + /// + /// Maps to the GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN environment variable. + /// + [MemberNotNullWhen(true, nameof(IsCodespace))] + public string? PortForwardingDomain { get; set; } + + /// + /// When set it is the name of the GitHub Codespace in which the app host is running. + /// + /// + /// Maps to the CODESPACE_NAME environment variable. + /// + [MemberNotNullWhen(true, nameof(IsCodespace))] + public string? CodespaceName { get; set; } +} + +internal class ConfigureCodespacesOptions(IConfiguration configuration) : IConfigureOptions +{ + private const string CodespacesEnvironmentVariable = "CODESPACES"; + private const string CodespaceNameEnvironmentVariable = "CODESPACE_NAME"; + private const string GitHubCodespacesPortForwardingDomain = "GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN"; + + private string GetRequiredCodespacesConfigurationValue(string key) + { + ArgumentNullException.ThrowIfNullOrEmpty(key); + return configuration.GetValue(key) ?? throw new DistributedApplicationException($"Codespaces was detected but {key} environment missing."); + } + + public void Configure(CodespacesOptions options) + { + if (!configuration.GetValue(CodespacesEnvironmentVariable, false)) + { + options.IsCodespace = false; + return; + } + + options.IsCodespace = true; + options.PortForwardingDomain = GetRequiredCodespacesConfigurationValue(GitHubCodespacesPortForwardingDomain); + options.CodespaceName = GetRequiredCodespacesConfigurationValue(CodespaceNameEnvironmentVariable); + } +} \ No newline at end of file diff --git a/src/Aspire.Hosting/Codespaces/CodespacesUrlRewriter.cs b/src/Aspire.Hosting/Codespaces/CodespacesUrlRewriter.cs index e72c3657c2..4d6a113c72 100644 --- a/src/Aspire.Hosting/Codespaces/CodespacesUrlRewriter.cs +++ b/src/Aspire.Hosting/Codespaces/CodespacesUrlRewriter.cs @@ -3,35 +3,22 @@ using System.Collections.Immutable; using Aspire.Hosting.ApplicationModel; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; namespace Aspire.Hosting.Codespaces; -internal sealed class CodespacesUrlRewriter(ILogger logger, IConfiguration configuration, ResourceNotificationService resourceNotificationService) : BackgroundService +internal sealed class CodespacesUrlRewriter(ILogger logger, IOptions options, ResourceNotificationService resourceNotificationService) : BackgroundService { - private const string CodespacesEnvironmentVariable = "CODESPACES"; - private const string CodespaceNameEnvironmentVariable = "CODESPACE_NAME"; - private const string GitHubCodespacesPortForwardingDomain = "GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN"; - - private string GetRequiredCodespacesConfigurationValue(string key) - { - ArgumentNullException.ThrowIfNullOrEmpty(key); - return configuration.GetValue(key) ?? throw new DistributedApplicationException($"Codespaces was detected but {key} environment missing."); - } - protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - if (!configuration.GetValue(CodespacesEnvironmentVariable, false)) + if (!options.Value.IsCodespace) { logger.LogTrace("Not running in Codespaces, skipping URL rewriting."); return; } - var gitHubCodespacesPortForwardingDomain = GetRequiredCodespacesConfigurationValue(GitHubCodespacesPortForwardingDomain); - var codespaceName = GetRequiredCodespacesConfigurationValue(CodespaceNameEnvironmentVariable); - do { try @@ -58,7 +45,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) // which is typically ".app.github.dev". The VSCode instance is typically // hosted at codespacename.github.dev whereas the forwarded ports // would be at codespacename-port.app.github.dev. - Url = $"{uri.Scheme}://{codespaceName}-{uri.Port}.{gitHubCodespacesPortForwardingDomain}{uri.AbsolutePath}" + Url = $"{uri.Scheme}://{options.Value.CodespaceName}-{uri.Port}.{options.Value.PortForwardingDomain}{uri.AbsolutePath}" }; remappedUrls.Add(originalUrlSnapshot, newUrlSnapshot); diff --git a/src/Aspire.Hosting/DistributedApplicationBuilder.cs b/src/Aspire.Hosting/DistributedApplicationBuilder.cs index cf2d5824f1..c1ced8265c 100644 --- a/src/Aspire.Hosting/DistributedApplicationBuilder.cs +++ b/src/Aspire.Hosting/DistributedApplicationBuilder.cs @@ -269,6 +269,7 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options) _innerBuilder.Services.AddSingleton(); // Codespaces + _innerBuilder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, ConfigureCodespacesOptions>()); _innerBuilder.Services.AddHostedService(); Eventing.Subscribe(BuiltInDistributedApplicationEventSubscriptionHandlers.InitializeDcpAnnotations); diff --git a/tests/Aspire.Hosting.Tests/Codespaces/CodespacesUrlRewriterTests.cs b/tests/Aspire.Hosting.Tests/Codespaces/CodespacesUrlRewriterTests.cs index b004b8bdf2..e8444a2496 100644 --- a/tests/Aspire.Hosting.Tests/Codespaces/CodespacesUrlRewriterTests.cs +++ b/tests/Aspire.Hosting.Tests/Codespaces/CodespacesUrlRewriterTests.cs @@ -15,6 +15,10 @@ public class CodespacesUrlRewriterTests(ITestOutputHelper testOutputHelper) public async Task VerifyUrlsRewriterStopsWhenNotInCodespaces() { using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper); + + // Explicitly disable codespace behavior for this test. + builder.Configuration["CODESPACES"] = "false"; + builder.Services.AddLogging(logging => { logging.AddFakeLogging();