-
Notifications
You must be signed in to change notification settings - Fork 761
Initial integration of codespace URL rewriting logic into hosting. #6183
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
ee584c7
8a8b879
5901dd8
f580e60
54fee06
5f406e9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Collections.Immutable; | ||
| using Aspire.Hosting.ApplicationModel; | ||
| using Microsoft.Extensions.Configuration; | ||
| using Microsoft.Extensions.Hosting; | ||
| using Microsoft.Extensions.Logging; | ||
|
|
||
| namespace Aspire.Hosting.Codespaces; | ||
|
|
||
| internal class CodespacesUrlRewriter(ILogger<CodespacesUrlRewriter> logger, IConfiguration configuration, ResourceNotificationService resourceNotificationService) : BackgroundService | ||
| { | ||
| protected override async Task ExecuteAsync(CancellationToken stoppingToken) | ||
| { | ||
| if (!configuration.GetValue<bool>("CODESPACES", false)) | ||
| { | ||
| logger.LogTrace("Not running in Codespaces, skipping URL rewriting."); | ||
| return; | ||
| } | ||
|
|
||
| var gitHubCodespacesPortForwardingDomain = configuration.GetValue<string>("GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN") ?? throw new DistributedApplicationException("Codespaces was detected but GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN environment missing."); | ||
| var codespaceName = configuration.GetValue<string>("CODESPACE_NAME") ?? throw new DistributedApplicationException("Codespaces was detected but CODESPACE_NAME environment missing."); | ||
mitchdenny marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| do | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe add a comment that this runs for the lifetime of the host so it can react to resources as they start and new resources being added? |
||
| { | ||
| var resourceEvents = resourceNotificationService.WatchAsync(stoppingToken); | ||
|
|
||
| await foreach (var resourceEvent in resourceEvents.ConfigureAwait(false)) | ||
| { | ||
| Dictionary<UrlSnapshot, UrlSnapshot>? remappedUrls = null; | ||
|
|
||
| foreach (var originalUrlSnapshot in resourceEvent.Snapshot.Urls) | ||
| { | ||
| var uri = new Uri(originalUrlSnapshot.Url); | ||
|
|
||
| if (!originalUrlSnapshot.IsInternal && (uri.Scheme == "http" || uri.Scheme == "https") && uri.Host == "localhost") | ||
|
||
| { | ||
| if (remappedUrls is null) | ||
| { | ||
| remappedUrls = new(); | ||
| } | ||
mitchdenny marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| var newUrlSnapshot = originalUrlSnapshot with | ||
| { | ||
| Url = $"{uri.Scheme}://{codespaceName}-{uri.Port}.{gitHubCodespacesPortForwardingDomain}{uri.AbsolutePath}" | ||
mitchdenny marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }; | ||
|
|
||
| remappedUrls.Add(originalUrlSnapshot, newUrlSnapshot); | ||
| } | ||
| } | ||
|
|
||
| if (remappedUrls is not null) | ||
| { | ||
| var transformedUrls = from originalUrl in resourceEvent.Snapshot.Urls | ||
| select remappedUrls.TryGetValue(originalUrl, out var remappedUrl) ? remappedUrl : originalUrl; | ||
|
|
||
| await resourceNotificationService.PublishUpdateAsync(resourceEvent.Resource, resourceEvent.ResourceId, s => s with | ||
| { | ||
| Urls = transformedUrls.ToImmutableArray() | ||
| }).ConfigureAwait(false); | ||
| } | ||
| } | ||
|
|
||
| // Short delay if we crash just to avoid spinning CPU. | ||
| await Task.Delay(5000, stoppingToken).ConfigureAwait(false); | ||
| } while (!stoppingToken.IsCancellationRequested); | ||
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using Aspire.Hosting.Utils; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.Extensions.Logging; | ||
| using Xunit; | ||
| using Xunit.Abstractions; | ||
|
|
||
| namespace Aspire.Hosting.Tests.Codespaces; | ||
|
|
||
| public class CodespacesUrlRewriterTests(ITestOutputHelper testOutputHelper) | ||
| { | ||
| [Fact] | ||
| public async Task VerifyUrlsRewriterStopsWhenNotInCodespaces() | ||
| { | ||
| using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper); | ||
| builder.Services.AddLogging(logging => | ||
| { | ||
| logging.AddFakeLogging(); | ||
| logging.AddXunit(testOutputHelper); | ||
| }); | ||
|
|
||
| var resource = builder.AddResource(new CustomResource("resource")); | ||
|
|
||
| var abortToken = new CancellationTokenSource(TimeSpan.FromSeconds(60)); | ||
|
|
||
| using var app = builder.Build(); | ||
| var rns = app.Services.GetRequiredService<ResourceNotificationService>(); | ||
|
|
||
| await app.StartAsync(abortToken.Token); | ||
|
|
||
| var collector = app.Services.GetFakeLogCollector(); | ||
|
|
||
| var urlRewriterStopped = false; | ||
|
|
||
| while (!abortToken.Token.IsCancellationRequested) | ||
| { | ||
| var logs = collector.GetSnapshot(); | ||
| urlRewriterStopped = logs.Any(l => l.Message.Contains("Not running in Codespaces, skipping URL rewriting.")); | ||
| if (urlRewriterStopped) | ||
| { | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| Assert.True(urlRewriterStopped); | ||
|
|
||
| await app.StopAsync(abortToken.Token); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task VerifyUrlsRewrittenWhenInCodespaces() | ||
| { | ||
| using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper); | ||
|
|
||
| builder.Configuration["CODESPACES"] = "true"; | ||
| builder.Configuration["GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN"] = "app.github.dev"; | ||
| builder.Configuration["CODESPACE_NAME"] = "test-codespace"; | ||
|
|
||
| var resource = builder.AddResource(new CustomResource("resource")); | ||
|
|
||
| var abortToken = new CancellationTokenSource(TimeSpan.FromSeconds(60)); | ||
|
|
||
| using var app = builder.Build(); | ||
| var rns = app.Services.GetRequiredService<ResourceNotificationService>(); | ||
|
|
||
| await app.StartAsync(abortToken.Token); | ||
|
|
||
| // Push the URL to the resource state. | ||
| var localhostUrlSnapshot = new UrlSnapshot("Test", "http://localhost:1234", false); | ||
| await rns.PublishUpdateAsync(resource.Resource, s => s with | ||
| { | ||
| State = KnownResourceStates.Running, | ||
| Urls = [localhostUrlSnapshot] | ||
| }); | ||
|
|
||
| // Wait until | ||
| var resourceEvent = await rns.WaitForResourceAsync( | ||
| resource.Resource.Name, | ||
| (re) => { | ||
| var match = re.Snapshot.Urls.Length > 0 && re.Snapshot.Urls[0].Url.Contains("app.github.dev"); | ||
| return match; | ||
| }, | ||
| abortToken.Token); | ||
|
|
||
| Assert.Collection( | ||
| resourceEvent.Snapshot.Urls, | ||
| u => | ||
| { | ||
| Assert.Equal("Test", u.Name); | ||
| Assert.Equal("http://test-codespace-1234.app.github.dev/", u.Url); | ||
| Assert.False(u.IsInternal); | ||
| } | ||
| ); | ||
|
|
||
| await app.StopAsync(abortToken.Token); | ||
| } | ||
|
|
||
| private sealed class CustomResource(string name) : Resource(name) | ||
| { | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.