Skip to content

Commit

Permalink
Enable devcontainers in repo. (#6491)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mitchdenny authored Oct 29, 2024
1 parent af83cc5 commit 9336436
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 20 deletions.
94 changes: 94 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -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"
}
4 changes: 2 additions & 2 deletions playground/Redis/Redis.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
5 changes: 4 additions & 1 deletion playground/waitfor/WaitForSandbox.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
.WithPasswordAuthentication()
.RunAsContainer(c =>
{
c.WithPgAdmin();
c.WithPgAdmin(c =>
{
c.WithHostPort(15551);
});
})
.AddDatabase("db");

Expand Down
11 changes: 11 additions & 0 deletions src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<IConfiguration>();
if (context.ExecutionContext.IsRunMode && config.GetValue<bool>("CODESPACES", false))
{
context.EnvironmentVariables["PGADMIN_CONFIG_PROXY_X_HOST_COUNT"] = "1";
context.EnvironmentVariables["PGADMIN_CONFIG_PROXY_X_PREFIX_COUNT"] = "1";
}
}

/// <summary>
Expand Down
66 changes: 66 additions & 0 deletions src/Aspire.Hosting/Codespaces/CodespacesOptions.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// GitHub Codespaces configuration values.
/// </summary>
internal class CodespacesOptions
{
/// <summary>
/// When set to true, the app host is running in a GitHub Codespace.
/// </summary>
/// <remarks>
/// Maps to the CODESPACE environment variable.
/// </remarks>
public bool IsCodespace { get; set; }

/// <summary>
/// When set it is the domain suffix used when port forwarding services hosted on the Codespace.
/// </summary>
/// <remarks>
/// Maps to the GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN environment variable.
/// </remarks>
[MemberNotNullWhen(true, nameof(IsCodespace))]
public string? PortForwardingDomain { get; set; }

/// <summary>
/// When set it is the name of the GitHub Codespace in which the app host is running.
/// </summary>
/// <remarks>
/// Maps to the CODESPACE_NAME environment variable.
/// </remarks>
[MemberNotNullWhen(true, nameof(IsCodespace))]
public string? CodespaceName { get; set; }
}

internal class ConfigureCodespacesOptions(IConfiguration configuration) : IConfigureOptions<CodespacesOptions>
{
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<string>(key) ?? throw new DistributedApplicationException($"Codespaces was detected but {key} environment missing.");
}

public void Configure(CodespacesOptions options)
{
if (!configuration.GetValue<bool>(CodespacesEnvironmentVariable, false))
{
options.IsCodespace = false;
return;
}

options.IsCodespace = true;
options.PortForwardingDomain = GetRequiredCodespacesConfigurationValue(GitHubCodespacesPortForwardingDomain);
options.CodespaceName = GetRequiredCodespacesConfigurationValue(CodespaceNameEnvironmentVariable);
}
}
21 changes: 4 additions & 17 deletions src/Aspire.Hosting/Codespaces/CodespacesUrlRewriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<CodespacesUrlRewriter> logger, IConfiguration configuration, ResourceNotificationService resourceNotificationService) : BackgroundService
internal sealed class CodespacesUrlRewriter(ILogger<CodespacesUrlRewriter> logger, IOptions<CodespacesOptions> 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<string>(key) ?? throw new DistributedApplicationException($"Codespaces was detected but {key} environment missing.");
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
if (!configuration.GetValue<bool>(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
Expand All @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/Aspire.Hosting/DistributedApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options)
_innerBuilder.Services.AddSingleton<IKubernetesService, KubernetesService>();

// Codespaces
_innerBuilder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<CodespacesOptions>, ConfigureCodespacesOptions>());
_innerBuilder.Services.AddHostedService<CodespacesUrlRewriter>();

Eventing.Subscribe<BeforeStartEvent>(BuiltInDistributedApplicationEventSubscriptionHandlers.InitializeDcpAnnotations);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit 9336436

Please sign in to comment.