Skip to content

Commit 4f21893

Browse files
danegstaCopilot
andauthored
Fix persistent container endpoint allocation (#17960)
* Fix persistent container endpoint allocation Default persistent container endpoints to proxied unless proxy support is disabled, and remove delayed proxyless container endpoint allocation in favor of target-port public port defaults. Preserve endpoint and connection string event timing from release/13.3 and add coverage for the KeyVault emulator-style health check path.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Restore health check URI binding timing Set HTTP health check URIs during BeforeResourceStartedEvent again so surrogate resource builders that forward startup events continue to initialize their health checks. Add DCP coverage for the KeyVault-emulator-style surrogate pattern.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Bump Aspire patch version to 13.4.3 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Preserve blocking endpoint allocation dispatch Keep ResourceEndpointsAllocatedEvent dispatch aligned with release/13.4 so subscriber exceptions propagate and endpoint allocation callbacks complete before startup proceeds.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Stabilize Kafka persistent reuse test port Use a fixed public Kafka port for the persistent reuse test and let the shared persistent-container helper disable DCP test port randomization when a test needs explicit ports to remain stable.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 59e06a4 commit 4f21893

14 files changed

Lines changed: 362 additions & 295 deletions

eng/Versions.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<!-- This repo version -->
44
<MajorVersion>13</MajorVersion>
55
<MinorVersion>4</MinorVersion>
6-
<PatchVersion>2</PatchVersion>
6+
<PatchVersion>3</PatchVersion>
77
<VersionPrefix>$(MajorVersion).$(MinorVersion).$(PatchVersion)</VersionPrefix>
88
<PreReleaseVersionLabel>preview.1</PreReleaseVersionLabel>
99
<DefaultTargetFramework>net8.0</DefaultTargetFramework>

src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,6 @@ public int? Port
209209
}
210210
}
211211

212-
internal int? SpecifiedPort => _port;
213-
214212
/// <summary>
215213
/// This is the port the resource is listening on. If the endpoint is used for the container, it is the container port.
216214
/// </summary>

src/Aspire.Hosting/ApplicationModel/EndpointReference.cs

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -221,26 +221,6 @@ public ReferenceExpression GetTlsValue(ReferenceExpression enabledValue, Referen
221221
GetAllocatedEndpoint()
222222
?? throw new InvalidOperationException($"The endpoint `{EndpointName}` is not allocated for the resource `{Resource.Name}`.");
223223

224-
internal Task<AllocatedEndpoint> GetAllocatedEndpointAsync(NetworkIdentifier networkId, CancellationToken cancellationToken = default)
225-
{
226-
var endpointAnnotation = EndpointAnnotation;
227-
if (endpointAnnotation.AllAllocatedEndpoints.TryGetAllocatedEndpoint(networkId, out var endpoint))
228-
{
229-
return Task.FromResult(endpoint);
230-
}
231-
232-
foreach (var allocationAnnotation in Resource.Annotations.OfType<OnDemandEndpointAllocationAnnotation>())
233-
{
234-
endpoint = allocationAnnotation.TryAllocate(endpointAnnotation, networkId);
235-
if (endpoint is not null)
236-
{
237-
return Task.FromResult(endpoint);
238-
}
239-
}
240-
241-
return endpointAnnotation.AllAllocatedEndpoints.GetAllocatedEndpointAsync(networkId, cancellationToken);
242-
}
243-
244224
private EndpointAnnotation? GetEndpointAnnotation()
245225
{
246226
if (_endpointAnnotation is not null)
@@ -391,7 +371,7 @@ public class EndpointReferenceExpression(EndpointReference endpointReference, En
391371

392372
async ValueTask<string?> ResolveValueWithAllocatedAddress()
393373
{
394-
var allocatedEndpoint = await Endpoint.GetAllocatedEndpointAsync(networkContext, cancellationToken).ConfigureAwait(false);
374+
var allocatedEndpoint = await Endpoint.EndpointAnnotation.AllAllocatedEndpoints.GetAllocatedEndpointAsync(networkContext, cancellationToken).ConfigureAwait(false);
395375

396376
return Property switch
397377
{

src/Aspire.Hosting/ApplicationModel/OnDemandEndpointAllocationAnnotation.cs

Lines changed: 0 additions & 24 deletions
This file was deleted.

src/Aspire.Hosting/Dcp/ContainerCreator.cs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ public IEnumerable<RenderedModelResource<Container>> PrepareObjects()
199199
}
200200

201201
var containerAppResource = new RenderedModelResource<Container>(container, ctr);
202-
DcpModelUtilities.AddServicesProducedInfo(containerAppResource, _appResources.Get(), _logger);
202+
DcpModelUtilities.AddServicesProducedInfo(containerAppResource, _appResources.Get());
203203
_appResources.Add(containerAppResource);
204204
result.Add(containerAppResource);
205205
}
@@ -316,19 +316,13 @@ private async Task BuildAndCreateContainerAsync(RenderedModelResource<Container>
316316
spec.RunArgs = runArgs;
317317

318318
var (configuration, pemCertificates, createFiles) = await BuildContainerConfiguration(cr, logger, cToken).ConfigureAwait(false);
319-
// Configuration callbacks are the last pre-creation point where on-demand allocation can run.
320-
cr.ModelResource.Annotations
321-
.OfType<OnDemandEndpointAllocationAnnotation>()
322-
.SingleOrDefault()
323-
?.StopAllocating();
324319

325320
if (configuration.Exception is not null)
326321
{
327322
throw new FailedToApplyEnvironmentException($"Failed to apply configuration to container {cr.ModelResource.Name}", configuration.Exception);
328323
}
329324

330-
// Environment callbacks can resolve proxyless endpoint ports and commit a fallback host port,
331-
// so build ports afterward.
325+
// Configuration callbacks can update endpoint metadata, so build ports afterward.
332326
if (cr.ServicesProduced.Count > 0)
333327
{
334328
spec.Ports = BuildContainerPorts(cr);
@@ -993,7 +987,7 @@ private static List<ContainerPortSpec> BuildContainerPorts(RenderedModelResource
993987
ContainerPort = ea.TargetPort,
994988
};
995989

996-
if (!ea.IsProxied && ea.SpecifiedPort is int hostPort)
990+
if (!ea.IsProxied && ea.Port is int hostPort)
997991
{
998992
sp.Service.Spec.Port ??= hostPort;
999993
portSpec.HostPort = hostPort;

src/Aspire.Hosting/Dcp/DcpExecutor.cs

Lines changed: 21 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ public DcpExecutor(ILogger<DcpExecutor> logger,
113113
_executionContext = executionContext;
114114
_appResources = appResources;
115115

116-
_resourceWatcher = new DcpResourceWatcher(logger, kubernetesService, loggerService, executorEvents, model, _appResources, _configuration, PublishLateEndpointsAllocatedEventAsync, profilingTelemetry, _shutdownCancellation.Token);
116+
_resourceWatcher = new DcpResourceWatcher(logger, kubernetesService, loggerService, executorEvents, model, _appResources, _configuration, profilingTelemetry, _shutdownCancellation.Token);
117117

118118
DeleteResourceRetryPipeline = DcpPipelineBuilder.BuildDeleteRetryPipeline(logger);
119119

@@ -195,59 +195,47 @@ public async Task RunApplicationAsync(CancellationToken ct = default)
195195

196196
var createContainerNetworks = Task.Run(() => CreateAllDcpObjectsAsync<ContainerNetwork>(ct), ct);
197197

198-
var createWorkloadEndpoints = Task.Run(async () =>
198+
var createExecutableEndpoints = Task.Run(async () =>
199199
{
200-
await Task.WhenAll([getProxyAddresses, createContainerNetworks]).WaitAsync(ct).ConfigureAwait(false);
200+
await getProxyAddresses.ConfigureAwait(false);
201201

202-
List<IResource> endpointAllocatedResources = [];
203202
foreach (var executable in executables)
204203
{
205204
if (DcpModelUtilities.TryAddWorkloadAllocatedEndpoints(
206205
executable,
207206
_options.Value.EnableAspireContainerTunnel,
208-
ContainerHostName,
209-
allowPendingDynamicProxylessContainerEndpoints: false))
210-
{
211-
endpointAllocatedResources.Add(executable.ModelResource);
212-
}
213-
}
214-
215-
foreach (var container in containers)
216-
{
217-
if (DcpModelUtilities.TryAddWorkloadAllocatedEndpoints(
218-
container,
219-
_options.Value.EnableAspireContainerTunnel,
220-
ContainerHostName,
221-
allowPendingDynamicProxylessContainerEndpoints: true))
207+
ContainerHostName))
222208
{
223-
endpointAllocatedResources.Add(container.ModelResource);
209+
await PublishEndpointsAllocatedEventAsync(executable.ModelResource, ct).ConfigureAwait(false);
224210
}
225211
}
226-
227-
// Allocate every endpoint that is known before workload creation before publishing any
228-
// resource-specific endpoint events. URL callbacks can reference endpoints on other
229-
// resources, so publishing per-resource while another resource is still allocating can
230-
// make a valid cross-resource callback observe an unallocated endpoint.
231-
foreach (var resource in endpointAllocatedResources.Distinct())
232-
{
233-
await PublishEndpointsAllocatedEventAsync(resource, ct).ConfigureAwait(false);
234-
}
235212
}, ct);
236213

237214
var createExecutables = Task.Run(async () =>
238215
{
239-
await createWorkloadEndpoints.ConfigureAwait(false);
216+
await createExecutableEndpoints.ConfigureAwait(false);
240217

241218
await CreateRenderedResourcesAsync(_executableCreator, executables, EmptyCreationContext.s_instance, ct).ConfigureAwait(false);
242219
}, ct);
243220

244221
// Configuring containers that use the tunnel require these host network-side endpoints for Executables to be ready.
245-
var cctx = new ContainerCreationContext(createContainerNetworks, createWorkloadEndpoints, ct);
222+
var cctx = new ContainerCreationContext(createContainerNetworks, createExecutableEndpoints, ct);
246223
_containerContextSource.SetResult(cctx);
247224

248225
var createContainers = Task.Run(async () =>
249226
{
250-
await createWorkloadEndpoints.ConfigureAwait(false);
227+
await Task.WhenAll([getProxyAddresses, createContainerNetworks]).WaitAsync(ct).ConfigureAwait(false);
228+
229+
foreach (var container in containers)
230+
{
231+
if (DcpModelUtilities.TryAddWorkloadAllocatedEndpoints(
232+
container,
233+
_options.Value.EnableAspireContainerTunnel,
234+
ContainerHostName))
235+
{
236+
await PublishEndpointsAllocatedEventAsync(container.ModelResource, ct).ConfigureAwait(false);
237+
}
238+
}
251239

252240
await CreateRenderedResourcesAsync(_containerCreator, containers, cctx, ct).ConfigureAwait(false);
253241
}, ct);
@@ -670,9 +658,7 @@ private void PrepareServices()
670658
}
671659
else
672660
{
673-
port = sp.ModelResource.IsContainer() && !endpoint.IsProxied
674-
? endpoint.SpecifiedPort
675-
: endpoint.Port;
661+
port = endpoint.Port;
676662
}
677663
svc.Spec.Port = port;
678664
svc.Spec.Protocol = PortProtocol.FromProtocolType(endpoint.Protocol);
@@ -711,7 +697,7 @@ static bool GetEffectiveIsProxied(IResource resource, EndpointAnnotation endpoin
711697
return isProxied;
712698
}
713699

714-
return !resource.HasPersistentLifetime();
700+
return !resource.HasPersistentLifetime() || resource.IsContainer();
715701
}
716702

717703
var containers = _model.Resources.Where(r => r.IsContainer());
@@ -1224,14 +1210,6 @@ private async Task<bool> PublishEndpointsAllocatedEventAsync(IResource resource,
12241210
return true;
12251211
}
12261212

1227-
private async Task PublishLateEndpointsAllocatedEventAsync(IResource resource, CancellationToken ct)
1228-
{
1229-
if (await PublishEndpointsAllocatedEventAsync(resource, ct).ConfigureAwait(false))
1230-
{
1231-
await PublishConnectionStringAvailableEventAsync(resource, ct).ConfigureAwait(false);
1232-
}
1233-
}
1234-
12351213
private async Task PublishConnectionStringAvailableEventAsync(IResource resource, CancellationToken ct)
12361214
{
12371215
if (!DcpModelUtilities.AreResourceEndpointsAllocated(resource))

src/Aspire.Hosting/Dcp/DcpModelUtilities.cs

Lines changed: 9 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
using System.Net;
88
using Aspire.Hosting.ApplicationModel;
99
using Aspire.Hosting.Dcp.Model;
10-
using Microsoft.Extensions.Logging;
1110

1211
namespace Aspire.Hosting.Dcp;
1312

@@ -34,8 +33,7 @@ internal static bool ShouldDeferCreateForExplicitStart(IResource modelResource,
3433
/// </summary>
3534
internal static void AddServicesProducedInfo<TDcpResource>(
3635
RenderedModelResource<TDcpResource> appResource,
37-
IEnumerable<IAppResource> appResources,
38-
ILogger? logger = null)
36+
IEnumerable<IAppResource> appResources)
3937
where TDcpResource : CustomResource, IKubernetesStaticMetadata
4038
{
4139
var modelResource = appResource.ModelResource;
@@ -89,16 +87,6 @@ internal static void AddServicesProducedInfo<TDcpResource>(
8987
appResource.ServicesProduced.Add(sp);
9088
}
9189

92-
if (appResource.ServicesProduced.Any(sp => IsDynamicProxylessContainerEndpoint(appResource, sp)) &&
93-
!modelResource.Annotations.OfType<OnDemandEndpointAllocationAnnotation>().Any())
94-
{
95-
// These endpoints normally get their host port during container creation. If a
96-
// reference needs the allocated endpoint while building the container configuration,
97-
// commit the fallback port before waiting would deadlock resource creation.
98-
modelResource.Annotations.Add(new OnDemandEndpointAllocationAnnotation(
99-
(endpoint, networkId) => TryAllocateDynamicProxylessContainerEndpoint(appResource, endpoint, networkId, logger)));
100-
}
101-
10290
static bool HasMultipleReplicas(CustomResource resource)
10391
{
10492
if (resource is Executable exe && exe.Metadata.Annotations.TryGetValue(CustomResource.ResourceReplicaCount, out var value) && int.TryParse(value, CultureInfo.InvariantCulture, out var replicas) && replicas > 1)
@@ -117,22 +105,19 @@ internal static void AddWorkloadAllocatedEndpoints<TDcpResource>(
117105
{
118106
foreach (var res in resources)
119107
{
120-
TryAddWorkloadAllocatedEndpoints(res, enableAspireContainerTunnel, containerHostName, allowPendingDynamicProxylessContainerEndpoints: false);
108+
TryAddWorkloadAllocatedEndpoints(res, enableAspireContainerTunnel, containerHostName);
121109
}
122110
}
123111

124112
internal static bool TryAddWorkloadAllocatedEndpoints<TDcpResource>(
125113
RenderedModelResource<TDcpResource> resource,
126114
bool enableAspireContainerTunnel,
127-
string containerHostName,
128-
bool allowPendingDynamicProxylessContainerEndpoints)
115+
string containerHostName)
129116
where TDcpResource : CustomResource, IKubernetesStaticMetadata
130117
{
131118
foreach (var sp in resource.ServicesProduced)
132119
{
133-
if (TryAddLocalhostAllocatedEndpoint(
134-
sp,
135-
allowPending: allowPendingDynamicProxylessContainerEndpoints && IsDynamicProxylessContainerEndpoint(resource, sp)))
120+
if (TryAddLocalhostAllocatedEndpoint(sp, allowPending: false))
136121
{
137122
AddContainerNetworkAllocatedEndpoint(resource, sp);
138123
AddExecutableContainerNetworkAllocatedEndpoint(resource, sp, enableAspireContainerTunnel, containerHostName);
@@ -142,41 +127,33 @@ internal static bool TryAddWorkloadAllocatedEndpoints<TDcpResource>(
142127
return AreResourceEndpointsAllocated(resource.ModelResource);
143128
}
144129

145-
internal static bool TryApplyServiceAddressToEndpoint(Service observedService, IEnumerable<IAppResource> appResources, [NotNullWhen(true)] out IResource? modelResource)
130+
internal static void ApplyServiceAddressToEndpoint(Service observedService, IEnumerable<IAppResource> appResources)
146131
{
147132
var serviceResource = appResources.OfType<ServiceWithModelResource>()
148133
.FirstOrDefault(swr => string.Equals(swr.DcpResource.Metadata.Name, observedService.Metadata.Name, StringComparison.Ordinal));
149134

150135
if (serviceResource is null)
151136
{
152-
modelResource = null;
153-
return false;
137+
return;
154138
}
155139

156140
serviceResource.Service.ApplyAddressInfoFrom(observedService);
157-
var isDynamicProxylessContainerEndpoint = appResources.OfType<RenderedModelResource<Container>>()
158-
.Any(resource => ReferenceEquals(resource.ModelResource, serviceResource.ModelResource) &&
159-
IsDynamicProxylessContainerEndpoint(resource, serviceResource));
160141
if (!TryAddLocalhostAllocatedEndpoint(serviceResource, allowPending: true))
161142
{
162-
modelResource = null;
163-
return false;
143+
return;
164144
}
165145

166146
foreach (var containerResource in appResources.OfType<RenderedModelResource<Container>>()
167147
.Where(resource => ReferenceEquals(resource.ModelResource, serviceResource.ModelResource)))
168148
{
169149
AddContainerNetworkAllocatedEndpoint(containerResource, serviceResource);
170150
}
171-
172-
modelResource = serviceResource.ModelResource;
173-
return isDynamicProxylessContainerEndpoint && AreResourceEndpointsAllocated(modelResource);
174151
}
175152

176-
private static bool TryAddLocalhostAllocatedEndpoint(ServiceWithModelResource sp, bool allowPending, int? fallbackPort = null)
153+
private static bool TryAddLocalhostAllocatedEndpoint(ServiceWithModelResource sp, bool allowPending)
177154
{
178155
var svc = sp.DcpResource;
179-
var allocatedPort = svc.AllocatedPort ?? fallbackPort;
156+
var allocatedPort = svc.AllocatedPort;
180157

181158
if (sp.EndpointAnnotation.AllocatedEndpoint is not null)
182159
{
@@ -283,50 +260,6 @@ internal static bool AreResourceEndpointsAllocated(IResource resource)
283260
return !resource.TryGetEndpoints(out var endpoints) || endpoints.All(e => e.AllocatedEndpoint is not null);
284261
}
285262

286-
private static bool IsDynamicProxylessContainerEndpoint<TDcpResource>(RenderedModelResource<TDcpResource> resource, ServiceWithModelResource sp)
287-
where TDcpResource : CustomResource, IKubernetesStaticMetadata
288-
{
289-
return resource.DcpResource is Container &&
290-
!sp.EndpointAnnotation.IsProxied &&
291-
sp.EndpointAnnotation.SpecifiedPort is null;
292-
}
293-
294-
private static AllocatedEndpoint? TryAllocateDynamicProxylessContainerEndpoint<TDcpResource>(
295-
RenderedModelResource<TDcpResource> resource,
296-
EndpointAnnotation endpoint,
297-
NetworkIdentifier networkId,
298-
ILogger? logger)
299-
where TDcpResource : CustomResource, IKubernetesStaticMetadata
300-
{
301-
var sp = resource.ServicesProduced.SingleOrDefault(sp =>
302-
ReferenceEquals(sp.EndpointAnnotation, endpoint) &&
303-
IsDynamicProxylessContainerEndpoint(resource, sp));
304-
if (sp is null)
305-
{
306-
return null;
307-
}
308-
309-
Debug.Assert(endpoint.TargetPort is not null);
310-
311-
var targetPort = endpoint.TargetPort.Value;
312-
endpoint.Port = targetPort;
313-
logger?.LogInformation(
314-
"Endpoint '{EndpointName}' on container resource '{ResourceName}' was resolved before the container was created, so Aspire is assigning public port {PublicPort} to match target port {TargetPort} for proxyless access.",
315-
endpoint.Name,
316-
sp.ModelResource.Name,
317-
targetPort,
318-
targetPort);
319-
320-
if (TryAddLocalhostAllocatedEndpoint(sp, allowPending: false, fallbackPort: targetPort))
321-
{
322-
AddContainerNetworkAllocatedEndpoint(resource, sp);
323-
}
324-
325-
return endpoint.AllAllocatedEndpoints.TryGetAllocatedEndpoint(networkId, out var allocatedEndpoint)
326-
? allocatedEndpoint
327-
: null;
328-
}
329-
330263
internal static void AddContainerTunnelAllocatedEndpoints(
331264
IEnumerable<IResource> affectedResources,
332265
DcpAppResourceStore allAppResources,

0 commit comments

Comments
 (0)