Skip to content

Commit 9336436

Browse files
authored
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.
1 parent af83cc5 commit 9336436

File tree

8 files changed

+186
-20
lines changed

8 files changed

+186
-20
lines changed

.devcontainer/devcontainer.json

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
2+
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
3+
{
4+
"name": "C# (.NET)",
5+
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
6+
"image": "mcr.microsoft.com/devcontainers/dotnet:1-9.0",
7+
"features": {
8+
"ghcr.io/devcontainers/features/azure-cli:1": {},
9+
"ghcr.io/azure/azure-dev/azd:0": {},
10+
"ghcr.io/devcontainers/features/docker-in-docker": {},
11+
"ghcr.io/devcontainers/features/dotnet": {
12+
"additionalVersions": [
13+
"8.0.403"
14+
]
15+
}
16+
},
17+
18+
"hostRequirements": {
19+
"cpus": 8,
20+
"memory": "32gb",
21+
"storage": "64gb"
22+
},
23+
24+
// Features to add to the dev container. More info: https://containers.dev/features.
25+
// "features": {},
26+
27+
// Use 'forwardPorts' to make a list of ports inside the container available locally.
28+
"forwardPorts": [
29+
15887,
30+
5180,
31+
7024,
32+
15551,
33+
33803,
34+
5350,
35+
41567,
36+
15306
37+
],
38+
"portsAttributes": {
39+
"5180": {
40+
"label": "WaitFor Playground: ApiService",
41+
"protocol": "http"
42+
},
43+
"5350": {
44+
"label": "Redis Playground: Api Service"
45+
},
46+
"7024": {
47+
"label": "WaitFor Playground: Frontend",
48+
"protocol": "https"
49+
},
50+
"15306": {
51+
"label": "Redis Playground: App Host"
52+
},
53+
"15551": {
54+
"label": "WaitFor Playground: PGAdmin",
55+
"protocol": "http"
56+
},
57+
"15887": {
58+
"label": "WaitFor Playground: AppHost",
59+
"protocol": "https"
60+
},
61+
"33803": {
62+
"label": "Redis Playground: Redis Commander"
63+
},
64+
"41567": {
65+
"label": "Redis Playground: Redis Insight"
66+
}
67+
},
68+
"otherPortsAttributes": {
69+
"onAutoForward": "ignore"
70+
},
71+
72+
// Use 'postCreateCommand' to run commands after the container is created.
73+
"customizations": {
74+
"vscode": {
75+
"extensions": [
76+
"ms-dotnettools.csdevkit",
77+
"ms-azuretools.vscode-bicep",
78+
"ms-azuretools.azure-dev"
79+
],
80+
"settings": {
81+
"remote.autoForwardPorts": false,
82+
"dotnet.defaultSolution": "Aspire.sln"
83+
}
84+
}
85+
},
86+
"onCreateCommand": "dotnet restore",
87+
"postStartCommand": "dotnet dev-certs https --trust"
88+
89+
// Configure tool-specific properties.
90+
// "customizations": {},
91+
92+
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
93+
// "remoteUser": "root"
94+
}

playground/Redis/Redis.AppHost/Program.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
var redis = builder.AddRedis("redis")
44
.WithDataVolume()
5-
.WithRedisCommander()
6-
.WithRedisInsight();
5+
.WithRedisCommander(c => c.WithHostPort(33803))
6+
.WithRedisInsight(c => c.WithHostPort(41567));
77

88
var garnet = builder.AddGarnet("garnet")
99
.WithDataVolume();

playground/waitfor/WaitForSandbox.AppHost/Program.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
.WithPasswordAuthentication()
88
.RunAsContainer(c =>
99
{
10-
c.WithPgAdmin();
10+
c.WithPgAdmin(c =>
11+
{
12+
c.WithHostPort(15551);
13+
});
1114
})
1215
.AddDatabase("db");
1316

src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Aspire.Hosting.ApplicationModel;
77
using Aspire.Hosting.Postgres;
88
using Aspire.Hosting.Utils;
9+
using Microsoft.Extensions.Configuration;
910
using Microsoft.Extensions.DependencyInjection;
1011

1112
namespace Aspire.Hosting;
@@ -333,6 +334,16 @@ private static void SetPgAdminEnvironmentVariables(EnvironmentCallbackContext co
333334
// You need to define the PGADMIN_DEFAULT_EMAIL and PGADMIN_DEFAULT_PASSWORD or PGADMIN_DEFAULT_PASSWORD_FILE environment variables.
334335
context.EnvironmentVariables.Add("PGADMIN_DEFAULT_EMAIL", "admin@domain.com");
335336
context.EnvironmentVariables.Add("PGADMIN_DEFAULT_PASSWORD", "admin");
337+
338+
// When running in the context of Codespaces we need to set some additional environment
339+
// varialbes so that PGAdmin will trust the forwarded headers that Codespaces port
340+
// forwarding will send.
341+
var config = context.ExecutionContext.ServiceProvider.GetRequiredService<IConfiguration>();
342+
if (context.ExecutionContext.IsRunMode && config.GetValue<bool>("CODESPACES", false))
343+
{
344+
context.EnvironmentVariables["PGADMIN_CONFIG_PROXY_X_HOST_COUNT"] = "1";
345+
context.EnvironmentVariables["PGADMIN_CONFIG_PROXY_X_PREFIX_COUNT"] = "1";
346+
}
336347
}
337348

338349
/// <summary>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics.CodeAnalysis;
5+
using Microsoft.Extensions.Configuration;
6+
using Microsoft.Extensions.Options;
7+
8+
namespace Aspire.Hosting.Codespaces;
9+
10+
/// <summary>
11+
/// GitHub Codespaces configuration values.
12+
/// </summary>
13+
internal class CodespacesOptions
14+
{
15+
/// <summary>
16+
/// When set to true, the app host is running in a GitHub Codespace.
17+
/// </summary>
18+
/// <remarks>
19+
/// Maps to the CODESPACE environment variable.
20+
/// </remarks>
21+
public bool IsCodespace { get; set; }
22+
23+
/// <summary>
24+
/// When set it is the domain suffix used when port forwarding services hosted on the Codespace.
25+
/// </summary>
26+
/// <remarks>
27+
/// Maps to the GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN environment variable.
28+
/// </remarks>
29+
[MemberNotNullWhen(true, nameof(IsCodespace))]
30+
public string? PortForwardingDomain { get; set; }
31+
32+
/// <summary>
33+
/// When set it is the name of the GitHub Codespace in which the app host is running.
34+
/// </summary>
35+
/// <remarks>
36+
/// Maps to the CODESPACE_NAME environment variable.
37+
/// </remarks>
38+
[MemberNotNullWhen(true, nameof(IsCodespace))]
39+
public string? CodespaceName { get; set; }
40+
}
41+
42+
internal class ConfigureCodespacesOptions(IConfiguration configuration) : IConfigureOptions<CodespacesOptions>
43+
{
44+
private const string CodespacesEnvironmentVariable = "CODESPACES";
45+
private const string CodespaceNameEnvironmentVariable = "CODESPACE_NAME";
46+
private const string GitHubCodespacesPortForwardingDomain = "GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN";
47+
48+
private string GetRequiredCodespacesConfigurationValue(string key)
49+
{
50+
ArgumentNullException.ThrowIfNullOrEmpty(key);
51+
return configuration.GetValue<string>(key) ?? throw new DistributedApplicationException($"Codespaces was detected but {key} environment missing.");
52+
}
53+
54+
public void Configure(CodespacesOptions options)
55+
{
56+
if (!configuration.GetValue<bool>(CodespacesEnvironmentVariable, false))
57+
{
58+
options.IsCodespace = false;
59+
return;
60+
}
61+
62+
options.IsCodespace = true;
63+
options.PortForwardingDomain = GetRequiredCodespacesConfigurationValue(GitHubCodespacesPortForwardingDomain);
64+
options.CodespaceName = GetRequiredCodespacesConfigurationValue(CodespaceNameEnvironmentVariable);
65+
}
66+
}

src/Aspire.Hosting/Codespaces/CodespacesUrlRewriter.cs

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,22 @@
33

44
using System.Collections.Immutable;
55
using Aspire.Hosting.ApplicationModel;
6-
using Microsoft.Extensions.Configuration;
76
using Microsoft.Extensions.Hosting;
87
using Microsoft.Extensions.Logging;
8+
using Microsoft.Extensions.Options;
99

1010
namespace Aspire.Hosting.Codespaces;
1111

12-
internal sealed class CodespacesUrlRewriter(ILogger<CodespacesUrlRewriter> logger, IConfiguration configuration, ResourceNotificationService resourceNotificationService) : BackgroundService
12+
internal sealed class CodespacesUrlRewriter(ILogger<CodespacesUrlRewriter> logger, IOptions<CodespacesOptions> options, ResourceNotificationService resourceNotificationService) : BackgroundService
1313
{
14-
private const string CodespacesEnvironmentVariable = "CODESPACES";
15-
private const string CodespaceNameEnvironmentVariable = "CODESPACE_NAME";
16-
private const string GitHubCodespacesPortForwardingDomain = "GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN";
17-
18-
private string GetRequiredCodespacesConfigurationValue(string key)
19-
{
20-
ArgumentNullException.ThrowIfNullOrEmpty(key);
21-
return configuration.GetValue<string>(key) ?? throw new DistributedApplicationException($"Codespaces was detected but {key} environment missing.");
22-
}
23-
2414
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
2515
{
26-
if (!configuration.GetValue<bool>(CodespacesEnvironmentVariable, false))
16+
if (!options.Value.IsCodespace)
2717
{
2818
logger.LogTrace("Not running in Codespaces, skipping URL rewriting.");
2919
return;
3020
}
3121

32-
var gitHubCodespacesPortForwardingDomain = GetRequiredCodespacesConfigurationValue(GitHubCodespacesPortForwardingDomain);
33-
var codespaceName = GetRequiredCodespacesConfigurationValue(CodespaceNameEnvironmentVariable);
34-
3522
do
3623
{
3724
try
@@ -58,7 +45,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
5845
// which is typically ".app.github.dev". The VSCode instance is typically
5946
// hosted at codespacename.github.dev whereas the forwarded ports
6047
// would be at codespacename-port.app.github.dev.
61-
Url = $"{uri.Scheme}://{codespaceName}-{uri.Port}.{gitHubCodespacesPortForwardingDomain}{uri.AbsolutePath}"
48+
Url = $"{uri.Scheme}://{options.Value.CodespaceName}-{uri.Port}.{options.Value.PortForwardingDomain}{uri.AbsolutePath}"
6249
};
6350

6451
remappedUrls.Add(originalUrlSnapshot, newUrlSnapshot);

src/Aspire.Hosting/DistributedApplicationBuilder.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options)
269269
_innerBuilder.Services.AddSingleton<IKubernetesService, KubernetesService>();
270270

271271
// Codespaces
272+
_innerBuilder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<CodespacesOptions>, ConfigureCodespacesOptions>());
272273
_innerBuilder.Services.AddHostedService<CodespacesUrlRewriter>();
273274

274275
Eventing.Subscribe<BeforeStartEvent>(BuiltInDistributedApplicationEventSubscriptionHandlers.InitializeDcpAnnotations);

tests/Aspire.Hosting.Tests/Codespaces/CodespacesUrlRewriterTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ public class CodespacesUrlRewriterTests(ITestOutputHelper testOutputHelper)
1515
public async Task VerifyUrlsRewriterStopsWhenNotInCodespaces()
1616
{
1717
using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
18+
19+
// Explicitly disable codespace behavior for this test.
20+
builder.Configuration["CODESPACES"] = "false";
21+
1822
builder.Services.AddLogging(logging =>
1923
{
2024
logging.AddFakeLogging();

0 commit comments

Comments
 (0)