Skip to content

Commit bab3ebb

Browse files
DamianEdwardsmitchdennyCopilot
authored
Ensure resource non-endpoint URLs are active when initialized (#9696)
* Ensure resource non-endpoint URLs are active when initialized Fixes #9516 * WIP * Split static URLs upfront * Remove commented out line * Refactoring * Test tweak * Tweak test * Update src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update WithUrlsTests.cs * Fix race in test * Add comment --------- Co-authored-by: Mitch Denny <midenn@microsoft.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent ee6ebbb commit bab3ebb

File tree

5 files changed

+193
-47
lines changed

5 files changed

+193
-47
lines changed

src/Aspire.Hosting/Dcp/DcpExecutor.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using Aspire.Hosting.ConsoleLogs;
1717
using Aspire.Hosting.Dashboard;
1818
using Aspire.Hosting.Dcp.Model;
19+
using Aspire.Hosting.Eventing;
1920
using Aspire.Hosting.Utils;
2021
using Json.Patch;
2122
using k8s;
@@ -47,6 +48,7 @@ internal sealed class DcpExecutor : IDcpExecutor, IConsoleLogsService, IAsyncDis
4748
private readonly ILogger<DcpExecutor> _logger;
4849
private readonly DistributedApplicationModel _model;
4950
private readonly DistributedApplicationOptions _distributedApplicationOptions;
51+
private readonly IDistributedApplicationEventing _distributedApplicationEventing;
5052
private readonly IOptions<DcpOptions> _options;
5153
private readonly DistributedApplicationExecutionContext _executionContext;
5254
private readonly List<AppResource> _appResources = [];
@@ -75,6 +77,7 @@ public DcpExecutor(ILogger<DcpExecutor> logger,
7577
DistributedApplicationModel model,
7678
IKubernetesService kubernetesService,
7779
IConfiguration configuration,
80+
IDistributedApplicationEventing distributedApplicationEventing,
7881
DistributedApplicationOptions distributedApplicationOptions,
7982
IOptions<DcpOptions> options,
8083
DistributedApplicationExecutionContext executionContext,
@@ -92,6 +95,7 @@ public DcpExecutor(ILogger<DcpExecutor> logger,
9295
_executorEvents = executorEvents;
9396
_logger = logger;
9497
_model = model;
98+
_distributedApplicationEventing = distributedApplicationEventing;
9599
_distributedApplicationOptions = distributedApplicationOptions;
96100
_options = options;
97101
_executionContext = executionContext;
@@ -715,6 +719,13 @@ private async Task CreateContainersAndExecutablesAsync(CancellationToken cancell
715719

716720
await _executorEvents.PublishAsync(new OnEndpointsAllocatedContext(cancellationToken)).ConfigureAwait(false);
717721

722+
// Fire the endpoints allocated event for all DCP managed resources with endpoints.
723+
foreach (var resource in toCreate.Select(r => r.ModelResource).OfType<IResourceWithEndpoints>())
724+
{
725+
var resourceEvent = new ResourceEndpointsAllocatedEvent(resource, _executionContext.ServiceProvider);
726+
await _distributedApplicationEventing.PublishAsync(resourceEvent, EventDispatchBehavior.NonBlockingConcurrent, cancellationToken).ConfigureAwait(false);
727+
}
728+
718729
var containersTask = CreateContainersAsync(toCreate.Where(ar => ar.DcpResource is Container), cancellationToken);
719730
var executablesTask = CreateExecutablesAsync(toCreate.Where(ar => ar.DcpResource is Executable), cancellationToken);
720731

src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,7 @@ public ApplicationOrchestrator(DistributedApplicationModel model,
5252
dcpExecutorEvents.Subscribe<OnResourceStartingContext>(OnResourceStarting);
5353
dcpExecutorEvents.Subscribe<OnResourceFailedToStartContext>(OnResourceFailedToStart);
5454

55-
_eventing.Subscribe<ResourceEndpointsAllocatedEvent>(ProcessResourcesWithoutLifetime);
56-
_eventing.Subscribe<ResourceEndpointsAllocatedEvent>(PublishInitialResourceUrls);
55+
_eventing.Subscribe<ResourceEndpointsAllocatedEvent>(OnResourceEndpointsAllocated);
5756
_eventing.Subscribe<ConnectionStringAvailableEvent>(PublishConnectionStringValue);
5857
// Implement WaitFor functionality using BeforeResourceStartedEvent.
5958
_eventing.Subscribe<BeforeResourceStartedEvent>(WaitForInBeforeResourceStartedEvent);
@@ -118,32 +117,32 @@ private async Task OnEndpointsAllocated(OnEndpointsAllocatedContext context)
118117
{
119118
await lifecycleHook.AfterEndpointsAllocatedAsync(_model, context.CancellationToken).ConfigureAwait(false);
120119
}
121-
122-
// Fire the endpoints allocated event for all resources.
123-
foreach (var resource in _model.Resources)
124-
{
125-
await _eventing.PublishAsync(new ResourceEndpointsAllocatedEvent(resource, _serviceProvider), EventDispatchBehavior.NonBlockingConcurrent, context.CancellationToken).ConfigureAwait(false);
126-
}
127120
}
128121

129-
private async Task PublishInitialResourceUrls(ResourceEndpointsAllocatedEvent @event, CancellationToken cancellationToken)
122+
private async Task PublishResourceEndpointUrls(IResource resource, CancellationToken cancellationToken)
130123
{
131-
var resource = @event.Resource;
132-
133124
// Process URLs for the resource.
134-
await ProcessUrls(resource, cancellationToken).ConfigureAwait(false);
125+
await ProcessResourceUrlCallbacks(resource, cancellationToken).ConfigureAwait(false);
126+
127+
// Publish update with URLs.
128+
var urls = GetResourceUrls(resource);
129+
await _notificationService.PublishUpdateAsync(resource, s => s with { Urls = [.. urls] }).ConfigureAwait(false);
130+
}
135131

132+
private static IEnumerable<UrlSnapshot> GetResourceUrls(IResource resource)
133+
{
136134
IEnumerable<UrlSnapshot> urls = [];
137135
if (resource.TryGetUrls(out var resourceUrls))
138136
{
139137
urls = resourceUrls.Select(url => new UrlSnapshot(Name: url.Endpoint?.EndpointName, Url: url.Url, IsInternal: url.DisplayLocation == UrlDisplayLocation.DetailsOnly)
140138
{
141-
IsInactive = true,
139+
// Endpoint URLs are inactive (hidden in the dashboard) when published here. It is assumed they will get activated later when the endpoint is considered active
140+
// by whatever allocated the endpoint in the first place, e.g. for resources controlled by DCP, when DCP detects the endpoint is listening.
141+
IsInactive = url.Endpoint is not null,
142142
DisplayProperties = new(url.DisplayText ?? "", url.DisplayOrder ?? 0)
143143
});
144144
}
145-
146-
await _notificationService.PublishUpdateAsync(resource, s => s with { Urls = [.. urls] }).ConfigureAwait(false);
145+
return urls;
147146
}
148147

149148
private async Task OnResourceStarting(OnResourceStartingContext context)
@@ -196,16 +195,17 @@ private async Task OnResourcesPrepared(OnResourcesPreparedContext context)
196195
await PublishResourcesInitialStateAsync(context.CancellationToken).ConfigureAwait(false);
197196
}
198197

199-
private async Task ProcessUrls(IResource resource, CancellationToken cancellationToken)
198+
private async Task ProcessResourceUrlCallbacks(IResource resource, CancellationToken cancellationToken)
200199
{
201-
// Project endpoints to URLS
202200
var urls = new List<ResourceUrlAnnotation>();
203201

202+
// Project endpoints to URLs
204203
if (resource.TryGetEndpoints(out var endpoints) && resource is IResourceWithEndpoints resourceWithEndpoints)
205204
{
206205
foreach (var endpoint in endpoints)
207206
{
208207
// Create a URL for each endpoint
208+
Debug.Assert(endpoint.AllocatedEndpoint is not null, "Endpoint should be allocated at this point as we're calling this from ResourceEndpointsAllocatedEvent handler.");
209209
if (endpoint.AllocatedEndpoint is { } allocatedEndpoint)
210210
{
211211
var url = new ResourceUrlAnnotation { Url = allocatedEndpoint.UriString, Endpoint = new EndpointReference(resourceWithEndpoints, endpoint) };
@@ -214,6 +214,12 @@ private async Task ProcessUrls(IResource resource, CancellationToken cancellatio
214214
}
215215
}
216216

217+
if (resource.TryGetUrls(out var existingUrls))
218+
{
219+
// Static URLs added to the resource via WithUrl(string name, string url), i.e. not callback-based
220+
urls.AddRange(existingUrls);
221+
}
222+
217223
// Run the URL callbacks
218224
if (resource.TryGetAnnotationsOfType<ResourceUrlsCallbackAnnotation>(out var callbacks))
219225
{
@@ -228,7 +234,7 @@ private async Task ProcessUrls(IResource resource, CancellationToken cancellatio
228234
}
229235

230236
// Clear existing URLs
231-
if (resource.TryGetUrls(out var existingUrls))
237+
if (existingUrls is not null)
232238
{
233239
var existing = existingUrls.ToArray();
234240
for (var i = existing.Length - 1; i >= 0; i--)
@@ -257,34 +263,36 @@ private async Task ProcessUrls(IResource resource, CancellationToken cancellatio
257263
}
258264
}
259265

260-
private async Task ProcessResourcesWithoutLifetime(ResourceEndpointsAllocatedEvent @event, CancellationToken cancellationToken)
266+
private async Task OnResourceEndpointsAllocated(ResourceEndpointsAllocatedEvent @event, CancellationToken cancellationToken)
261267
{
262-
if (@event.Resource is not IResourceWithoutLifetime resource)
263-
{
264-
return;
265-
}
268+
await ProcessResourceWithoutLifetime(@event.Resource, cancellationToken).ConfigureAwait(false);
269+
await PublishResourceEndpointUrls(@event.Resource, cancellationToken).ConfigureAwait(false);
270+
}
266271

267-
if (resource is not IValueProvider valueProvider)
272+
private async Task ProcessResourceWithoutLifetime(IResource resource, CancellationToken cancellationToken)
273+
{
274+
if (resource is not IResourceWithoutLifetime resourceWithoutLifetime
275+
|| resourceWithoutLifetime is not IValueProvider valueProvider)
268276
{
269277
return;
270278
}
271279

272280
try
273281
{
274-
var value = await valueProvider.GetValueAsync(default).ConfigureAwait(false);
282+
var value = await valueProvider.GetValueAsync(cancellationToken).ConfigureAwait(false);
275283

276-
await _notificationService.PublishUpdateAsync(resource, s =>
284+
await _notificationService.PublishUpdateAsync(resourceWithoutLifetime, s =>
277285
{
278286
return s with
279287
{
280-
Properties = s.Properties.SetResourceProperty("Value", value ?? "", resource is ParameterResource p && p.Secret)
288+
Properties = s.Properties.SetResourceProperty("Value", value ?? "", resourceWithoutLifetime is ParameterResource p && p.Secret)
281289
};
282290
})
283291
.ConfigureAwait(false);
284292
}
285293
catch (Exception ex)
286294
{
287-
await _notificationService.PublishUpdateAsync(resource, s =>
295+
await _notificationService.PublishUpdateAsync(resourceWithoutLifetime, s =>
288296
{
289297
return s with
290298
{
@@ -294,7 +302,7 @@ await _notificationService.PublishUpdateAsync(resource, s =>
294302
})
295303
.ConfigureAwait(false);
296304

297-
_loggerService.GetLogger(resource.Name).LogError("{Message}", ex.Message);
305+
_loggerService.GetLogger(resourceWithoutLifetime.Name).LogError("{Message}", ex.Message);
298306
}
299307
}
300308

@@ -407,12 +415,14 @@ private async Task PublishResourcesInitialStateAsync(CancellationToken cancellat
407415
var parent = resource is IResourceWithParent hasParent
408416
? hasParent.Parent
409417
: resource.Annotations.OfType<ResourceRelationshipAnnotation>().LastOrDefault(r => r.Type == KnownRelationshipTypes.Parent)?.Resource;
418+
var urls = GetResourceUrls(resource);
410419

411420
await _notificationService.PublishUpdateAsync(resource, s =>
412421
{
413422
return s with
414423
{
415424
Relationships = relationships,
425+
Urls = [.. urls],
416426
Properties = parent is null ? s.Properties : s.Properties.SetResourceProperty(KnownProperties.Resource.ParentName, parent.GetResolvedResourceNames()[0]),
417427
HealthReports = GetInitialHealthReports(resource)
418428
};

src/Aspire.Hosting/ResourceBuilderExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -843,7 +843,7 @@ public static IResourceBuilder<T> WithUrl<T>(this IResourceBuilder<T> builder, s
843843
ArgumentNullException.ThrowIfNull(builder);
844844
ArgumentNullException.ThrowIfNull(url);
845845

846-
return builder.WithAnnotation(new ResourceUrlsCallbackAnnotation(c => c.Urls.Add(new() { Url = url, DisplayText = displayText })));
846+
return builder.WithAnnotation(new ResourceUrlAnnotation { Url = url, DisplayText = displayText });
847847
}
848848

849849
/// <summary>

tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1270,6 +1270,7 @@ private static DcpExecutor CreateAppExecutor(
12701270
distributedAppModel,
12711271
kubernetesService ?? new TestKubernetesService(),
12721272
configuration,
1273+
new Hosting.Eventing.DistributedApplicationEventing(),
12731274
new DistributedApplicationOptions(),
12741275
Options.Create(dcpOptions),
12751276
new DistributedApplicationExecutionContext(new DistributedApplicationExecutionContextOptions(DistributedApplicationOperation.Run)

0 commit comments

Comments
 (0)